Skip to content

Commit fced79c

Browse files
Fail closed on PR head drift before Dependabot auto-approve/merge
The Anthropic gate can wait up to 30 minutes while evaluating a specific head SHA. Re-read the current PR head before approving or enabling auto-merge, bind approvals to commit_id, and pass --match-head-commit so an older workflow run cannot approve or queue merge for a newer commit. Co-authored-by: Jonathan Kingston <jonathanKingston@users.noreply.github.com>
1 parent 30dac4a commit fced79c

3 files changed

Lines changed: 51 additions & 2 deletions

File tree

.github/scripts/dependabot-anthropic-gate.mjs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,16 @@ function setOutput(name, value) {
5050
appendFileSync(outputPath, `${name}<<${delimiter}\n${value}\n${delimiter}\n`);
5151
}
5252

53+
/**
54+
* Fail closed when the PR head advanced after the gate started evaluating a
55+
* specific commit. Approval and auto-merge must only act on the assessed SHA.
56+
*/
57+
export function assertPrHeadUnchanged({ currentHead, assessedHead }) {
58+
if (currentHead !== assessedHead) {
59+
throw new Error(`PR head advanced from ${assessedHead} to ${currentHead}; refusing to approve or auto-merge using stale evidence.`);
60+
}
61+
}
62+
5363
export function truncate(value, limit = MAX_BODY_CHARS) {
5464
if (!value) return '';
5565
if (value.length <= limit) return value;
@@ -552,6 +562,7 @@ async function main() {
552562
};
553563

554564
const decision = await askAnthropic({ apiKey: anthropicApiKey, model, evidence });
565+
setOutput('assessed_head_sha', headSha);
555566
setOutput('safe_to_merge', String(decision.safe_to_merge));
556567
setOutput('reason', decision.reason);
557568
setOutput('confidence', decision.confidence ?? 'unknown');

.github/scripts/dependabot-anthropic-gate.test.mjs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
matchedCursorSources,
2121
evidenceForRun,
2222
parseAnthropicDecision,
23+
assertPrHeadUnchanged,
2324
truncate,
2425
parseLinkHeader,
2526
} from './dependabot-anthropic-gate.mjs';
@@ -471,6 +472,28 @@ describe('truncate', () => {
471472
});
472473
});
473474

475+
describe('assertPrHeadUnchanged', () => {
476+
it('allows approval when the current head matches the assessed SHA', () => {
477+
assert.doesNotThrow(() =>
478+
assertPrHeadUnchanged({
479+
currentHead: HEAD_SHA,
480+
assessedHead: HEAD_SHA,
481+
}),
482+
);
483+
});
484+
485+
it('fails closed when the PR head advanced after the gate assessment', () => {
486+
assert.throws(
487+
() =>
488+
assertPrHeadUnchanged({
489+
currentHead: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
490+
assessedHead: HEAD_SHA,
491+
}),
492+
/PR head advanced/,
493+
);
494+
});
495+
});
496+
474497
describe('parseLinkHeader', () => {
475498
it('extracts the next-page URL from a GitHub Link header', () => {
476499
const header = '<https://api.github.com/x?page=2>; rel="next", <https://api.github.com/x?page=3>; rel="last"';

.github/workflows/dependabot-auto-merge.yml

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,19 +69,34 @@ jobs:
6969
env:
7070
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
7171
PR_NUMBER: ${{ github.event.pull_request.number }}
72+
PR_HEAD_SHA: ${{ steps.anthropic_gate.outputs.assessed_head_sha }}
7273
REPO: ${{ github.repository }}
7374
run: |
7475
set -euo pipefail
75-
gh pr review "$PR_NUMBER" --repo "$REPO" --approve || true
76+
current_head="$(gh pr view "$PR_NUMBER" --repo "$REPO" --json headRefOid --jq '.headRefOid')"
77+
if [[ "$current_head" != "$PR_HEAD_SHA" ]]; then
78+
echo "PR head advanced from $PR_HEAD_SHA to $current_head; refusing to approve using stale evidence."
79+
exit 1
80+
fi
81+
gh api "repos/$REPO/pulls/$PR_NUMBER/reviews" \
82+
--method POST \
83+
-f event=APPROVE \
84+
-f commit_id="$PR_HEAD_SHA" || true
7685
7786
- name: Enable auto-merge for npm update
7887
if: steps.anthropic_gate.outputs.safe_to_merge == 'true'
7988
env:
8089
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
8190
PR_NUMBER: ${{ github.event.pull_request.number }}
91+
PR_HEAD_SHA: ${{ steps.anthropic_gate.outputs.assessed_head_sha }}
8292
REPO: ${{ github.repository }}
8393
run: |
8494
set -euo pipefail
95+
current_head="$(gh pr view "$PR_NUMBER" --repo "$REPO" --json headRefOid --jq '.headRefOid')"
96+
if [[ "$current_head" != "$PR_HEAD_SHA" ]]; then
97+
echo "PR head advanced from $PR_HEAD_SHA to $current_head; refusing to enable auto-merge using stale evidence."
98+
exit 1
99+
fi
85100
86101
merge_state="$(gh pr view "$PR_NUMBER" --repo "$REPO" --json mergeStateStatus,isDraft --jq '.mergeStateStatus + " " + (if .isDraft then "draft" else "ready" end)')"
87102
echo "merge_state=$merge_state"
@@ -99,7 +114,7 @@ jobs:
99114
;;
100115
esac
101116
102-
if ! out="$(gh pr merge "$PR_NUMBER" --repo "$REPO" --auto --merge 2>&1)"; then
117+
if ! out="$(gh pr merge "$PR_NUMBER" --repo "$REPO" --auto --merge --match-head-commit "$PR_HEAD_SHA" 2>&1)"; then
103118
echo "Could not enable auto-merge for PR; leaving it unqueued."
104119
echo "$out"
105120
exit 0

0 commit comments

Comments
 (0)