Skip to content

ci(release): publish stable and beta desktop releases from v tags - #3150

Draft
baxen wants to merge 2 commits into
mainfrom
mbot/stable-release-lane
Draft

baxen wants to merge 2 commits into
mainfrom
mbot/stable-release-lane

Conversation

@baxen

@baxen baxen commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator

What

A public stable release lane for the desktop app, in GitHub Actions only (buzz-releases and its desktop-v* tags are unchanged).

  • desktop-stable.yml runs on a pushed v[0-9]* tag (preview tags v*-preview.* are filtered out). Before building it requires:

    • the tag is exactly vX.Y.Z or vX.Y.Z-beta.N;
    • an active tag ruleset protects refs/tags/v*;
    • the tag is the run's commit, and that commit is in main history.

    It then builds every platform with the tag's version, not tauri.conf.json's. A stable tag publishes a full release marked Latest; a beta publishes a prerelease. Both use --verify-tag, so they publish on the existing tag and never create one.

  • desktop-build.yml (new, reusable) holds the macOS aarch64/x64 sign-and-notarize, Windows, Linux, updater-signing and publication jobs moved out of release.yml. Previews, Windows/Linux candidates and stable/beta releases all call it, so they publish the same twelve assets and the same SHA256SUMS. Stable and beta builds embed the same BUZZ_UPDATER_PUBLIC_KEY with …/stable-feed/latest.json, and each platform's existing endpoint check now checks that endpoint.

  • desktop-stable-feed.yml is a manual dispatch from main, behind the desktop-stable-feed environment, that writes stable-feed/latest.json for one version. It runs the preview feed's gate, which is now scripts/promote-updater-feed.sh, shared with preview promotion: signatures, embedded endpoint/key, no rollback, read-back compare. It accepts only X.Y.Z and a published, non-draft, non-prerelease release, so betas never reach the stable feed. It also refuses to run unless the environment has required reviewers and deploys only from main, because GitHub creates a missing environment without protection (needs actions: read). scripts/preview-feed.mjs picks the feed, endpoint and version format from the version itself.

  • docs/releases.md covers the new lane, the feed, and the admin setup.

Previews are unchanged: same versions, assets, checks, feed promotion and pruning, except that preview promotion now also refuses a draft release (the shared gate checks isDraft). The visible differences are check names (desktop / …), the publish job's name, and the macOS version step, which now runs scripts/candidate-version.mjs like Windows/Linux instead of an inline copy. Preview pruning only matches -preview.N.M tags, and its test now covers a beta prerelease and the stable-feed release as well.

tauri.conf.json is not changed.

Admin setup needed before the first tag

  1. A second tag ruleset: include refs/tags/v*, exclude refs/tags/v*-preview.*, restrict creation/update/deletion, block force pushes, bypass = admin role. The workflow refuses to publish until this ruleset exists.
    • Why the exclusion: previews create their v…-preview tags with GITHUB_TOKEN (gh release create --target), and pruning deletes them the same way. GITHUB_TOKEN can't bypass a ruleset, so a ruleset covering all of v* would break every preview publication and cleanup. Making GitHub Actions a bypass actor would let any workflow on any branch create a stable tag.
    • It must be a separate ruleset. publish-desktop-release-tag.sh requires the desktop-v* ruleset to have no exclusions.
  2. Environment desktop-stable-feed: required reviewers, custom deployment branch policy with only main. Promotion fails until both are set.
  3. Signing role: confirm OSX_CODESIGN_ROLE's OIDC trust policy (see below).

Not verified

  • Whether GITHUB_TOKEN with actions: read can read the environment and its branch policies. GitHub documents that permission for these endpoints, but it hasn't run. If it can't, promotion fails closed with an API error.

  • OIDC trust for macOS signing. Tag runs present sub: repo:block/buzz-app:ref:refs/tags/v…. If the role only trusts refs/heads/main, stable macOS signing will fail. If it also pins job_workflow_ref to release.yml, moving the job into desktop-build.yml would break previews as well. I can't read the policy from here.

  • No workflow has run yet. Previews run through the reusable workflow for the first time on the next scheduled or manual run after merge. A manual gh workflow run release.yml --ref main right after merging is the cheapest end-to-end check.

  • The fnmatch exclusion behaviour (v*-preview.* matching v0.7.0-preview.22.1) is from GitHub's documented ruleset syntax. It hasn't been tried on this repository.

Validation

  • node --test on desktop-release, desktop-stable (new), packaging, preview-feed, prune-preview-releases and buzz-cli: 32/32 pass.
  • The ruleset, environment and release checks run their real jq filters against fixture responses in desktop-stable.test.mjs.
  • Mutation checks: each of these makes a test fail:
    • dropping the main-history check;
    • dropping the stable-only version check in promotion;
    • swapping the stable/preview endpoint;
    • giving stable releases --target;
    • pointing the stable channel at preview-feed;
    • accepting a draft release;
    • accepting an environment with no reviewers, or one that deploys from more than main;
    • accepting a ruleset with other exclusions.
  • actionlint 1.7.7 (with shellcheck) passes on release.yml, desktop-stable.yml and desktop-stable-feed.yml. desktop-build.yml reports one SC2015 info on the Linux check that moved verbatim from release.yml, and shellcheck passes on the new script.
  • Full tests/integration/*.test.mjs: 300/301 pass. The one failure is worktree-icon.test.mjs "macOS launchers preserve…", which fails on a missing scripts/dev-credential-helper.mjs in its temp checkout and doesn't touch these files.

--latest stays forced for stable tags. Leaving it off doesn't help, because the API's make_latest defaults to true for new non-prerelease releases. Holding Latest for an older-line hotfix would need a version comparison, and there's no older line yet.

Draft until drone00 has reviewed it and a human has watched the first preview run go through the reusable workflow.

Pushing a protected vX.Y.Z or vX.Y.Z-beta.N tag now builds every desktop
platform and publishes it on the Releases page: a stable tag as Latest,
a beta as a prerelease. The macOS, Windows, Linux, updater-signing and
publication jobs move from release.yml into the reusable desktop-build.yml,
which previews and stable releases both call, so both lanes publish the
same verified assets. Stable builds embed the stable-feed endpoint.

Serving a stable release to installed apps stays a separate, approved
dispatch (desktop-stable-feed.yml), which reuses the preview feed's
signature, endpoint and rollback gate. Betas never reach that feed.

Co-authored-by: mbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz>
Signed-off-by: Bradley Axen <baxen@squareup.com>
@baxen

baxen commented Nov 9, 2026

Copy link
Copy Markdown
Collaborator Author

🤖 Review from drone00 (agent reviewer) at head bcdfb323b

Verdict: Not ready. Two small should-fix items, no blockers. The design matches what was agreed, and preview behaviour is preserved.

What's solid

  • Preview lane unchanged. I diffed old release.yml jobs against desktop-build.yml. Preview versions still come from the caller's run number and attempt. The macOS config JSON is identical apart from the file name. Asset names, checks and job gating are the same. promote-updater-feed.sh is the old inline promotion step with the feed made a parameter.
  • Tag gate fails closed. It checks the strict tag regex, the tag pointing at github.sha, and the commit being in main history (compare merge-base). It also requires an active refs/tags/v* ruleset. I ran the ruleset jq filter, with the include swapped to desktop-v*, against the live desktop-release-tags ruleset (25970814) through gh api --jq. It returns the id, so the filter works on the real API shape.
  • Feeds stay separate. The version format picks the feed in preview-feed.mjs, the script checks feed and version format, and promotion checks the release type. Betas can't reach either feed. Prune still only matches preview-pattern tag names, so v1.0.0, betas and stable-feed are ignored.
  • Ruleset vs preview tags. The documented exclusion refs/tags/v*-preview.* covers preview tag creation (gh release create --target) and prune's tag deletion. stable-feed and preview-feed don't match v*. desktop-v* doesn't match refs/tags/v*.
  • OIDC. The repo uses the default sub with an immutable-subject prefix (repo:block@185116535/buzz-app@1364527927). The reusable-workflow move doesn't change the preview sub. Stable runs present …:ref:refs/tags/vX.Y.Z. The docs already say to confirm the signing role trusts that before the first tag.

Should-fix

  1. Stable promotion accepts a draft release (scripts/promote-updater-feed.sh, the gh release view … isPrerelease check). Publication retries three times, and its last failure leaves a draft with all 12 assets. gh release view/download fall back to drafts when you're signed in, so promotion verifies the assets and writes stable-feed/latest.json. That manifest points at releases/download/vX.Y.Z/… URLs, which 404 for the public while the release is a draft. The result: every stable install's update check fails. The docs say promotion "requires a published non-prerelease", and the code doesn't enforce the "published" part. Smallest fix: --json isDraft,isPrerelease --jq '"\(.isDraft) \(.isPrerelease)"' | grep -qx "false $prerelease", plus a test case. It applies to the preview feed too.
  2. The approval gate isn't verified (desktop-stable-feed.yml). GitHub auto-creates a missing environment with no protection. If this merges and someone dispatches before an admin sets up desktop-stable-feed, the promotion runs without approval. desktop-stable-feed doesn't exist today. The sibling desktop-release-tag environment currently has no protection rules or branch policy (as read with my token), even though its workflow comment says it's restricted to main, so environments do drift from the docs. Smallest fix, in the same fail-closed style as the ruleset check: before writing, require gh api repos/$GH_REPO/environments/desktop-stable-feed to show a required_reviewers rule and a non-null deployment_branch_policy. That needs actions: read on the job, so please confirm GITHUB_TOKEN can read it. If it can't, I'd accept a docs fix: say the environment must exist before merge, and add it to the PR checklist.

Optional

  1. Forced Latest (desktop-build.yml publish, stable case). --latest always marks the new release Latest, so a later v1.0.1 hotfix cut after v1.1.0 would take Latest. gh's default picks Latest by date and version, and previews and betas are prereleases anyway, so dropping the flag (or documenting why it's forced) is enough.
  2. The ruleset filter has no test. The fake gh in desktop-stable.test.mjs ignores --jq, so the jq filter is never run. A fixture run through jq would protect it from future edits. I checked it by hand against the live ruleset (above).
  3. Where the signing key lives (a product/security call, out of scope here). TAURI_SIGNING_* and the codesign secrets are repository secrets, so any branch workflow can read them. The v* ruleset gates this lane but not the key. Now that releases go public, consider moving them into an environment limited to main and v* tags.

Evidence (at bcdfb323bd07)

  • node --test tests/integration/*.test.mjs: 300 pass, 1 fail. The failure is worktree-icon.test.mjs "macOS launchers preserve icon…". That file isn't touched by this PR, and the test passes when run alone.
  • actionlint 1.7.7 (shellcheck integration off) is clean on all four workflows. shellcheck is clean on promote-updater-feed.sh.
  • CI was still in progress when I checked; zizmor, Semgrep and DCO had passed.

Suggestion for the first release: cut v1.0.0-beta.1 first. It exercises the whole tag lane, including OIDC signing for tag refs and annotated-tag github.sha, without becoming Latest or reaching the feed.

…ironment

Promotion now requires a published, non-draft release: a failed
publication can leave a draft whose asset URLs 404 publicly. The stable
feed dispatch fails closed unless desktop-stable-feed has required
reviewers and deploys only from main, since GitHub creates a missing
environment without protection. Tests run the real ruleset, environment
and release jq filters against fixtures.

Co-authored-by: mbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz>
Signed-off-by: Bradley Axen <baxen@squareup.com>
@baxen

baxen commented Nov 9, 2026

Copy link
Copy Markdown
Collaborator Author

🤖 Re-review from drone00 at head 1df3b7656: Ready to proceed. No blockers or should-fix items remain.

  1. Draft releases: fixed. promote-updater-feed.sh now requires "false $prerelease" from isDraft,isPrerelease. As a mutation check, I loosened the grep back to the prerelease value alone, and desktop-stable.test.mjs failed. I then restored the file.
  2. Approval gate: fixed. The new step fails closed: under set -euo pipefail with the pipeline, an API error fails the step. I ran its jq filter against the live desktop-release-tag environment, which has no protection rules, and it returns false, so an auto-created or unprotected environment is refused. Whether GITHUB_TOKEN with actions: read can read these endpoints will only show on the first run. If it can't, the step fails closed, and the PR and docs already say so.
  3. --latest: mbot's pushback is right. The API's make_latest defaults to true for new full releases, so dropping the flag changes nothing. Leave it as is.
  4. Ruleset jq test: done.
  5. Signing secrets: leaving them as they are is fine. This one is for baxen to decide outside this PR.

No regressions in the diff since bcdfb323b. Evidence at 1df3b7656:

  • The six release-related integration files pass 32/32.
  • actionlint is clean on all workflows, and shellcheck is clean on the script.
  • CI was still running when I checked.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant