mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
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:
@@ -12,16 +12,16 @@ git -C "$tmp" config user.email test@example.com
|
||||
echo first >"$tmp/file"
|
||||
git -C "$tmp" add file
|
||||
git -C "$tmp" commit -qm first
|
||||
git -C "$tmp" tag -m "desktop release" v1.2.3
|
||||
git -C "$tmp" tag -m "desktop release" desktop-v1.2.3
|
||||
|
||||
(
|
||||
cd "$tmp"
|
||||
GITHUB_REF=refs/tags/v1.2.3 "$verify" v 1.2.3
|
||||
GITHUB_REF=refs/tags/desktop-v1.2.3 "$verify" desktop-v 1.2.3
|
||||
)
|
||||
|
||||
if (
|
||||
cd "$tmp"
|
||||
GITHUB_REF=refs/heads/main "$verify" v 1.2.3
|
||||
GITHUB_REF=refs/heads/main "$verify" desktop-v 1.2.3
|
||||
); then
|
||||
echo "branch-backed desktop release was accepted" >&2
|
||||
exit 1
|
||||
@@ -31,7 +31,7 @@ echo second >>"$tmp/file"
|
||||
git -C "$tmp" commit -qam second
|
||||
if (
|
||||
cd "$tmp"
|
||||
GITHUB_REF=refs/tags/v1.2.3 "$verify" v 1.2.3
|
||||
GITHUB_REF=refs/tags/desktop-v1.2.3 "$verify" desktop-v 1.2.3
|
||||
); then
|
||||
echo "release accepted HEAD after the tag commit" >&2
|
||||
exit 1
|
||||
@@ -61,6 +61,73 @@ grep -q 'private-key:.*secrets\.BUZZ_RELEASE_TAGGER_PRIVATE_KEY' "$auto_tag"
|
||||
grep -q 'permission-contents: write' "$auto_tag"
|
||||
grep -q 'GH_TOKEN:.*steps\.release-tagger\.outputs\.token' "$auto_tag"
|
||||
grep -Fq 'git/refs' "$auto_tag"
|
||||
grep -Fq 'TAG_PREFIX="desktop-v"' "$auto_tag"
|
||||
grep -Fq 'target_sha=${{ github.event.pull_request.head.sha }}' "$auto_tag"
|
||||
grep -Fq 'scripts/verify-desktop-release-merge.sh' "$auto_tag"
|
||||
review_filter="$repo_root/scripts/review-decision-approved.jq"
|
||||
for fixture in \
|
||||
'{"reviewDecision":"CHANGES_REQUESTED"}' \
|
||||
'{"reviewDecision":"REVIEW_REQUIRED"}' \
|
||||
'{"reviewDecision":null}' \
|
||||
'{}'; do
|
||||
if jq -e -f "$review_filter" <<<"$fixture" >/dev/null; then
|
||||
echo "review-decision filter accepted non-approved fixture: $fixture" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
jq -e -f "$review_filter" >/dev/null <<'JSON' || {
|
||||
{"reviewDecision":"APPROVED"}
|
||||
JSON
|
||||
echo "review-decision filter rejected approved GraphQL response" >&2
|
||||
exit 1
|
||||
}
|
||||
required_check_filter="$repo_root/scripts/required-check-succeeded.jq"
|
||||
check_fixture() {
|
||||
local expected="$1" conclusion="$2" status="${3:-completed}"
|
||||
local payload
|
||||
payload=$(jq -n --arg status "$status" --arg conclusion "$conclusion" '{check_runs: [{name: "Web", status: $status, conclusion: $conclusion, started_at: "2026-01-01T00:00:00Z"}]}')
|
||||
if jq -e --arg name Web -f "$required_check_filter" <<<"[$payload]" >/dev/null; then
|
||||
actual=pass
|
||||
else
|
||||
actual=fail
|
||||
fi
|
||||
[[ "$actual" == "$expected" ]] || {
|
||||
echo "required-check filter: expected $conclusion/$status to $expected" >&2
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
check_fixture pass success
|
||||
check_fixture pass skipped
|
||||
check_fixture pass neutral
|
||||
check_fixture fail failure
|
||||
check_fixture fail success in_progress
|
||||
# A newer failure must not be hidden by an older successful run of the same check.
|
||||
jq -e --arg name Web -f "$required_check_filter" >/dev/null <<'JSON' && {
|
||||
[{"check_runs":[
|
||||
{"name":"Web","status":"completed","conclusion":"success","started_at":"2026-01-01T00:00:00Z"},
|
||||
{"name":"Web","status":"completed","conclusion":"failure","started_at":"2026-01-02T00:00:00Z"}
|
||||
]}]
|
||||
JSON
|
||||
echo "required-check filter accepted a stale pass over a newer failure" >&2
|
||||
exit 1
|
||||
}
|
||||
release_workflow="$repo_root/.github/workflows/release.yml"
|
||||
[[ "$(grep -c 'contents: write' "$release_workflow")" -eq 1 ]] || {
|
||||
echo "desktop release must have exactly one GitHub contents writer" >&2; exit 1;
|
||||
}
|
||||
grep -Fq "needs.release.result == 'success'" "$release_workflow"
|
||||
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 'group: desktop-release-${{ github.ref }}' "$release_workflow"
|
||||
grep -Fq 'cancel-in-progress: false' "$release_workflow"
|
||||
grep -Fq 'release artifact basename collision' "$release_workflow"
|
||||
[[ "$(grep -c 'gh release upload' "$release_workflow")" -eq 2 ]] || {
|
||||
echo "only the final writer may upload versioned and rolling release assets" >&2; exit 1;
|
||||
}
|
||||
grep -Fq 'if: env.already_published' "$release_workflow"
|
||||
grep -Fq 'if gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$TAG" --silent 2>/dev/null; then' "$auto_tag"
|
||||
if grep -F 'git/ref/tags/$TAG' "$auto_tag" | grep -Fq '|| true'; then
|
||||
echo "auto-tag ignores a failed tag lookup, so a 404 body can look like an existing tag" >&2
|
||||
|
||||
Reference in New Issue
Block a user