mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(release): make desktop tagging squash-safe (#3965)
## Summary - validate desktop release candidates before merge and keep the repository squash-only - tag the squash commit only after proving frozen-base parent and complete-tree identity with the validated PR head - accept either an exact-head approval or the durable Default-ruleset bypass record as release authorization - remove the unusable App-backed preparation workflow; retain `just release-desktop` ## Ruleset follow-up After this PR merges, update Default ruleset `13596885` to: - enable strict required status checks - dismiss stale reviews on push and require approval after the last push - require the integration-bound `Desktop Release Candidate` check The next desktop release should be cut only after that settings update. ## Verification At commit `d8c254db427eedbcffac1a6e078e90d1d0f5e151` with a clean worktree: - `scripts/test-release-ref-contract.sh` - `scripts/test-desktop-release-candidate.sh` - `bash -n scripts/verify-desktop-release-merge.sh scripts/prepare-desktop-release.sh scripts/test-release-ref-contract.sh` - `git diff --check` The bypass test fixture is the captured rule-suite shape from real squash merge PR #2864 / suite `3520068134`. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
def expected_default_pull_request_rule($ruleset_id):
|
||||
[.rule_evaluations[] | select(
|
||||
.rule_source.type == "ruleset" and
|
||||
.rule_source.id == $ruleset_id and
|
||||
.enforcement == "active" and
|
||||
.rule_type == "pull_request" and
|
||||
.result == "fail"
|
||||
)] | length == 1;
|
||||
|
||||
.result == "bypass" and expected_default_pull_request_rule($ruleset_id)
|
||||
@@ -0,0 +1 @@
|
||||
{"id":3520068134,"actor_id":15384764,"actor_name":"wpfleger96","before_sha":"6e02e0a9022a1a098c44ee611b4d9784addb10c7","after_sha":"209536ade6c5ebf7fa82671d7ca0b74f599a40cc","ref":"refs/heads/main","repository_id":1174789082,"repository_name":"buzz","pushed_at":"2026-07-31T11:06:21-06:00","result":"bypass","rule_evaluations":[{"rule_source":{"type":"ruleset","id":13596885,"name":"Default"},"enforcement":"active","result":"fail","rule_type":"pull_request","details":"1 review requesting changes by reviewers with write access."},{"rule_source":{"type":"ruleset","id":13596885,"name":"Default"},"enforcement":"active","result":"pass","rule_type":"required_status_checks"},{"rule_source":{"type":"ruleset","id":13596885,"name":"Default"},"enforcement":"active","result":"pass","rule_type":"non_fast_forward"},{"rule_source":{"type":"ruleset","id":13596885,"name":"Default"},"enforcement":"active","result":"pass","rule_type":"deletion"}]}
|
||||
@@ -71,7 +71,7 @@ cat >"$body" <<EOF
|
||||
- **Previous desktop release:** \`$previous_tag\`
|
||||
- **Proposed immutable tag:** \`desktop-v$version\`
|
||||
|
||||
This PR must be merged with **Create a merge commit**. Squash/rebase, stale-head approval, incomplete notes, or a candidate mismatch produce no tag.
|
||||
This PR must be **squash merged** only after the Desktop Release Candidate check passes. The branch must remain based directly on current `main`; stale base, payload drift, incomplete notes, or an unauthorized merge produce no tag.
|
||||
|
||||
The checked-in changelog accounts for every non-merge commit in the release range. Publication remains bound to the immutable candidate tag.
|
||||
EOF
|
||||
|
||||
@@ -62,8 +62,26 @@ 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 'target_sha=${{ github.event.pull_request.merge_commit_sha }}' "$auto_tag"
|
||||
grep -Fq 'scripts/verify-desktop-release-merge.sh' "$auto_tag"
|
||||
|
||||
bypass_filter="$repo_root/scripts/desktop-release-bypass-authorized.jq"
|
||||
bypass_fixture="$repo_root/scripts/fixtures/desktop-release-rule-suite-bypass.json"
|
||||
jq -e --argjson ruleset_id 13596885 -f "$bypass_filter" "$bypass_fixture" >/dev/null || {
|
||||
echo "real squash-bypass fixture was rejected" >&2
|
||||
exit 1
|
||||
}
|
||||
for mutation in \
|
||||
'.result = "pass"' \
|
||||
'(.rule_evaluations[] | select(.rule_type == "pull_request")).result = "pass"' \
|
||||
'(.rule_evaluations[] | select(.rule_type == "pull_request")).rule_source.id = 0' \
|
||||
'(.rule_evaluations[] | select(.rule_type == "pull_request")).enforcement = "evaluate"' \
|
||||
'del(.rule_evaluations[] | select(.rule_type == "pull_request"))'; do
|
||||
if jq "$mutation" "$bypass_fixture" | jq -e --argjson ruleset_id 13596885 -f "$bypass_filter" >/dev/null; then
|
||||
echo "bypass filter accepted invalid fixture mutation: $mutation" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
review_filter="$repo_root/scripts/review-decision-approved.jq"
|
||||
for fixture in \
|
||||
'{"reviewDecision":"CHANGES_REQUESTED"}' \
|
||||
|
||||
@@ -3,10 +3,14 @@ set -euo pipefail
|
||||
|
||||
: "${PR_HEAD_SHA:?}"
|
||||
: "${MERGE_SHA:?}"
|
||||
: "${MERGED_BY:?}"
|
||||
: "${VERSION:?}"
|
||||
: "${PR_NUMBER:?}"
|
||||
: "${GH_TOKEN:?}"
|
||||
|
||||
# This ID is the release-authority policy anchor. A bypass of another ruleset
|
||||
# must never authorize a desktop release.
|
||||
readonly DEFAULT_RULESET_ID=13596885
|
||||
required_checks=(
|
||||
"Desktop E2E Integration"
|
||||
"Desktop"
|
||||
@@ -21,6 +25,7 @@ required_checks=(
|
||||
"Relay E2E"
|
||||
"Desktop Build (macOS)"
|
||||
"DCO Check"
|
||||
"Desktop Release Candidate"
|
||||
)
|
||||
|
||||
expected_branch="version-bump/$VERSION"
|
||||
@@ -30,21 +35,53 @@ expected_branch="version-bump/$VERSION"
|
||||
|
||||
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; }
|
||||
[[ "${#parents[@]}" -eq 1 ]] || { echo "desktop release was not squash merged" >&2; exit 1; }
|
||||
base_sha="$(git show "$PR_HEAD_SHA:.release/desktop-candidate.json" | jq -r .base_sha)"
|
||||
[[ "${parents[0]}" == "$base_sha" ]] || { echo "squash parent is not the frozen candidate base" >&2; exit 1; }
|
||||
[[ "$(git show -s --format=%T "$MERGE_SHA")" == "$(git show -s --format=%T "$PR_HEAD_SHA")" ]] || {
|
||||
echo "squash tree differs from the validated candidate" >&2
|
||||
exit 1
|
||||
}
|
||||
git merge-base --is-ancestor "$MERGE_SHA" origin/main || { echo "squash commit 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
|
||||
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')"
|
||||
reviews="$(gh api --paginate --slurp "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")"
|
||||
review_authorized=false
|
||||
if jq -e -f scripts/review-decision-approved.jq <<<"$review" >/dev/null && [[ "$valid_approvals" -gt 0 ]]; then
|
||||
review_authorized=true
|
||||
fi
|
||||
|
||||
# Rule suites are GitHub's durable record that a permitted bypass actor landed
|
||||
# this exact main update. The suite does not identify the matching bypass grant,
|
||||
# so the Default ruleset's bypass list is itself the release-authority policy.
|
||||
bypass_authorized=false
|
||||
for attempt in {1..5}; do
|
||||
suites="$(gh api --paginate --slurp "repos/$GITHUB_REPOSITORY/rulesets/rule-suites?ref=refs/heads/main&per_page=100")"
|
||||
mapfile -t suite_ids < <(jq -r --arg before "$base_sha" --arg after "$MERGE_SHA" --arg actor "$MERGED_BY" '
|
||||
.[][] | select(.ref == "refs/heads/main" and .before_sha == $before and .after_sha == $after and .actor_name == $actor and .result == "bypass") | .id
|
||||
' <<<"$suites")
|
||||
if [[ "${#suite_ids[@]}" -gt 1 ]]; then
|
||||
echo "multiple rule suites matched the release landing" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${#suite_ids[@]}" -eq 1 ]]; then
|
||||
suite="$(gh api "repos/$GITHUB_REPOSITORY/rulesets/rule-suites/${suite_ids[0]}")"
|
||||
if jq -e --argjson ruleset_id "$DEFAULT_RULESET_ID" -f scripts/desktop-release-bypass-authorized.jq <<<"$suite" >/dev/null; then
|
||||
bypass_authorized=true
|
||||
fi
|
||||
break
|
||||
fi
|
||||
[[ "$attempt" -eq 5 ]] || sleep "$attempt"
|
||||
done
|
||||
|
||||
[[ "$review_authorized" == true || "$bypass_authorized" == true ]] || {
|
||||
echo "release lacks an exact-head approval or authorized Default-ruleset bypass" >&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
|
||||
@@ -59,4 +96,4 @@ jq -e '(.total_count == 0) or (.state == "success")' <<<"$status" >/dev/null ||
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "verified reviewed desktop candidate $PR_HEAD_SHA at merge $MERGE_SHA"
|
||||
echo "verified desktop candidate $PR_HEAD_SHA at squash $MERGE_SHA"
|
||||
|
||||
Reference in New Issue
Block a user