Files
buzz/scripts/verify-desktop-release-merge.sh
4674750b7e fix(release): tag immutable desktop candidates (#4811)
## Summary

Redesign the permanent Desktop release flow so unrelated merges to
`main` cannot invalidate an already reviewed, green release candidate.

- Tag the immutable, API-confirmed release PR head instead of its later
squash commit.
- Treat the merged PR—including an authorized owner/admin bypass—as
publication authorization, while requiring trusted check evidence that
was complete at merge time.
- Make tag creation idempotent and collision-safe: an existing tag
succeeds only at the exact candidate SHA, and create races refetch
before accepting equality.
- Replace ancestry-based previous-release discovery with a validated
metadata ledger for side-history candidate tags.
- Compute the next release from the prior frozen base to the new frozen
base, excluding only the prior release squash SHA so unrelated commits
remain in the changelog.
- Preserve schema-1 production-tag migration and reject malformed
metadata or equal/decreasing versions.
- Update operator documentation for the normal squash-merge workflow.

This is the reusable release process for `0.5.6` onward, not the retired
one-shot `0.5.5` recovery path.

### Invariants covered

- Candidate creation → unrelated `main` merge → authorized squash merge
→ immutable candidate tag.
- Trusted producer IDs and merge-time completion timestamps; DCO's
bounded post-merge exception remains isolated.
- Missing/spoofed checks, tampered candidates, ambiguous PR
associations, conflicting tags, and equal/decreasing versions fail
closed.
- Same-SHA retries succeed; different-SHA collisions fail.
- Legacy schema-1 tag-on-main migration and schema-2 side-history
accounting both preserve the correct next-release changelog.

### Related issue

N/A — follows the Desktop release failures in #4788 and #4800 and the
recovery revert in #4808.

### Testing

At clean commit `6a91fbed8147a48cf174997de0c3e4cb2fb26474`:

- `scripts/test-desktop-release-candidate.sh`
- `scripts/test-release-ref-contract.sh`

Both focused suites passed with HEAD unchanged. Princess Donut cleared
the security/provenance surface, including the hostile merge-time
timestamp cases. Mongo cleared the side-history ledger, migration,
version-order, documentation, and contract-test surface.

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-05 11:00:14 -07:00

87 lines
3.6 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
: "${PR_HEAD_SHA:?}"
: "${MERGE_SHA:?}"
: "${MERGED_AT:?}"
: "${VERSION:?}"
: "${PR_NUMBER:?}"
: "${GH_TOKEN:?}"
# Keep this list aligned with the main ruleset. Producer IDs prevent a check
# with a copied display name from authorizing a release. Every current required
# gate is a check run; add explicit legacy-status verification before introducing
# any required context that reports only through the commit-status API.
required_checks=(
"Desktop E2E Integration:15368"
"Desktop:15368"
"Rust Lint:15368"
"Security:15368"
"Unit Tests:15368"
"Windows Rust (x86_64-pc-windows-msvc):15368"
"Mobile:15368"
"Web:15368"
"Backend Integration (relay e2e):15368"
"Desktop E2E Relay:15368"
"Relay E2E:15368"
"Desktop Build (macOS):15368"
"DCO Check:1455659"
"Desktop Release Candidate:15368"
)
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; }
# The API identity must match the closed event. Branch names are mutable and are
# never used to resolve the artifact.
pr="$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER")"
jq -e \
--arg head "$PR_HEAD_SHA" --arg head_ref "$PR_HEAD_REF" --arg head_repo "$PR_HEAD_REPO" \
--arg base "$PR_BASE_REF" --arg merge "$MERGE_SHA" --arg merged_at "$MERGED_AT" \
'.merged == true and .head.sha == $head and .head.ref == $head_ref and
.head.repo.full_name == $head_repo and .base.ref == $base and
.merge_commit_sha == $merge and .merged_at == $merged_at' <<<"$pr" >/dev/null || {
echo "pull request API identity does not match the closed merge event" >&2
exit 1
}
# Pin trusted verifier code from the candidate's frozen base, not from the
# candidate or its squash. A release PR cannot alter the code that validates it.
git fetch origin main --no-tags
git fetch origin "$PR_HEAD_SHA" --no-tags
candidate_parents="$(git show -s --format=%P "$PR_HEAD_SHA")"
[[ "$candidate_parents" =~ ^[0-9a-f]{40}$ ]] || {
echo "desktop candidate must have exactly one parent before validation" >&2
exit 1
}
git merge-base --is-ancestor "$candidate_parents" origin/main || {
echo "desktop candidate base is not protected main history" >&2
exit 1
}
verifier_dir="$(mktemp -d)"
trap 'rm -rf "$verifier_dir"' EXIT
git show "$candidate_parents:scripts/desktop_release.py" > "$verifier_dir/desktop_release.py"
git show "$candidate_parents:scripts/required-check-succeeded.jq" > "$verifier_dir/required-check-succeeded.jq"
git checkout --detach "$PR_HEAD_SHA"
DESKTOP_RELEASE_ROOT="$PWD" python3 "$verifier_dir/desktop_release.py" \
validate --candidate "$PR_HEAD_SHA" --version "$VERSION" --repo "$GITHUB_REPOSITORY"
# `filter=latest` is deliberate: GitHub exposes no per-rerun creation time. A
# post-merge rerun replaces the visible attempt and fails closed below.
checks="$(gh api --paginate --slurp "repos/$GITHUB_REPOSITORY/commits/$PR_HEAD_SHA/check-runs?filter=latest&per_page=100")"
for entry in "${required_checks[@]}"; do
required="${entry%:*}"
integration_id="${entry##*:}"
jq -e --arg name "$required" --argjson integration_id "$integration_id" \
--arg merged_at "$MERGED_AT" \
-f "$verifier_dir/required-check-succeeded.jq" <<<"$checks" >/dev/null || {
echo "trusted required check was not successful at merge: $required" >&2
exit 1
}
done
echo "verified immutable desktop candidate $PR_HEAD_SHA authorized by merged PR $PR_NUMBER"