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:
- bump: a PR on the release repository raising the workspace version in
Cargo.tomland its path packages inCargo.lock. - 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. - 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. - callers: once that merges into the default branch, a PR on each caller moving its commit pin of the wrappers to that merge commit.
- 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.Zcomment are not read, so the wrappers step stalls (tracker#887).