Repository navigation
🐛 [v6] stage: a fork PR red only on a maintainer-run fork guard goes to the maintainer on its first red head, and its author is not nudged - #17406
Conversation
…f nudging its author On projectbluefin/utah-packages#972 (2026-11-09) a PR opened from Karibhou's fork at 14:41:50Z failed only its Canary fork guard at 14:42:24Z ("This is expected, not something the author can fix"). At 14:45:32Z the stage pass nudged the author with required-check-failing:Canary and queued relay rework: the fork-guard index had not read that head's annotation yet, and the stage record had never been parked on a guard, so foldForkGuard had nothing to carry. The PR reached 085 only at 14:50:57Z. foldForkGuard now classifies such a head itself when neither the index nor the record knows a guard: it reads the head's red checks' latest runs and their annotations once per head and red set, applies the index's own rule (hgithub.ForkGuardChecksFor), and when every red check is a fork guard the head goes to 085 with no nudge or adoption, and the record keeps the guard for later heads. A head red on anything else stays with its author and is not read again. The latest-run lookup is shared with forkGuardHeldOnHead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BQYnPGFJx81c8JXR3o1sg1 Signed-off-by: Danathar <Danathar@users.noreply.github.com>
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
Open PR overlap checkBase branch: Conflicts are checked with every PR merged onto the current tip of No conflicting open PRs or same-file overlaps were found. |
There was a problem hiding this comment.
Read the diff against v6 7423d91 together with foldForkGuard (automerge_stage_sharedci.go:88-148), headChecks (automerge_stage_human.go:89-104), ForkGuardChecksFor (pkg/github/fork_guard.go:43-63) and Store.NoteForkGuard (stagelabels/store.go:2587). The change does what the body says: a fork head that is red and has no guard in the index or the record now gets classified from its own annotations with the same rule the index uses, and the forkGuardHeldOnHead split into latestCheckRunsOnHead preserves the original held/not-held logic exactly. Looks correct to me.
One low-severity edge worth a look, not a blocker:
automerge_fork_guard_carry.go—knownNotForkGuardkeys on(key, head)only, whileheadForkGuardkeys its cache on(key, head, red-set).headChecksrefreshes the digest everyhumanChecksTTL(10m) on the same head, so the red set can change without a push: a fork head red onUnit tests(flake) +Canaryis classified not-a-guard, then the flake is re-run green and onlyCanaryis red.foldUnclassifiedForkGuardreturns at theknownNotForkGuardcheck beforeheadForkGuardever sees the new red set, so the PR stays at 072 with the author nudged until the index path catches up. KeyingknownNotForkGuardon the sameredKey(or dropping it and letting theheadForkGuardcache hit do the short-circuit — it already returnsnil, falsewithout a read) would close it. Pre-existing index fallback still applies, hence low.
Also noted: Store.NoteForkGuard is a no-op when the record does not exist yet (store.go:2591), so on a brand-new PR's first pass the guard lives only on the observations; the second pass writes it. The test covers that second pass, so this is fine as written.
— hive: agent=reviewer backend=copilot model=claude-fable-5.1 copilot=1.0.88
There was a problem hiding this comment.
Read the diff against the v6 tree (foldForkGuard, ForkGuardChecksFor, Store.NoteForkGuard). The main mechanism is sound: moving the !ok || len(guard)==0 early-return in automerge_stage_sharedci.go below the red-check loop means nothing is read for a green head, and foldUnclassifiedForkGuard applies the same hgithub.ForkGuardChecksFor rule the index uses (fork_guard.go:43), on the latest run per check (latestCheckRunsOnHead, shared with forkGuardHeldOnHead and behaviourally identical there). An in-progress re-run returns errForkGuardUnsettled and is not cached, so it is re-read next pass. NoteForkGuard is a no-op when the record does not exist yet (stagelabels/store.go:2591), but the observation still carries the guard for that pass and the tracker cache answers the next pass, so the record picks it up once it exists.
One low finding — the negative cache ignores the red set (automerge_fork_guard_carry.go:202, :227-231):
headForkGuard(:244) keys its cache on head and sorted red set, and the comment says "once per head and red set".- But
knownNotForkGuard(:231) short-circuits on head alone whenever the storedguardis empty, beforeheadForkGuardruns. - Mechanism: a fork head first seen red on
{Canary, Unit tests}(say a flakyUnit tests) storesguard=nil. A maintainer re-runsUnit tests, it goes green, and the head is now red only onCanary— exactly the #972 shape — yet line 202 returns before re-classifying. The same applies to the commit-status case (:274,nil, nilis cached as a negative). - Consequence: that head keeps the author-facing route (072 + nudge + relay rework) until the fork-guard index catches up — i.e. the pre-PR behaviour, for that head only. Not a regression against the tip; it is a hole in the new coverage. Dropping the
knownNotForkGuardpre-check (or making it compareentry.redtoo) would make the cache match its own docstring at the cost of one annotation read per red-set change.
Not blocking in my view; flagging so the author can decide whether a red set that shrinks on the same head is worth the extra read.
— hive: agent=reviewer-2 backend=copilot model=claude-fable-5.1 copilot=1.0.88
|
Test-coverage read of
All three fit the existing 🐝 Hive Agent: — hive: agent=quality backend=copilot model=claude-fable-5.1 copilot=1.0.88 |
|
Hive status — updated in place; edits do not notify. Full history is in the Activity Monitor. Evidence bundle `#17406348be98` sha256:9f798704c079fefd982167f961e1dda76a9415fd41a40583525bf320092da4f3 — download from the dashboard or `hivectl review evidence #17406`.Review findings from Style re-review of the delta Hub keys retry policy off a free-text and on the hub side accept either: func relayDidNotTakeFix(msg WSMessage) bool {
- return strings.EqualFold(strings.TrimSpace(msg.Reason), relayRefusedNotActiveSlot)
+ return strings.EqualFold(strings.TrimSpace(msg.FailureKind), "not_taken") ||
+ strings.EqualFold(strings.TrimSpace(msg.Reason), relayRefusedNotActiveSlot)
}Minor: the Previous finding |
Summary
A fork PR whose only red check was a maintainer-run fork guard (here the fork-gated Canary) was sent back to its author on its first red head. The author was nudged to fix
Canaryand their relay was handed rework, although the check itself says the author cannot fix it. This happened because the hive only knew about the fork guard from the fork-guard index (not yet read for that head) or from the stage record (only set once a PR has been parked on a guard). With this PR the stage pass reads such a head's own check annotations once, using the same rule the index uses. A head red only on fork guards goes straight tostage/085-needs-decisionfor a maintainer, and the guard is recorded for later heads. A fork head with any other red check still goes to its author, as before.What went wrong
Evidence (projectbluefin hive log, build 467557e; GitHub timeline and check runs of projectbluefin/utah-packages#972):
Karibhou/utah-packages(headd24a12f).Canaryfailed ond24a12f. Its annotation reads: "a fork pull request cannot publish the canary image, and no maintainer-run Canary has passed on d24a12f… This is expected, not something the author can fix: a maintainer reviews the change, then runs the 'Canary for a fork pull request' workflow".stage/072for a body issue (closing-contradiction:#42), and Karibhou fixed that at 14:44:43Z.stage labels: contributor nudged ... pr 972 ... reason required-check-failing:Canary. The hive rewrote its nudge comment (edited 14:45:31Z) to "Failing checks:Canary" with "Queued in your relay fix queue". The same nudge refreshed the fix already in flight for that PR so that it named Canary, and after the relay refused that task (see 🐛 [v6] relay: a fix for your own PR is taken from any hive you subscribe to, and a polling-slot refusal no longer counts as a failed attempt #17405), it went out again asfix-karibhou-2at 14:46:11Z.stage/085-needs-decision(fork-guard:Canary), once the index read the annotation.The hive already has this rule.
hgithub.ForkGuardChecksForparks a fork PR at 085 when every failing check's annotation says it refuses forks. But the stage pass only learns the result in two ways:foldForkGuardandcarriedForkGuardcarry a guard across heads only after the PR has once been parked on it (fork-guard:reason orForkGuardChecks).A fork PR's first red head has neither. #972's record said
closing-contradiction, sofoldForkGuardreturned early and the nudge went out. (#17380 fixed the same gap for branch updates. The stage path's record fallback was already there, but it cannot help a PR that was never parked on a guard.)What changes
foldForkGuard: when the PR is from a fork, its head is red, and neither the index nor the record names a guard, it calls the newfoldUnclassifiedForkGuard.ForkGuardChecksFor. It stops at the first red check that is not a fork guard.NoteForkGuardrecords it for later heads.forkGuardHeldOnHeadintolatestCheckRunsOnHead, and both callers use it.Cost: at most one check-run list plus one annotation read per red check, once per fork head that nothing has classified yet. A head with a real failure costs one annotation read.
Out of scope: Canary itself and merge policy. If the annotations cannot be read, the head keeps today's behaviour (nudge) until the index catches up.
Files
src/pkg/github/automerge/automerge_fork_guard_carry.go:foldUnclassifiedForkGuard,headForkGuard,forkGuardFromAnnotations,latestCheckRunsOnHeadsrc/pkg/github/automerge/automerge_stage_sharedci.go:foldForkGuardcalls it when nothing knows a guardsrc/pkg/github/automerge/automerge_stage_labels.go: the tracker's per-head cachesrc/docs/labels-and-control-signals/stage-labels.md(fork-guard row),changelog.d/fixed-fork-guard-first-head.md, and two line citations in ADR-0025 that had already driftedsrc/pkg/github/automerge/automerge_fork_guard_first_head_test.go(new)Tests
No tests were run locally (repo policy); CI is the check.
TestForkGuardFirstHeadReadFromAnnotationsreplays #972 with an empty index and no record guard. The mock serves the real Canary annotation text from check run 129228619506.Canary. It must land at 085 with reasonfork-guard:Canary, with no nudge comment and noNudgeto the relay sink. On the v6 tipfoldForkGuardreturns early (no record guard), so the PR goes to 072 and is nudged, and the test fails there.Unit tests: it still goes to 072 with one nudge, and its annotations are read once across two passes.Refs projectbluefin/utah-packages#972
Refs #17380
🤖 Generated with Claude Code
https://claude.ai/code/session_01BQYnPGFJx81c8JXR3o1sg1