# Org Policies & Freezes

Each repository setting its own rules leaves gaps an auditor will find; org policies set one floor for the whole workspace, for platform and security teams.

> **Beta, early access.** A workspace without access gets `403 FEATURE_EARLY_ACCESS` and a link to request it.

An org policy is one gate policy for your whole workspace. The [Release Gate](/docs/release-gate) merges it with each repository's `bumpkit.yml`. You can lock keys so a repository can only make them stricter. Change freezes and release windows stop releases at set times, and the version consistency check catches manifests and tags that disagree with the version being released.

This page covers four features. Three of them are behind their own flag:

| Feature | Flag |
|---------|------|
| Org-wide policies | `org_policies` |
| Release windows and change freezes | `change_freezes` |
| Version consistency | `version_consistency` |
| Replay-safe checks | none |

## Org-wide policies

> **Beta, early access.** A workspace without access gets `403 FEATURE_EARLY_ACCESS` and a link to request it. Flag: `org_policies`.

An org policy is written exactly like a `bumpkit.yml`, with one extra top-level key: `locked`, a list of gate keys that repositories may only tighten.

```yaml
locked:
  - block_on
  - require_checks
  - on_error
gate:
  block_on: [secret, data_loss, breaking]
  require_checks: [ci/test]
  on_error: fail
  security_alerts: high
```

You can lock any of these keys:

`block_on`, `require_approval`, `on_error`, `require_checks`, `require_check_apps`, `security_alerts`, `vulnerabilities`, `break_glass`, `hotfix`, `blast_radius`, `deploy`, `cross_repo`, `timezone`, `windows`, `outside_windows`, `freezes`, `quality`, `exports`, `behavior`, `fix_tasks`, `stacks`

An unknown key in `locked` is rejected when you save the policy. For the meaning of each key, see the [bumpkit.yml Reference](/docs/bumpkit-yml).

### Where the org policy lives

You store the org policy in one of two ways:

- **In Bumpkit.** You upload the policy text. Every change is saved as a new numbered version with a message, a diff and an audit record. The policy must be under 64 KB.
- **In a config repository.** You point Bumpkit at a repository, such as `acme/.github`. The gate reads that repository's `bumpkit.yml` from its default branch every time it runs. The Bumpkit GitHub App must be installed on it, and the installation must belong to the same workspace.

If Bumpkit can't read or parse the org policy, the gate fails closed. The API returns `503` with code `ORG_POLICY_UNAVAILABLE`, and the GitHub check run fails with "Couldn't read the org policy".

When the `org_policies` flag is off, the gate ignores any stored org policy and uses only the repository's `bumpkit.yml`.

### How the org policy and bumpkit.yml combine

For each key:

- If the repository's `bumpkit.yml` doesn't set the key, the org policy's value applies.
- If the key isn't locked, the repository's value replaces the org value.
- If the key is locked, the stricter of the two applies. A value that would loosen the policy is ignored, and the gate decision says why with a reason named `org_policy: <key> is locked`.

How "stricter" works for each locked key:

| Key | Result when locked |
|-----|--------------------|
| `block_on` | Union of both lists. The repository can add kinds, not remove them. |
| `require_checks` | Union of both lists. |
| `require_check_apps` | The org's issuers always apply. The repository can add checks with their own Apps, but can't drop a check or point it at another App; the decision says "bumpkit.yml can't remove or replace trusted check issuers; the org policy keeps them." |
| `require_approval` | Org rules plus any extra rules from the repository. |
| `on_error` | If the org sets `fail`, it stays `fail`. |
| `security_alerts`, `vulnerabilities` | The repository can lower the severity threshold, not raise it. |
| `break_glass` | If the org disallows overrides, the repository can't allow them. |
| `hotfix` | The stricter of each setting: union of `block_on`, the smaller `max_files`, the shorter `review_within`, the stricter `backports`. |
| `blast_radius` | Union of `approve_areas`. The org's `max_depth` always applies; a different value in `bumpkit.yml` is ignored with the note "bumpkit.yml can't loosen blast_radius.max_depth; the org policy's 2 applies." |
| `timezone` | Must match the org's. A different value is ignored. |
| `windows` | The repository can't change the org's release windows. Add `freezes` to narrow them. If the org sets no windows, the repository's apply. |
| `outside_windows` | If the org sets `block`, it stays `block`. |
| `freezes` | Org freezes plus any extra freezes from the repository. |
| `fix_tasks` | The repository can only tighten it: `off` < `approve` < `block`. Unset counts as `block`. |
| `deploy`, `cross_repo`, `quality`, `exports`, `behavior` | Must match the org's exactly. A different value is ignored. |

### See the effective policy

Run this in a repository to see the policy the gate applies there and where each value came from:

```bash
bumpkit policy show --effective
```

```text
Org policy v4, merged with this repository's bumpkit.yml
KEY              VALUE                        FROM
block_on         - secret - data_loss - breaking - version_mismatch  org+repo (locked)
require_checks   - ci/test                    org (locked)
on_error         fail                         org (locked)
security_alerts  high                         repo
! bumpkit.yml can't remove breaking from block_on; the org policy keeps them.
```

The `FROM` column is one of `default`, `org`, `repo`, `org+repo` or `not set`. The command reads `bumpkit.yml` as it is at `--base` (default `HEAD`), the same way the gate reads it at a pull request's base. Use `--file PATH` to test an unsaved file, and `--repository owner/name` to override the repository detected from the `origin` remote.

The same merge is available from the API:

```bash
curl -X POST https://bumpkit.dev/v1/policy/effective \
  -H "x-api-key: $BUMPKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"repository": "acme/api", "policy": "gate:\n  on_error: pass\n"}'
```

```json
{
  "org_version": 4,
  "policy": "gate:\n  on_error: fail\n",
  "sources": [
    {"key": "on_error", "value": "fail", "from": "org", "locked": true, "note": "bumpkit.yml can't loosen on_error; the org policy's fail applies."}
  ]
}
```

`org_repository` is also set when the org policy is read from a config repository.

### Change the org policy

Only workspace owners and admins can change the org policy. Anyone in the workspace can read it.

```bash
bumpkit policy set org-policy.yml --message "Require CI everywhere"
bumpkit policy set --repository acme/.github
bumpkit policy set --clear
```

`bumpkit policy set` validates the file, shows the diff against the current version and asks you to confirm. Pass `-y` to skip the prompt. Pass exactly one of a file, `--content YAML`, `--repository` or `--clear`. Clearing saves a new, empty version, so the history is kept.

To review changes:

```bash
bumpkit policy show
bumpkit policy history --limit 50
bumpkit policy diff v3 v4
```

`bumpkit policy diff FROM [TO]` compares two versions. `TO` defaults to the latest version.

You can also edit the org policy on the [Policy page](https://dashboard.bumpkit.dev/policy) of the dashboard, under the **Policy and freezes** tab.

Every gate decision made under an org policy records the version it used. The `POST /v1/gate` response includes it as `org_policy_version`.

## Release windows and change freezes

> **Beta, early access.** A workspace without access gets `403 FEATURE_EARLY_ACCESS` and a link to request it. Flag: `change_freezes`.

Release windows say when releases may ship. Change freezes say when they may not. You declare both under `gate:` in `bumpkit.yml` or in the org policy, and you can declare a freeze on demand from the CLI, the API or the dashboard.

### Release windows

```yaml
gate:
  timezone: America/New_York
  windows:
    - days: [mon-thu]
      from: "09:00"
      to: "17:00"
  outside_windows: approve
```

| Key | Description |
|-----|-------------|
| `timezone` | IANA timezone name for windows and freezes that don't set their own. Defaults to UTC. |
| `windows[].days` | Days such as `mon`, `tue`, or a range such as `mon-fri`. Full names like `monday` also work. Omit for every day. |
| `windows[].from`, `windows[].to` | Times like `09:00`. `from` defaults to `00:00`, `to` to `24:00`. |
| `windows[].timezone` | Overrides `timezone` for this window. |
| `outside_windows` | `approve` (the default) asks for an approval outside the windows. `block` fails the gate. |

Hotfixes may always ship outside the windows. See [Hotfixes & Break Glass](/docs/hotfixes).

### Scheduled freezes

Each freeze needs a `name` and one of three shapes: a date range, a list of dates, or a weekly schedule.

```yaml
gate:
  timezone: America/New_York
  freezes:
    - name: Holiday freeze
      start: 2026-12-20
      end: 2027-01-02
    - name: Black Friday
      dates: [2026-11-27, 2026-11-30]
      mode: approve
    - name: Friday afternoons
      days: [fri]
      from: "15:00"
      hotfix: allow
```

| Key | Description |
|-----|-------------|
| `name` | Required. Shown on every gated pull request. |
| `start`, `end` | A date range, both required. Dates like `2026-12-24` or times like `2026-12-24T18:00`. A date-only `end` includes that whole day. |
| `dates` | Individual days that are frozen all day. |
| `days`, `from`, `to` | A weekly freeze, written like a window. |
| `timezone` | Overrides the policy's `timezone`. |
| `mode` | `block` (the default) fails the gate. `approve` asks for an approval. |
| `hotfix` | What hotfixes get: `allow`, `approve` (the default) or `block`. |

For the full schema, see the [bumpkit.yml Reference](/docs/bumpkit-yml#freezes).

### What the gate does during a freeze

| Situation | Outcome |
|-----------|---------|
| Freeze with `mode: block` | Fails. Only a break-glass override lets the commit through. |
| Freeze with `mode: approve` | Needs an approval. Passes once approved. |
| Hotfix, freeze `hotfix: allow` | Passes. |
| Hotfix, freeze `hotfix: approve` | Needs an approval. |
| Hotfix, freeze `hotfix: block` | Fails. Only a break-glass override lets it through. |
| Outside every window, `outside_windows: approve` | Needs an approval. |
| Outside every window, `outside_windows: block` | Fails. |
| Hotfix outside every window | Passes. |

A [break-glass override](/docs/hotfixes) still works during a freeze when the repository allows overrides.

### Declare a freeze on demand

Declare a freeze during an incident or over the holidays:

```bash
bumpkit freeze start --reason "Incident 42: payments degraded" --for 4h
bumpkit freeze start --reason "Year-end holiday freeze" --starts 2026-12-20T00:00:00Z --until 2027-01-04T00:00:00Z --mode approve
bumpkit freeze start --reason "Database migration window" --repository acme/api --starts 2027-01-12T06:00:00Z --for 2h
```

| Flag | Default | Description |
|------|---------|-------------|
| `--reason` | required | Why releases are frozen, 10 to 500 characters. Shown on gated pull requests and kept in the audit log. |
| `--repository` | every repository | `owner/name` to freeze. |
| `--mode` | `block` | `block`, or `approve` to ask for an approval instead. |
| `--hotfix` | `approve` | What hotfixes get: `allow`, `approve` or `block`. |
| `--starts` | now | When it starts, RFC 3339. |
| `--until` | until ended | When it ends, RFC 3339. |
| `--for` | none | How long it lasts, such as `4h`. Use `--until` or `--for`, not both. |

List and end freezes:

```bash
bumpkit freeze list
bumpkit freeze end 3f2b9c1e-7a4d-4e0b-9a51-2d6c8e1f0a77
```

`bumpkit freeze list` shows each freeze as `active`, `upcoming` or `over`. Owners, admins and policy admins can declare one. Ending it early takes one of them signed in to the dashboard, so an API key, the CLI or an agent can't lift a freeze. Anyone else gets `403` with "Only owners, admins and policy admins can declare or end a change freeze." Declaring and ending freezes are both audited, and so are refused attempts (`freeze.declare.denied`, `freeze.end.denied`).

You can also declare and end freezes on the [Policy page](https://dashboard.bumpkit.dev/policy) of the dashboard, under the **Policy and freezes** tab. The page shows those controls only to the roles that can use them.

## Version consistency

> **Beta, early access.** A workspace without access gets `403 FEATURE_EARLY_ACCESS` and a link to request it. Flag: `version_consistency`.

When the flag is on, every check compares the version Bumpkit decides for the release with the versions declared in your repository and its tags. Each mismatch is a `version_mismatch` finding with the file and line.

Bumpkit reads these files at the root of the repository:

| File | What it checks |
|------|----------------|
| `package.json` | The top-level `version`. |
| `pyproject.toml` | `version` under `[project]` or `[tool.poetry]`. |
| `Cargo.toml` | `version` under `[package]`. |
| `VERSION` | The first non-empty line. |
| `go.mod` | The module path has the `/vN` suffix that major version 2 and up need, and no suffix for v0 and v1. |
| `CHANGELOG.md` | The newest `#`, `##` or `###` version heading. |

A manifest may still carry the previous version until the release bumps it. Bumpkit reports:

- A manifest that names neither the release's version nor the previous one.
- A manifest left behind when another one was already bumped, so the published packages would disagree.
- A tag that already exists for this version, or a newer tag than this release.
- A `go.mod` module path with the wrong major version suffix.
- A `CHANGELOG.md` whose newest heading is neither this release nor the previous one.

The CLI sends the manifests and tags from your local checkout. For a check against a GitHub repository, Bumpkit reads them from GitHub at the head commit.

`version_mismatch` findings are high severity, so they show on every check. To fail the gate on them, add the kind to `block_on`:

```yaml
gate:
  block_on: [secret, data_loss, version_mismatch]
```

See [block_on](/docs/bumpkit-yml#block-on).

## Replay-safe checks

> **Beta.** Replay-safe requests are on for every workspace, with no flag to enable.

`POST /v1/checks` and `POST /v1/gate` are replay-safe. If a CI job retries after a timeout, or the same request arrives twice, you get the first decision back instead of a second, possibly different, one.

A request is replayed in two cases:

- **Pinned requests.** A request that sends a `diff`, or sends `base` and `head` as full 40-character commit SHAs, is identified by its content. Sending the same request again returns the stored result, as long as the workspace's enabled features are unchanged. For the gate, the approvals, the org policy, and any active freezes must also be unchanged. No header is needed.
- **Idempotency keys.** Any request with an `Idempotency-Key` header is identified by that key. The key is 1 to 200 letters, digits, dots, colons, dashes or underscores.

```bash
curl -X POST https://bumpkit.dev/v1/gate \
  -H "x-api-key: $BUMPKIT_API_KEY" \
  -H "Idempotency-Key: ci-run-8812-attempt" \
  -H "Content-Type: application/json" \
  -d @gate-request.json
```

| Situation | Response |
|-----------|----------|
| Same request again | `200` with the stored result and `"replayed": true`. |
| Same key, different request body | `422` with code `IDEMPOTENCY_KEY_REUSED`. |
| Same request still running | Waits for it, up to 2 minutes, then `409` with code `REQUEST_IN_PROGRESS`. |
| Invalid key | `400` with code `INVALID_IDEMPOTENCY_KEY`. |

Replayed responses don't count toward your rate limit. Bumpkit keeps stored results for one day.

The CLI sends `bumpkit check` and `bumpkit gate` up to three times in all, retrying on `502`, `503`, `504` and `REQUEST_IN_PROGRESS`, with the same key on every attempt. It generates a key for each run. To reuse a key across CI job retries, set it yourself:

```bash
export BUMPKIT_IDEMPOTENCY_KEY="$GITHUB_RUN_ID-$GITHUB_JOB"
bumpkit gate
```

## API endpoints

All endpoints take the `x-api-key` header. While a feature is in early access and your workspace isn't on it yet, its endpoints return `403` with code `FEATURE_EARLY_ACCESS`; when the feature is off, they return `404`.

| Method | Path | Who | Description |
|--------|------|-----|-------------|
| `GET` | `/v1/policy/org` | any role | The current org policy. `404` if there is none. |
| `PUT` | `/v1/policy/org` | owners, admins | Save a new version. Send one of `content`, `repository` or `clear`, plus an optional `message`. Returns the new version and its `diff`. |
| `GET` | `/v1/policy/org/versions` | any role | Version history. `?limit=` from 1 to 200, default 50. |
| `GET` | `/v1/policy/org/versions/{version}` | any role | One version. |
| `POST` | `/v1/policy/effective` | any key | Merge the org policy with a `bumpkit.yml`. Send `repository` and `policy`. |
| `GET` | `/v1/freezes` | any role | Current, upcoming and recent declared freezes. |
| `POST` | `/v1/freezes` | owners, admins, policy admins | Declare a freeze. Send `reason`, and optionally `repository`, `mode`, `hotfix`, `starts_at` and `ends_at` (RFC 3339). Returns `201`. |
| `POST` | `/v1/freezes/{id}/end` | owners, admins, policy admins | End a freeze early. |

A freeze object looks like this:

```json
{
  "id": "3f2b9c1e-7a4d-4e0b-9a51-2d6c8e1f0a77",
  "repository": "acme/api",
  "reason": "Incident 42: payments degraded",
  "mode": "block",
  "hotfix": "approve",
  "starts_at": "2026-10-22T14:00:00Z",
  "ends_at": "2026-10-22T18:00:00Z",
  "actor_email": "oncall@acme.dev",
  "active": true,
  "created_at": "2026-10-22T14:00:00Z"
}
```

`repository` is omitted for a workspace-wide freeze. `ended_at` and `ended_by` are set once a freeze is ended early.

Agents use the MCP tools `policy_show`, `policy_history`, `policy_diff`, `policy_set`, `freeze_list`, `freeze_start` and `freeze_end`. For the full command and endpoint references, see the [CLI Reference](/docs/cli) and the [API Reference](/docs/api-reference).
