Repository navigation
✨ feat(automerge): approve held workflow runs for registry-trusted contributors - #14520
Conversation
|
[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 |
…ntributors The contributor lane now approves action_required pull_request runs once per head when the registry path admitted the author at a require_tiers tier and the lane stops at required-check-missing:<ctx>. Claim-marker, auto-promoted and association admissions, sentinel/exclude labels and .github/workflows/** diffs are never approved for. Toggle: auto_merge.contributor_lane.approve_held_runs_for_registry_trusted (default on). One audit comment per head; structured log line 'contributor-lane approved held runs'. Fixes #14519 Signed-off-by: Andy Anderson <andy@clubanderson.com>
69f3a9f to
ee7de82
Compare
Open PR overlap checkBase branch: Conflicts are checked with every PR merged onto the current tip of
|
|
@clubanderson is auto-promoted for contributor-lane merging on this PR: the last 5 merged PRs by this author in this repository have no revert or sentinel-alert evidence. CI, reviewer verdict, diff fences, soak, holds and branch protection still apply. |
|
@clubanderson this PR is held at Failing checks:
Next: the hive does not push to your branch. Push a new head (or reply here); CI and the review swarm re-run automatically and the PR returns to the normal path. |
…Settings The settings-parity test requires every hive.yaml leaf to be referenced by the Settings UI; add the contributor-lane toggle next to registry_tiers_sufficient. Signed-off-by: Andy Anderson <andy@clubanderson.com>
There was a problem hiding this comment.
Reviewed (correctness) — approving; one advisory, no blocker.
Side effect in the relay's read-only evaluation (low, in-scope). evaluateContributorLanePR documents relay=true as "the relay's read-only evaluation" (src/pkg/github/automerge/automerge_contributor_lane.go:1149-1152), and the behind/update-branch and effects-reconcile steps honour that (:1223, :1241). The new call at :1256-1258 does not — a merge-request relay evaluation (automerge_relay_gate.go:81) will now approve runs and post the audit comment. Once-per-head bookkeeping keeps this from double-firing against the sweep, so the impact is only the broken invariant, but gating on !relay keeps the contract intact and is a one-liner:
if !relay && strings.HasPrefix(reason, requiredCheckMissingPrefix) {
Nit (not blocking): on a 403 the loop in automerge_contributor_held_runs.go:88-95 keeps trying each held run and warns per run; #14519 asks for "logged once per head". Breaking out of the loop on isGitHubStatus(err, http.StatusForbidden) would match that wording. The approve forbidden test currently asserts both runs are attempted, so adjust wantApproved there if you take this.
Verified: registry-only gating via claim.admittedByRegistry() (lane.go:531), label/workflow-path fences re-checked defensively (held_runs.go:52-58), fork head matched by head_repository.full_name (held_runs.go:120-131), comment marker dedupe via paginated listIssueComments (lane.go:1089). Table tests cover fork/same-repo/auto-promoted/marker/sentinel/exclude/workflows/toggle/no-runs/403 across two ticks.
— hive: agent=adjudicator backend=copilot model=claude-fable-5.1 copilot=1.0.88
There was a problem hiding this comment.
Logic reads correct to me; CI is red for a reason this PR owns — the new config leaf is missing from the Settings UI parity check.
Finding (medium) — test (dashboard 4/4) / dashboard shuffle 2/3 / coverage
TestSettingsDialogReferencesEveryHiveYAMLLeaf (src/pkg/dashboard/settings_parity_test.go:33) fails on every shard that runs it:
hive.yaml settings are not referenced by the Settings UI and are not allowlisted:
auto_merge.contributor_lane.approve_held_runs_for_registry_trusted
The PR adds the leaf in src/pkg/config/config.go:7747 and plumbs it through /api/config/auto-merge GET/PUT (src/pkg/dashboard/api_config_automerge.go:172,326,428), but src/pkg/dashboard/static/index.html never mentions it — the sibling registry_tiers_sufficient is wired at index.html:49639, 49848, 51365, 52161, and this toggle has no equivalent. Either add the Settings toggle alongside those four sites (the API side is already done, so this is the consistent fix) or add the path to settings_parity_allowlist.txt. coverage fails only because that shard failed ("Coverage requires successful test shards (... sliced=failure ...)"), so fixing the parity test should turn the run green.
What I verified in the tree
approveHeldRunsForRegistryTrusted(automerge_contributor_held_runs.go:47-105) is only reached fromevaluateContributorLanePRafterprefilterContributorLanePR,contributorLaneDiffReason(workflow-path fence returns atautomerge_contributor_lane.go:1210), reporter-trust hold and changes-requested gates — so the in-helper re-checks are genuinely defensive, as the body says.- Admission is gated on
claim.admittedByRegistry()only; claim-marker/auto-promotion/association paths return nil at:50. heldWorkflowRunsForHead(:124-136) filters onaction_required+ head-repo full name, so a fork head is matched by the fork's name — matches the stated intent and differs deliberately fromheldSameRepoWorkflowRuns(automerge_held_runs.go:561).- The once-per-head mark is set before the API calls (
:84-86), so a 403 does not retry every tick; the audit comment is suppressed by marker scan + effects claim (:195-218).
Open question (low, not a defect)
evaluateContributorLanePR is documented as read-only when relay=true (automerge_contributor_lane.go:1147-1150), but the new call at :1256-1258 runs regardless of relay, so a merge-request relay evaluation can now approve runs and post a comment. The sweep would do the same within a tick and the once-per-head mark prevents duplication, so I don't think it's wrong — just worth a one-line note in that doc comment, or a !relay guard if you'd rather keep the relay side-effect-free.
— hive: agent=reviewer backend=copilot model=claude-fable-5.1 copilot=1.0.88
There was a problem hiding this comment.
Logic looks correct to me; the red CI is a verified, self-contained omission — the new config leaf is missing from the Settings UI / parity allowlist.
Finding (medium) — TestSettingsDialogReferencesEveryHiveYAMLLeaf fails on this head
Both red shards (test (dashboard 4/4), test (dashboard shuffle 2/3), which also fail coverage and test) are the same failure:
settings_parity_test.go:33: hive.yaml settings are not referenced by the Settings UI and are not allowlisted:
auto_merge.contributor_lane.approve_held_runs_for_registry_trusted
Mechanism: src/pkg/config/config.go:7747 adds the approve_held_runs_for_registry_trusted leaf, and src/pkg/dashboard/api_config_automerge.go:172,326,428 plumb it through GET/PUT — but src/pkg/dashboard/settings_parity_test.go:27 requires every Config leaf to be referenced in static/index.html or listed in settings_parity_allowlist.txt. Neither mentions the new key (the contributor-lane help text at static/index.html:49848 lists the other eight keys and has no checkbox for this one). Fix is either a toggle in the contributor-lane Settings card (the PUT handler already accepts the field) or one line in settings_parity_allowlist.txt; the former matches the PR body's "dashboard GET/PUT" intent.
What I verified in the tree (no defects)
automerge_contributor_held_runs.go:48-62: gated onpolicy.ApproveHeldRunsForRegistryTrusted,claim.admittedByRegistry()and therequired-check-missing:prefix.admittedRegistryTier(automerge_contributor_lane.go:579-597) returns""unless the tier is inpolicy.RequireTiers, so "registry-admitted" really does mean "a tier the lane would merge for" — the header comment's claim holds.- Call site
automerge_contributor_lane.go:1256:filesis already fetched andcontributorLaneDiffReason(files)has already returned at:1206, so the.github/workflows/**refusal at:59is a true defensive re-check, and the sentinel/exclude/hold label checks mirror the sweep'slabelBlockReason(automerge_sweep.go:1919). heldWorkflowRunsForHeaduses the sameaction_requiredconclusion match asheldSameRepoWorkflowRuns(automerge_held_runs.go:564), differing only in accepting the fork's full name — which is the point of the change.- Once-per-head bookkeeping reuses
Engine.heldRunApprovalswith its ownregistry-held-approve:prefix, matchingbotTakeoverKey/rebaseNudgeKey; marking before the API call means a 403 (missing Actions: write) is logged once and never retried for that head until a new push — consistent with whatgithub-app-setup.mdnow says. - Security read: the approve call runs under the App token only for authors the operator explicitly placed at a mergeable tier, never for claim-marker/auto-promotion/association admissions, and never when the diff touches workflows. The toggle defaults on; an operator who does not want fork runs approved unattended must set it
false— the docs say so.
Not a defect, just a note: the audit comment body embeds author straight from pr.GetUser(); it's a GitHub login so it's safe, but it is interpolated into a <!-- … --> marker line.
— hive: agent=reviewer-2 backend=copilot model=claude-fable-5.1 copilot=1.0.88
There was a problem hiding this comment.
Style / maintainability review (read at head f35e65b).
1. heldWorkflowRunsForHead is a verbatim copy of heldSameRepoWorkflowRuns — src/pkg/github/automerge/automerge_contributor_held_runs.go:132-144 vs automerge_held_runs.go:561-573. Same loop, same action_required + HeadRepository.FullName EqualFold filter; only the doc comment differs. #14519 explicitly asked to "reuse the detection … in automerge_held_runs.go". Two identical filters will drift (e.g. if the bot path ever starts tolerating a nil head repository). Suggest dropping the new function and calling the existing one with the fork-aware head name:
--- a/src/pkg/github/automerge/automerge_contributor_held_runs.go
+++ b/src/pkg/github/automerge/automerge_contributor_held_runs.go
@@
- held := heldWorkflowRunsForHead(runs.WorkflowRuns, contributorLaneHeadRepo(pr, owner, repo))
+ // Unlike the bot path, the expected head repository may be a fork: the
+ // author's registry tier, not the head's origin, admits the approval.
+ held := heldSameRepoWorkflowRuns(runs.WorkflowRuns, contributorLaneHeadRepo(pr, owner, repo))
@@
-// heldWorkflowRunsForHead returns the runs waiting on approval whose head
-// repository is headRepo. Unlike heldSameRepoWorkflowRuns the expected head
-// repository may be a fork: the PR author's registry tier, not the head's
-// origin, is what admits the approval.
-func heldWorkflowRunsForHead(runs []*gh.WorkflowRun, headRepo string) []*gh.WorkflowRun {
- var held []*gh.WorkflowRun
- for _, run := range runs {
- if run == nil || !strings.EqualFold(run.GetConclusion(), workflowRunConclusionActionRequired) {
- continue
- }
- if run.GetHeadRepository() == nil || !strings.EqualFold(run.GetHeadRepository().GetFullName(), headRepo) {
- continue
- }
- held = append(held, run)
- }
- return held
-}and in automerge_held_runs.go:559-560 widen the doc comment ("whose head repository is fullName — the base repo for the bot lanes, the PR head (fork included) for the contributor lane"). TestHeldWorkflowRunsForHeadMatchesForkHead (automerge_contributor_held_runs_test.go:220) then targets heldSameRepoWorkflowRuns, which is still worth keeping since the fork-name case is new coverage.
2. Doc sentence now contradicts the paragraph it ends — src/docs/github-app-setup.md:46. The paragraph opens by describing the new direct fork-head approval, then closes with "Hive never approves fork heads or PRs carrying sentinel-alert." A reader skimming the permissions table takes the last sentence as the rule.
Outside that contributor-lane exception, Hive never approves fork heads, and it never approves PRs carrying `sentinel-alert`.
Advisory (no verdict effect): the requiredCheckMissingPrefix guard is checked both at the call site (automerge_contributor_lane.go:1256) and inside the helper (automerge_contributor_held_runs.go:53); and the helper's sentinel/exclude/workflow-path re-checks (:56-62) are unreachable from the lane (it returns at automerge_contributor_lane.go:495 / :1209 first), so the three "refuses" table cases exercise the lane's gates, not the helper's. Fine as defence-in-depth per the PR body; just noting the tests don't cover those branches.
— hive: agent=reviewer-2 backend=copilot model=claude-fable-5.1 copilot=1.0.88
|
@clubanderson this PR is held at Why:
Next: the hive does not push to your branch. Push a new head (or reply here); CI and the review swarm re-run automatically and the PR returns to the normal path. |
|
merged by Hive trusted-author auto-merge: author @clubanderson holds role owner and GitHub permission admin on repo; CI green at f35e65b. |
|
Thank you for your contribution! Your PR has been merged. We'd love to hear how your experience was: share feedback |
Fixes #14519
What
hivecommons/docs#481 by a
trustedregistry contributor sat 17+ hours: after admission (contributor-lane:registry-trusted) the lane skipped withrequired-check-missing:Audit production dependenciesbecause GitHub held the fork head'spull_requestruns inaction_requiredand nothing would ever start them.automerge_held_runs.godeliberately never nudges human-authored lanes — right for unknown contributors, wrong for one the operator already trusts to merge.How
automerge_contributor_held_runs.go: inevaluateContributorLanePR, whenclaim.admittedByRegistry()(registry path only — not auto-promotion, claim marker or association) and the CI reason isrequired-check-missing:<ctx>, list the head'spull_requestruns, pick theaction_requiredones whose head repository is the PR head (fork included), mark once-per-head inEngine.heldRunApprovals, andPOST /repos/{o}/{r}/actions/runs/{id}/approve(raw request; go-github v72 has no binding)..github/workflows/**diffs (reusescontributorLaneDiffReason); the lane's own gates already return earlier for those, the helper re-checks defensively.<!-- hive:contributor-held-runs-approved head=… author=… tier=… runs=… -->via the effects boundary; structured logmsg="contributor-lane approved held runs"with repo/pr/head/author/tier/run_ids.auto_merge.contributor_lane.approve_held_runs_for_registry_trusted(defaulttrue) in config,cmd/hivepolicy plumbing, and dashboard/api/config/auto-mergeGET/PUT.automerge_held_runs.goand docs (hive-merge.md,operator-reference.md,github-app-setup.md) describe the exception and the App's Actions: Read and write requirement (403 → logged once per head, lane keeps waiting).Tests
Table-driven against the contributor-lane httptest fixture (extended with branch-protection required contexts,
actions/runslisting and the approve endpoint; posted PR comments now become visible to later GETs): registry-trusted fork/same-repo → approves once across two ticks + one comment; auto-promoted / claim-marker → no approval; sentinel label, exclude label, workflows path → refuse; toggle off → nothing; no held runs → nothing; 403 → no comment. Plus helper unit tests, config default test, dashboard GET/PUT field tests. Notime.Sleep.