fix(ci): validate stacked PR merge refs (#12457)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Pull request checks protect the application and its contributors. > - The trusted CI gate verifies signed event data against live GitHub state. > - GitHub gives a stacked pull request a synthetic merge commit for its base stack. > - The current validator expects the raw base SHA as the first merge parent. > - That assumption sends safe stacked pull requests to GitHub-hosted runners. > - This pull request validates the synthetic base merge and its ancestry explicitly. > - The benefit is safe automatic AWS routing for approved stacked pull requests. ## Linked Issues or Issue Description Refs #12455 Refs #12456 **What existing behavior does this improve?** The trusted runner gate validates merge commits for master-based pull requests but rejects GitHub synthetic base merges for stacked pull requests. **Subsystem affected** GitHub Actions pull request identity and merge-state validation. **Current behavior** A trusted stacked pull request passes all numeric identity checks. The gate fails closed because the first event merge parent is a GitHub synthetic base merge instead of the raw base snapshot SHA. **Proposed behavior** The gate verifies the current base-ref tip, base-snapshot ancestry, identical event and live merge parents, the child head parent, the synthetic base merge parents, and identical event and live merge trees. **Reason and benefit** The change preserves fail-closed live-state validation while allowing approved stacked pull requests to use the isolated AWS Fleet. **Breaking changes** None for untrusted contributors. Trusted stacked pull requests can select AWS after the caller pins this workflow version. **Additional context** GitHub builds a stacked test merge in two steps. It first merges the stack base into its own current base. It then uses that synthetic commit as the first parent of the child test merge. ## What Changed - Fetch and validate the current base branch ref. - Require the event base snapshot to remain an ancestor of that ref. - Validate direct and synthetic base merge parent shapes. - Preserve the existing child-head, live-state, merge-tree, repository, and actor checks. ## Verification - Ran actionlint on the trusted workflow. - Ran the external routing suite. - Added positive coverage for the GitHub stacked merge shape. - Added fail-closed coverage for replaced base ancestry and a synthetic merge that omits the current base ref. - Replayed the checks against the live merge shape for pull request #12340. ## Risks The gate has more GitHub API reads. Its five-minute timeout and fail-closed behavior limit the effect of API errors. The accepted synthetic commit must be the same in the event and live merge, must include the current base branch as a direct parent, and must produce the same merge tree. > For core feature work, check ROADMAP.md first and discuss it in #dev before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See CONTRIBUTING.md. ## Model Used OpenAI Codex on GPT-5. The exact deployment ID and context-window size are not exposed. The model used reasoning, tool use, GitHub API access, and local code execution. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used with version and capability details - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have linked existing public items and described the issue in-PR - [x] I have not referenced internal or instance-local Paperclip issues or links - [x] My branch name describes the change and contains no internal Paperclip ticket id - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge
This commit is contained in:
parent
e48e0bd3c2
commit
7b199fcafa
|
|
@ -120,6 +120,26 @@ jobs:
|
|||
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 inspect the current base branch'
|
||||
live_base_ref_sha="$(jq -r '.object.sha // empty' <<< "$base_ref_json")"
|
||||
is_commit_sha "$live_base_ref_sha" || fail_closed 'current base branch has no valid commit SHA'
|
||||
|
||||
base_comparison="$(gh api \
|
||||
-H 'Accept: application/vnd.github+json' \
|
||||
-H 'X-GitHub-Api-Version: 2022-11-28' \
|
||||
"/repos/paperclipai/paperclip/compare/$EVENT_BASE_SHA...$live_base_ref_sha" 2>/dev/null)" \
|
||||
|| fail_closed 'could not compare the triggering and current base branches'
|
||||
jq -e \
|
||||
--arg event_base_sha "$EVENT_BASE_SHA" '
|
||||
.merge_base_commit.sha == $event_base_sha and
|
||||
(.status == "ahead" or .status == "identical")
|
||||
' <<< "$base_comparison" >/dev/null 2>&1 \
|
||||
|| fail_closed 'current base branch does not descend from the triggering base snapshot'
|
||||
|
||||
event_merge_json="$(gh api \
|
||||
-H 'Accept: application/vnd.github+json' \
|
||||
-H 'X-GitHub-Api-Version: 2022-11-28' \
|
||||
|
|
@ -137,12 +157,11 @@ jobs:
|
|||
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
|
||||
(.parents[0].sha | test("^[0-9a-f]{40}$")) and
|
||||
(.tree.sha | test("^[0-9a-f]{40}$"))
|
||||
' <<< "$merge_json" >/dev/null 2>&1
|
||||
}
|
||||
|
|
@ -152,6 +171,28 @@ jobs:
|
|||
validate_merge_commit "$live_merge_json" "$live_merge_sha" \
|
||||
|| fail_closed 'current merge commit does not match the triggering base and head'
|
||||
|
||||
event_merge_parent="$(jq -r '.parents[0].sha' <<< "$event_merge_json")"
|
||||
live_merge_parent="$(jq -r '.parents[0].sha' <<< "$live_merge_json")"
|
||||
[[ "$event_merge_parent" == "$live_merge_parent" ]] \
|
||||
|| fail_closed 'current merge commit uses a different base merge parent'
|
||||
|
||||
if [[ "$event_merge_parent" != "$live_base_ref_sha" ]]; then
|
||||
base_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_parent" 2>/dev/null)" \
|
||||
|| fail_closed 'could not inspect the stacked base merge commit'
|
||||
jq -e \
|
||||
--arg expected_sha "$event_merge_parent" \
|
||||
--arg live_base_ref_sha "$live_base_ref_sha" '
|
||||
.sha == $expected_sha and
|
||||
(.parents | length) == 2 and
|
||||
any(.parents[]; .sha == $live_base_ref_sha) and
|
||||
(.tree.sha | test("^[0-9a-f]{40}$"))
|
||||
' <<< "$base_merge_json" >/dev/null 2>&1 \
|
||||
|| fail_closed 'stacked base merge commit does not contain the current base branch'
|
||||
fi
|
||||
|
||||
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" ]] \
|
||||
|
|
|
|||
Loading…
Reference in New Issue