Mobile release candidates (`mobile-vX.Y.Z-rc.N`) are currently created
by the `mobile-release-candidate.yml` workflow, using the
`buzz-release-bot` App key that also serves the desktop, relay and chart
lanes. This PR takes mobile off that key. After it lands, the only way
to create a mobile candidate tag is for a person on the mobile release
team to push it.
### What changes
- `scripts/mobile-release.sh candidate` keeps all its existing checks:
version, source branch, clean tree, canonical origin, ancestry and next
rc number. It no longer dispatches a workflow. It now:
- verifies the mobile tag ruleset 26588677 (active, the expected rules,
scoped to `refs/tags/mobile-*` or `refs/tags/mobile-v*`, and bypassable
by the current user), and that the App-only general tag ruleset 14378754
excludes `refs/tags/mobile-*`;
- requires origin's effective push URLs to be exactly its fetch URL, and
fetches a pinned `--commit` missing from a shallow clone;
- creates the annotated tag locally and pushes exactly that ref
(`--no-follow-tags`, explicit refspec);
- confirms through the GitHub API that `block/buzz` holds that exact tag
object, then verifies the remote tag as before.
A rejected push removes the local tag only when origin lacks it, so a
retry starts clean. When the push reports failure but origin has the
tag, the script keeps the local tag and still runs the canonical GitHub
check, reporting success only if it passes. When GitHub can't confirm
the tag, the local tag is kept and the message says how to reconcile it.
- `scripts/release-rulesets.sh`: `require_mobile_release_tag_ruleset`
replaces the App-oriented check. `MOBILE_RELEASE_TAG_RULESET_ID` is
`26588677`.
- Removed `.github/workflows/mobile-release-candidate.yml`,
`scripts/publish-mobile-release-candidate.sh` and its test, and their CI
references. Updated `RELEASING.md` and the auto-tag workflow header
comment.
- The desktop, relay and chart auto-tagging is unchanged.
### Rollout
Tag ruleset 26588677 ("Restrict Mobile Tags") is active with the two
mobile release managers as bypass actors. The App (Integration 4349119)
is temporarily a bypass actor of 26588677 and 19321162 so the current
`main` workflow keeps working; it is removed from both right before this
merges. General tag ruleset 14378754 now excludes `refs/tags/mobile-*`;
without that, rulesets stack and it would reject release managers'
mobile tag pushes. The branch ruleset 19321162 now has the same bypass
actors, so `require_mobile_release_branch_ruleset` no longer requires
"no bypass"; it still checks the review and status check policy. Release
managers must be bypass actors of both rulesets.
### Before merge
- [x] Remove Integration 4349119 (`buzz-release-bot`) from the bypass
actors of rulesets 26588677 and 19321162.
### Validation
- `shellcheck -x scripts/release-rulesets.sh scripts/mobile-release.sh
scripts/mobile-release-source.sh
scripts/test-mobile-release-contract.sh`: clean.
- `scripts/test-mobile-release-contract.sh`: passes. It runs against a
local bare remote with a stubbed `gh` and covers:
- a successful annotated push at the pinned SHA;
- ruleset failures (not bypassable, inactive, wrong rules, wrong scope,
excludes, non-numeric ID), with `refs/tags/mobile-v*` also accepted as
the include, each failing before any tag is created;
- a push rejected by a pre-receive hook, which leaves no local tag and
allows a clean retry;
- `push.followTags=true` with a stray local annotated mobile tag
(exactly one ref published), a push that lands but exits non-zero (tag
kept), and a GitHub API outage after the push (tag kept, rerun publishes
the next rc);
- origin push URLs pointing at a fork (pushurl, a second pushurl,
pushInsteadOf, insteadOf), a shallow clone pinning a historical commit,
and 14378754 lacking the mobile exclusion;
- rc numbering, source branch rules and `--commit` pinning.
- `scripts/test-release-ref-contract.sh` and
`scripts/test-desktop-release-candidate.sh`: pass.
- Live read-only check against real GitHub as a bypass actor: sourced
`scripts/release-rulesets.sh` and `scripts/mobile-release-source.sh`,
then ran `require_mobile_release_tag_ruleset` (including the 14378754
exclusion check) and `require_mobile_release_branch_ruleset
mobile-v0.27`. Both returned success. No tag was created.
- No mobile app test: this is a script and workflow change only.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Signed-off-by: Tom Brow <tomb@squareup.com>
Co-authored-by: Tom Brow <tomb@squareup.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@localhost>
20 KiB
Releasing Buzz
Buzz has three independent release lanes. Desktop and relay use release PRs.
Mobile uses immutable release-candidate tags cut from remote main by default,
with an optional stable source branch:
| Lane | Entry point | Artifact |
|---|---|---|
| Desktop | just release-desktop <version> |
Packaged desktop app (signed/notarized macOS, unsigned Windows, and Linux) |
| Relay | just release-relay |
ghcr.io/block/buzz container image |
| Mobile | scripts/mobile-release.sh candidate X.Y.Z |
Exact mobile-vX.Y.Z-rc.N source identity |
The lanes version independently. Desktop reads its manifests, relay reads its
crate manifest, and mobile derives both source and marketing version from the
exact candidate tag. Mobile candidate tags are pushed by a human release
manager, not by CI or a GitHub App. The handoff to the private buzz-releases
pipeline remains manual because OSS CI cannot trigger private CI.
Quick Start
Prepare desktop releases locally from an up-to-date, clean main checkout:
just release-desktop 0.5.3
The recipe generates the immutable candidate and opens or updates its pull request. Candidate branch creation uses the operator's GitHub permissions; the release App is intentionally limited to creating protected release tags.
# Relay release
just release-relay
just release-relay 0.4.0
# Publish the next mobile candidate from the exact current remote main commit
# (requires bypass of the mobile tag ruleset, see Mobile below)
scripts/mobile-release.sh candidate 0.5.0
Desktop uses an immutable generated candidate PR; relay continues using its
metadata PR. Mobile does not. Each mobile-vX.Y.Z-rc.N tag is an immutable
candidate and the artifact of record.
Optional stable branches use mobile-vMAJOR.MINOR. There is no
stable mobile tag alias, finalization step,
or mobile GitHub Release.
How It Works
Desktop
- Run
just release-desktop <version>from a clean, up-to-datemaincheckout. The script creates one deterministic candidate commit and records both its frozen base and the verified prior release ledger in candidate metadata. - Review the exact candidate SHA, complete changelog, and CI. Regenerating or pushing the branch creates a new candidate and requires checks to run again.
- Squash merge the PR after all protected-branch checks pass. The merge is
the human authorization event; an authorized owner/admin bypass is treated
the same way. Unrelated changes reaching
maindo not invalidate the reviewed candidate. auto-tag-on-release-pr-mergeverifies the closed event against GitHub's PR identity, validates candidate content, and proves every required check came from its trusted producer and was successful when the PR merged. It createsdesktop-v<version>at the exact reviewed PR head—not the squash commit. Retries accept that tag only at the same SHA and never move it. GitHub does not expose when an individual check rerun was created, so an ordinary rerun after merge deliberately makes tag verification fail closed; inspect that run and create a new candidate version rather than retrying the blocked tag.- The tag triggers
release.yml. It builds and stages all platform artifacts, publishes the versioned release only after the complete set succeeds, then updates the rolling updater manifest last for stable versions.
Because squash merging leaves immutable candidate tags on side history, the next release uses validated prior candidate metadata as its ledger boundary. It includes unrelated commits after the prior frozen base and excludes exactly the prior release's recorded squash commit; tag ancestry is deliberately irrelevant.
Relay
just release-relayruns locally onmain, creates or updates arelay-release/<version>PR, bumpscrates/buzz-relay/Cargo.toml, regeneratesCargo.lock, and updates the relay changelog.- Merge the PR.
auto-tag-on-release-pr-mergepushesrelay-v<version>. - The tag triggers
docker.yml. Stable releases update the version aliases andlatest; prereleases do not. Each release also publishes an optimized, symbol-bearing image under matchingdebug-tags (for example,debug-0.3.0anddebug-latest) for native profiling. The ordinary tags remain stripped and are the default for deployments that do not need it.
Every push to main continues to publish the rolling relay :main and
:sha-<7> tags, plus matching :debug-main and :debug-sha-<7> variants.
Mobile
- Publish a candidate. From a clean checkout whose
originis the canonicalblock/buzzrepository, runscripts/mobile-release.sh candidate X.Y.Z. The script resolves and fetches a snapshot oforigin/main, derives the next number from exact remote tags for that marketing version, and publishes an annotatedmobile-vX.Y.Z-rc.Ntag at that snapshot withgit tag -a(your own tag signing configuration applies) and pushes exactly that one tag toorigin. The push is allowed only because you are a bypass actor of the dedicated mobile tag ruleset26588677forrefs/tags/mobile-*; bypass is granted to the mobile release managers. Thebuzz-release-botApp and its key are not involved in mobile. The script never uses the operator's checked-out commit and never moves an existing candidate. To select a historical commit, add--commit FULL_SHA. The script requires that commit to be in the selected source branch's history. Later branch advances do not change or invalidate the tag. If the push is rejected and origin has no such tag, the script deletes the local tag and fails; check that you are a bypass actor of the ruleset. If the candidate may already be on GitHub, the script keeps the local tag and says so: use the published tag rather than rerunning. - Build the exact tag. Enter the candidate tag as
mobile_refin the private Buzz mobile Buildkite pipeline. OSS CI deliberately cannot trigger that private pipeline. The tag supplies both source commit and release version. Flutter receives clean marketing versionX.Y.Z; Buildkite's monotonically increasing build number supplies the platform build number. - Promote tested artifacts. Promote the already-built signed artifact for each platform through its store workflow. Record the exact tag with the build or rollout record. No source ref is changed and no final build is cut.
The iOS and Android artifacts for one marketing version may come from different
RC tags. For example, iOS can ship mobile-v0.5.0-rc.2 while Android ships
mobile-v0.5.0-rc.3. Each platform's exact candidate tag is its source record.
There is intentionally no single selected or final candidate for the marketing
version.
Optional stable release branches
For a release that needs isolation from ongoing development, create a branch
such as mobile-v0.25 from an explicitly selected, verified source commit.
Publish candidates from its current remote tip:
scripts/mobile-release.sh candidate 0.25.0 --source-branch mobile-v0.25
The branch must match the marketing version's major and minor components:
mobile-v0.25 accepts 0.25.x, but rejects 0.26.x and 1.25.x. Without the option,
the source remains main. The script validates the source;
a missing branch or a commit outside its history fails rather than falling back
to main. --commit FULL_SHA also works with a stable source branch. Its
review and check protections bind everyone except the mobile release managers,
who bypass them: a release manager can push an unreviewed commit to a stable
branch and tag it as a candidate.
The private pipeline still builds only the resulting immutable candidate tag.
For the October 2026 stabilization period, all distributed mobile builds,
including Company Portal dogfood builds, must come from mobile-v0.25 until
after the conference. Do not upload builds from main or a later minor version
that would displace the stable dogfood build. The branch starts from
mobile-v0.24.0-rc.2, the source of uploaded iOS 0.24.0 (7).
General fix PRs must target main and land there first. At each fix landing,
ask the release owner whether to
backport the merged commit to the stable branch. For each explicitly approved
fix, cherry-pick with -x --signoff onto a backport branch and open a backport-only
PR targeting the stable branch. Require review and passing checks before merging
that PR. Preserve the original commit provenance and validate the resulting
stable source. A correction for a regression introduced by an earlier backport
may target the stable branch directly; identify the faulty backport and keep the
correction scoped to that regression. The same review and checks apply.
Never merge main wholesale.
Remote agents and App Clip are post-conference work. There is no preselected
fix list and no requirement to create a stable branch for every minor release.
The initial 0.25.0 candidate pins that baseline with --commit. The second
candidate uses an empty commit above that baseline to verify
that the existing pipeline builds a commit outside main. Verify identical
source trees, absence of that commit from remote main, and the exact tag,
commit, marketing version and build number in the uploaded artifacts. Upload
and external TestFlight review do not establish production App Store approval.
mobile/pubspec.yaml keeps 0.0.0+1 only as a valid, visibly non-release
fallback for local development and validation builds. Release jobs always
inject both version fields. mobile/CHANGELOG.md is retained as historical
release data. It is not a release ledger for this flow.
Version Sources
| Lane | Release version authority |
|---|---|
| Desktop | desktop/package.json and synchronized desktop manifests |
| Relay | crates/buzz-relay/Cargo.toml |
| Mobile | Exact mobile-vX.Y.Z-rc.N remote tag |
just bump-desktop-version <version> updates the desktop manifests and
regenerates their lockfiles. just bump-relay-version <version> updates the
relay crate and regenerates Cargo.lock. Mobile has no bump recipe or
release-metadata PR.
Signed macOS Canary
Use the manual Signed macOS Canary workflow when you need an Apple Silicon
build of current main for explicit testing without publishing a release:
gh workflow run signed-macos-canary.yml --repo block/buzz --ref main
The workflow derives a -test.<run-number> version, signs and notarizes the
DMG, verifies it with Gatekeeper, and uploads it as a short-lived Actions
artifact with seven-day retention. Because this is a public repository, any
signed-in GitHub user can download that artifact while it exists; it is
unpublished, not private. The workflow has no release permissions, does not
create or move tags, and cannot update buzz-desktop-latest or latest.json.
Download the artifact from the completed run:
gh run download <run-id> --repo block/buzz --name <artifact-name>
The workflow intentionally accepts only main. Use the normal release process
for distributable builds or builds from an immutable release tag.
Release Retry
release.yml has no manual dispatch and cannot build from main or another
caller-selected ref. If a run for an existing immutable
desktop-v<version> tag fails, rerun that failed workflow from GitHub Actions
(or use gh run rerun <run-id> --failed --repo block/buzz). A rerun
repairs the versioned draft if publication did not complete. It does not
promote that version to the auto-updater; promotion is a separate manual
action. Do not recreate, move, or push the immutable tag again.
Mobile intentionally has no branch or arbitrary-ref fallback. The private Buildkite pipeline accepts only an exact candidate tag.
Internal Releases
For mobile, trigger the private
Release Mobile pipeline with
an exact RC tag for the platform build being cut. For desktop, start
Release Desktop and enter the
exact public source tag as desktop_ref=desktop-v<version>; a generic
v<version> tag is intentionally rejected. See the
buzz-releases README
for the rest of the private pipeline contract.
What Gets Published
Desktop publishes two GitHub releases:
desktop-v<version>: the user-facing release with installers and the exactupdater-manifest.jsonpromotion candidate. Publishing this release does not expose it through in-app auto-update.buzz-desktop-latest: the rolling auto-updater release. Itslatest.jsonchanges only through the manual promotion workflow.
Promote an OSS desktop release to auto-update
After installing and testing the published desktop-v<version> artifacts, run
Promote OSS Desktop Auto-Update from the main branch and enter the exact
stable X.Y.Z version. The workflow validates the immutable tag and release,
the retained manifest and every referenced updater asset, and requires the
version to be newer than the currently promoted version before replacing
buzz-desktop-latest/latest.json. Same-version retries succeed only when the
manifest is identical; downgrades are rejected.
Withholding promotion leaves existing clients on the previous version. If a promoted release is bad, ship and promote a higher patch version; changing the manifest to an older version does not downgrade clients that already updated.
Mobile publishes only annotated mobile-vX.Y.Z-rc.N git tags. Store artifacts
and rollout records retain the exact tag they used. Mobile does not publish a
GitHub Release or a stable mobile-vX.Y.Z alias.
Platform Support
The release workflow builds two separate macOS DMGs: Apple
Silicon (darwin-aarch64, the release job) and Intel
(darwin-x86_64, the release-macos-x64 job), an unsigned Windows x64
NSIS installer (its filename includes _alpha-unsigned), and Linux .deb and
.AppImage packages. Both macOS DMGs are codesigned, notarized, and attached
to the same desktop-v<version> release. Intel users
download the _x64.dmg.
The Linux AppImage is post-processed by desktop/scripts/fix-appimage.sh,
which strips infra libraries over-bundled by linuxdeploy (they crash on
Mesa 25+ / GLib 2.88 distros; see
tauri-apps/tauri#15665)
and re-signs the artifact. As a result the AppImage relies on the
host's Wayland/GStreamer/graphics stack and requires GLib >= 2.72
(Ubuntu 22.04 or newer). The release-linux job builds inside a
ubuntu:22.04 container for broad GLIBC compatibility.
Prerequisites
- Write access to the
block/buzzGitHub repository - An
originremote whose configured URL is the canonicalblock/buzzrepository ghCLI authenticated with permission to push the candidate branch and open its pull request- The Default
mainruleset configured for squash-only merging, strict required checks, stale-review dismissal, and the Desktop Release Candidate check - Release tag ruleset
14378754active for all tags (~ALL), with creation, update, deletion, and non-fast-forward protections andbuzz-release-botas its sole always-bypass actor. It must excluderefs/tags/mobile-*: rulesets stack, so otherwise it rejects release managers' mobile tag pushes.scripts/mobile-release.shchecks that exclusion before tagging. - The
buzz-release-botApp credentials configured for GitHub Actions (desktop, relay, and chart lanes only; mobile does not use them) - The following GitHub Actions variables and secrets configured for the
desktop release lane:
Name Kind Purpose BUZZ_RELEASE_TAGGER_CLIENT_IDVariable GitHub App client ID used to create protected release tags BUZZ_RELEASE_TAGGER_PRIVATE_KEYSecret GitHub App private key OSX_CODESIGN_ROLESecret macOS signing role used by block/apple-codesign-actionCODESIGN_S3_BUCKETSecret macOS signing exchange bucket BUZZ_UPDATER_PUBLIC_KEYorSPROUT_UPDATER_PUBLIC_KEYSecret Tauri updater public key TAURI_SIGNING_PRIVATE_KEYSecret Tauri updater private key TAURI_SIGNING_PRIVATE_KEY_PASSWORDSecret Password for the private key
Mobile candidate publication requires being a bypass actor of the mobile tag
ruleset 26588677 ("Restrict
Mobile Tags"), recorded as MOBILE_RELEASE_TAG_RULESET_ID in
scripts/release-rulesets.sh. Bypass is granted to the mobile release
managers. The ruleset must be active for refs/tags/mobile-* (or the narrower
refs/tags/mobile-v*), with creation, update, deletion, and non-fast-forward
protections and no exclusions. The buzz-release-bot App must not be a bypass
actor. On every publication the script checks the public policy and that the
caller's effective current_user_can_bypass is always; GitHub hides the bypass
actor list from non-admin tokens, so an administrator audits that list.
Stable sources additionally require active branch ruleset
19321162 covering
refs/heads/mobile-v*, with no exclusions, deletion and force-push prevention,
at least one approval, stale-approval dismissal, last-push approval, resolved
review threads, and strict required Mobile and DCO Check checks. The mobile
release managers are also its bypass actors, so they can publish from a stable
branch; the ruleset still forces review and checks on everyone else. Because of
that bypass, a release manager can push an unreviewed commit to a stable branch
and publish it as a candidate; nothing else enforces review for them. The script
verifies the public policy on every publication but does not check bypass.
Troubleshooting
The desktop candidate is stale or cannot be squash merged
Do not update the branch manually and do not weaken the ruleset. Run
just release-desktop <version> again from current main; this regenerates the
candidate, reruns CI, and requires a fresh trusted approval on the new exact
head. The post-merge verifier refuses to tag a squash whose parent differs from
the recorded candidate base or whose tree differs from the validated PR head.
Local just release-desktop fails with "must be on main branch"
Switch to main and pull latest before running the release recipe.
Local just release-desktop fails with "working tree is dirty"
Commit or stash your changes before running the release recipe.
New commits land after publishing a mobile candidate
Run scripts/mobile-release.sh candidate <version> again after the intended
fix reaches the intended remote source branch. Use the same --source-branch
option for a stable release. The command publishes a new immutable RC tag at the new exact
remote commit. Continue referring to each tested or shipped platform artifact by
its own exact tag.
The source branch advances during candidate publication
Publication remains pinned to the selected SHA. The script verifies that it is an ancestor of a resolved source-branch snapshot, then creates, pushes, and verifies an immutable annotated tag for that exact SHA. It verifies the tag, not the branch's later tip. A newer commit does not require retrying or creating another candidate. Missing or unverifiable ancestry still fails closed.
A mobile candidate command selects the wrong RC number
Do not retry by moving or deleting a tag. Inspect the exact remote mobile-v*
tags and resolve the unexpected state. Candidate numbers are monotonically
increasing remote identities.
A mobile candidate publication is rejected by repository rules
Confirm you are a bypass actor of the active mobile tag ruleset 26588677
(the mobile release managers). The script removes its local tag after a rejected
push, so retry after an administrator fixes your access. Do not add yourself to
the bypass list, grant the App bypass, or weaken update or deletion protection. Existing
candidate tags must remain immutable.
Auto-updater reports "no update available"
Verify that the buzz-desktop-latest release exists and contains a
valid latest.json. The manifest covers all four platform keys
(darwin-aarch64, darwin-x86_64, linux-x86_64,
windows-x86_64); a missing entry usually means that platform's
release job failed. Check the workflow run.