Cut a release
A release is one tag, vX., that releases every binary at the
repository’s one version. You choose the version in a release pull request
and merge it. release-plz then creates the tag and starts the
release workflow
on it, which builds and publishes everything. The versions
page and docs/versioning. explain what a version means and how bumps
are proposed.
Set up once: let workflows open the release PR
Section titled “Set up once: let workflows open the release PR”There is no token or secret to create: release-plz runs on the workflow’s
own GITHUB_TOKEN, and starts the release and the release PR’s CI by
dispatching them (docs/versioning. says why that works). The one
switch it needs is a repository setting, off by default:
Settings > Actions > General > Workflow permissions > Allow GitHub
Actions to create and approve pull requests. The same thing from a
terminal (an admin’s gh):
gh api -X PUT repos/scoot-sh/scoot/actions/permissions/workflow \ -f default_workflow_permissions=read -F can_approve_pull_request_reviews=trueWhile it is off, the “Open or update the release PR” job fails with GitHub’s “not permitted to create or approve pull requests” error. Tagging a merged release PR does not need it.
Cut the first release (v0.2.0) by hand (historical)
Section titled “Cut the first release (v0.2.0) by hand (historical)”The first tag was v0.2.0, never a 0.1.x: FlakeHub already published
rolling 0. versions, and a 0.1 tag would sort below them. The
workspace read 0. at the time. release-plz waited for this first
tag, so it was cut by hand, once. Both v0. and v0. exist now;
this section is the record for the next project that bootstraps
release-plz.
First, a small PR that adds ## [0. to CHANGELOG.,
dated the day you tag, directly under the Unreleased heading, so
everything written so far becomes v0.2.0’s notes. Merge it, then tag
that merge commit on an up-to-date main and push the tag:
git switch main && git pull --ff-onlyscripts/workspace-version # must print 0.2.0git tag -a v0.2.0 -m "scoot v0.2.0"scripts/release-resolve v0.2.0 --no-github # the workflow's own gate, locallygit push origin v0.2.0release-resolve refuses a tag whose version is not the workspace
version, or that is not on main. The push starts the release workflow;
watch its first run. The next sections say what it does and how to
recover.
Every release after that
Section titled “Every release after that”On every push to main, release-plz keeps one PR open titled
chore: release vX.. It bumps [workspace. and
writes the version’s section in CHANGELOG.: a list generated from
the commits since the last tag, grouped by component, above the hand
notes written under Unreleased.
- Read the PR. The proposed version follows the commits (below 1.0: a
breaking change bumps the minor, anything else the patch). To release
a different version, edit
versionunder[workspace.inpackage] Cargo.and the heading intoml CHANGELOG.on the PR branch, then re-runmd scripts/sync-packaging-versionsthere so the distro packaging follows your edit. - Check the two sync commits release-plz pushed to the PR
(the fuzz lockfiles, then the distro packaging:
pkgverin the five Arch release PKGBUILDs with their.,SRCINFO Versionplus a%changelogentry in the five RPM specs). The debs need no bump: they read the workspace version at build time. If a sync commit is missing or wrong, run the script by hand on the PR branch. - Tidy the changelog section if it needs it, and let CI go green:
ci.,yml packaging.,yml release.’s lint job and the distro packaging gate (yml deb.,yml rpm.,yml arch.) were all dispatched on the PR branch, and the three packaging runs must be green before merging. If a dispatch failed, run it yourself on the PR’syml release-plz-…branch (gh workflow run ci., likewiseyml --ref <branch> packaging.,yml deb.,yml rpm.,yml arch., andyml release.with noyml tagfor its lint job), or approve the held runs. - Merge. release-plz pushes
vX., and the release workflow builds and publishes it, then its verify job downloads the published assets and runs every binary.Y. Z
What the workflow does with the tag
Section titled “What the workflow does with the tag”The run resolves the tag (scripts/release-resolve: the vX. shape,
the workspace version at the tagged commit, every crate inheriting it,
on main; a release already complete is a green no-op), stages the vendored tarball and the
SBOM once (the union of every shipped binary’s locked dependencies),
builds scoot, scootbg, scootbar, scootlaunch and scootnotify as portable glibc binaries for
x86_64-linux and aarch64-linux (cargo --locked --offline from
that staged tree in a Debian 12 container with Rust 1.97.0: glibc floor
2.36), checks that every binary starts on the bare base, assembles
SHA256SUMS, attests every file with keyless build provenance (verify
with the --signer-workflow pin on the releases page),
takes the version’s CHANGELOG. section as the release notes
(scripts/release-notes), and publishes the GitHub Release. scootctl
is the client library inside scoot msg and ships no binary of its own.
Then the verify job downloads the release’s own assets and proves them:
every file against SHA256SUMS and its attestation, and each binary on
bare Ubuntu and Debian containers (--version names the release, the
bar’s, the launcher’s and the notification daemon’s --check, the
compositor’s default-config render).
Dry-run a tag
Section titled “Dry-run a tag”Dispatch the workflow by hand with dry-run on (the default) against a tag that already exists on main. It runs resolve, vendor, both arch builds, the SBOM, the checksums and the notes, then prints what it would publish and stops, with no release and no attestation. It does upload the run’s own build artifacts, kept 7 days, which is how the build and publish jobs hand files over:
gh workflow run release --ref main -f tag=v0.2.0 -f dry_run=truedry_run=false publishes for real, and only on the tag itself
(--ref v0.); from a branch it is refused. That is the escape hatch
for re-cutting after a deleted release, and the call release-plz makes
after it creates a tag:
gh workflow run release --ref v0.2.0 -f tag=v0.2.0 -f dry_run=falseA dry run exercises the whole artifact round trip (vendor, build, download, assemble, checksums) but not the attestation or the release creation. If a real run fails after the build, use the recovery steps below before anyone downloads anything.
Verify an existing release without publishing
Section titled “Verify an existing release without publishing”Dispatch the workflow with verify_only against a tag that already has
a release. It runs only resolve plus the verify job, from any ref: it
downloads the tag’s assets, checks every file against SHA256SUMS and
its attestation, and runs each binary on bare Ubuntu and Debian
containers for both architectures. It builds and publishes nothing:
gh workflow run release --ref main -f tag=v0.4.0 -f verify_only=trueUse it to test the verify job itself, and to re-prove an old release after a workflow change.
When a run fails
Section titled “When a run fails”Symptom: the workflow went red on a tag push.
Re-run the failed run (the run’s page, “Re-run jobs”). The gates resume an incomplete release (a draft counts as incomplete), re-uploading only what is missing, and a run that finds the release complete stops green with “Already released”. So a retry converges instead of duplicating, and a tag that both you and release-plz started releases once.
Symptom: the published release is wrong (bad notes, wrong files) and a re-run only says “Already released”.
Delete the release and its tag, then re-cut from the same commit:
TAG=v0.2.0gh release delete "$TAG" --yesgit push origin ":refs/tags/$TAG"Local tags are untouched by either command; drop yours too if you
made one (git tag -d "$TAG").
Symptom: a release PR merged but no release appeared.
Look at the release-plz workflow’s “Tag a merged release PR” job. If it
created the tag but the dispatch step failed, run the dispatch yourself
(the gh workflow run release --ref vX. line above). If it made no
tag, re-run the job, or push the tag by hand as for the first release:
either way the release runs once.
Symptom: the release PR exists but CI never ran on it.
Its pull_request runs wait for “Approve workflows to run”, because
GITHUB_TOKEN opened it. The dispatched runs (ci., packaging.,
release.’s lint job, and the deb./rpm./arch.
packaging gate) are the real checks. If the “Dispatch the
release PR’s checks” step failed, run them yourself on the PR’s
release-plz-… branch (gh workflow run ci., likewise
packaging., deb., rpm., arch., and release.
with no tag), or approve the held runs.
Downstream bumps (try-scoot and scoot-iso)
Section titled “Downstream bumps (try-scoot and scoot-iso)”Two repos pin scoot by release tag: try-scoot and scoot-iso. After a release is published, bump each by running its own workflow:
gh workflow run bump-scoot.yml -R scoot-sh/scoot-iso -f tag=vX.Y.Zgh workflow run bump-scoot.yml -R scoot-sh/try-scoot -f tag=vX.Y.ZEach run takes scripts/bump-downstream from that tag of this repo,
moves the pin (the flake input and lock, and for the ISO the installer
target flake and the patch-consistency constants), pushes
ci/bump-scoot-<tag> and opens or updates one PR. It never merges. It
needs no secret: it uses the repo’s own GITHUB_TOKEN.
A PR opened with GITHUB_TOKEN starts no pull_request run, so the
workflow dispatches ci. on the branch itself. Those checks attach to
the PR’s head commit but gh pr checks does not list them. Read them with
gh api repos/scoot-sh/<repo>/commits/<head-sha>/check-runs. In
try-scoot that run skips the push jobs, which only publish PR-tagged
images. Review the lock diff, then merge once the checks are green.
What else the runs need
Section titled “What else the runs need”Release creation and provenance run on GITHUB_TOKEN: there is no
signing key in secrets and no environment to approve. The runs need
only what this repo already uses elsewhere: GitHub-hosted runners with
Docker (the arch builders), and third-party actions allowed (every
action is pinned by commit SHA). A human bumps two pins over time: the
distro base (debian:bookworm-slim, the glibc floor) and the rustup
toolchain (1. in .; the portable
binaries are built with Rust 1.97.0). The toolchain must stay new enough
to compile the tree (the workspace floor predates the let-chains in it),
and the workflow asserts the toolchain and the glibc it produces on
every run. Verify downloads with gh attestation verify <file> --owner scoot-sh --signer-workflow scoot-sh/scoot/. (see the
releases page for why the signer pin matters).
