feat(release): make desktop releases immutable (#3568)

## Summary
- add a manual desktop release preparer that regenerates one
version-only candidate from current `origin/main`
- validate deterministic complete changelog accounting, candidate
authorship, allowed files, exact-head approval, required checks, and
two-parent merge topology before tagging the reviewed candidate
- move desktop tags/releases from `v*` to `desktop-v*` while preserving
relay, chart, push-chart, and mobile behavior
- stage all four platform outputs in Actions artifacts and grant GitHub
release write access only to one final all-platform-gated publisher
- publish the versioned release only after complete artifact assembly;
update stable `latest.json` last; never promote prereleases or published
rebuild outputs

## Safety properties
- desktop tags point to the reviewed candidate SHA, not the merge commit
- release builds remain tag-bound and reverify tag == checked-out HEAD
- one final writer fails closed on artifact basename collisions
- per-tag concurrency serializes publication without cancellation
- published reruns do not replace immutable versioned assets or promote
signatures from a rebuild
- candidate branches use an explicit remote OID lease when regenerated

## Validation
- `scripts/test-desktop-release-candidate.sh`
- `scripts/test-release-ref-contract.sh`
- `scripts/test-mobile-release-contract.sh`
- changed workflow YAML parsing (Ruby Psych)
- changed shell syntax (`bash -n`)
- `git diff --check`
- push hooks: branch-skew, Rust workspace tests (1,853 passed), desktop
Tauri tests (3 passed)

## Coordinated companion
- squareup/buzz-releases#79 updates the manually entered desktop
source-tag contract to stable-only `desktop-v*`
- merge the private contract companion before the first namespaced
desktop release

## Rollout blockers (no settings changed here)
Before the first candidate/release:
1. enable merge commits in repository settings
2. allow `merge` in ruleset `13596885`
3. require approval after the last push in ruleset `13596885`
4. include `refs/tags/desktop-v*` explicitly in release ruleset
`14378754`
5. prove the non-publishing candidate/merge/tag/artifact validation path
before any production release

Do not test the old workflow with a prerelease: it can still mutate the
production rolling updater release.

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
This commit is contained in:
Wes
2026-07-30 11:27:56 -07:00
committed by GitHub
co-authored by Carl
parent cca8839034
commit 1dfd89ea67
13 changed files with 763 additions and 198 deletions
+11 -12
View File
@@ -5,7 +5,7 @@ Mobile uses immutable release-candidate tags cut directly from remote `main`:
| Lane | Entry point | Artifact |
|------|-------------|----------|
| Desktop | `just release-desktop` | Signed desktop app (macOS/Linux) |
| Desktop | `Prepare Desktop Release` / `just release-desktop` | Signed desktop app (macOS/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 |
@@ -31,7 +31,7 @@ just release-relay 0.4.0
scripts/mobile-release.sh candidate 0.5.0
```
Desktop and relay releases use metadata PRs. Mobile does not. Each
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.
There is no mobile release branch, stable mobile tag alias, finalization step,
or mobile GitHub Release.
@@ -42,12 +42,11 @@ or mobile GitHub Release.
### Desktop
1. **`just release-desktop`** runs locally on `main`, creates or updates a
`version-bump/<version>` PR, bumps the desktop manifests, regenerates
lockfiles, and updates `CHANGELOG.md`.
2. **Merge the PR.** `auto-tag-on-release-pr-merge` pushes `v<version>`.
3. **The tag triggers `release.yml`.** It builds, signs, notarizes, and
publishes the desktop app for macOS and Linux.
1. Run **Prepare Desktop Release** with a version (or `just release-desktop <version>`). Automation records current `origin/main`, regenerates `version-bump/<version>` as one deterministic candidate commit, and opens or updates the PR.
2. Review the full-SHA changelog, CI, recorded base, and candidate SHA. Any regeneration creates a new head and requires fresh approval.
3. Merge with **Create a merge commit**. Squash and rebase are invalid for desktop release PRs.
4. `auto-tag-on-release-pr-merge` proves that merge parent 2 is the exact approved candidate, then tags that candidate `desktop-v<version>`.
5. The tag triggers `release.yml`. It creates a draft, builds and stages every platform, publishes the complete versioned release, and updates the rolling updater manifest last for stable versions.
### Relay
@@ -147,8 +146,8 @@ for distributable builds or builds from an immutable release tag.
## Manual Release Retry
The **Release** workflow's manual dispatch is only a retry mechanism for an
existing immutable `v<version>` tag. Select that tag in the ref picker and
provide the matching semver version without the `v` prefix. It cannot build
existing immutable `desktop-v<version>` tag. Select that tag in the ref picker and
provide the matching semver version without the `desktop-v` prefix. It cannot build
from `main` or another caller-selected source ref.
Mobile intentionally has no branch or arbitrary-ref fallback. The private
@@ -171,7 +170,7 @@ for the private pipeline contract.
Desktop publishes two GitHub releases:
1. **`v<version>`**: the user-facing release with installers.
1. **`desktop-v<version>`**: the user-facing release with installers.
2. **`buzz-desktop-latest`**: the rolling auto-updater release.
Mobile publishes only annotated `mobile-vX.Y.Z-rc.N` git tags. Store artifacts
@@ -186,7 +185,7 @@ 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), plus Linux `.deb` and
`.AppImage`. Both macOS DMGs are codesigned, notarized, and attached to
the same `v<version>` release. Intel users download the `_x64.dmg`.
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