Releasing
Releasing
A release is one tag push. Everything else is automatic, except one step that needs a credential nobody but a maintainer can create.
# Bump the version in Cargo.toml, packaging/telemetryd.rb and the installation guide,
# then:
git tag -a v0.12.0 -m "…" && git push origin v0.12.0
What the workflow does
- Builds all four targets, and fails the release if a musl build came out dynamically linked — "one static binary, no glibc surprises" is a promise on the README, not an aspiration.
- Runs the end-to-end soak against the binary it just built, on the target that can execute here. This is the last gate before anything is published.
- Builds
.debpackages, using the systemd unit committed inpackaging/rather than one generated by running the cross-compiled binary. - Signs
SHA256SUMSwith a keyless Sigstore signature, then verifies its own signature with the same command the documentation gives users. - Publishes the archives, packages, checksums, signature bundle and SBOM.
The Homebrew tap is updated by a separate workflow, formula.yml, on
release: published.
Why the tap is not part of the release
publish-formula.py reads the digests out of the published SHA256SUMS, so it
cannot run before the release exists. It used to sit inside the release job, above the
publishing step, where the file it fetches had not been uploaded yet. That never fired
only because the credential was absent and the step always took its skip branch — for
four releases it looked green. The first tag after a token was configured would have
404'd, failed the job, and left a release built and signed but never published.
Splitting it also bounds the damage: a tap that cannot be written to now costs nothing
worse than Homebrew serving the previous version until the workflow is re-run, and
workflow_dispatch makes that a single command.
The credential
The tap is a different repository, so GITHUB_TOKEN cannot write to it.
A GitHub App provides the credential, not a personal access token. An App's
credentials do not expire, so nothing stops working on a date nobody wrote down, and
it does not belong to a person — a PAT leaves with whoever created it. What reaches
the runner is an installation token scoped to homebrew-tap alone that expires an
hour later.
Set up once: create an App under the cboxdk organisation with Contents: read and
write and no webhook, install it on cboxdk/homebrew-tap only, put its client id in
formula.yml, and store the private key:
gh secret set HOMEBREW_APP_PRIVATE_KEY --repo cboxdk/telemetryd < app.private-key.pem
That is the only secret involved. The client id identifies the App rather than
authenticating as it — any organisation member can read it back from
/orgs/cboxdk/installations — so it lives in the workflow, where it also documents
which App is doing the publishing.
The private key goes in as the whole PEM, header and footer included — hence < a
file rather than a paste.
Without it the release still succeeds. formula.yml warns and prints the command
to run by hand:
python3 scripts/publish-formula.py v0.20.9
That is deliberate. A missing tap update should not fail a release that is otherwise
good, but it must not pass silently either: a tap that has quietly stopped being
updated is indistinguishable from one that is current, and brew install would go on
serving the previous version.
What the publisher refuses to do
scripts/publish-formula.py will not publish if the formula's version disagrees with
the tag, if a checksum is missing from the release, if the archives are listed in an
order that would pair digests with the wrong targets, or if a placeholder survives
substitution. A tap serving a wrong digest fails on someone else's machine, which is
the worst place to find out.