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:
@@ -91,7 +91,7 @@ jobs:
|
||||
echo "enabled=true"
|
||||
echo "tag=${TAG_PREFIX}${VERSION}"
|
||||
if [[ "$TAG_PREFIX" == desktop-v ]]; then
|
||||
echo "target_sha=${{ github.event.pull_request.head.sha }}"
|
||||
echo "target_sha=${{ github.event.pull_request.merge_commit_sha }}"
|
||||
echo "desktop=true"
|
||||
else
|
||||
echo "target_sha=$GITHUB_SHA"
|
||||
@@ -111,7 +111,7 @@ jobs:
|
||||
PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
||||
PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
|
||||
PR_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
PR_PUSHER: ${{ github.event.pull_request.head.user.login }}
|
||||
MERGED_BY: ${{ github.event.pull_request.merged_by.login }}
|
||||
MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
run: |
|
||||
VERSION="${VERSION#desktop-v}"
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
name: Desktop Release Candidate
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
name: Desktop Release Candidate
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Validate immutable desktop candidate
|
||||
if: startsWith(github.event.pull_request.head.ref, 'version-bump/')
|
||||
env:
|
||||
VERSION: ${{ github.event.pull_request.head.ref }}
|
||||
run: |
|
||||
VERSION="${VERSION#version-bump/}"
|
||||
scripts/desktop_release.py validate --candidate HEAD --version "$VERSION" --repo "$GITHUB_REPOSITORY"
|
||||
@@ -1,38 +0,0 @@
|
||||
name: Prepare Desktop Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: Semver to prepare (for example 0.5.1)
|
||||
required: true
|
||||
|
||||
env:
|
||||
RELEASE_AUTOMATION_NAME: Carl
|
||||
RELEASE_AUTOMATION_EMAIL: c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
if: github.repository == 'block/buzz'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Create short-lived release preparer token
|
||||
id: preparer
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
with:
|
||||
client-id: ${{ vars.BUZZ_RELEASE_TAGGER_CLIENT_ID }}
|
||||
private-key: ${{ secrets.BUZZ_RELEASE_TAGGER_PRIVATE_KEY }}
|
||||
permission-contents: write
|
||||
permission-pull-requests: write
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ steps.preparer.outputs.token }}
|
||||
- uses: cashapp/activate-hermit@cea9af7913204a965fd488637a8d1811bba2e616 # v1
|
||||
- name: Prepare immutable candidate and open or update PR
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.preparer.outputs.token }}
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: scripts/prepare-desktop-release.sh "$VERSION"
|
||||
+29
-40
@@ -5,7 +5,7 @@ Mobile uses immutable release-candidate tags cut directly from remote `main`:
|
||||
|
||||
| Lane | Entry point | Artifact |
|
||||
|------|-------------|----------|
|
||||
| Desktop | `Prepare Desktop Release` | Packaged desktop app (signed/notarized macOS, unsigned Windows, and Linux) |
|
||||
| Desktop | `just release-desktop <version>` | Packaged desktop app (signed/notarized macOS, unsigned Windows, and Linux) |
|
||||
| Relay | `just release-relay` | `ghcr.io/block/buzz` container image |
|
||||
| Mobile | `scripts/mobile-release.sh candidate X.Y.Z` | Exact `mobile-vX.Y.Z-rc.N` source identity |
|
||||
|
||||
@@ -16,20 +16,15 @@ remains manual because OSS CI cannot trigger private CI.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Desktop releases are prepared from the current remote `main` by GitHub Actions:
|
||||
Prepare desktop releases locally from an up-to-date, clean `main` checkout:
|
||||
|
||||
```sh
|
||||
gh workflow run prepare-desktop-release.yml \
|
||||
--repo block/buzz \
|
||||
--ref main \
|
||||
-f version=0.5.3
|
||||
just release-desktop 0.5.3
|
||||
```
|
||||
|
||||
The equivalent GitHub UI path is **Actions → Prepare Desktop Release → Run
|
||||
workflow**, select `main`, enter the version without a `v` prefix, and run it.
|
||||
The local `just release-desktop <version>` recipe uses the same candidate script,
|
||||
but the Actions workflow is the canonical operator path because it runs with the
|
||||
release App identity and does not depend on an operator checkout.
|
||||
The recipe generates the immutable candidate and opens or updates its pull
|
||||
request. Candidate branch creation uses the operator's GitHub permissions; the
|
||||
release App is intentionally limited to creating protected release tags.
|
||||
|
||||
```sh
|
||||
# Relay release
|
||||
@@ -52,21 +47,23 @@ or mobile GitHub Release.
|
||||
|
||||
### Desktop
|
||||
|
||||
1. Run **Prepare Desktop Release** with an explicit version. Automation fetches
|
||||
the current `origin/main`, regenerates `version-bump/<version>` as one
|
||||
1. Run `just release-desktop <version>` from a clean, up-to-date `main` checkout.
|
||||
The script fetches the current `origin/main`, regenerates
|
||||
`version-bump/<version>` as one
|
||||
deterministic candidate commit, records the frozen base and proposed
|
||||
`desktop-v<version>` tag in `.release/desktop-candidate.json`, updates every
|
||||
desktop manifest and lockfile, writes a full-SHA changelog, and opens or
|
||||
updates the PR.
|
||||
2. Review the recorded base and candidate SHA, the complete changelog, and CI.
|
||||
The candidate must receive an approval on its exact current head. Any
|
||||
regeneration changes that head and therefore requires a fresh approval.
|
||||
3. Merge with **Create a merge commit**. Squash and rebase are invalid for
|
||||
desktop release PRs. Repository settings and the `main` ruleset must allow
|
||||
merge commits for this option to exist.
|
||||
4. `auto-tag-on-release-pr-merge` verifies the two-parent merge, exact candidate
|
||||
approval, and every required check, then tags the reviewed candidate—not the
|
||||
merge commit—as `desktop-v<version>`.
|
||||
The required **Desktop Release Candidate** check validates the exact head.
|
||||
Authorization is either an approval on that exact head or a permitted Default
|
||||
ruleset bypass at merge time. Any regeneration changes the head and requires
|
||||
the checks—and, for the review path, approval—to run again.
|
||||
3. **Squash merge** the PR. The protected branch must still be exactly the
|
||||
recorded base; otherwise regenerate the candidate from current `main`.
|
||||
4. `auto-tag-on-release-pr-merge` verifies the frozen parent, full-tree identity,
|
||||
required checks, and one of the two authorization paths, then tags the squash
|
||||
commit as `desktop-v<version>`.
|
||||
5. The tag triggers `release.yml`. It builds and stages Apple Silicon and Intel
|
||||
macOS, Windows, and Linux artifacts; publishes the versioned release only
|
||||
after the complete set succeeds; then updates the rolling updater manifest
|
||||
@@ -233,10 +230,10 @@ host's Wayland/GStreamer/graphics stack and requires GLib >= 2.72
|
||||
- **Write access** to the `block/buzz` GitHub repository
|
||||
- An `origin` remote whose configured URL is the canonical `block/buzz`
|
||||
repository
|
||||
- `gh` CLI version 2.87.0 or newer, authenticated with permission to dispatch
|
||||
the candidate workflow
|
||||
- Repository settings and the `main` ruleset configured to allow **merge
|
||||
commits**; desktop release PRs cannot be squash- or rebase-merged
|
||||
- `gh` CLI authenticated with permission to push the candidate branch and open
|
||||
its pull request
|
||||
- The Default `main` ruleset configured for squash-only merging, strict required
|
||||
checks, stale-review dismissal, and the **Desktop Release Candidate** check
|
||||
- Release tag ruleset [`14378754`](https://github.com/block/buzz/rules/14378754)
|
||||
active for `desktop-v*` and `mobile-v*`, with creation, update, deletion, and
|
||||
non-fast-forward protections and `buzz-release-bot` as its sole always-bypass
|
||||
@@ -247,7 +244,7 @@ host's Wayland/GStreamer/graphics stack and requires GLib >= 2.72
|
||||
|
||||
| Name | Kind | Purpose |
|
||||
|------|------|---------|
|
||||
| `BUZZ_RELEASE_TAGGER_CLIENT_ID` | Variable | GitHub App client ID used to prepare candidates and create tags |
|
||||
| `BUZZ_RELEASE_TAGGER_CLIENT_ID` | Variable | GitHub App client ID used to create protected release tags |
|
||||
| `BUZZ_RELEASE_TAGGER_PRIVATE_KEY` | Secret | GitHub App private key |
|
||||
| `OSX_CODESIGN_ROLE` | Secret | macOS signing role used by `block/apple-codesign-action` |
|
||||
| `CODESIGN_S3_BUCKET` | Secret | macOS signing exchange bucket |
|
||||
@@ -268,21 +265,13 @@ actor list.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### The release PR does not offer **Create a merge commit**
|
||||
### The desktop candidate is stale or cannot be squash merged
|
||||
|
||||
The immutable desktop flow cannot release until both the repository merge
|
||||
settings and the `main` ruleset allow merge commits. Do not squash the PR: the
|
||||
auto-tagger deliberately rejects a one-parent squash commit. Enable merge
|
||||
commits, then merge the already-approved exact candidate head with **Create a
|
||||
merge commit**.
|
||||
|
||||
### `Prepare Desktop Release` fails before opening a PR
|
||||
|
||||
Check the workflow run first. Confirm `BUZZ_RELEASE_TAGGER_CLIENT_ID` and
|
||||
`BUZZ_RELEASE_TAGGER_PRIVATE_KEY` are configured and that the release App can
|
||||
write contents and pull requests. Rerunning the preparer regenerates the
|
||||
candidate from the then-current `origin/main`; if its head changes, obtain a new
|
||||
approval before merging.
|
||||
Do not update the branch manually and do not weaken the ruleset. Run
|
||||
`just release-desktop <version>` again from current `main`; this regenerates the
|
||||
candidate, reruns CI, and requires a fresh approval when using the review path.
|
||||
The post-merge verifier refuses to tag a squash whose parent differs from the
|
||||
recorded candidate base or whose tree differs from the validated PR head.
|
||||
|
||||
### Local `just release-desktop` fails with "must be on main branch"
|
||||
Switch to `main` and pull latest before running the release recipe.
|
||||
|
||||
@@ -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