|
| 1 | +name: SAFE-COSMETIC auto-merge |
| 2 | + |
| 3 | +# Operator-sanctioned exception to the per-push approval rule |
| 4 | +# (operator pick 2026-06-06 13:52 +04, relayed by integrator). Auto-merges a PR |
| 5 | +# carrying the `SAFE-COSMETIC` label once ALL required Linux green contexts pass |
| 6 | +# AND a head-SHA-bound HMAC authorization from the integrator is present. |
| 7 | +# Intended scope: comment / whitespace / doc-only cosmetic changes. |
| 8 | +# |
| 9 | +# SAFETY CONTROLS (all must hold to merge): |
| 10 | +# 1. Repo variable SAFE_COSMETIC_ENABLED == "true" (operator kill-switch; fail-closed) |
| 11 | +# 2. PR carries the `SAFE-COSMETIC` label (applied by integrator only) |
| 12 | +# 3. PR base branch == master (stated merge target) |
| 13 | +# 4. PR open, not draft |
| 14 | +# 5. Every context in REQUIRED below == success |
| 15 | +# 6. A valid HMAC-SHA256 authorization comment bound to the CURRENT head SHA |
| 16 | +# (operator-ratified 2026-06-06; see HMAC GATE below). Fail-closed. |
| 17 | +# |
| 18 | +# CHECK-RUN NAME MATCHING — DO NOT "FIX" TO THE UI FORM: |
| 19 | +# The check-runs REST API returns the BARE job `name:` ("Linux x86_64"), NOT |
| 20 | +# the "Workflow / Job" form shown in the PR UI ("CI / Linux x86_64"). Matching |
| 21 | +# the prefixed strings makes the gate silently never fire. The ASan job name is |
| 22 | +# literally "(AsAN+UBSan)" (sic). Both verified against live check-run data on |
| 23 | +# PR #68 on 2026-06-06. The legacy "BTC embedded smoke (Linux x86_64)" job is |
| 24 | +# deliberately NOT required here (coin-matrix "btc smoke" is the live gate). |
| 25 | +# |
| 26 | +# HMAC GATE — why head-SHA binding matters: |
| 27 | +# The integrator posts a PR comment of the EXACT shape: |
| 28 | +# SAFE-COSMETIC-AUTH: <hmac> head=<head-sha> |
| 29 | +# where <hmac> = HMAC-SHA256(INTEGRATOR_LABEL_KEY, "<repo>:<pr>:<head-sha>") |
| 30 | +# as lowercase hex. The workflow recomputes the expected value over the PR's |
| 31 | +# CURRENT head SHA and only merges if a matching comment exists. Any new push |
| 32 | +# changes the head SHA and silently invalidates every prior signature, so an |
| 33 | +# authorization can never be replayed across PRs or across SHAs. The signing |
| 34 | +# key lives only as the INTEGRATOR_LABEL_KEY Actions secret; this workflow |
| 35 | +# never emits it. |
| 36 | + |
| 37 | +on: |
| 38 | + check_suite: |
| 39 | + types: [completed] |
| 40 | + pull_request_target: |
| 41 | + types: [labeled, synchronize, reopened] |
| 42 | + |
| 43 | +permissions: |
| 44 | + contents: write |
| 45 | + pull-requests: write |
| 46 | + checks: read |
| 47 | + |
| 48 | +concurrency: |
| 49 | + group: safe-cosmetic-automerge-${{ github.event.pull_request.number || github.event.check_suite.head_sha }} |
| 50 | + cancel-in-progress: false |
| 51 | + |
| 52 | +jobs: |
| 53 | + evaluate: |
| 54 | + runs-on: ubuntu-24.04 |
| 55 | + steps: |
| 56 | + - name: Evaluate gate and merge if green and authorized |
| 57 | + env: |
| 58 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 59 | + REPO: ${{ github.repository }} |
| 60 | + ENABLED: ${{ vars.SAFE_COSMETIC_ENABLED }} |
| 61 | + INTEGRATOR_LABEL_KEY: ${{ secrets.INTEGRATOR_LABEL_KEY }} |
| 62 | + run: | |
| 63 | + set -euo pipefail |
| 64 | +
|
| 65 | + if [ "${ENABLED:-}" != "true" ]; then |
| 66 | + echo "SAFE_COSMETIC_ENABLED != true (kill-switch off) — auto-merge disabled. Exiting." |
| 67 | + exit 0 |
| 68 | + fi |
| 69 | +
|
| 70 | + LABEL="SAFE-COSMETIC" |
| 71 | + BASE_REQUIRED="master" |
| 72 | + REQUIRED=( |
| 73 | + "Linux x86_64" |
| 74 | + "Linux x86_64 (AsAN+UBSan)" |
| 75 | + "ltc smoke (Linux x86_64)" |
| 76 | + "doge smoke (Linux x86_64)" |
| 77 | + "dash smoke (Linux x86_64)" |
| 78 | + "btc smoke (Linux x86_64)" |
| 79 | + ) |
| 80 | +
|
| 81 | + # Candidate PR numbers from whichever event fired. |
| 82 | + PRS=$(jq -r '[ .pull_request.number, (.check_suite.pull_requests[]?.number) ] | map(select(. != null)) | unique | .[]' "$GITHUB_EVENT_PATH") |
| 83 | + if [ -z "${PRS:-}" ]; then |
| 84 | + echo "No candidate PRs in event payload — nothing to do." |
| 85 | + exit 0 |
| 86 | + fi |
| 87 | +
|
| 88 | + for PR in $PRS; do |
| 89 | + echo "::group::Evaluating PR #$PR" |
| 90 | + META=$(gh api "repos/$REPO/pulls/$PR" --jq '{state:.state, draft:.draft, base:.base.ref, sha:.head.sha, labels:[.labels[].name]}') |
| 91 | + STATE=$(jq -r '.state' <<<"$META") |
| 92 | + DRAFT=$(jq -r '.draft' <<<"$META") |
| 93 | + BASE=$(jq -r '.base' <<<"$META") |
| 94 | + SHA=$(jq -r '.sha' <<<"$META") |
| 95 | + HAS_LABEL=$(jq -r --arg L "$LABEL" 'any(.labels[]; . == $L)' <<<"$META") |
| 96 | +
|
| 97 | + [ "$STATE" = "open" ] || { echo "PR not open — skip."; echo "::endgroup::"; continue; } |
| 98 | + [ "$DRAFT" = "false" ] || { echo "PR is draft — skip."; echo "::endgroup::"; continue; } |
| 99 | + [ "$HAS_LABEL" = "true" ] || { echo "No $LABEL label — skip."; echo "::endgroup::"; continue; } |
| 100 | + [ "$BASE" = "$BASE_REQUIRED" ] || { echo "Base $BASE != $BASE_REQUIRED — out of scope, skip."; echo "::endgroup::"; continue; } |
| 101 | +
|
| 102 | + RUNS=$(gh api "repos/$REPO/commits/$SHA/check-runs" --paginate \ |
| 103 | + --jq '.check_runs[] | "\(.name)\t\(.conclusion)"') |
| 104 | +
|
| 105 | + ok=1 |
| 106 | + for ctx in "${REQUIRED[@]}"; do |
| 107 | + concl=$(awk -F'\t' -v c="$ctx" '$1==c {print $2; exit}' <<<"$RUNS" || true) |
| 108 | + if [ -z "$concl" ]; then echo " MISSING required context: $ctx"; ok=0; continue; fi |
| 109 | + echo " $ctx => $concl" |
| 110 | + [ "$concl" = "success" ] || ok=0 |
| 111 | + done |
| 112 | +
|
| 113 | + if [ "$ok" != "1" ]; then |
| 114 | + echo "Required contexts not all green for PR #$PR — not merging (waiting)." |
| 115 | + echo "::endgroup::" |
| 116 | + continue |
| 117 | + fi |
| 118 | +
|
| 119 | + # --- HMAC authorization gate (operator-ratified 2026-06-06) --- |
| 120 | + # Only enforced once the PR is otherwise mergeable, so an in-flight |
| 121 | + # (not-yet-green) PR is never punished for a missing signature. When a |
| 122 | + # PR IS green but lacks valid authorization bound to the current head, |
| 123 | + # fail closed: strip the label, post a refusal, and fail the job. |
| 124 | + if [ -z "${INTEGRATOR_LABEL_KEY:-}" ]; then |
| 125 | + echo "INTEGRATOR_LABEL_KEY secret unavailable — cannot verify authorization. Fail-closed." |
| 126 | + exit 1 |
| 127 | + fi |
| 128 | +
|
| 129 | + MSG="$REPO:$PR:$SHA" |
| 130 | + EXPECTED=$(printf '%s' "$MSG" | openssl dgst -sha256 -hmac "$INTEGRATOR_LABEL_KEY" | awk '{print $NF}') |
| 131 | + WANT="SAFE-COSMETIC-AUTH: $EXPECTED head=$SHA" |
| 132 | + echo " Authorization required for head $SHA (HMAC over \"$MSG\")." |
| 133 | +
|
| 134 | + if gh api "repos/$REPO/issues/$PR/comments" --paginate --jq '.[].body' \ |
| 135 | + | tr -d '\r' | grep -Fqx "$WANT"; then |
| 136 | + echo " Valid SAFE-COSMETIC authorization found for head $SHA — proceeding." |
| 137 | + else |
| 138 | + echo " NO valid authorization comment bound to head $SHA — fail-closed." |
| 139 | + gh api -X DELETE "repos/$REPO/issues/$PR/labels/$LABEL" >/dev/null 2>&1 || true |
| 140 | + gh pr comment "$PR" --repo "$REPO" --body \ |
| 141 | + "SAFE-COSMETIC auto-merge refused: no valid integrator authorization bound to head \`$SHA\`. The \`$LABEL\` label has been removed. Re-apply it after the integrator posts a fresh \`SAFE-COSMETIC-AUTH:\` signature for this exact head SHA." || true |
| 142 | + exit 1 |
| 143 | + fi |
| 144 | + # --- end HMAC gate --- |
| 145 | +
|
| 146 | + echo "All required contexts green AND authorization valid on PR #$PR — merging into $BASE_REQUIRED." |
| 147 | + gh pr merge "$PR" --repo "$REPO" --merge --delete-branch=false |
| 148 | + echo "::endgroup::" |
| 149 | + done |
0 commit comments