Files
buzz/scripts/verify-desktop-release-merge.sh
T
1dfd89ea67 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>
2026-07-30 11:27:56 -07:00

63 lines
2.9 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
: "${PR_HEAD_SHA:?}"
: "${MERGE_SHA:?}"
: "${VERSION:?}"
: "${PR_NUMBER:?}"
: "${GH_TOKEN:?}"
required_checks=(
"Desktop E2E Integration"
"Desktop"
"Rust Lint"
"Security"
"Unit Tests"
"Windows Rust (x86_64-pc-windows-msvc)"
"Mobile"
"Web"
"Backend Integration (relay e2e)"
"Desktop E2E Relay"
"Relay E2E"
"Desktop Build (macOS)"
"DCO Check"
)
expected_branch="version-bump/$VERSION"
[[ "${PR_HEAD_REF:-}" == "$expected_branch" ]] || { echo "unexpected release branch" >&2; exit 1; }
[[ "${PR_BASE_REF:-}" == main ]] || { echo "desktop release must target main" >&2; exit 1; }
[[ "${PR_HEAD_REPO:-}" == "$GITHUB_REPOSITORY" ]] || { echo "desktop release must be internal" >&2; exit 1; }
git fetch origin "$MERGE_SHA" "$PR_HEAD_SHA" refs/heads/main:refs/remotes/origin/main --no-tags
mapfile -t parents < <(git show -s --format='%P' "$MERGE_SHA" | tr ' ' '\n')
[[ "${#parents[@]}" -eq 2 ]] || { echo "desktop release was not merged with a true merge commit" >&2; exit 1; }
[[ "${parents[1]}" == "$PR_HEAD_SHA" ]] || { echo "merge parent 2 is not the reviewed candidate" >&2; exit 1; }
git merge-base --is-ancestor "$PR_HEAD_SHA" origin/main || { echo "candidate is not reachable from current main" >&2; exit 1; }
git checkout --detach "$PR_HEAD_SHA"
scripts/desktop_release.py validate --candidate "$PR_HEAD_SHA" --version "$VERSION" --repo "$GITHUB_REPOSITORY"
review=$(gh api graphql -f query='query($owner:String!,$repo:String!,$number:Int!){repository(owner:$owner,name:$repo){pullRequest(number:$number){reviewDecision}}}' -F owner="${GITHUB_REPOSITORY%/*}" -F repo="${GITHUB_REPOSITORY#*/}" -F number="$PR_NUMBER" --jq '.data.repository.pullRequest')
jq -e -f scripts/review-decision-approved.jq <<<"$review" >/dev/null || {
echo "pull request effective review decision is not APPROVED" >&2
exit 1
}
reviews="$(gh api --paginate "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews?per_page=100")"
valid_approvals="$(jq --arg sha "$PR_HEAD_SHA" '[.[] | select(.state == "APPROVED" and .commit_id == $sha and (.author_association == "MEMBER" or .author_association == "OWNER" or .author_association == "COLLABORATOR"))] | length' <<<"$reviews")"
[[ "$valid_approvals" -gt 0 ]] || { echo "candidate lacks an exact-head approval from a repository member or collaborator" >&2; exit 1; }
checks="$(gh api --paginate --slurp "repos/$GITHUB_REPOSITORY/commits/$PR_HEAD_SHA/check-runs?per_page=100")"
for required in "${required_checks[@]}"; do
jq -e --arg name "$required" -f scripts/required-check-succeeded.jq <<<"$checks" >/dev/null || {
echo "required check is missing or unsuccessful: $required" >&2
exit 1
}
done
status="$(gh api "repos/$GITHUB_REPOSITORY/commits/$PR_HEAD_SHA/status")"
jq -e '(.total_count == 0) or (.state == "success")' <<<"$status" >/dev/null || {
echo "candidate has a failing or pending combined commit status" >&2
exit 1
}
echo "verified reviewed desktop candidate $PR_HEAD_SHA at merge $MERGE_SHA"