Skip to content
This text was generated by an LLM (how cgwalters uses LLMs). cgwalters has not reviewed it yet.

Release train

agent-board release-train --config FILE [--apply] runs one pass of a release train: it cuts a release of one Rust repository and then moves the pins of the repositories that use it, one step a pass. It is tracker's .github/release-train/train.cjs (at 1a550cc) moved here, with the same decisions, except for the checks before a release, the second listing before opening a train and the wrappers' commit pins described below. A scheduled workflow in the caller's repository runs it, for example hourly.

The steps of a train, each a pass:

  1. bump: a PR on the release repository raising the workspace version in Cargo.toml and its path packages in Cargo.lock.
  2. release: once it merges into the default branch, and that branch's workspace version is still the train's, dispatch the release repository's release workflow with version. A tracking comment records the dispatch first, and only the bot's run of the workflow at the recorded commit counts as its run, so a later pass never dispatches it again; a recorded dispatch with no run after an hour stalls.
  3. wrappers: once the release is out, a PR on the wrappers repository moving its pins of the release's workflows to the commit of the release tag, checked to be on the default branch, with "NAME vX.Y.Z" beside them. A pin is never a tag, which could move after its review.
  4. callers: once that merges into the default branch, a PR on each caller moving its commit pin of the wrappers to that merge commit.
  5. done: close the tracking issue.

A pass keeps no state: it reads the stage from the one open tracking issue (an issue by the bot titled NAME vX.Y.Z release train, carrying the tracking label) and the PRs on release-train/vX.Y.Z from the bot's forks. With no open train, it follows a release cut outside the train whose version the wrappers are behind, from step 3; otherwise it opens one when a release is due: at once after a PR labelled urgent merges, or once threshold PRs have merged and intervalHours have passed since the last release. A PR labelled breaking makes a minor rather than a patch release. Before opening a train it lists the open trains again without GitHub's creator filter, and opens one only if that read also finds none of the bot's; otherwise it ends the pass without writing (tracker#882).

A pass that finds something a person must fix (a PR closed unmerged or merged into another branch, a failed release run, a reverted or superseded version bump, a pin it cannot read, a fork that is not the repository's, a release tag off the default branch) labels the tracking issue with the stalled label and says why in a comment; the train waits until the label is removed. Close the tracking issue to drop a train.

Preview and apply

Without --apply a pass only reads, and prints each decision on a line, each write it would make as would: .... Outside those changed steps the lines are the ones train.cjs printed, so the two can be diffed on the same state. --apply makes the writes with GH_TOKEN, which must be the bot's: it opens the PRs from the bot's forks, comments on and labels the tracking issue, and dispatches the release workflow. A pass makes at most 200 requests. Run one pass at a time, for example in a workflow concurrency group without cancel-in-progress: passes that overlap race on the same step, though only one of them dispatches a release.

Configuration

The file comes from the caller's protected default branch; every key is required and unknown keys are refused. An example, with placeholder names:

{
"bot": "example-bot",
"release": {"repository": "example/tool", "fork": "example-bot/tool", "workflow": "release.yml"},
"tracking": "example/tracker",
"threshold": 5,
"intervalHours": 24,
"labels": {"urgent": "P0", "breaking": "breaking", "tracking": "release-train", "stalled": "needs-human"},
"wrappers": {"repository": "example/actions", "fork": "example-bot/actions", "files": [".github/workflows/tool.yml"]},
"callers": [
{"repository": "example/app", "fork": "example-bot/app", "files": [".github/workflows/review.yml"]}
]
}

The shape is train.cjs's config.json with bot (formerly read from the poll's agent-board.json) added and pageSize, maxRequests and requestTimeoutSeconds removed: listings are one page of 100, the request bound is fixed, and every request has the client's 30-second timeout.

Known limits

These are carried over unchanged; the replay fixtures in fixtures/release-train/ record them:

  • Wrappers that pin the release by commit with a bare # vX.Y.Z comment are not read, so the wrappers step stalls (tracker#887).