# Configuration as code: `bucker.yml`

`bucker.yml` describes an organization's configuration in a file you keep in a
repository and change by pull request. Bucker reads the file, shows you a **plan** (the
exact list of changes it would make), and changes nothing until you **apply** that plan.

A file can manage:

- org **retention**, per data stream;
- for each project, its **alert rules**, **cron monitors**, **DSN key** rate limits, its
  **quota** override, and its **ownership rules**;
- the chat alert **routes** and **ceilings** from version 1 of the file. These are planned
  here but applied from chat. See [Chat routes](#chat-routes).

## The commands

```bash
bucker config export > bucker.yml       # what the org is configured to do today
bucker config plan bucker.yml           # the diff; writes nothing
bucker config apply bucker.yml          # re-plans, then applies exactly that plan
```

The CLI wraps three API routes, which are also the JSON import and export:

```
GET  /orgs/{org}/config[?project=<slug>]   export, as a version-2 document
POST /orgs/{org}/config/plan               { "source": "<file text>" } or { "config": {…} }
POST /orgs/{org}/config/apply              the same body, plus an optional "planHash"
```

A plan and an apply body carry the document in exactly one of two forms:

- `source`: the file's raw text, as YAML or JSON;
- `config`: the document, already decoded.

JSON is valid YAML, so an exported document can be saved as `bucker.yml` without
changes. If you plan an exported document against the org it came from, the plan has no
changes.

### Plan

```json
{
  "changes": [
    { "action": "create", "resource": "monitor", "key": "checkout/nightly-billing",
      "summary": "create monitor \"nightly-billing\"" },
    { "action": "unchanged", "resource": "project_key", "key": "checkout/default",
      "summary": "DSN key \"default\"" }
  ],
  "errors": [],
  "planHash": "9f2c…"
}
```

- `action` is one of `create`, `update`, `delete` or `unchanged`.
- `resource` is one of `retention`, `alert_rule`, `monitor`, `project_key`, `quota`,
  `ownership_rule`, `chat_route` or `chat_ceiling`.
- If the file fails to parse, the response is still a 200. It contains every error, each
  with a path such as `projects.checkout.monitors.0.schedule`, and no changes.
- A plan never writes. You can run it in CI on every pull request.

### Apply

Apply plans the file again, then makes each change. The plan you reviewed and the
changes apply makes are the same list.

- **Pass `planHash`** from the plan you reviewed. If the file or the org changed since
  that plan was made, apply returns **409** and changes nothing. Plan again and review the
  new result.
- A plan with errors is refused with **422**, and nothing is changed.
- Each change goes through the same write path as the matching API route, so each one
  writes its usual audit row (`monitor.create`, `alert_rule.update`,
  `project_key.update`, …). Apply also writes one `config.apply` row. That row names the
  plan hash and the list of changes, and it is written before the first change.
- The response lists the changes that were applied. Unchanged entries are omitted.

**Permissions.** Plan and export need read access to the organization. Each project you
plan must also be readable by you; a project you cannot read is reported as unknown.
Apply needs the narrowest permission for each change, and checks every one before the
first write:

| Change | Needs |
| --- | --- |
| alert rules, monitors, DSN keys, ownership rules | `write` on the project (a team admin, or an org admin or owner) |
| retention | `admin` on the org (an org owner) |
| quota | billing management on the org |

If one change needs a permission you lack, apply returns **403** and nothing is changed.
A file that only tunes a project's monitors can be applied by that project's team admin.
A file that also sets retention needs an owner.

**Apply is not one transaction.** Each change uses its resource's own write path,
including that path's validation and audit row. Planning already checks everything the
file controls: the schema, unknown projects, teams and members, ambiguous key names, and
alert-rule channels and queries. A write can still fail for a reason the file does not
control, for example a quota above the plan's volume. If that happens, apply stops at
that change and returns the error. Plan again to see what remains.

## The file

```yaml
version: 2

retention:                 # org-wide; null returns a stream to the plan's maximum
  spanDays: 14
  logRecordDays: 14

projects:
  checkout:                # the project slug
    prune: false           # see "Deletion" below
    alertRules:            # the alert-rule API's body, matched by name
      - name: New issues in checkout
        type: issue
        conditions: [{ id: first_seen }]
        actions: [{ kind: notify_channel, channelId: <channel id> }]
    monitors:              # the monitor API's body plus a slug, matched by slug
      - slug: nightly-billing
        name: Nightly billing
        schedule: "0 2 * * *"
        timezone: America/New_York
        gracePeriodMinutes: 10
    keys:                  # DSN keys, matched by name
      - name: default
        rateLimitPerMinute: 600
    quota:
      monthlyEventLimit: 2000000
      capsEnabled: true
    ownership:             # first matching rule wins
      - pattern: "src/billing/**"
        teams: [payments]
        members: [ana@example.com]
```

- **Each resource uses the same schema as its API route.** An alert rule is the body of
  [`POST …/alert-rules`](alert-rules.md). A monitor is the body of `POST …/monitors`, plus
  its `slug`. Defaults and validation rules are the route's own, so a rule the API refuses
  is also refused in the file.
- **Validation is strict.** An unknown key is an error, not ignored. A misspelled
  `gracePeriodMinutes` fails the plan.
- **A missing section is not managed by the file.** If a project block has no `monitors:`
  key, the file leaves that project's monitors alone. You can adopt the file for one
  concern at a time.
- **Owners are referenced by name.** List teams by slug and members by email. During
  planning, each name is resolved to an id. A name that does not exist in the
  organization fails the plan.
- **DSN keys are created or updated, never deleted.** Deleting a key breaks ingest for
  every app that uses it. To stop a key, set `isActive: false`.
- **Org-wide alert rules are not part of the file.** These are covered by
  [Org-wide rules](alert-rules.md#org-wide-rules). The export also leaves them out.

### Deletion

To delete something, the file must say so twice. A project block must:

1. list the resource kind, for example `monitors: [...]`; and
2. set `prune: true`.

With both, apply deletes that project's alert rules or monitors that the file does not
name. Without `prune`, unlisted resources are left alone. A file written for a new
project therefore cannot delete the rules of an existing one.

**Ownership rules work differently.** A file deletes only ownership rules that it created.
Bucker marks those rules with `source: bucker.yml`. Rules created by a person, and rules
imported from `CODEOWNERS`, are never deleted from a file. This follows the same rule as
the `CODEOWNERS` import. If the file lists a rule that already exists, wherever that rule
came from, the plan reports it as `unchanged`.

### Chat routes

Version 1 of `bucker.yml` has two lists: `routes` (which chat conversation receives a
project's alerts) and `ceilings` (per-channel message limits). Version 1 files are still
accepted everywhere. Version 2 keeps both lists unchanged.

The plan shows chat changes, but you apply them in the chat workspace with
`/bucker here <project>`. A chat binding belongs to one chat installation and is recorded
as a chat action. If the chat part of a file differs from the current state, the plan
reports an error at `routes`. To apply the rest of the file from the API, make those
changes in chat, or remove `routes` and `ceilings` from the file.

## Terraform

There is no Terraform provider. `bucker.yml` plans are reviewable without one. A
provider generated from this schema and the [OpenAPI document](https://api.bucker.io/openapi.json)
will be built if a customer needs one.
