镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

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

Merged
baxen merged 5 commits into
mainfrom
mbot/stable-release-lane
Nov 10, 2026
Merged

baxen merged 5 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.

@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.

@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.

@baxen
baxen marked this pull request as ready for review November 10, 2026 00:27
@baxen
baxen requested a review from a team as a code owner November 10, 2026 00:27
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-10T00:31:32.614839Z 1df3b76 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1df3b76569

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/desktop-stable.yml Resolved
baxen and others added 2 commits November 9, 2026 16:32
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>
…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 force-pushed the mbot/stable-release-lane branch from 1df3b76 to bc174e1 Compare November 10, 2026 00:32
The tag check accepted a matching ruleset whatever its bypass list, and
GITHUB_TOKEN cannot read bypass actors. A write role or App given a
bypass could then cut a stable release from any main commit.

Require the tag's pusher (github.actor, kept across re-runs) to have
admin permission, failing closed when it cannot be read, and reject a
ruleset the workflow's own token can bypass.

Co-authored-by: mbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz>
@baxen

baxen commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator Author

🤖 drone00 re-review of 42f35e2cc (publish only admin-pushed stable tags): Ready to proceed.

  • Admin pusher check: github.actor is the original pusher and stays the same on re-runs, so a non-admin can't re-run their way past it. The check fails closed when the permission can't be read, for example for an App's [bot] login or on an API error. Live: collaborators/baxen/permission returns admin.
  • Bypass check: current_user_can_bypass must be never, so always, pull_requests_only and exempt are all rejected. Live, the stable-release-tags ruleset has a single bypass actor, RepositoryRole 5 (admin), in always mode, so GITHUB_TOKEN should read never. That will only be confirmed on the first run, and a wrong reading fails closed.
  • Evidence at 42f35e2cc: desktop-stable.test.mjs passes. Swapping the admin comparison for a non-empty check, or dropping the bypass clause, each fails the suite. actionlint 1.7.12 is clean on desktop-stable, desktop-stable-feed and release.

Optional, no change needed: // "never" treats a missing field as safe. The repo-level endpoint does return the field, and the admin check covers this anyway.

This reverts commit 42f35e2.

The tag ruleset is the authority on who can cut a release: its bypass
list is an intentional admin choice, and requiring an admin pusher in
the workflow duplicates that policy and blocks deliberate changes to it.

Co-authored-by: mbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz>

@kalvinnchau kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 One release-policy timing concern and one regression-test gap. The timing concern is based on static tracing; hosted rerun behavior remains unverified.

Comment thread .github/workflows/desktop-stable.yml Resolved
Comment thread .github/workflows/desktop-build.yml Resolved
Execute desktop-build.yml's release source step: previews only from main
with no version, releases only from their own version's tag, and every
signing secret required. Disabling the comparison, or always expecting
main, now fails the suite.

The stable workflow checks only that the tag ruleset is active when it
runs, so the setup docs now require activating it before the workflow
reaches main, while no stable or beta tags exist.

Co-authored-by: mbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz>

@kalvinnchau kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Approved at Kalvin’s explicit direction, with the review concerns classified as nonblocking P3s. The release-authorization timing concern is downgraded from P2; the earlier hold recommendation is withdrawn.

Inspected the delta from a79cb57 to 241eed1: it adds the protection setup/audit instructions and production source-ref guard regression cases, addressing both comments. No production implementation changed in this delta. Earlier validation applies to a79cb57; I did not rerun tests for this documentation/test-only update. Native updater, hosted signing and live release validation remain unverified. This approval does not claim those checks were performed.

@baxen
baxen merged commit 602944d into main Nov 10, 2026
44 of 45 checks passed
@baxen
baxen deleted the mbot/stable-release-lane branch November 10, 2026 04:36
matt2e added a commit that referenced this pull request Nov 10, 2026
main renamed the preview build job from `build` to `desktop` (#3150),
so after rebasing, the download-manifest step's
`needs.build.outputs.version` resolved to an empty string on automatic
promotions. The step then failed after the updater feed had moved,
skipping downloads.json and preview cleanup. Use the same VERSION
expression as the preceding updater-feed step.

The ordering test anchored on a `cmp` call that now lives in
promote-updater-feed.sh, so findIndex returned -1 and the order check
passed vacuously. Anchor on the `promote-updater-feed.sh preview-feed`
step, require every step to be found, and require the publish step's
VERSION to match the feed step's. The old test passed against the stale
expression; the new one fails on it and passes after the fix.

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gilgtm added a commit that referenced this pull request Nov 10, 2026
…nt-progress-rows

* origin/main:
  Revert "ci: use larger GitHub runners for Rust and browser journeys (#3090)" (#3180)
  Show Goose's unset thinking level as Default, not Off (#3124)
  ci(release): publish stable and beta desktop releases from v tags (#3150)
  fix(messages): end the New messages hold when no repair can sample (#3169)

Signed-off-by: Gil Birman <gil@squareup.com>
damienrj added a commit that referenced this pull request Nov 10, 2026
…rminal-font-picker

* 'main' of https://github.com/block/buzz-app: (168 commits)
  Unify Membership sharing and live roster updates (#3147)
  fix(activity): show one working cue per agent request (#3166)
  Introduce opt-in first-agent setup (#2804)
  fix(codex): drop the security-review sign-in prompt from harness defaults (#3188)
  feat(releases): publish desktop download manifest (#3154)
  fix(mcp): preserve current attachments across edits (#2937)
  fix(agents): call the Claude and Codex first-run download "the integration" (#3165)
  chore(goose): bump bundled goose-acp to v1.58.1 (#3161)
  Revert "ci: use larger GitHub runners for Rust and browser journeys (#3090)" (#3180)
  Show Goose's unset thinking level as Default, not Off (#3124)
  ci(release): publish stable and beta desktop releases from v tags (#3150)
  fix(messages): end the New messages hold when no repair can sample (#3169)
  fix(search): make Command F search the Bestie conversation (#3084)
  fix(plugins): keep failed update checks on the plugin's Settings row instead of a toast (#3088)
  Show single-agent Stop on the activity bubble (#3140)
  fix: support explicit hosted browser origins (#3149)
  fix: keep Codex tool namespace descriptions bounded (#3128)
  fix(messages): show "New messages" in channels longer than one page (#3071)
  Keep shared agent guidance generic (#3134)
  fix(typing): a thread reply also ends the agent's channel-level typing (#3148)
  ...

Signed-off-by: damienrj <damien@squareup.com>

# Conflicts:
#	src-tauri/Cargo.toml
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.

2 participants