Releasing
How a release is cut, what it publishes, and what to do when it fails partway through.
The normal path
From an up-to-date
main,scripts/release.sh <level>previews the version bump and the changelog. With--executeit commits them onrelease/vX.Y.Z, pushes, and opens an issue and a pull request. Before merging it, remove the docs' "Unreleased" notes on that branch (CLAUDE.md): the release ships everything they describe. This finds them, and the script lists them in the pull request:shgrep -rn 'Unreleased\.' README.md docs/*.md site/index.htmlMerging that pull request (with CI green; branch protection on
mainis what guarantees this, sincetag-release.ymldoes not check CI itself) runstag-release.yml. It sees a workspace version that has aCHANGELOG.mdsection and no tag, tags the merge commitvX.Y.Z, and startsrelease.ymlon the tag.release.ymlbuilds the five targets, checks that each binary reports the version (x86_64 macOS under Rosetta), packages them withTHIRD-PARTY-NOTICESand SHA-256 checksums, attests their build provenance, and publishes the GitHub Release with the changelog section as notes. It then redeploys the site frommain.
Then check the result:
gh release view vX.Y.Z --repo tlockney/ganymede
gh release download vX.Y.Z --repo tlockney/ganymede -p '*x86_64-unknown-linux-gnu.tar.gz'
gh attestation verify ganymede-X.Y.Z-x86_64-unknown-linux-gnu.tar.gz --repo tlockney/ganymede
curl -fsSL https://ganymede-server.com/install.sh | sh -s -- --version X.Y.Z --no-kernelWhen it fails
Find the failed release.yml run under the tag in the Actions tab. The tag already exists, so pushing to main or rerunning tag-release.yml does nothing ("vX.Y.Z exists; nothing to do"). Recover from the release workflow itself, depending on how far it got.
A job failed and nothing was published
A flaky runner, a network error, or a download that timed out: the code is fine. Rerun the failed jobs from the run's page, or start a fresh run on the tag:
gh run rerun <run-id> --failed --repo tlockney/ganymede
# or
gh workflow run release.yml --ref vX.Y.Z --repo tlockney/ganymedeThe tagged commit cannot be released
A build or version check fails for a reason that needs a code change. If no GitHub Release exists for the tag, nobody can have installed it, so move the tag:
- Delete the tag:
git push origin :refs/tags/vX.Y.Z. - Fix the problem in a pull request to
main, as usual. - Retag the new head of
mainand start the release:gh workflow run tag-release.yml --ref main --repo tlockney/ganymede.
The changelog section stays as scripts/release.sh wrote it; the fix appears in the next release's notes.
The release was published, but is wrong or incomplete
Once a release is public, someone may have installed it, and install.sh installs the latest by default. Don't replace its assets or move its tag. Fix the problem on main and cut a patch release with scripts/release.sh patch. If the broken release must not be installed, mark it a prerelease, so releases/latest (what install.sh picks) moves back to the one before:
gh release edit vX.Y.Z --prerelease --repo tlockney/ganymedePublishing failed after the release was created
If the release job created the GitHub Release and then failed to upload part of it, a rerun fails because the release exists. Within minutes of the failure, while it is plainly incomplete, delete the release but keep the tag, then rerun the job:
gh release delete vX.Y.Z --yes --repo tlockney/ganymede # no --cleanup-tag
gh run rerun <run-id> --failed --repo tlockney/ganymedeA rerun of only the failed job reuses the archives the build jobs already uploaded, so it attests the same files again, which is harmless. A fresh run rebuilds them, and the new archives get their own attestations.