From 3b295b05dc8c8dd82c12e4a9c6f721446c5cb2e8 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Fri, 28 Aug 2026 11:16:42 -0500 Subject: [PATCH] fix(ci): validate equivalent PR merge refs (#12441) ## Summary - validate the live master ref and event base SHA before AWS routing - accept GitHub synthetic merge commits only when the event and live commits have the exact expected base/head parents and identical tree - preserve fail-closed routing for malformed, stale, replaced, or untrusted events ## Canary finding A trusted reopened PR produced two synthetic merge SHAs with different timestamps but identical current base/head parents and tree. The former exact-SHA comparison safely fell back to GitHub-hosted runners, but could not route a valid event to AWS. ## Validation - `actionlint .github/workflows/pr-trusted.yml .github/workflows/pr.yml` - `node --test ./scripts/__tests__/e2e-shard.test.mjs` - real-event gate simulation selects the Fleet label for equivalent merge commits - negative simulations keep an untrusted sender and replaced head on `ubuntu-latest` - AWS routing remains disabled during rotation --- .github/workflows/pr-trusted.yml | 68 ++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pr-trusted.yml b/.github/workflows/pr-trusted.yml index 960c415ba6..30fc834655 100644 --- a/.github/workflows/pr-trusted.yml +++ b/.github/workflows/pr-trusted.yml @@ -40,6 +40,7 @@ jobs: EVENT_SENDER_ID: ${{ github.event.sender.id }} EVENT_BASE_REPOSITORY_ID: ${{ github.event.pull_request.base.repo.id }} EVENT_BASE_REF: ${{ github.event.pull_request.base.ref }} + EVENT_BASE_SHA: ${{ github.event.pull_request.base.sha }} EVENT_HEAD_SHA: ${{ github.event.pull_request.head.sha }} EVENT_MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }} RUN_ID: ${{ github.run_id }} @@ -59,6 +60,10 @@ jobs: [[ "$1" =~ ^[1-9][0-9]*$ ]] } + is_commit_sha() { + [[ "$1" =~ ^[0-9a-f]{40}$ ]] + } + is_allowed() { local user_id="$1" jq -e --argjson user_id "$user_id" 'index($user_id) != null' \ @@ -86,6 +91,10 @@ jobs: is_allowed "$user_id" || fail_closed "GitHub user ID $user_id is not allowlisted" done + for commit_sha in "$EVENT_BASE_SHA" "$EVENT_HEAD_SHA" "$EVENT_MERGE_SHA"; do + is_commit_sha "$commit_sha" || fail_closed 'event contains a malformed commit SHA' + done + pr_json="$(gh api \ -H 'Accept: application/vnd.github+json' \ -H 'X-GitHub-Api-Version: 2022-11-28' \ @@ -96,18 +105,69 @@ jobs: --argjson repository_id 1170821064 \ --argjson author_id "$EVENT_PR_AUTHOR_ID" \ --arg base_ref "$EVENT_BASE_REF" \ + --arg base_sha "$EVENT_BASE_SHA" \ --arg head_sha "$EVENT_HEAD_SHA" \ - --arg merge_sha "$EVENT_MERGE_SHA" ' + ' .state == "open" and .user.id == $author_id and .base.repo.id == $repository_id and .base.ref == $base_ref and - .head.sha == $head_sha and - ($merge_sha != "") and - .merge_commit_sha == $merge_sha + .base.sha == $base_sha and + .head.sha == $head_sha ' <<< "$pr_json" >/dev/null 2>&1 \ || fail_closed 'current pull request state does not match the triggering event' + live_merge_sha="$(jq -r '.merge_commit_sha // empty' <<< "$pr_json")" + is_commit_sha "$live_merge_sha" || fail_closed 'current pull request has no valid merge SHA' + + base_ref_json="$(gh api \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + "/repos/paperclipai/paperclip/git/ref/heads/$EVENT_BASE_REF" 2>/dev/null)" \ + || fail_closed 'could not refresh the base branch ref' + + jq -e --arg base_sha "$EVENT_BASE_SHA" ' + .object.type == "commit" and .object.sha == $base_sha + ' <<< "$base_ref_json" >/dev/null 2>&1 \ + || fail_closed 'base branch moved after the triggering event' + + event_merge_json="$(gh api \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + "/repos/paperclipai/paperclip/git/commits/$EVENT_MERGE_SHA" 2>/dev/null)" \ + || fail_closed 'could not inspect the event merge commit' + + live_merge_json="$(gh api \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + "/repos/paperclipai/paperclip/git/commits/$live_merge_sha" 2>/dev/null)" \ + || fail_closed 'could not inspect the current merge commit' + + validate_merge_commit() { + local merge_json="$1" + local expected_sha="$2" + jq -e \ + --arg expected_sha "$expected_sha" \ + --arg base_sha "$EVENT_BASE_SHA" \ + --arg head_sha "$EVENT_HEAD_SHA" ' + .sha == $expected_sha and + (.parents | length) == 2 and + .parents[0].sha == $base_sha and + .parents[1].sha == $head_sha and + (.tree.sha | test("^[0-9a-f]{40}$")) + ' <<< "$merge_json" >/dev/null 2>&1 + } + + validate_merge_commit "$event_merge_json" "$EVENT_MERGE_SHA" \ + || fail_closed 'event merge commit does not match the current base and head' + validate_merge_commit "$live_merge_json" "$live_merge_sha" \ + || fail_closed 'current merge commit does not match the triggering base and head' + + event_merge_tree="$(jq -r '.tree.sha' <<< "$event_merge_json")" + live_merge_tree="$(jq -r '.tree.sha' <<< "$live_merge_json")" + [[ "$event_merge_tree" == "$live_merge_tree" ]] \ + || fail_closed 'current merge tree differs from the triggering merge tree' + run_json="$(gh api \ -H 'Accept: application/vnd.github+json' \ -H 'X-GitHub-Api-Version: 2022-11-28' \