# Release Batches

The dashboard shows this workflow as the **Batches** tab on the **Releases** page; the CLI and API keep the `train` commands and the `release_trains` feature flag.

When every merge cuts its own version, version numbers stop meaning anything, and nobody can say whether a group of related changes is ready. A release train fixes that. It is for release managers and engineering leads who ship several pull requests together.

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

A release train is a named group of pull requests that ship together as one version. Each pull request still merges and deploys on its own. When the whole group is ready, you cut the train, and Bumpkit makes one version, one changelog entry, one tag and one GitHub Release for all of it.

## How it works

1. You start a train with a name, for example "Enterprise".
2. Pull requests join it in one of three ways: a label (`train:<slug>` by default), a milestone, or `bumpkit train add`. Agents can join their own pull requests through the API or MCP.
3. Bumpkit works out the train's readiness from each member's [release gate](/docs/release-gate) decision for its head commit, and, if you ask for it, each member's [release health](/docs/release-health) in production.
4. A release manager approves the current members.
5. You cut the train. Bumpkit versions every commit since the last tag, writes one changelog entry carrying the train's name, and creates the tag and GitHub Release.

Cutting a train needs the [GitHub App](/docs/#install-the-github-app) on the repository, and the repository needs at least one release tag such as `v1.0.0` to version from.

## Start a train

```bash
bumpkit train new "Checkout v2"
bumpkit train new Enterprise --milestone "Q4 enterprise"
```

Bumpkit prints the label to use. Label a pull request with it and the pull request joins the train.

| Flag | Meaning |
|---|---|
| `--branch` | The branch the train ships. Default: the repository's default branch. |
| `--label` | The pull request label that joins it. Default: `train.label` in bumpkit.yml, else `train:<slug>`. |
| `--milestone` | A milestone whose pull requests join it. |
| `--repository` | `owner/name`. Default: the `origin` remote. Works on every `bumpkit train` command. |

## Add and remove pull requests

```bash
bumpkit train add "Checkout v2" 412
bumpkit train remove "Checkout v2" 409
```

`add` takes `--head SHA` when the Bumpkit GitHub App can't read the pull request. A head commit given this way is marked as asserted, and the train asks for approval instead of trusting it.

## Check readiness

```bash
bumpkit train list --status open
bumpkit train show "Checkout v2"
```

`show` lists every pull request with the latest gate decision Bumpkit made on a diff it fetched itself (through the GitHub App or a connected GitLab, Bitbucket or Azure DevOps repository; a `bumpkit gate` decision doesn't count) and its health, plus the train's overall verdict and the reasons behind it. The verdict is the worst of the reasons:

| Verdict | Meaning |
|---|---|
| `ready` | Every rule is met. You can cut the train. |
| `waiting` | Something is still in progress: a member isn't merged, has no gate decision against your default branch for its head commit yet, or the train has no members. |
| `needs_approval` | A member needs approval, a head commit was asserted rather than read from GitHub, or the train needs more release manager approvals. |
| `blocked` | A member fails the gate, the train carries more breaking changes than allowed, a member is unhealthy in production, or Bumpkit couldn't read the train policy. |

Readiness also lists what the train carries across all members: breaking changes, migrations, new environment variables and the product areas it reaches.

`bumpkit train list --status` accepts `open`, `ready`, `blocked`, `cut` or `abandoned`.

## Approve and cut

Approving and cutting need a person signed in to the dashboard. Open the batch under **Releases**, then choose **Approve batch** and **Start release**. An API key is refused: `bumpkit train approve`, `bumpkit train cut`, the MCP tools and the API answer `403 HUMAN_APPROVAL_REQUIRED` with the link to the batch page.

An approval covers the train's members as they are when you approve. If members change, earlier approvals stop counting.

Only a workspace owner or admin, or a release manager named in `train.require_approval` who approved the current members, can cut a train.

If the train isn't ready, a workspace owner or admin can still cut it by entering a reason of 10 to 500 characters on the batch page.

The reason is kept in the audit record. This is refused when the repository's bumpkit.yml sets `gate.break_glass: false`. See [Hotfixes & Break Glass](/docs/hotfixes).

## Policy

Add a `train:` section to `bumpkit.yml` on the branch the train ships:

```yaml
train:
  label: "train:{slug}"
  require_all_pass: true
  max_breaking: 0
  require_approval:
    teams: ["@acme/release-managers"]
    count: 1
  block_on_unhealthy_deploys: true
  release_workflow: release.yml
```

| Key | Default | Meaning |
|---|---|---|
| `label` | `train:{slug}` | The label that joins a pull request to a train. At most 50 characters. |
| `require_all_pass` | `true` | When true, members that need approval or have no gate decision yet hold the train. A failing member always blocks it. |
| `max_breaking` | no limit | The most breaking changes the train may carry. Must not be negative. |
| `require_approval` | none | Release manager approvals needed: `approvers`, `teams` (as `@org/team`) and `count` (up to 10; leave it out for one approval). |
| `block_on_unhealthy_deploys` | `false` | Block the train when a member is `regressed` or `degraded` in production. |
| `release_workflow` | none | A workflow file name such as `release.yml`. When set, cutting the train dispatches that workflow with the train's name as the `name` input, instead of Bumpkit creating the GitHub Release itself. |

## From an agent

The MCP server has `train_list`, `train_new`, `train_sync`, `train_status`, `join_train`, `train_remove`, `train_approve` and `train_cut`. Approving and cutting a train needs a person signed in to the dashboard: `train_approve` and `train_cut` are refused with the link to the batch page. See [MCP Server](/docs/mcp).

## API

| Method and path | What it does |
|---|---|
| `GET /v1/trains` | List trains |
| `POST /v1/trains` | Start a train |
| `GET /v1/trains/{id}` | Get a train |
| `DELETE /v1/trains/{id}` | Abandon a train (member key) |
| `POST /v1/trains/{id}/members` | Add a pull request |
| `DELETE /v1/trains/{id}/members/{number}` | Remove a pull request |
| `POST /v1/trains/{id}/sync` | Refresh members from the label and milestone (member key) |
| `GET /v1/trains/{id}/readiness` | Readiness and reasons |
| `POST /v1/trains/{id}/approve` | Always `403 HUMAN_APPROVAL_REQUIRED` for a key. A person approves in the dashboard |
| `POST /v1/trains/{id}/cut` | Always `403 HUMAN_APPROVAL_REQUIRED` for a key. A person cuts in the dashboard |

Endpoints marked "member key" need an API key that belongs to a workspace member, so Bumpkit can record who approved or cut the train. See the [API Reference](/docs/api-reference) for authentication and errors, and the [Worked Examples](/docs/examples) for a train from start to cut.

## Release across repositories

> **Beta, early access.** A workspace without access gets `403 FEATURE_EARLY_ACCESS` and a link to request it. Flag: `multi_repo_trains`. It also needs `release_trains` and `cross_repo_impact`.

When a change spans a library and the services that use it, the order matters: the library ships first. A multi-repo train ships one release across repositories in dependency order, and tells you why each step comes where it does.

1. Start the train on the **Batches** tab of the **Releases** page, or from the CLI. Name it and pick at least two repositories, up to 20.
2. Each repository gets its own batch with the same name and label. Label pull requests in each repository to join its step, as with any batch. These batches show on the train's page, not in the list of single-repository batches, and `bumpkit train list` still lists them.
3. Bumpkit reads the order from your [service graph](/docs/cross-repo#the-service-graph). A repository ships after whatever it depends on, including through a repository that isn't on the train.
4. A person approves and starts each step in the dashboard, in order.

```bash
bumpkit train multi new "October release" --repo acme/lib --repo acme/api
bumpkit train multi show "October release"
bumpkit train multi sync "October release"
```

### The order and why

Every step says why it sits where it does, with the file and line behind each edge, like `After acme/lib: it depends on lib ^1.0.0 (package.json:4).` Repositories with no known edge between them, on a train the graph otherwise connects, share a step, and either can go first.

Bumpkit doesn't guess. No step can start, and the page asks a person to set the order, when:

| What the train shows | What it means |
|---|---|
| `not_mapped` | A repository isn't on the service map, so Bumpkit can't see what it depends on. |
| `truncated` | A repository has more files than Bumpkit could read for the graph, so its dependencies may be incomplete. |
| `cycle` | These repositories depend on each other, so no order is safe to prove. |
| `no_edge` | No dependency connects these repositories, so Bumpkit can't say which goes first. |
| `order_conflict` | The service map changed and now disagrees with the saved order. |

Setting the order needs a person signed in to the dashboard, with write access. Their email goes on the order and in the audit log. Bumpkit refuses an order that contradicts an edge it can prove, except inside a cycle, and steps that already started stay ahead of the rest. `bumpkit train multi order` with an API key gets `403 HUMAN_APPROVAL_REQUIRED` and the link to the train's page.

### Each step's state

Each step has its own gate result, from its batch's [readiness](#check-readiness). A step that isn't released holds every later step, and Bumpkit never skips one. That holds on the batch's own page too: cutting a held step answers `409 MULTI_TRAIN_HELD`. An owner's break-glass reason covers a step that isn't ready, never one that's out of turn.

| State | What it means |
|---|---|
| `needs_order` | A person has to set the order first. |
| `held` | An earlier step hasn't released. The step still shows its own gate result. |
| `waiting`, `needs_approval`, `blocked` | This step's turn, and its gate result is what holds it. |
| `ready` | This step can start. |
| `publishing` | The release started and hasn't finished. |
| `failed` | The release didn't finish. The step says why, with its number of attempts. |
| `released` | The tag and GitHub Release exist, or the release workflow was requested. |
| `abandoned` | The step's batch was abandoned, so later steps stay held. Abandon the train and start a new one. |
| `unreadable` | Bumpkit couldn't read this step's gate results. Try again in a minute. |

A partial failure stays visible: the train says how many repositories released and which one stopped. Retry a failed step from the train's page; Bumpkit picks up where it stopped, and doesn't touch the steps already out. Syncing tries every repository, and one that fails shows its own error without stopping the others. Abandoning a train leaves releases that are already out alone.

Agents can start and read a train, but approving, cutting, retrying and setting the order all need a person in the dashboard. Each step's approvals follow that repository's `train.require_approval`.

### The service map

The **Batches** tab shows your services and the edges between them, providers on the left. Each service shows its health from its latest recorded [deployment](/docs/release-health) and its last published release. A service with no deployment says so; record deploys with `bumpkit deploy` to see its health. Pick a service to open its version history. Tab through the map with a keyboard, and on a phone it becomes a list with the same data.

```bash
bumpkit train map
```

## Grouped releases

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

A grouped release is everything merged to the default branch since the last release tag, shipped as one version. You don't pick pull requests: Bumpkit takes the whole range.

1. **Prepare.** Run `bumpkit release prepare`, choose **Prepare release** on the dashboard's Releases page, or type `/bumpkit prepare owner/repo` in Slack. Bumpkit finds the last `vX.Y.Z` tag (or the first commit when there's none), lists the merged pull requests and commits up to the branch head, runs one check over the range, suggests the next version and writes the notes. There's one open draft per repository; preparing again refreshes it.

   With **Draft a release after every merge** on (the default), you don't have to ask. Every time a pull request merges to the default branch, Bumpkit refreshes the draft and posts or updates its Slack card, so the next release is always ready to review. Owners turn it off on the Releases page or with `bumpkit settings set --draft-after-merge off`; then a release is drafted only when someone prepares one.
2. **Review the notes.** Bumpkit writes the notes once per audience from the same changelog and proven findings as [notes per audience](/docs/release-notes#notes-per-audience). Internal-only work, like internal tooling, tests and test infrastructure, docs-internal changes and redirects, drafts, unreleased or not-yet-public work, detection internals and internal process, goes to the `Internal` section, which the customer notes leave out. So customer notes only list what customers can use. Read any audience with `bumpkit release show RELEASE_ID --audience customer` (or `engineering`, `support`, `security`, `leadership`, or one you defined), or pick its tab on the release page. To change them, edit the box on that tab, run `bumpkit release notes RELEASE_ID --audience customer --file notes.md` (or pipe the notes on stdin), or regenerate them with `--regenerate`. Agents use `show_release` with `audience` and `edit_release_notes`. Anyone who can prepare releases can edit a draft; once a release is approved, only owners, admins, policy admins and approvers can, and every change goes in the audit log. A draft or approved release from before notes per audience gets them written the first time you open it or publish it.
3. **Approve.** A person approves or denies the draft on the dashboard, in Slack, or with `bumpkit release approve RELEASE_ID` (or `deny`), which asks you to confirm in the browser. API keys and MCP tools can't vote: `POST /v1/releases/{id}/approve` answers `403 HUMAN_APPROVAL_REQUIRED`. A vote is tied to the head it was prepared at, so a refreshed draft needs a new review.
4. **Publish.** After approval, publish from the dashboard, Slack or `bumpkit release publish RELEASE_ID`. Bumpkit creates the tag and GitHub Release through the GitHub App and updates the public changelog with the customer notes, posts the engineering notes to Slack, and updates any connected Notion or Confluence page with the engineering notes. Owners change which audience a target gets on the Releases page, with `bumpkit settings set --publish-audience slack=support`, the `settings_set` MCP tool (`publish_audiences`) or `PATCH /v1/settings`. Each target is recorded on its own: if one fails, publish again and only the failed ones are retried.

   Merging never publishes anything: a release ships only when a person publishes it. Because the GitHub App creates the release, GitHub workflows that run `on: release: types: [published]` start right after you publish, so they can build your artifacts and attach them to that same release. Leave the notes alone; Bumpkit already wrote them.

Published releases make up the version history on the Releases page and in `bumpkit release list --status published`. If published notes need a fix, owners and admins can [correct them](/docs/release-notes#correct-published-notes) everywhere they were published with `bumpkit release correct`. Agents can prepare and read releases with the `prepare_release`, `show_release` and `list_releases` MCP tools and edit draft notes with `edit_release_notes`, but can't approve or publish them.
