Skip to content

Gate the Approval Check on the claude Label and Skip It Otherwise #513

Description

@b-at-neu

Problem

.github/workflows/approval-check.yml requires the approved pipeline label on every PR into dev. Two categories can never satisfy it:

Dependabot — its PRs never enter the pipeline, so nothing applies approved. They are permanently unmergeable. PR #296 (Bump prettier from 3.8.4 to 3.9.3) has been open since 14 August for this reason.

Human PRs — the workflow comment says the gate is satisfied when "review-agent (or a human, for manual PRs) applies approved", so a person must hand-apply a pipeline label to their own PR purely to satisfy a check.

Author-based detection is not available: pipeline PRs are opened under the operator's gh token, so agent and human PRs share the same authors (e.g. PR #500, a pipeline PR, is authored by cielbellerose). A label is the only usable signal.

Design

Every PR authored by the pipeline carries the claude label. The approval gate applies only to those PRs.

  • PR has claude → must carry approved and no blocking pipeline label
  • PR lacks claude → the job is skipped; the PR merges on its own merits

This covers Dependabot without a special case — Dependabot PRs have no claude label.

Skip at the job level, not the workflow level

Use a job-level if: condition. The workflow still triggers, a check run is created with conclusion skipped, and GitHub treats a skipped required check as satisfied — so the PR shows "Skipped" and merges.

Never filter the trigger (paths, branches, removing the event) to achieve this. A workflow that does not run never creates the check run, which then sits pending forever and blocks the merge permanently.

Keep types: [opened, reopened, synchronize, labeled, unlabeled] so the condition re-evaluates when labels change — adding claude to a PR must activate the gate.

Trade-off, recorded deliberately

This inverts the failure mode from fail-closed to fail-open.

  • Today: no approved label → blocked
  • After: no claude label → no gate at all

So a pipeline PR that never received the claude label silently loses its gate, and its absence is indistinguishable from a human PR. Removing one label also bypasses the gate deliberately.

Accepted, because the gate's purpose is to stop mid-pipeline PRs merging early, not to enforce review universally. The mitigation is documentation plus a cockpit check (below), not a stricter workflow.

Documentation — the load-bearing part

Because the gate now depends on a label being applied, the label being applied has to be impossible to forget. Update every place that describes how pipeline PRs are opened:

  • .claude/agents/impl-agent.md — the PR-opening step must apply claude alongside the existing title/base/assignee flags. This is the single most important change: it is where the label actually gets set.
  • .claude/docs/PIPELINE.md — state in the label state machine that claude marks pipeline ownership of a PR and is what activates the approval gate; note that a pipeline PR missing it will merge ungated. Add it to the permission/CI section describing approval-check.yml.
  • .claude/skills/pipeline/SKILL.md — the cockpit should report any PR it is tracking that lacks claude, so a dropped label surfaces rather than silently disabling the gate.
  • CLAUDE.md — the Commits/Branches/PRs section lists title, base branch, body and assignee conventions; add the claude label for pipeline-authored PRs.
  • .github/workflows/approval-check.yml — rewrite the header comment, which currently says humans must hand-apply approved.

Acceptance criteria

  • impl-agent applies the claude label to every PR it opens
  • The approval job runs only when the PR carries claude, via a job-level if:
  • A PR without claude shows Skipped and can merge
  • A Dependabot PR into dev merges with no label and no special case
  • A PR with claude still requires approved and no blocking label, exactly as today
  • Adding claude to an existing PR activates the gate on the next event
  • The trigger is not filtered — no paths/branches change that would leave the check pending
  • branches: [dev] scoping and the warning about the main/dev ruleset are preserved — release PRs carry no pipeline labels
  • The cockpit reports tracked PRs missing the claude label
  • All five documents above are updated
  • PR Bump prettier from 3.8.4 to 3.9.3 #296 is unblocked, or closed if the bump is stale

Note

This ticket edits .claude/ files, so under #503 it is implemented from an operator session rather than dispatched.


Implementation Plan

Overview

One-line change in CI, four documentation changes, one new cockpit sweep. The gate becomes conditional on the claude label via a job-level if: on run-approval-check; impl-agent starts applying claude at gh pr create time so the gate is live from the PR's first event; the cockpit gains an "ungated PR" sweep so a dropped label is visible rather than silent. No source, schema, or UI changes.

The job ID run-approval-check must not change — that string is the registered required check on the dev ruleset. Renaming it would leave the required check pending forever, which is the same failure the ticket forbids.

Changes

  • .github/workflows/approval-check.yml — job-level if: gate on the claude label; rewritten header comment; updated ::error:: copy (no longer tells humans to hand-apply approved).
  • .claude/agents/impl-agent.md--label "claude" in the step-6 gh pr create block, plus one line on why it matters.
  • .claude/docs/PIPELINE.mdclaude row in the PR labels table; a line in the permission/CI narrative describing approval-check.yml's new condition; a recovery-runbook row for a pipeline PR showing Skipped.
  • .claude/skills/pipeline/SKILL.md — the ungated-PR sweep query in the tick procedure + its report line.
  • CLAUDE.mdclaude label added to the PR bullet in Commits, Branches, PRs.

Implementation

  • Add the job-level condition to run-approval-check in .github/workflows/approval-check.yml, directly under runs-on: if: contains(github.event.pull_request.labels.*.name, 'claude'). Do not touch on:branches: [dev] and the five types stay exactly as they are, and the job ID stays run-approval-check.
  • Rewrite the header comment: the gate now applies only to pipeline PRs (those labeled claude); a PR without claude produces a skipped check run, which GitHub counts as satisfied; the trigger must never be narrowed (a workflow that never runs leaves the required check pending forever); keep the existing branches: [dev] / ruleset-15248252 warning verbatim.
  • Update the missing-approved ::error:: message — it currently references "a human, for manual PRs". New copy: Missing the 'approved' label. This PR carries 'claude', so it is a pipeline PR and merges only once review-agent applies 'approved'. If it is not a pipeline PR, remove the 'claude' label and this check will skip. Leave the blocking-label message and the success line as they are.
  • .claude/agents/impl-agent.md step 6: add --label "claude" to the gh pr create invocation (alongside --base/--title/--body-file/--assignee/--head) and one sentence after the --assignee note: the claude label is what activates approval-check.yml; a PR opened without it merges with no approval gate.
  • .claude/docs/PIPELINE.md — add a claude row to the PR labels table: Set by impl-agent (at gh pr create), Type marker, Meaning "Pipeline owns this PR — activates the approved merge gate in approval-check.yml; a pipeline PR missing it merges ungated".
  • .claude/docs/PIPELINE.md — one short paragraph (near the permission/CI material, after the "Deny rules" / branch-protection discussion) stating: approval-check.yml gates PRs into dev on approved only when the PR carries claude; every other PR (human, Dependabot) gets a skipped check run and merges on its own merits; this is deliberately fail-open, and the mitigations are impl-agent's --label plus the cockpit's ungated-PR sweep.
  • .claude/docs/PIPELINE.md — recovery-runbook row: Symptom "run-approval-check shows Skipped on a pipeline PR" / Cause "the PR is missing the claude label, so the gate is inactive" / Fix "gh pr edit <n> --add-label \"claude\" — the labeled event re-evaluates the condition and the gate activates on that run".
  • .claude/skills/pipeline/SKILL.md — add one query to the tick block (in the sweep group, after the unowned sweep) that lists this operator's open PRs carrying any pipeline label but not claude:
    gh pr list --repo SGAOperations/aplio --assignee "@me" --json number,title,labels --jq '[.[] | select((.labels | map(.name)) as $l | ($l | any(. == "ready for review" or . == "reviewing" or . == "needs revision" or . == "revising" or . == "approved" or . == "refresh branch" or . == "refreshing" or . == "needs human")) and ($l | index("claude") | not)) | {number, title}]'
  • .claude/skills/pipeline/SKILL.md — add an Ungated-PR sweep (each tick) note beside the unowned/denial reports: report only when the set changes, one line, never act automatically — e.g. ⚠️ Pipeline PRs without the 'claude' label (approval gate inactive): #501. Say "gate #501" or add the label on GitHub. Add the sweep to the ordered tick steps (fold it into step 4 alongside the unowned report) and to the status command's output so it is reachable on demand.
  • CLAUDE.md — extend the PR bullet: after the assignee clause, add "pipeline-authored PRs also carry the claude label — it is what activates the approved merge gate (approval-check.yml); without it the PR merges ungated."
  • Verify the docs stay consistent: no remaining text claims a human must hand-apply approved to a manual PR (approval-check.yml header, PIPELINE.md, CLAUDE.md).
  • Run npm run prettier:check — the four markdown files and the YAML are all prettier-formatted.

Testing

  • On this PR itself (it carries claude, and pull_request workflows run from the merge ref, so the new condition is live here): run-approval-check runs and fails on the missing approved label, with the new error copy.
  • Remove claude from this PR → the next event's run-approval-check reports Skipped, and GitHub's merge box no longer lists it as failing. Re-add claude → the gate runs again and fails. This exercises both acceptance criteria for label-driven re-evaluation.
  • Confirm the check run name in the merge box is still run-approval-check (unchanged required check, no "Expected — waiting for status" entry).
  • Add approved with claude present → the job passes with the "merge gate satisfied" line.
  • Add a blocking label (e.g. reviewing) alongside claude + approved → the job still fails on the blocking label.
  • After merge into dev: on PR Bump prettier from 3.8.4 to 3.9.3 #296, comment @dependabot rebase so its merge ref picks up the new workflow, then confirm run-approval-check reports Skipped and the PR is mergeable (or close it if the prettier bump is stale — Dependabot will reopen a current one).
  • Confirm a dev → main release PR is unaffected: the workflow's branches: [dev] scoping means it never runs there, and no pipeline label is involved.
  • In a cockpit session, run status with a pipeline-labeled PR that lacks claude → it is reported on the ungated line; with claude present → it is not.

Risks / notes

  • Fail-open is the accepted trade-off (recorded in the ticket). The only technical mitigations are impl-agent's --label and the cockpit sweep; nothing in CI can distinguish an unlabeled pipeline PR from a human one.
  • gh pr create --label applies the label in the creation request, so the opened payload already contains it and the gate is active on the PR's first check run. Even if it were applied afterwards, labeled is in types and would activate it — no ordering hazard either way.
  • Dependabot PRs opened before this lands run the old workflow from their own merge ref until rebased; @dependabot rebase is the fix, not a workflow change.
  • claude is a repo-wide label already used on issues (this one included). No new label needs creating, and GitHub shares the label set between issues and PRs.
  • Per Implement .claude Config Tickets From an Operator Session #503 this ticket edits .claude/, so implementation happens in an operator session rather than a dispatched impl-agent.

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingclaudeWill be worked on by Claudepr openedPull request has been opened

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions