Releasing
How a SatBooth release is made. Operators need only deploy/README.md (“Release images”), SECURITY.md (“Release
signing key”) and the threat model’s §6; this page is the maintainer’s procedure. A release is three container images
(satbooth, satbooth-relay and satbooth-tools, each for linux/amd64 and linux/arm64) published to GHCR, a
signed annotated tag and a GitHub release. The tag is v<major>.<minor>.<patch>, or
v<major>.<minor>.<patch>-rc.<n> for a release candidate; the workflow and every release script refuse any other name.
The deploy bundles pin the images by tag@digest (deploy/public/compose.yml, deploy/hub/compose.yml and the
tools_image= line of deploy/setup.sh), so the pins of a release must be in the tree that the tag names. That is why
the digests are computed first, in a pull request, and the tag is made afterwards on the merged commit.
The release signing key
Section titled “The release signing key”Release tags are signed with the OpenPGP key whose fingerprint is 5BEB DE79 EDDE D015 7398 1D4F 70C6 001B 0A49 776E,
published at https://github.com/fishcakeday.gpg. Operators import it, compare the full fingerprint and run git tag -v <tag> (SECURITY.md, “Release signing key”). A change of the key is announced by a commit signed with the old key.
Before the first release
Section titled “Before the first release”The release workflow names the environment release. A workflow that names an environment that does not exist gets
one created with no protection, so these settings must exist and be read back before the first tag is pushed. An
organisation owner does this once.
-
The
releaseenvironment (the repository’s Settings, Environments):- required reviewers: the maintainer;
- “Prevent self-review”: off, because the maintainer both tags and approves;
- deployment branches and tags: “Selected branches and tags”, with one rule of type tag,
v*, and none of type branch; - “Allow administrators to bypass configured protection rules”: off.
-
Two tag rulesets (Settings, Rules, Rulesets), both on the target “Tag” with the pattern
refs/tags/v*, and both with enforcement “Active”:- one that restricts deletion and restricts updates, with an empty bypass list;
- one that restricts creation, with the repository-admin role as its only bypass actor.
-
Read both back and compare with the lists above:
Terminal window gh api repos/SatBooth/SatBooth/environments/releasegh api repos/SatBooth/SatBooth/environments/release/deployment-branch-policiesgh api repos/SatBooth/SatBooth/rulesetsgh api repos/SatBooth/SatBooth/rulesets/<id>The environment must show the reviewer,
prevent_self_reviewfalse,can_admins_bypassfalse and a custom policy whose one entry is of typetagand namedv*. Each ruleset must showenforcementactive, therefs/tags/v*condition, its rules and its bypass actors as above. -
Immutable releases. Step 6 of the procedure relies on a published release being locked. Turn on the repository’s immutable-releases setting (Settings, General, Releases) and read it back:
Terminal window gh api repos/SatBooth/SatBooth/immutable-releasesIt must show
"enabled": true.
Procedure
Section titled “Procedure”-
Release-prep pull request. Build the images into a directory outside the checkout (the build writes large files) and write their digests into the three pin sites:
Terminal window out=$(mktemp -d)scripts/release-build.sh "$out"scripts/release-pins.sh set <version> "$out/digests.txt"Run the build from a clean checkout that is up to date with
main: CI builds the pull request’s merge commit, and an untracked.gofile would enter the build context and change the digests.<version>is the tag,vincluded. Commit the three changed lines. Theagreejob ofrelease.ymlruns on the pull request, because it changes the pins: it builds the images on an amd64 and an arm64 runner, requires both to produce the same digests, and fails the pull request when they differ from the pins. A pull request that leaves the pins as they are gets the comparison as a note only.agreeis not a required check ofmain: merge the release-prep pull request only whenagreeis green. -
Dry run. Merge the pull request, then run the
releaseworkflow by hand (workflow dispatch) onmain. It does everything up to the comparison with the pins and pushes nothing. It must be green on the exact commit that will be tagged. -
The tag. The maintainer makes the annotated tag on that commit, signed with the release signing key, and pushes it. From the commit being tagged, first check that the pins hold exactly the tag name:
Terminal window git switch --detach <commit>scripts/release-pins.sh version # must print exactly <tag>git tag -s <tag> -m "SatBooth <tag>" <commit>git tag -v <tag>git push origin <tag>The workflow does not verify the signature: it runs from the tagged commit, so a check written there would stop no one who controls that tree. It checks that the tag is annotated, named as above and at the run’s commit. Who can make the workflow publish is decided by the settings of “Before the first release” and by the operators’ own
git tag -v. -
Approve the
releaseenvironment. The tag push starts the run:buildandagreeas in the dry run, this time with the tag’s version compared against the pins;publishruns only ifagreepassed, and waits for the approval. Before approving, read the threematchlines in the summary ofagree. A refused approval pushes nothing.publishrebuilds the images, checks them against the pins again, pushes them to the three packages by digest, points the tag<version>at each, and attests each image.release-push.shnever moves a registry tag that holds another digest that was there when it read the tag; it fails instead. It reads a tag and creates it a moment later, so a concurrent writer with package-write rights (threat model §6) could still create the tag in between; operators who pull by digest are unaffected. Thensmokepulls the published images on an amd64 and an arm64 runner and runsscripts/image-run-test.shon them; only when it passes on both architectures doesdraftcreate a draft release (a prerelease for an rc) fromscripts/release-notes.sh <tag>, whose “Changes:” placeholder is one line to replace. -
Check that the packages are public. GitHub’s documentation says that a package published by a workflow takes the visibility of the repository, so the three packages should be public. The first release confirms which case applies. If a package is not public, an organisation owner sets it public in the package’s settings. Making a package public cannot be undone, and package administrators can also change its visibility, so look at all three before the check. Then, logged out of the registry and from a clean checkout of the tag, pull and run the images as an operator would:
Terminal window docker logout ghcr.iogit tag -v <tag>git switch --detach "refs/tags/<tag>^{commit}"git rev-parse HEAD # must equal the next linegit rev-parse "refs/tags/<tag>^{commit}"scripts/release-smoke.sh <tag>It requires the registry tag to hold the pinned digest, pulls the three images by
tag@digestand runs them. Do this before publishing the draft: thesmokejob logs in, so it does not prove that an anonymous operator can pull. Once the packages are public, a later release needs this check only to confirm it. -
Publish. Publishing is the maintainer’s step and comes last: only after the
smokejob is green on both architectures and after the anonymous check of step 5. Edit the draft’s prose (the line “Changes: (to be written before publishing)”), then publish it. The release notes already hold the three pinned refs and the verification commands. Once the release is published, GitHub locks its tag and assets; the title and notes stay editable, so they are not authoritative: the signed tag’s tree is. -
The next release. After an rc, the next rc or the final release is another prep pull request that changes the version only. When no build input changed (the Go sources,
go.mod,vendor/, the filescheckoutembeds,deploy/image/Dockerfile,.dockerignore, and the pins of the builders and the exporter settings inscripts/release-lib.sh), the digests stay the same andrelease-pins.sh setwrites the same digests under the new version.
When a run is refused
Section titled “When a run is refused”If agree fails, the pins and the build disagree: nothing is pushed. If the approval is refused, nothing is pushed;
reject the waiting deployment, so that it cannot be approved later.
A failure that is not a disagreement (a network error, a runner that stopped, a failed call to the registry or to the
attestation service) is retried on the same run with “Re-run failed jobs”; publish, when it is one of them, asks for
the approval again. It is safe to repeat: it pushes the same digests, leaves a registry tag that already holds the
pinned digest, and fails on one that holds another. draft runs only after smoke passed on both architectures, and
leaves an existing release as it is.
When the cause is in the tagged tree (a wrong pin, an error in a script or in the workflow, an image that fails
smoke), the answer is the next version: fix the cause in a pull request, then tag that version. Never move or delete
a tag: the tag rulesets forbid it, and an operator who verified the old tag must keep seeing the same commit under it.
A tag that was pushed and did not publish stays, unreleased; images already pushed for it keep their registry tag, and
no release names them.
Between releases
Section titled “Between releases”The pins on main name the last release, while the code around them may be newer. Deploy from a release tag, not from
main.
Documentation from SatBooth v0.1.0-rc.1, Apache-2.0