Skip to content

ci: machine-block needs-* labels at the PR gate (#3751) - #3754

Merged
EffortlessSteven merged 4 commits into
mainfrom
impl/3751-needs-label-gate
Jul 11, 2026
Merged

ci: machine-block needs-* labels at the PR gate (#3751)#3754
EffortlessSteven merged 4 commits into
mainfrom
impl/3751-needs-label-gate

Conversation

@EffortlessSteven

@EffortlessSteven EffortlessSteven commented Jul 11, 2026

Copy link
Copy Markdown
Member

Intent

Repo-authorable half of M4 (epic #3612): make the CLAUDE.md rule "no needs-* label may merge" machine-checkable instead of prose. Today a PR carrying needs-deep-review can and has merged past that rule.

Controlling issue

#3751 (M4 capability enforcement; program state #3750).

Audit of existing workflows (done before writing anything)

  • .github/workflows/pr-title-check.yml — owns needs-issue-link (adds it when a title uses the (#0000) placeholder, self-clears it on edited/synchronize once a real issue number is present). This PR does not touch that file or that label's lifecycle.
  • .github/workflows/pipeline-labels.yml — owns in-review / needs-deep-review / merge-ready transitions on PR review events. Not touched.
  • .ci/policies/label-contradictions.toml + .github/workflows/methodology-gate.yml — already encode a narrower related rule (merge-ready/auto-merge cannot coexist with needs-*), but it only fires when merge-ready/auto-merge is also present, runs in --enforce-less (non-blocking) mode, and doesn't re-trigger on labeled/unlabeled. No existing workflow implements "fail on ANY needs-* label, independent of merge-ready, re-evaluated on every label change (including bot-authored ones)." That's the gap this PR fills.

Scope and architecture (revised twice during review — see below)

New, additive workflow: .github/workflows/needs-label-gate.yml. Triggers: pull_request: [opened, synchronize, labeled, unlabeled, reopened] and workflow_run (on Pipeline Labels / PR Title Check completion — see "bot-authored label backstop" below).

The verdict is published as a commit status (Statuses API, context needs-label-gate) on the resolved PR head SHA, fetched live via pulls.get for every triggering path. This design went through two review-driven corrections, both real defects caught by independent review (not self-found):

  1. Original design simply called core.setFailed() in a pull_request/workflow_run-triggered job. Maintainer review caught that for workflow_run events, the job's own GITHUB_SHA is the default-branch head, not the PR head (GitHub Actions docs) — so a bot-authored label change (pipeline-labels.yml/pr-title-check.yml writing with their own GITHUB_TOKEN, which suppresses labeled/unlabeled retriggers for other workflows) would fail the job against main while the PR's own check stayed stale-green. Fixed by resolving the PR number, fetching the live PR (head SHA + labels) via pulls.get, and posting an explicit createCommitStatus to that resolved head — this is now the sole authoritative signal for the workflow_run path. Added narrowly-scoped statuses: write (only new permission; contents/pull-requests stay read-only).
  2. Second-order defect: the job's own check-run happened to carry the same context string (needs-label-gate, from the job id) as the commit status it posts. If a required-check admin later registers needs-label-gate, a stale-green pull_request-path check-run could satisfy that context even after the workflow_run path posts a failing status — reopening the exact bypass this gate exists to close. Fixed by renaming the job id to post-needs-label-status; only the commit status carries needs-label-gate now. Verified via gh pr checks 3754: the context appears exactly once (0s duration, no /job/ URL suffix — i.e. the status, not a check-run).

Permissions: contents: read, pull-requests: read, statuses: write (least privilege for what the mechanism needs — no label-write permission anywhere).

Added a [[lane]] entry to policy/ci-lane-whitelist.toml for this workflow (job field kept in sync with the rename) so the advisory workflow-policy-lint --check-lane-whitelist gate doesn't flag it as unregistered.

Bot-authored label backstop

GitHub suppresses labeled/unlabeled pull_request triggers for events caused by a workflow's own GITHUB_TOKEN. Both pipeline-labels.yml (adds/removes needs-deep-review) and pr-title-check.yml (adds/removes needs-issue-link) write labels with their default token. The workflow_run trigger re-checks live label state after either workflow completes and posts the same commit status to the real PR head — closing the gap without touching either file. Note: workflow_run trigger definitions are read from the default branch, so this backstop only becomes fully active once this file lands on main; the direct pull_request triggers already cover the common (human-driven) case on this PR itself.

needs-issue-link interaction

This gate deliberately includes needs-issue-link in what it blocks — no carve-out, per the issue's explicit instruction that the policy is "no needs-* may merge." A PR opened with the (#0000) placeholder will show this gate red until the title is corrected, at which point pr-title-check.yml's existing self-clear logic removes the label and this gate's next re-evaluation (via synchronize/labeled/unlabeled, or the workflow_run backstop) goes green.

Merge queue and required-check app binding (verified live, not assumed)

  • gh api repos/.../rulesets → a merge-queue ruleset exists but enforcement: "disabled"merge_group events don't fire on this repo today, so no merge_group handling was added (would be dead code; documented in the workflow header, revisit if that ruleset is ever enabled).
  • gh api repos/.../branches/main/protection → the two existing required checks are bound to app_id: 15368 (the GitHub Actions app). createCommitStatus called with the workflow's own GITHUB_TOKEN is attributed to that same app, so needs-label-gate will bind correctly if/when an admin registers it as required.

Non-goals (per issue #3751)

  • Did not change which needs-* labels exist or their semantics.
  • Did not touch the two existing required checks (Perl LSP Rust Small Result, ripr+ New Gap Gate) or .ci/policies/required-checks.toml.
  • Did not attempt to register this check as a required status check or edit any branch-protection ruleset — needs org/repo admin, which this session does not have. Explicit remaining external blocker: an admin (@EffortlessSteven) must add the needs-label-gate context to the branch-protection required-checks list (or ruleset) for this to actually block merge. Until then it is visible red/green but advisory.
  • Did not build M5 writer/build isolation, and did not build reviewer-capability-read-only enforcement (that's a separate M4b concern per maintainer review — kept out of scope here).
  • Did not touch pipeline-labels.yml or pr-title-check.yml.

Behavioral proof

  1. YAML/TOML syntax — both files parse cleanly (yaml.safe_load, tomllib.load).
  2. cargo xtask workflow-policy-lint --check-lane-whitelist run locally after every revision: 0 errors, 0 new warnings throughout (19 pre-existing warnings, all in unrelated files). Confirms: no LABEL_EVENT_CANCELS_PR_RUN violation (concurrency cancel-in-progress is conditioned on github.event.action == 'synchronize', not unconditional), no STALE_WHITELIST_JOB regression after the job rename (lane entry updated in the same commit).
  3. Logic dry-runs (standalone Node scripts, no network) — 11 cases against the label-matching/blocker logic (no labels, unrelated labels, single/multiple needs-*, needs-issue-link specifically, merge-ready + needs-* coexisting, label removed) plus 4 cases against the workflow_run re-fetch path (no associated PRs, bot-added label caught, bot-cleared label clears, multiple associated PRs) plus 8 cases proving the commit status always targets the resolved PR head SHA, using deliberately distinct fixture SHAs for "PR head" vs. "default branch head" to prove the workflow_run path never targets the latter (the exact defect from review pass 1).
  4. Live evidence on this PR itself — after each push, gh pr checks 3754 shows the mechanism working against the real head SHA, not just mocks. Final state on head 95d645360:
    needs-label-gate            pass  0s   .../actions/runs/29136222090              No needs-* labels present
    post-needs-label-status     pass  8s   .../actions/runs/29136222090/job/86500924210
    
    needs-label-gate appears exactly once (confirmed via grep -c), carried solely by the commit status.

What was not run

  • Could not demonstrate the check as a blocking, required status (admin-only step, out of scope).
  • Live PR-head proof performed (2026-07-11, head 95d645360): on this PR — clean → needs-label-gate commit-status pass ("No needs-* labels present"); added needs-deep-reviewfail ("Blocked by: needs-deep-review") on the head; removed it → pass again. The authoritative signal is the commit-status (0s, no /job/ URL); the informational job check-run is separately named post-needs-label-status. grep -c "^needs-label-gate" on gh pr checks = 1.
  • No Cargo test suite beyond building/running xtask for the lint checks (CI-config change; no Rust production code path touched).

Claim boundary

This PR ships the mechanism — a workflow that correctly resolves the PR head SHA (including for bot-authored, GITHUB_TOKEN-suppressed label changes) and posts a single, unambiguous commit status for it — proven by dry-run plus live evidence on this PR. It does not claim the merge gate is enforced; that requires the one remaining admin step described above.

Risk & rollback

Purely additive: a new workflow file plus an advisory-policy TOML entry. No existing job, permission, or required check is modified. Rollback is git revert; no other workflow depends on this one.

Remaining work

  • External/admin step (out of scope here): register the needs-label-gate status context as a required check on main (binds to app_id: 15368, already verified compatible).
  • Once required, verify end-to-end on a scratch PR: apply needs-deep-review (both as a human and, ideally, by exercising the pipeline-labels.yml bot path) → confirm merge is blocked by GitHub itself → remove the label → confirm it clears and merge becomes available.
  • Optionally fold this and label-contradictions.toml's narrower merge-ready/auto-merge vs needs-* check into one canonical enforcement point in a follow-up.
  • Reviewer-capability-read-only enforcement (workflow subagent tool allowlists) is a separate M4b concern, intentionally not built here.

Claude-Session: https://claude.ai/code/session_011o9wuB2nsoT5fF6NmATgSA

Claim boundary (honest)

This gate is event-driven and eventually-consistent, NOT an atomic merge rule. A prior success remains on an unchanged commit SHA until a label-triggered (or workflow_run) run posts the new verdict — so a stale-green interval can exist between a label add and the re-post. The correct claim is: the required status converges to current label state through the covered event paths. It is not yet: a PR carrying needs-* is physically incapable of merging under every interleaving. Closing the bot-write window (set needs-label-gate pending on the head before label mutation via a canonical status publisher, then publish the final verdict after) is tracked as a follow-up; the human-label interval (labels change without changing SHA) relies additionally on review-convergence + auto-merge discipline. Merge this as advisory M4a machinery; do NOT make it required until the bot-path is live-proven on a scratch PR and the publisher hardening lands.

Enforcement authority (verified via API, 2026-07-11)

The two current required checks are in classic branch protection (repos/.../branches/main/protection/required_status_checks), bound to app_id 15368 (GitHub Actions), strict=false. The active main ruleset (16664791) carries NO required_status_checks (only required_review_thread_resolution). So the admin registration target is classic branch protection (add context needs-label-gate, GitHub-Actions source), preserving the existing two + strictness — unless the maintainer chooses to migrate status checks to the ruleset.

M4 capability enforcement (epic #3612): the "no needs-* label may
merge" rule has been prose policy in CLAUDE.md with no gate, so PRs
carrying needs-deep-review have merged and shipped regressions this
session (#3637 -> #3687/#3703, #3627 -> #3728/#3738).

Add .github/workflows/needs-label-gate.yml: a new, additive
pull_request check (opened/synchronize/labeled/unlabeled/reopened)
that fails when the PR carries any needs-* label and reports the
offending label(s) by name. It reads labels straight from the PR
event payload (no hardcoded list) and re-evaluates on label add/
remove so it clears automatically. Least-privilege permissions
(contents: read, pull-requests: read).

Register the lane in policy/ci-lane-whitelist.toml so
workflow-policy-lint's advisory lane-whitelist check has an entry
for it.

Does not touch the two existing required checks (Perl LSP Rust Small
Result, ripr+ New Gap Gate) or pr-title-check.yml's needs-issue-link
handling. Making this check *required* (branch-protection admin) is
an explicit external blocker, out of scope here -- see PR body.

Claude-Session: https://claude.ai/code/session_011o9wuB2nsoT5fF6NmATgSA
@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@sourcery-ai

sourcery-ai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds a new CI workflow that fails PRs carrying any needs-* labels and registers it in the CI lane whitelist, without changing existing workflows or required checks.

Sequence diagram for the new needs-label-gate CI workflow

sequenceDiagram
  participant GitHub
  participant Needs_Label_Gate as needs-label-gate_workflow
  participant GithubScript as actions_github_script

  GitHub->>Needs_Label_Gate: pull_request [opened/synchronize/labeled/unlabeled/reopened]
  Needs_Label_Gate->>GithubScript: run script
  GithubScript->>GithubScript: extract pull_request.labels
  GithubScript->>GithubScript: blockers = labels.filter(name.startsWith('needs-'))
  alt [blockers.length > 0]
    GithubScript->>GitHub: core.setFailed("Merge gate: needs-* label(s) present")
  else [no needs-* labels]
    GithubScript->>GitHub: core.info("No needs-* labels present. Gate clear.")
  end
Loading

File-Level Changes

Change Details Files
Introduce a new GitHub Actions workflow that blocks merges when any needs-* label is present on a pull request.
  • Create a dedicated workflow triggered on pull_request events including label add/remove to continuously evaluate label state.
  • Use actions/github-script to inspect the pull request payload and collect all labels whose names start with 'needs-'.
  • Fail the job with a detailed message listing all blocking needs-* labels when any are found, and log a clear success message when none are present.
  • Run the workflow on the existing em-ci-nano self-hosted runner group with minimal read-only permissions for contents and pull-requests.
.github/workflows/needs-label-gate.yml
Register the new needs-label gate workflow in the CI lane whitelist for policy lint compliance.
  • Add a new lane entry describing the needs-label gate workflow, job, and its intent to machine-enforce the "no needs-* label may merge" policy.
  • Mark the lane as non-blocking hygiene with default_pr=true, using the self_hosted_workflow_nano runner and ci ownership.
  • Document failure mode, proof obligation, triggers, and review/expiry dates in line with existing lane metadata conventions.
policy/ci-lane-whitelist.toml

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@diffray diffray Bot added the diffray-review-failed diffray review status: failed label Jul 11, 2026
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds a GitHub Actions workflow that evaluates needs-* labels on pull requests and a CI lane whitelist entry for the workflow job, including its triggers, permissions, runner, evidence, and review dates.

Changes

Needs-label merge gate

Layer / File(s) Summary
Label gate workflow
.github/workflows/needs-label-gate.yml
Runs on pull request and label events, reads pull request labels, and fails when any label begins with needs-.
CI lane registration
policy/ci-lane-whitelist.toml
Registers the workflow job as a pull request CI lane with its enforcement metadata, evidence source, and lifecycle dates.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related issues

  • #3751 — Directly addresses the machine-enforced needs-* label merge gate.

Suggested labels: deep-reviewed

Poem

A rabbit hops where labels gleam,
“Needs-none!” becomes the checkmark dream.
Pull requests dance, the gate runs bright,
Badges clear when tags take flight,
And carrots cheer the workflow night.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: blocking merges when PRs have needs-* labels.
Description check ✅ Passed The description directly matches the PR’s workflow and policy-lane changes and explains the intended gate behavior.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch impl/3751-needs-label-gate

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@factory-droid

factory-droid Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Droid finished @EffortlessSteven's task —— View job


Phase 2 (validation) complete.

Summary: LGTM. The new needs-label-gate workflow correctly implements the M4 capability using a read-only actions/github-script on pull_request events with minimal permissions, and the matching lane whitelist entry follows the established schema (frontdoor/hygiene/self_hosted_workflow_nano, blocking=false until admin-promoted). No high-confidence actionable issues found: trigger types cover all label-state transitions, no concurrency block correctly avoids LABEL_EVENT_CANCELS_PR_RUN, the startsWith('needs-') filter matches the documented intent (including needs-issue-link as explicitly called out), and the lane entry is structurally consistent with siblings like pr_title_check and pr_plan.

Validated: 0 candidates (none generated in Pass 1)
Approved: 0
Rejected: 0

No inline review comments to post.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5191ec0571

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/needs-label-gate.yml

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/needs-label-gate.yml:
- Around line 23-36: Add a concurrency block to the needs-label-gate workflow,
using a pull-request-specific group derived from the workflow name and PR
number, and cancel-in-progress: true so newer label events cancel stale runs
before they report status.
- Line 39: Pin the actions/github-script step to the immutable commit SHA
3a2844b7e9c422d3c10d287c895573f7108da1b3 instead of the floating `@v9` reference
in the merge-gate workflow.

In `@policy/ci-lane-whitelist.toml`:
- Around line 1084-1086: Remove the duplicate consecutive [[lane]] table header
near the needs_label_gate entry, leaving exactly one header associated with id =
"needs_label_gate" so the TOML contains no empty lane element.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6f511167-6788-4725-ac3f-cf99b17bdd26

📥 Commits

Reviewing files that changed from the base of the PR and between d6a8da5 and 5191ec0.

📒 Files selected for processing (2)
  • .github/workflows/needs-label-gate.yml
  • policy/ci-lane-whitelist.toml

Comment thread .github/workflows/needs-label-gate.yml
Comment thread .github/workflows/needs-label-gate.yml Outdated
Comment thread policy/ci-lane-whitelist.toml
@codecov

codecov Bot commented Jul 11, 2026

Copy link
Copy Markdown

❌ 3 Tests Failed:

Tests completed Failed Passed Skipped
27 3 24 1
View the top 3 failed test(s) by shortest run time
gate-meta::fmt
Stack Traces | 16s run time
Gate: fmt
Status: fail
Command: cargo xtask fmt --check
pr-fast::fmt
Stack Traces | 16.2s run time
Gate: fmt
Status: fail
Command: cargo xtask fmt --check
pr-fast::unit_core
Stack Traces | 116s run time
Gate: unit_core
Status: fail
Command: cargo test -p perl-parser -p perl-lexer -p perl-parser-core --lib --locked -- --test-threads=4
Exit code: 101

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Fixes real findings from the first review pass on PR #3754:

- P1 (chatgpt-codex): bot-authored needs-* label changes made via a
  workflow's own GITHUB_TOKEN (pipeline-labels.yml/pr-title-check.yml)
  don't trigger labeled/unlabeled pull_request events for OTHER
  workflows -- a documented GitHub Actions suppression. Add a
  workflow_run trigger (on Pipeline Labels / PR Title Check
  completion) that re-fetches live PR label state and re-evaluates,
  closing the gap without touching either label-writer workflow.
- Major (coderabbit): add a concurrency block so rapid label events
  don't leave a stale status. Gated cancel-in-progress on
  `github.event.action == 'synchronize'` only (not unconditional true)
  to avoid tripping this repo's own workflow-policy-lint
  LABEL_EVENT_CANCELS_PR_RUN rule for labeled/unlabeled-triggered
  workflows -- verified via a local `cargo xtask workflow-policy-lint
  --check-lane-whitelist` run (0 errors, 0 new warnings).
- Minor (coderabbit): pin actions/github-script to the commit SHA for
  v9.0.0 (3a2844b7e9c422d3c10d287c895573f7108da1b3), independently
  verified via `gh api repos/actions/github-script/git/tags/v9.0.0`
  dereferenced through the tag object to the commit.

Not fixed (verified false positive, see review reply): the claimed
duplicate `[[lane]]` header in policy/ci-lane-whitelist.toml is not
present in the file -- confirmed by direct read and a clean
`tomllib.load` parse (47 lane entries, single needs_label_gate entry).

Not fixed (out of scope, unrelated to this PR): CI Gate shard (meta)
failure is pre-existing `cargo fmt` debt in 4 crates this PR's 2-file
diff never touches (perl-parser-core, perl-workspace, perl-lsp-rs,
perl-core-test-runner) and is advisory only per
.ci/policies/required-checks.toml (only Perl LSP Rust Small Result and
ripr+ New Gap Gate are required).

Claude-Session: https://claude.ai/code/session_011o9wuB2nsoT5fF6NmATgSA
@factory-droid

factory-droid Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Droid finished @EffortlessSteven's task —— View job


Summary: LGTM. The head SHA (92f1c32) has addressed every prior reviewer finding: the workflow_run backstop closes the GITHUB_TOKEN suppression gap for bot-authored label changes, the conditional concurrency.cancel-in-progress: ${{ github.event.action == 'synchronize' }} matches the documented LABEL_EVENT_CANCELS_PR_RUN pattern used by pr-plan.yml and policy-validators.yml, and actions/github-script is pinned to the v9.0.0 commit SHA. The new lane entry follows the established ci-lane-whitelist.toml schema (frontdoor / hygiene / self_hosted_workflow_nano, blocking=false until admin-promoted), and the pull_request (not pull_request_target) trigger with read-only permissions does not expose secrets. No high-confidence actionable issues remain.

Validated: 0 candidates (none generated in Pass 1)
Approved: 0
Rejected: 0

No inline review comments to post.

@EffortlessSteven

Copy link
Copy Markdown
Member Author

Follow-up on review findings (head now 92f1c32)

Fixed 3 real findings, verified 1 false positive, and root-caused the CI Gate shard (meta) failure. Summary (full detail in the per-thread replies, all 4 threads resolved):

  1. P1 (chatgpt-codex, bot-authored label changes) — real finding. GITHUB_TOKEN-authored labeled/unlabeled events (from pipeline-labels.yml/pr-title-check.yml) don't retrigger other workflows' pull_request runs. Fixed with a workflow_run trigger that re-fetches live PR label state after either of those workflows completes — closes the gap without touching either file. Dry-run tested (4 cases, all pass).
  2. Major (coderabbit, concurrency) — real finding, fixed, but with a variation on the suggested diff: cancel-in-progress is gated on github.event.action == 'synchronize', not unconditional true, because this repo's own workflow_policy_lint::cancel_in_progress_cancels_all_pr_events check fails any labeled/unlabeled-triggered workflow that cancels in-progress runs unconditionally. Verified locally: cargo xtask workflow-policy-lint --check-lane-whitelist → 0 errors, 0 new warnings.
  3. Minor (coderabbit, SHA pin) — real finding, fixed. Independently re-verified (not just trusted the bot): gh api repos/actions/github-script/git/refs/tags/v9.0.0 → tag object → dereferenced via gh api .../git/tags/<sha> → commit 3a2844b7e9c422d3c10d287c895573f7108da1b3. Pinned.
  4. Major (coderabbit, duplicate [[lane]] header)false positive, no change made. Read policy/ci-lane-whitelist.toml lines 1084-1104 directly: exactly one [[lane]] marker (line 1085) before id = "needs_label_gate" (line 1086). Independently re-confirmed with python3 -c "import tomllib; ..." — 47 lane entries, clean parse, no id-less entry (a genuine duplicate header would produce an empty array element and raise KeyError in the lookup — it didn't).

CI Gate shard (meta) root cause — not the lane duplicate (verified false above). Pulled the actual job log (gh api .../actions/jobs/86497642569/logs): it's cargo fmt --check failing on 4 crates this PR's 2-file diff never touches — perl-parser-core (hir/lower.rs), perl-workspace (workspace_index.rs), perl-lsp-rs (text_sync/tests.rs, pull_diagnostics_freshness_tests.rs), perl-core-test-runner (main.rs). This is pre-existing formatting debt on main that any PR branched from current main inherits, unrelated to this change. Per .ci/policies/required-checks.toml, CI Gate is not one of the two required checks (Perl LSP Rust Small Result, ripr+ New Gap Gate are) — this failure is advisory and does not block merge. Fixing those 4 unrelated crates' formatting is out of scope for this PR (one PR / one issue / one crate).

All 4 review threads replied-to and resolved.

Claude-Session: https://claude.ai/code/session_011o9wuB2nsoT5fF6NmATgSA

Maintainer review found a real architectural defect in the previous
workflow_run backstop: core.setFailed() fails the job against
GITHUB_SHA, which for workflow_run events is the DEFAULT BRANCH head,
not the PR head (GitHub Actions docs, "Events that trigger
workflows"). So when Pipeline Labels/PR Title Check add or remove a
needs-* label via their own GITHUB_TOKEN (suppressing the normal
labeled/unlabeled pull_request retrigger), the previous backstop would
fail on main while the PR's own check stayed stale-green -- exactly
the bypass this gate exists to close.

Re-architected so every path (pull_request and workflow_run) resolves
a PR number, fetches the LIVE PR (head sha + labels) via
`pulls.get` (never trusts a payload sha/label snapshot), and posts a
commit status (context "needs-label-gate") to that resolved head via
`repos.createCommitStatus`. Added narrowly-scoped `statuses: write`
(no other write permission). core.setFailed() is now only called for
the pull_request path (correctly attached there already); the
workflow_run path relies solely on the posted commit status so it
never reddens main's own status.

Verified before implementing:
- `gh api repos/.../rulesets` -> a "merge-queue" ruleset exists but
  `enforcement: "disabled"` -- merge_group events don't fire on this
  repo today, so no merge_group handling was added (documented in the
  workflow header; revisit if that ruleset is ever enabled).
- `gh api repos/.../branches/main/protection` -> both existing
  required checks are bound to `app_id: 15368` (GitHub Actions app).
  createCommitStatus called with the workflow's own GITHUB_TOKEN is
  attributed to that same app, so this context will bind correctly if
  registered as required later.

Concurrency group now resolves the PR number for the workflow_run
case too (from the raw payload, since expressions can't call the
API), not github.run_id.

Proof: extracted the exact resolve->fetch->post logic into a
standalone Node dry-run against a mocked pulls.get/createCommitStatus
harness. 8 cases confirm the status always targets the resolved PR
head sha and never the (distinct, fixture) default-branch sha --
covering a direct pull_request label add, a bot-added
needs-deep-review via workflow_run (the defect scenario), and a
bot-cleared needs-issue-link via workflow_run.

Re-ran `cargo xtask workflow-policy-lint --check-lane-whitelist`
locally: 0 errors, 0 new warnings (statuses: write and the
concurrency expression don't trip any local lint rule).

Claude-Session: https://claude.ai/code/session_011o9wuB2nsoT5fF6NmATgSA
@factory-droid

factory-droid Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Droid encountered an error —— View job


Maintainer review: gh pr checks 3754 showed "needs-label-gate" TWICE
with the same context string -- the job's own check-run (job id
happened to equal the status context) and the commit status this job
posts. If an admin later registers "needs-label-gate" as a required
context, that ambiguity is dangerous: a stale-green pull_request-path
check-run could satisfy the required context even after the
workflow_run path posts a failing commit-status for a bot-added
label, reopening the exact bypass this gate exists to close.

Rename the job id from `needs-label-gate` to
`post-needs-label-status` so its own check-run carries a distinct
context. The `context: 'needs-label-gate'` string passed to
createCommitStatus is unchanged -- that status remains the sole
carrier of the "needs-label-gate" context, from both the
pull_request and workflow_run paths. Updated the
policy/ci-lane-whitelist.toml lane entry's `job` field to match (and
noted the workflow_run trigger in allowed_triggers/evidence, which
was accurate but not yet reflected there).

Verified: `cargo xtask workflow-policy-lint --check-lane-whitelist`
locally -- 0 errors, 0 new warnings (in particular no
STALE_WHITELIST_JOB regression from the rename, since the lane entry
was updated in the same commit).

Claude-Session: https://claude.ai/code/session_011o9wuB2nsoT5fF6NmATgSA
@factory-droid

factory-droid Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Droid finished @EffortlessSteven's task —— View job


Summary: LGTM. The head SHA (95d6453) reflects all prior reviewer findings resolved: the workflow_run backstop closes the GITHUB_TOKEN suppression gap for bot-authored label changes, the concurrency.cancel-in-progress expression is gated on github.event.action == 'synchronize' (consistent with the LABEL_EVENT_CANCELS_PR_RUN lint and the established pr-plan.yml/policy-validators.yml pattern), the job is named post-needs-label-status so its own check-run does not collide with the createCommitStatus context needs-label-gate (the head-commit disambiguation), and the actions/github-script action is pinned to the v9.0.0 commit SHA. The lane entry follows the established ci-lane-whitelist.toml schema (frontdoor / hygiene / self_hosted_workflow_nano, blocking=false until admin-promoted). No high-confidence actionable issues remain.

Validated: 0 candidates (none generated in Pass 1)
Approved: 0
Rejected: 0

No inline review comments to post.

@EffortlessSteven EffortlessSteven added needs-deep-review Independent deep review in-flight/required — blocks merge until verifier clears and removed needs-deep-review Independent deep review in-flight/required — blocks merge until verifier clears labels Jul 11, 2026
@EffortlessSteven
EffortlessSteven enabled auto-merge (squash) July 11, 2026 02:26
@EffortlessSteven
EffortlessSteven merged commit 174d99e into main Jul 11, 2026
79 of 84 checks passed
@EffortlessSteven
EffortlessSteven deleted the impl/3751-needs-label-gate branch July 11, 2026 02:44
EffortlessSteven added a commit that referenced this pull request Jul 11, 2026
…3771)

Review/audit agent profiles previously carried no tools: restriction, so
they inherited every tool (Edit/Write/NotebookEdit/Agent/mutating-Bash) —
only a prompt-level "REVIEW ONLY" instruction kept them from writing. That
is not mechanical enforcement (workflow subagents run in acceptEdits and
inherit parent tools).

M4b makes the read-only boundary mechanical + regression-guarded:

- Config: 18 review/audit agents get an explicit fail-closed tools:
  allowlist (Read, Grep, Glob, Bash, WebSearch, WebFetch, TodoWrite),
  excluding Edit/Write/NotebookEdit/MultiEdit/Agent — the Explore shape.
  Writers (builder, pr-responder, green-*, red-tdd, ops, lead-*,
  spec-planner) are untouched.
- Machine check: `cargo xtask check-agent-capabilities` parses
  .claude/agents/*.md and fails if any review/audit agent grants a
  write/mutating tool or lacks an explicit allowlist. Wired as a #[test]
  (runs under cargo test) + a new Agent Capability Gate workflow + a just
  target. Fails on a deliberately-broken fixture, passes on the corrected set.
- Read-only shell: pre-tool-use.sh rejects mutating git/gh/filesystem
  commands before execution when CLAUDE_AGENT_READONLY=1, while read-only
  inspection (git diff, gh pr view, cargo check) passes. Guard test:
  .claude/hooks/tests/test_pre_tool_use_readonly.sh.

Claim boundary: M4b only (mechanical read-only boundary + machine check +
negative demonstration). Does not cover M4a (label gate #3754) or M5
(build isolation).

Claude-Session: https://claude.ai/code/session_011o9wuB2nsoT5fF6NmATgSA
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

diffray-review-failed diffray review status: failed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant