Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/fixed-fork-guard-first-head.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- A fork PR whose only red check is a maintainer-run fork guard (such as a fork-gated Canary) is parked for a maintainer at `stage/085-needs-decision` on its first red head, instead of being sent back to its author first. On projectbluefin/utah-packages#972 (2026-11-09) the PR was opened from a fork at 14:41:50Z, its Canary refused it at 14:42:24Z with "This is expected, not something the author can fix", and at 14:45:32Z the hive nudged the author with `required-check-failing:Canary` and queued relay rework, because neither the fork-guard index (not yet read for that head) nor the stage record (never parked on a guard) knew about it; the PR reached 085 only at 14:50:57Z. The stage pass now reads such a head's own check annotations once, with the index's rule, and records the guard for later heads. A fork head with any other red check still goes to its author.
4 changes: 2 additions & 2 deletions src/docs/adr/0025-central-stage-engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ hub read that store and nothing else (`stageStoreOrShared`,
`SyncIssueStageLabels` (`src/pkg/github/issue_stage_labels.go:205`,
`src/pkg/github/issue_stage_labels.go:337`) from the enumeration bucket. PRs
are staged by the automerge engine: each lane calls `recordStage`
(`src/pkg/github/automerge/automerge_stage_labels.go:365`), observations are
(`src/pkg/github/automerge/automerge_stage_labels.go:552`), observations are
sorted by `stageLanePrecedence` (self 3 > trusted 2 > contributor 1,
`src/pkg/github/automerge/automerge_stage_labels.go:177`), each is classified
`src/pkg/github/automerge/automerge_stage_labels.go:245`), each is classified
by `ClassifyStage` (`src/pkg/stageengine/derive_pr_classify.go:344`)
into a ranked opinion (`prstage.RankNone..prstage.RankOwned`,
`src/pkg/stageengine/prstage/types.go:145-148`), and
Expand Down
2 changes: 1 addition & 1 deletion src/docs/labels-and-control-signals/stage-labels.md
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ used to admit the PR:
| `head-changed-since-eval`, `merge-failed`, `merge-queue-required` (the base branch's ruleset only accepts merges through GitHub's merge queue and the transport is a static token that cannot enqueue, so the direct REST merge is refused; with App auth on a repo opted in through `auto_merge.native_merge_queue` these bases are enqueued instead, see [native-merge-queue.md](../native-merge-queue.md); logged as a skip rather than a merge error, [#14018](https://github.com/hivecommons/hive/issues/14018); the self-authored sweep then skips the direct merge on that base for 30 minutes and logs the skip once per PR head, [#14068](https://github.com/hivecommons/hive/issues/14068); the trusted-author, queued and contributor-lane sweeps skip it the same way, sharing one memory of such bases; while a base is remembered the dashboard shows its otherwise-green PRs as a person's to merge, not Merge-eligible; with `auto_merge.require_current_base` on, a head behind such a base is not updated for 24 hours after the last refusal and holds here instead, so the base moving does not reset its review), `required-checks-expected`, `behind`, `updated-branch`, `behind-update-budget-exhausted` (a behind self-authored PR the sweep did not update only because this tick's branch-update budget was spent; retried next tick, waits on the hive, [#12607](https://github.com/hivecommons/hive/issues/12607)), `release-hold*`, `native-merge-queued: queued (position N, STATE)` (the PR was enqueued into GitHub's native merge queue) and the failures `native-merge-queue-read`, `native-merge-queue-enqueue`, `native-merge-queue-method` β€” and no red check on the head | `100-merge-queue` |
| CI red (required or optional check) while the PR is deferred to an open `[shared-ci]` incident (`<!-- hive-shared-ci-<n> -->`, the governor's `deferred_incident`) for the same head | `050-review-pending` / `060-reviewed`, reason `shared-ci-incident:<n>`: the base or a shared check is what is red, so the PR is never sent to 070/071/072, never nudged and never stalled. A 072 nudge already posted for the head (the nudge goes out on entering 072, and the incident marker can land minutes later) is edited in place to say it was withdrawn, and the head's nudge count is reset. Once the incident closes the PR stays deferred until the shared-CI rerun sweep has re-run its failed jobs (at most 30 minutes); a head still red after that is handed back as usual, with a fresh nudge. Conflicts and requested changes stay author-facing ([#12606](https://github.com/hivecommons/hive/issues/12606)) |
| CI red (required or optional) while **every** failing check on the head is also red on the base branch's tip β€” the governor's `base_red_checks` / `base_red`, set during CI enrichment when a same-named latest check run on the base ref (meta checks excluded) concluded failure / timed_out / cancelled / action_required / startup_failure / stale **and** the PR's changed files include none of `.github/workflows/*`, `Dockerfile` / `Dockerfile.*` / `*.Dockerfile`, `go.mod` / `go.sum` (a PR touching those plausibly owns the check and stays author-facing; a lookup error or a diff beyond the file-list page budget also stays author-facing) | `050-review-pending` / `060-reviewed`, reason `base-branch-red:<check>,<check>` ("waiting for the base branch's CI to recover (…) β€” not this PR's fault"): never 070/071/072, never nudged (a 072 nudge already posted for the head is edited in place to say it was withdrawn), never stalled, still not merged (the lane's skip reason is the same token). The fix loop counts no attempt, kicks no fixer, and posts one `<!-- hive-base-branch-red -->` hold comment per head; an escalation the hive itself raised (ledger `label_applied`) over such a head has its `needs-human` removed with the hold wording. The next evaluation after the base goes green re-evaluates the head: a still-red PR is handed back as usual. Conflicts and requested changes stay author-facing (seen on [#13270](https://github.com/hivecommons/hive/pull/13270) and [#13258](https://github.com/hivecommons/hive/pull/13258), 2026-10-20) |
| CI red (required or optional) on a **fork** PR while **every** failing check's annotation says the check refuses forks (case-insensitive `fork pull request(s) cannot`, `maintainer must push this branch`, or `cannot publish` together with `fork` β€” a fork-publication / fork-secrets guard) | `085-needs-decision`, reason `fork-guard:<check>,<check>`: never 070/071/072, never nudged, never adopted (074) β€” neither the author nor the hive can turn the check green by pushing to the fork. The entry comment names the check and says a maintainer must push the branch to the repository (or run or approve it for head `<sha>` as the repository requires). A new head on the same fork PR (a merge of the base, or the author's push) keeps the guard while the lane's red check is one of the recorded `fork-guard:` checks, so the PR does not drop to 072 and nobody is nudged before the annotation for that head is read. The hive also does not update the branch of a fork head that waits on a fork-guarded check: the run a maintainer dispatches proves one exact head, and a merge of the base would discard it. Once the check is green the guard drops and updates resume before merge. While the PR stays parked across new heads, the entry comment is rewritten in place for each one, so it always names the current head, and it says that a run which passed on an earlier head does not cover this one even when the patch is the same, because a rebase or a merge of the base changes the code the check runs (projectbluefin/utah-packages#917). A same-repo PR with the same message, or a fork PR with any other red check, is handed back as usual ([#14273](https://github.com/hivecommons/hive/issues/14273), projectbluefin/utah-packages#862 and #869) |
| CI red (required or optional) on a **fork** PR while **every** failing check's annotation says the check refuses forks (case-insensitive `fork pull request(s) cannot`, `maintainer must push this branch`, or `cannot publish` together with `fork` β€” a fork-publication / fork-secrets guard) | `085-needs-decision`, reason `fork-guard:<check>,<check>`: never 070/071/072, never nudged, never adopted (074) β€” neither the author nor the hive can turn the check green by pushing to the fork. The entry comment names the check and says a maintainer must push the branch to the repository (or run or approve it for head `<sha>` as the repository requires). A new head on the same fork PR (a merge of the base, or the author's push) keeps the guard while the lane's red check is one of the recorded `fork-guard:` checks, so the PR does not drop to 072 and nobody is nudged before the annotation for that head is read. A fork PR head that nothing has classified yet (its first red head, or a PR that came to 072 for another reason first) is classified from its own check annotations with the same rule, once per head, before it can be nudged: a head red only on fork-guarded checks goes straight to 085 and the guard is recorded for later heads (projectbluefin/utah-packages#972, where the author was nudged and sent relay rework for a fork-gated Canary five minutes before the annotation was read). The hive also does not update the branch of a fork head that waits on a fork-guarded check: the run a maintainer dispatches proves one exact head, and a merge of the base would discard it. Once the check is green the guard drops and updates resume before merge. While the PR stays parked across new heads, the entry comment is rewritten in place for each one, so it always names the current head, and it says that a run which passed on an earlier head does not cover this one even when the patch is the same, because a rebase or a merge of the base changes the code the check runs (projectbluefin/utah-packages#917). A same-repo PR with the same message, or a fork PR with any other red check, is handed back as usual ([#14273](https://github.com/hivecommons/hive/issues/14273), projectbluefin/utah-packages#862 and #869) |
| any **non-required** check on the current head is red, whatever the lane's reason (the lane gates on required checks only, so it still soaks and merges; a reader of the rollup sees βœ—). Parking modifiers (`hold`, `needs-human`, admission reasons) still win | `070-fixing` / `071-needs-author` / `072-returned-to-contributor` (by author kind), reason `optional-check-failing:<check>` (the nudge lists every red check); never 080 or 100. A red *required* check is the lane's own `required-check-failing:*` β†’ 070/071/072 ([#12538](https://github.com/hivecommons/hive/issues/12538), seen on [#12457](https://github.com/hivecommons/hive/pull/12457)) |
| `label:hold`, `held`, `label:needs-human`, `exempt-label`, `excluded-label:*`, `sentinel`, `human-merge-path*`, `intent-*`, `contributor-lane:workflow-path` / `tests-removed` / `no-reviewer` / `review-off` (a verdict is required but the review swarm is off, so none will be requested) / `reporter-trust-hold`, `workflow-permission` (GitHub refused update-branch or the merge-forward with a 403 because the base changed `.github/workflows/` and the token the hive used lacks the workflows permission; no token the hive holds can push that update, so it is not sent to 070), `fork-workflow-update` (the same update-branch refusal on a fork head: the App is not installed on the fork, so no grant lets it push there), `linked-issue:hold:#N:<label>` / `linked-issue:needs-decision:#N:<label>` (an open issue the PR closes is held or parked β€” see [Holding an issue](glossary-and-operations.md#holding-an-issue)) | human hand-off, **decision** cause β†’ routed by the head's evidence (below); lands at `085-needs-decision` when the head is otherwise clean. `contributor-lane:workflow-path` reads "awaiting owner approval": a trusted merger's `APPROVED` review on the current head (or a `/hive decision approve`, typed or from the dashboard, which also answers a `requires_human` verdict on that head) releases the fence and the lane merges the PR itself (`owner_approval_releases_workflow_fence`, [#14664](https://github.com/hivecommons/hive/issues/14664)); only with the toggle off, or when the App lacks `workflows: write` for the merge (`hive:unauthorized`, `merge-workflow-path`), does an approved head park at `090-ready-for-human-merge` for a person's click |
| `merge-refused:review-required` (GitHub answered the merge with 405 "At least N approving review(s) is/are required by reviewers with write access"), `merge-refused:method-not-allowed` (405 "Merge commits / Squash merges / Rebase merges are not allowed on this repository") | human hand-off, **decision** cause, as above: lands at `085-needs-decision` when the head is otherwise clean, and the entry comment names the rule GitHub refused the merge on. Every sweep (self-authored, trusted-author, queued, contributor lane) logs these as skips, not `merge-failed` errors, and stops calling merge: a review refusal is remembered per PR head and re-armed when the head moves, when GitHub's `mergeable_state` for the PR changes (an approving review turns `blocked` into `clean`), or after 30 minutes; a method refusal is remembered per repository and method for 30 minutes. The hive never switches the merge method on its own (a forward merge or a `merge-method/merge` PR must not be squashed), so `method-not-allowed` is a manual-merge park. When a person's change request still stands on the PR (their latest review is `CHANGES_REQUESTED`, on any head, and it was neither dismissed nor followed by their approval), the `review-required` comment does not ask for an approval over it. It names each reviewer, the commit they reviewed and a compare link to what the hive pushed since, and asks them to re-review, or a maintainer to dismiss the review. GitHub keeps such a review standing across pushes ([projectbluefin/utah-packages#633](https://github.com/projectbluefin/utah-packages/pull/633)). A 405 "Pull Request has merge conflicts" is reported as `conflicting` (070), not `merge-failed`, and the self-authored sweep re-reads `mergeable_state` right before the merge call and skips a head GitHub now calls `dirty`, so it does not call merge on a conflicting PR. Any other 405, 409 or 422 is still `merge-failed` |
Expand Down
Loading
Loading