# Repository prerequisite: enable Settings → Actions → General → # "Allow GitHub Actions to create and approve pull requests". # The bot approval satisfies the "someone other than the last pusher" rule because # this workflow's bot never pushes. For SOC 2 four-eyes, this automated control # inherits the original human review of the commit already landed on protected dev. # # Security model (warden SEP-C6H): this runs on pull_request_target, so the # workflow definition AND scripts/ci/verify-clean-revert.mjs execute from the # trusted base branch (dev) — never from the PR. PR commits are fetched as git # data only: the verifier re-applies `git revert` to them as patch content and # compares tree hashes. A PR that modifies this workflow or the verifier cannot # change what executes here. actions/checkout is pinned to an immutable commit # (warden TBZ-HXQ) because this job holds a pull-request write token. The # same-repo guard below is mandatory under pull_request_target: the write token # must never run against fork-originated PRs. The approval is created through # the reviews API with commit_id bound to the verified head; if the head moves # before approval, it is withheld and the new push re-verifies. # GitHub offers no atomic compare-and-approve, so the residual read-then-approve # race is closed server-side by dev's ruleset (warden round 3): # dismiss_stale_reviews_on_push plus require_last_push_approval dismiss or ignore # any approval whose head moved afterward. Immediately before approving, the job # verifies both flags at runtime and withholds approval if either is disabled, so # the control cannot silently outlive its preconditions. # # Scope is limited to single-commit, non-merge reverts whose tree is an exact inverse; # hand-adjusted reverts (revert conflicts or edits after `git revert`) are deliberately # rejected and take the normal human-review path. name: Revert Fast Lane on: pull_request_target: types: [opened, reopened, synchronize] branches: [dev] permissions: contents: read pull-requests: write concurrency: group: revert-fastlane-${{ github.event.pull_request.number }} cancel-in-progress: true jobs: verify-and-approve: if: startsWith(github.event.pull_request.title, 'Revert "') && github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest steps: - name: Checkout trusted base (dev) uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 with: fetch-depth: 0 - name: Fetch PR commits as git data env: BASE_SHA: ${{ github.event.pull_request.base.sha }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: git fetch --no-tags origin "$BASE_SHA" "$HEAD_SHA" - name: Verify exact clean revert id: verify env: PR_BODY: ${{ github.event.pull_request.body }} BASE_SHA: ${{ github.event.pull_request.base.sha }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | set -uo pipefail reverted="$(node -e 'const match = (process.env.PR_BODY || "").match(/This reverts commit ([0-9a-f]{40})/i); process.stdout.write(match ? match[1] : "")')" args=(--base "$BASE_SHA" --head "$HEAD_SHA") if [[ -n "$reverted" ]]; then args+=(--reverted "$reverted") fi output="$(node scripts/ci/verify-clean-revert.mjs "${args[@]}" 2>&1)" status=$? printf '%s\n' "$output" verdict="" verified_reverted="" reason="verification failed" while IFS= read -r line; do case "$line" in verdict=*) verdict="${line#verdict=}" ;; reverted=*) verified_reverted="${line#reverted=}" ;; FAIL\ *) reason="${line#FAIL }" ;; esac done <<< "$output" if [[ $status -eq 0 && "$verdict" == "pass" ]]; then echo "verdict=pass" >> "$GITHUB_OUTPUT" else echo "verdict=fail" >> "$GITHUB_OUTPUT" fi echo "reverted=$verified_reverted" >> "$GITHUB_OUTPUT" echo "reason=$reason" >> "$GITHUB_OUTPUT" - name: Approve verified revert if: steps.verify.outputs.verdict == 'pass' env: GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ github.event.pull_request.number }} REVERTED_SHA: ${{ steps.verify.outputs.reverted }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | set -euo pipefail # This control's residual read-then-approve race is closed server-side by # dev's ruleset. Refuse to approve if those guarantees are not confirmed. rules="$(gh api "repos/$GITHUB_REPOSITORY/rules/branches/dev" --jq '[.[] | select(.type=="pull_request") | .parameters | (.dismiss_stale_reviews_on_push and .require_last_push_approval)] | any' 2>/dev/null || true)" if [[ "$rules" != "true" ]]; then printf '## Revert Fast Lane\n\nApproval withheld: could not confirm the dev ruleset still enforces dismiss_stale_reviews_on_push AND require_last_push_approval, which this control depends on to invalidate any approval whose head moves.\n' >> "$GITHUB_STEP_SUMMARY" exit 0 fi # Bind the approval to the verified head. If the head moved since # verification, withhold — the synchronize event re-runs verification. current="$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq .head.sha)" if [[ "$current" != "$HEAD_SHA" ]]; then printf '## Revert Fast Lane\n\nHead moved after verification (%s -> %s); approval withheld. The new push re-runs verification.\n' "$HEAD_SHA" "$current" >> "$GITHUB_STEP_SUMMARY" exit 0 fi body="Machine-verified at \`$HEAD_SHA\`: this PR's tree is the exact inverse of commit \`$REVERTED_SHA\`, which previously landed on protected \`dev\` via a reviewed PR. Auto-approval per the clean-revert fast-lane control, bound to the verified commit; any further push re-runs verification." gh api -X POST "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews" \ -f commit_id="$HEAD_SHA" \ -f event=APPROVE \ -f body="$body" > /dev/null printf '## Revert Fast Lane\n\nPASS: %s\n' "$body" >> "$GITHUB_STEP_SUMMARY" - name: Record ineligible revert if: steps.verify.outputs.verdict != 'pass' env: REASON: ${{ steps.verify.outputs.reason }} run: | printf '## Revert Fast Lane\n\nNot eligible for fast lane: %s. Needs normal human review.\n' "$REASON" >> "$GITHUB_STEP_SUMMARY"