fix(release): make immutable desktop release operable (#3943)

## Summary

- document `Prepare Desktop Release` as the canonical desktop release
entry point
- describe the frozen candidate, exact-head approval, and true
merge-commit contract
- document all platform outputs and complete release App/signing
configuration
- link the release runbook from the README
- allow stable reruns to repair the rolling updater manifest after the
versioned release has already published

## Release blocker

The live repository cannot currently complete this flow: repository
settings disable merge commits and the `main` ruleset allows only
squash, while `scripts/verify-desktop-release-merge.sh` requires a
two-parent merge whose second parent is the approved candidate. Those
settings must allow merge commits before a desktop release PR is merged.

## Validation

- `bash scripts/test-desktop-release-candidate.sh`
- `bash scripts/test-release-ref-contract.sh`
- `git diff --check`
- verified live repository merge settings, `main` ruleset, release tag
ruleset, Actions variable names, and secret names with GitHub API
- independent review by Princess Donut; incorporated all findings,
including the rolling-manifest retry gap and unsigned Windows labeling

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-31 09:39:14 -07:00
committed by GitHub
co-authored by Carl
parent c104eecfb3
commit 052174a148
4 changed files with 89 additions and 32 deletions
+1 -1
View File
@@ -948,5 +948,5 @@ jobs:
run: gh release edit "desktop-v${VERSION}" --draft=false
- name: Upload latest.json to rolling release last
if: ${{ env.already_published != 'true' && !contains(needs.setup.outputs.version, '-') }}
if: ${{ !contains(needs.setup.outputs.version, '-') }}
run: gh release upload buzz-desktop-latest latest.json --clobber
+1
View File
@@ -10,6 +10,7 @@
<a href="VISION_PROJECTS.md">Forge</a> ·
<a href="VISION_AGENT.md">Agents</a> ·
<a href="ARCHITECTURE.md">Architecture</a> ·
<a href="RELEASING.md">Releasing</a> ·
<a href="LICENSE">Apache 2.0</a>
</p>
+83 -30
View File
@@ -5,7 +5,7 @@ Mobile uses immutable release-candidate tags cut directly from remote `main`:
| Lane | Entry point | Artifact |
|------|-------------|----------|
| Desktop | `Prepare Desktop Release` / `just release-desktop` | Signed desktop app (macOS/Linux) |
| Desktop | `Prepare Desktop Release` | 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 |
@@ -16,13 +16,22 @@ remains manual because OSS CI cannot trigger private CI.
## Quick Start
Desktop releases are prepared from the current remote `main` by GitHub Actions:
```sh
# Desktop release (next patch version)
just release-desktop
gh workflow run prepare-desktop-release.yml \
--repo block/buzz \
--ref main \
-f version=0.5.3
```
# Desktop explicit version
just release-desktop 0.4.0
The equivalent GitHub UI path is **Actions → Prepare Desktop Release → Run
workflow**, select `main`, enter the version without a `v` prefix, and run it.
The local `just release-desktop <version>` recipe uses the same candidate script,
but the Actions workflow is the canonical operator path because it runs with the
release App identity and does not depend on an operator checkout.
```sh
# Relay release
just release-relay
just release-relay 0.4.0
@@ -31,8 +40,9 @@ just release-relay 0.4.0
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.
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,11 +52,26 @@ or mobile GitHub Release.
### Desktop
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.
1. Run **Prepare Desktop Release** with an explicit version. Automation fetches
the current `origin/main`, regenerates `version-bump/<version>` as one
deterministic candidate commit, records the frozen base and proposed
`desktop-v<version>` tag in `.release/desktop-candidate.json`, updates every
desktop manifest and lockfile, writes a full-SHA changelog, and opens or
updates the PR.
2. Review the recorded base and candidate SHA, the complete changelog, and CI.
The candidate must receive an approval on its exact current head. Any
regeneration changes that head and therefore requires a fresh approval.
3. Merge with **Create a merge commit**. Squash and rebase are invalid for
desktop release PRs. Repository settings and the `main` ruleset must allow
merge commits for this option to exist.
4. `auto-tag-on-release-pr-merge` verifies the two-parent merge, exact candidate
approval, and every required check, then tags the reviewed candidate—not the
merge commit—as `desktop-v<version>`.
5. The tag triggers `release.yml`. It builds and stages Apple Silicon and Intel
macOS, Windows, and Linux artifacts; publishes the versioned release only
after the complete set succeeds; then updates the rolling updater manifest
last for stable versions. A failed platform leaves no partially published
versioned release.
### Relay
@@ -143,12 +168,15 @@ for distributable builds or builds from an immutable release tag.
---
## Manual Release Retry
## Release Retry
The **Release** workflow's manual dispatch is only a retry mechanism for an
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.
`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 stable rerun also
repairs `buzz-desktop-latest/latest.json` if the original run published the
versioned release but failed during that final rolling-manifest upload. 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.
@@ -183,9 +211,11 @@ GitHub Release or a stable `mobile-vX.Y.Z` alias.
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 `desktop-v<version>` release. Intel users download the `_x64.dmg`.
(`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
@@ -205,18 +235,25 @@ host's Wayland/GStreamer/graphics stack and requires GLib >= 2.72
repository
- `gh` CLI version 2.87.0 or newer, authenticated with permission to dispatch
the candidate workflow
- Repository settings and the `main` ruleset configured to allow **merge
commits**; desktop release PRs cannot be squash- or rebase-merged
- Release tag ruleset [`14378754`](https://github.com/block/buzz/rules/14378754)
active for `mobile-v*`, with creation, update, deletion, and non-fast-forward
protections and `buzz-release-bot` as its sole always-bypass actor
active for `desktop-v*` and `mobile-v*`, with creation, update, deletion, and
non-fast-forward protections and `buzz-release-bot` as its sole always-bypass
actor
- The `buzz-release-bot` App credentials configured for GitHub Actions
- The following **GitHub Actions secrets** must also be configured for the
- The following **GitHub Actions variables and secrets** configured for the
desktop release lane:
| Secret | Purpose |
|--------|---------|
| `BUZZ_UPDATER_PUBLIC_KEY` | Tauri updater public key (minisign) |
| `TAURI_SIGNING_PRIVATE_KEY` | Tauri updater private key |
| `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` | Password for the private key |
| Name | Kind | Purpose |
|------|------|---------|
| `BUZZ_RELEASE_TAGGER_CLIENT_ID` | Variable | GitHub App client ID used to prepare candidates and create tags |
| `BUZZ_RELEASE_TAGGER_PRIVATE_KEY` | Secret | GitHub App private key |
| `OSX_CODESIGN_ROLE` | Secret | macOS signing role used by `block/apple-codesign-action` |
| `CODESIGN_S3_BUCKET` | Secret | macOS signing exchange bucket |
| `BUZZ_UPDATER_PUBLIC_KEY` or `SPROUT_UPDATER_PUBLIC_KEY` | Secret | Tauri updater public key |
| `TAURI_SIGNING_PRIVATE_KEY` | Secret | Tauri updater private key |
| `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` | Secret | Password for the private key |
Mobile candidate publication requires workflow-dispatch access and the existing
release App because strict tag protection denies direct human creation. The App
@@ -231,10 +268,26 @@ actor list.
## Troubleshooting
### `just release-desktop` fails with "must be on main branch"
### The release PR does not offer **Create a merge commit**
The immutable desktop flow cannot release until both the repository merge
settings and the `main` ruleset allow merge commits. Do not squash the PR: the
auto-tagger deliberately rejects a one-parent squash commit. Enable merge
commits, then merge the already-approved exact candidate head with **Create a
merge commit**.
### `Prepare Desktop Release` fails before opening a PR
Check the workflow run first. Confirm `BUZZ_RELEASE_TAGGER_CLIENT_ID` and
`BUZZ_RELEASE_TAGGER_PRIVATE_KEY` are configured and that the release App can
write contents and pull requests. Rerunning the preparer regenerates the
candidate from the then-current `origin/main`; if its head changes, obtain a new
approval before merging.
### Local `just release-desktop` fails with "must be on main branch"
Switch to `main` and pull latest before running the release recipe.
### `just release-desktop` fails with "working tree is dirty"
### 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
+4 -1
View File
@@ -120,7 +120,10 @@ grep -Fq "needs.release-macos-x64.result == 'success'" "$release_workflow"
grep -Fq "needs.release-linux.result == 'success'" "$release_workflow"
grep -Fq "needs.release-windows.result == 'success'" "$release_workflow"
grep -Fq "refs/tags/desktop-v{0}" "$release_workflow"
grep -Fq "if: \${{ env.already_published != 'true' && !contains(needs.setup.outputs.version, '-') }}" "$release_workflow"
grep -Fq "if: \${{ !contains(needs.setup.outputs.version, '-') }}" "$release_workflow"
if grep -Fq "env.already_published != 'true' && !contains(needs.setup.outputs.version, '-')" "$release_workflow"; then
echo "rolling updater retry is incorrectly gated by versioned publication state" >&2; exit 1
fi
grep -Fq 'group: desktop-release-${{ github.ref }}' "$release_workflow"
grep -Fq 'cancel-in-progress: false' "$release_workflow"
grep -Fq 'release artifact basename collision' "$release_workflow"