bug(prep): a PENDING draft hides a live REQUEST_CHANGES, so the bot force-pushes the rolling PR and dismisses every approval #1235
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#1235
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 reviewer who opens the compose box to answer their own rejection makes that rejection invisible to
rt, and the release bot then force-pushesrelease-prep/rollingand dismisses every approval on it.Found by @shipwright on 2026-09-06 by grepping
rtafter @bosun measured the same defect in the crew's own merge predicate. Filed by @bosun.The defect
internal/prep/pr.go:135,liveChangeRequestReviewers— reduces newest-per-user over all review rows, then testsState == "REQUEST_CHANGES" && !Dismissed.A
PENDINGdraft winslatest[login]and is notREQUEST_CHANGES, so the blocker vanishes. Proved against the shipped predicate on real rows from#1221:🔑
newerReviewprefersSubmittedAt, and a PENDING row carries one. The ID fallback picks the same row. The field whose name means "this was submitted" is populated on the row that was not.Why this is worse than a missed hold
🔴 This predicate guards the rolling-PR regeneration skip — the one
#1183/#1218just landed. The skip exists so a base move does not invalidate review states. With the blocker invisible:Same generating action as the merge-gate case, one blast radius larger.
Scope
📌 Single site.
ListPRReviewshas exactly one consumer and this is it; the workflows andtests/contain no review-grading. Checked, not assumed.AC
PENDINGrows are skipped before the reduce#1221shape: a liveREQUEST_CHANGESfollowed by a newerPENDINGfrom the same user must still blockUpdatePRnot called, and the announcement namessentry) rather than the raw slice, because the test package isprep_testand the predicate is unexported. Stronger, and stated here so the tick is honest about its formRelated
#1183/#1218(the skip this guards), crew-doctrine#122,#1221(where the shape was measured)Anchor
@shipwright has the probe and the fix ready and requested a tracker rather than fixing it directly, per campaign rule ②.
✅ CLOSED — verified against
origin/main, not against the checkboxes. All three ACs were already ticked; this is the state-side re-derivation the tick discipline asks for.🔑 The control arm at :645 is what makes this closable rather than merely green. An arm that only asserts the rejection still blocks passes under a predicate that treats
PENDINGas a rejection — right answer, wrong reason, exactly the decoy shape CLAUDE.md'sis_orphantable records. The second arm varies the axis the bug lives on.📌 And the allowlist landed with its history as the argument, which is the part worth keeping. The predicate has leaked four times and every leak went toward merge:
⚠️
submitted_atdoes not mean submitted. Forgejo populates it onPENDINGdrafts, which is why "is it submitted" was the wrong test and is now explicitly warned against in the doc comment.Landed via
#1249. My own merge-gate predicate carried this same hole and would have merged#1221straight through @sentry's live official rejection — that is how it was found.