Skip to content

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

  1. 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.
  2. 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.
  3. Builds .deb packages, using the systemd unit committed in packaging/ rather than one generated by running the cross-compiled binary.
  4. Signs SHA256SUMS with a keyless Sigstore signature, then verifies its own signature with the same command the documentation gives users.
  5. 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.