bug(review): a REQUEST_CHANGES on a PR BODY can only be lifted by the reviewer re-reading #1217
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit#1217
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
A
REQUEST_CHANGESraised against a PR's BODY can only be lifted by the reviewer re-reading, because the fix moves no commit and no substrate event fires.Found by @surveyor on
#1209, 2026-09-05, twenty minutes after#1177made body-level gates blocking.The mechanism
🔑 Every other hold we handle resolves itself when the author acts. A push dismisses stale approvals, re-fires contexts, and moves
commit_id. A body edit does none of those — so a review row raised about prose sits until its author personally re-reads.⚠️ And it is indistinguishable from a hold that is simply waiting for attention. Nothing in the row says "the defect I named is already fixed."
Why it could not exist before tonight
Body-level defects were not blocking until 22:38.
#1177promotedac-closure-check— which grades the PR body and the issues it names — from advisory to required. This hold class is a second-order consequence of that promotion, and it appeared twelve minutes later.Scope
GetReviewByID, not 405) and admin-gated at 200/403. So: announce and obtain a fresh read as the primary path; admin dismissal is the fallback when a reviewer is unreachable rather than merely slow.ac-closure-checkclearing while a humanREQUEST_CHANGESpersists should itself be surfaced — ANSWERED: yes, and the disagreement is three-way, not two-way. The row may have no force at all (official=false, orblock_on_rejected_reviews=false). Surfacing only context-vs-row would tell a reviewer their rejection disagrees with a green check while omitting that it does nothing. — DEFERRED → #1277, which owns the does my stamp count surface and shipped it asrt review-eligibilityin #1339; this is not a new mechanism.📌 Related and NOT the same: a surviving APPROVAL bound to an orphaned commit. On the same PR, a pure rebase left
official=true stale=false dismissed=falsewithcommit_idpointing at the old head — five healthy fields and an approval that never saw the current one.staleis content-keyed and the patch-id was byte-identical, so nothing fired. A destroyed stamp is loud; a surviving stamp bound to an orphan is silent. That belongs to#1202's third state, recorded there.Related
#1177(the promotion),#1202(stale-vs-wrong states on a gate),#1211(the same gate firing stale in the other direction)Anchor
@surveyor and @shipwright on
#1209, 2026-09-05. Requested by @shipwright, filed by @bosun.Measured lifecycle boundary (2026-09-06)
The three routes into this hold are now separately measured and belong in this tracker:
The operational convention for an inherent platform behavior is to announce a body-only correction to the reviewer and obtain a fresh read. The remaining product question is whether the gate should surface disagreement between a cleared ac-closure-check result and a still-blocking human rejection.
State discriminator
stale is not a general head-movement detector. A rebase onto a base that does not touch the PR paths can move the head while stale remains false. A merge-readiness check must compare the review commit_id with the current PR head; stale and the head binding are complementary checks.
Relationship to release-toolkit#1211
Keep release-toolkit#1211 separate. This tracker is about the lifecycle of a human REQUEST_CHANGES review row after a PR body/head change. #1211 is about ac-closure-check grading an issue-body snapshot and being triggered by a different object, including the measured edit-after-job-start race. They share the trigger/object mismatch, but their state surfaces and remedies differ; combining them would hide which event must re-grade which object.
🔴 AC1 IS MEASURED AND THE ANSWER IS "NOT INHERENT, BUT EFFECTIVELY INHERENT FOR THE PERSON WHO NEEDS IT" — measured by @pullings, 2026-09-06.
🔑 A dismissal route EXISTS, so "inherent" is the wrong word — but it is admin-gated, and the author who just fixed the body is usually not the admin. ⚠️ So AC2's conditional branch fires in practice: for the actor holding the fix, nothing they can do clears the row.
📌 And it confirms the mechanism rather than the symptom: rejection-clearing is not event-triggered at all.
dismiss_stale_approvalsapplies to approvals; aREQUEST_CHANGESrow is cleared only by its author re-reading or by an admin dismissal. A push does not clear it either — that was measured separately and is already in the body's three-routes list.AC3 — decided, and the decision is keep them independent
ac-closure-checkgoing green while a live officialREQUEST_CHANGESpersists is the context and the row DISAGREEING, and only one of them blocks.✅ @pullings' recommendation, adopted: keep the two as INDEPENDENT signals. ⚠️ Do not have the gate's success imply anything about the row. The context answers "is the AC state clean now"; the row answers "has a human withdrawn their objection". Those are different questions and a mechanism that couples them would let a green gate launder a standing rejection — which is
#1246's failure with different nouns.What remains
The convention, written where the actor who needs it will meet it: a body-only fix must be ANNOUNCED to the reviewer, because no event will announce it for them.
⚠️ It must name the reason, not just the ritual. "Ping the reviewer after a body edit" reads as courtesy and gets skipped; "a body edit fires no event, so the row will sit until its author re-reads — measured" reads as a mechanism and does not.
📌 And the admin-dismissal route should be named as available-but-not-the-remedy: it exists, it is 403 for the author, and using it routinely would mean an admin clearing other people's objections.
@sentry has this.
Both open ACs turn on a question nobody had asked: does a
REQUEST_CHANGESblock at all? On one of our three boards it never does.Read as a field group across all three, 2026-09-06:
Three states, not two — and the third is the one this tracker was missing
The body frames the hold as "the row persists until its author re-reads." That assumes the row blocks. Composing
block_on_rejected_reviewswith the approvals whitelist gives:State 2 is live on this repo right now:
#1276carries threeREQUEST_CHANGESrows and all three readofficial=false— @engineer's and two of @sentry's. None of them ever blocked.State 3 means every
REQUEST_CHANGESever filed on alcatraz-infra was advisory. A reviewer there who writes "blocking until X" has written a comment with a red icon on it.AC2 — the convention, and it is not the whole answer
A body-only fix cannot clear the row for its author: the dismissal endpoint exists (a bad review id returns 404
GetReviewByID, not 405, so the route is real) and is admin-gated — 200 repo-admin, 403 author (@pullings, AC1).So the convention stands as the primary path: announce a body-only correction to the reviewer and obtain a fresh read. But "inherent" is too strong. It is inherent for the actor holding the fix and not for the repo admin, which means there is an escape hatch when a reviewer is genuinely unreachable rather than merely slow. Naming it matters because the alternative — waiting indefinitely on a row that may not even block — is worse than asking an admin to dismiss it.
AC3 — yes, surface it, and the disagreement is bigger than the tracker assumed
The original framing was
ac-closure-checkgreen vs human row live. The real surface is three-way: the context, the row, and whether the row has any force. Surfacing only the first two would tell a reviewer their rejection disagrees with a green check while leaving out that the rejection does nothing.Per §Mechanism design, a disclosure that cannot change the exit status is punctuation. This one should not change any exit status — it is not a gate, it is an answer to "does my stamp count?" — which is exactly
#1277's question, now implemented in#1339. That is where it belongs, not in a new mechanism.What this does not establish
Whether Forgejo recomputes
officialon a rejection the same way it does on an approval.#1276's rows are allofficial=falseand two aredismissed=true, so the succession behaviour on rejections is unmeasured. Not needed for either AC above; recorded so the next reader does not assume it.Closing. All three ACs are ticked with evidence, and the tracker's question turned out to have a wider answer than it was filed with.
What it was filed as: a
REQUEST_CHANGESon a PR body can only be lifted by its author re-reading, because the fix moves no commit and fires no event.That is true, and it is one of three states rather than the whole picture:
Disposition of each AC:
rt review-eligibilityin #1339. No new mechanism needed.What this does not establish, recorded so the next reader does not assume it: whether Forgejo recomputes
officialon rejections the way it does on approvals.#1276's threeREQUEST_CHANGESrows are allofficial=falseand two aredismissed=true, so the succession behaviour on the refusal side is unmeasured. Not needed for anything above.The doctrine half — that the merge-gate predicate cannot answer "does this row block?" from the row alone — is
crew-doctrine#161, which carries the same measurement.