Repository navigation
fix(pr-merge): keep a cost hold when a newer lane run was deleted - #10515
Conversation
A cost decision's decided head is the newest cost notice a review lane's status job recorded before the decision. Deleting the lane runs of a newer head promoted an older notice to the decided head, and a later head with that older head's net change then carried the decision. The decided head now stands only while the head branch's push history (the repository activity API, which deleting a run leaves) shows its push before the decision and a run of every review lane, started before the decision, on each head pushed after that push and before the decision. No such push, a full page or an unreadable history carries nothing. The rule is merge-holds.mjs unranHead, mirrored by babysit_merge_policy.unran_head and called from the ci-status cost-hold step through the base's merge-holds.mjs. Closes #10437 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
claude-review reviewed No findings. I read the diff and the pull request context. I did not run any tests, so everything the PR says about passing suites is author-claimed and unverified by me. What I checked
Notes, not findings
|
|
claude-security-review reviewed critical-findings: 0 No security findings. I read the whole diff, including the trusted-path files The change only tightens the cost hold. The decided head is kept only if the branch's push history (
|
|
The pr-unblock lane stopped on this pull request and needs a person: the merge stages a change under .github/workflows/, which the lane does not push.
To resume, deal with the reason above, remove the gh workflow run pr-unblock.yml -R melodic-software/claude-code-plugins -f pr-number=10515 -f activity=resolve-conflicts |
merge-holds.test.mjs: keep this branch's unranHead import beside main's tempDir helper import (#10513), which replaced the test's own mkdtempSync/tmpdir use. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The pr-unblock lane escalated this pull request as |
Closes #10437
Summary
The cost hold took a decision's decided head from the newest cost notice that a review lane's status job recorded before the decision (#10357). If someone deleted the lane runs of a newer head, the older run's notice became the newest. An older head then became the decided head, and a later head with the same net change as that older head carried the owner's decision. This was the residual #10357 accepted, raised in review thread r4383606958.
The cost hold now checks the head branch's push history against the lane runs on each head pushed in that history. Deleting a run does not remove its push from that history. This change only tightens the hold: every new path keeps the hold, and no condition that cleared it before clears it now.
Fix
The rule is the same in all three readers:
per_page=100, newest first) of the repository activity API forrefs/heads/<head branch>:GET /repos/{repo}/activity?ref=refs%2Fheads%2F<branch>&direction=desc&per_page=100. Each entry with a non-zeroaftersha is a head the branch moved to.pr-review-hosted.ymlandpr-review-security-hosted.yml) from this repository, started before the decision. A run from only one lane is not enough: the lane runs left after a partial deletion would otherwise still promote the older notice.Changes per reader:
merge-holds.mjs: new exportedunranHead(activity, ran, decided, at).noticedHeadcollects each lane's run heads started before the decision. OncedecidedHeadnames a head, it reads the activity page with theactionsreader and returns null whenunranHeadnames a reason. The check runs before thedecided === headshortcut.babysit_merge_policy.py:unran_headmirrors it._noticed_headreads the page through_getwithBABYSIT_VERIFY_GH_TOKENwhen that is set. A failed read raisesPolicyReadError, which carries nothing..github/workflows/pr-require-checks.yml): the runs loop buildsran, then the step reads the activity page and runs the base'smerge-holds.mjsunranHeadthroughholds_js, beforecarries. A base withoutunranHead(this pull request's own runs, until it merges) keeps the hold. The step comment states the rule.source-control.The token scopes stay as they are. The activity endpoint needs
contents: read, per GitHub's permissions for GitHub Apps ("Contents",GET /repos/{owner}/{repo}/activity, read, UAT and IAT tokens). Theci-statusjob (pr-require-checks.yml) and thepr-merge.ymljobs that passBABYSIT_VERIFY_GH_TOKENalready grantcontents: read. That the ActionsGITHUB_TOKENcan read this endpoint in practice is UNVERIFIED: the docs list installation tokens, and only a localghtoken was used to probe it. If it cannot, every cost decision keeps its hold, so the change still fails closed.Known holds this keeps in the safe direction, where a person lifts the label as before:
pull_requestworkflow then;[skip ci]push or a push while the pull request was closed;Verification
Each new case was run against
origin/main'smerge-holds.mjs,babysit_merge_policy.pyandpr-require-checks.yml, and fails there because the decision carries. With the fix, every case passes.node --test .github/scripts/pr-merge/merge-holds.test.mjs: 41 pass, 0 fail. On main: 4 fail. Two new tests: a deleted run of either lane or both, a run started after the decision, a fork run, the decided head pushed back, no push of the decided head, a full page, a failed read, and the non-deleted and pushed-after-decision cases that still carry. Two existing read-order assertions gain theactivityread.node --test .github/scripts/pr-merge/enable-auto-merge.test.mjs: 137 pass. Its mock gains the activity route.python3 -m unittest tests.test_babysit_merge_cost tests.test_merge_holds_sync(babysit-prs scripts): 38 tests OK. On main: 9 failures and 1 error. The JS and Python readers agree on four new parity cases:scripts/ci-hold-cost-decision.test.sh: PASS=151 FAIL=0. On main: 7 fail.actionlint .github/workflows/pr-require-checks.yml: clean.shellcheck scripts/ci-hold-cost-decision.test.sh: clean.scripts/run-ruff.sh check: clean. The Python edits followruff format; the format drift left in those files was there before.scripts/check-changelog-fragments.sh --check: valid.scripts/check-affected.sh --tests: 15 checks passed; the tests check failed only on two suites that need PyYAML, which is not installed here:ci-test-node-stop-commands.test.shandrun-hygiene-gate.test.sh. fix(pr-merge): take a cost decision's decided head from the review lane's check-run annotation #10357 recorded the same two. In that run the babysit unittest discover ran 3207 tests, OK.node --test .github/scripts/pr-merge/*.test.mjs: 529 pass, 0 fail.ghtoken on fix(pr-merge): take a cost decision's decided head from the review lane's check-run annotation #10357's branch. It returned that branch's 6 entries, newest first.Related
apply-review-hold/hold.cjsin melodic-software/ci-workflows was not read or changed.🤖 Generated with Claude Code