Skip to content

ci: add one local entry point that runs the lint and lint-2 hygiene gates against the diff #6008

Description

@kyle-sexton

Problem

The hygiene lane is 66 continue-on-error gate steps spread over the lint and lint-2 jobs in .github/workflows/ci.yml (lines 540 to 1734 on main at 123c94b91). 35 of them call a scripts/ gate directly; the rest call ci-workflows composite actions (exec-bit, machine-specific paths and others) or inline shell. No script, Makefile, package.json script, or git hook runs them together on a workstation:

  • ls scripts has no runner; package.json has no scripts block.
  • The repo has no lefthook.yml, .pre-commit-config.yaml or .husky/. The only installed git hook is a prepare-commit-msg shim, and the lefthook binary it calls prints "No config files" on every commit.

So the only way to learn which gates a change trips is to push and wait for CI.

Evidence

On #5992, these failed in CI and each needed a separate push and CI round:

  • exec-bit, spoke-plugin-root (scripts/check-spoke-plugin-root.sh), plugin-options-docs (scripts/sync-plugin-options-docs.py --check), shell-portability-lint (scripts/check-shell-portability.sh) on run 37094085450, fixed in 78a01940c ("fix: satisfy the exec-bit, plugin-root, options-docs and portability lint").
  • changed-skills (scripts/check-changed-skills.sh), machine-specific-paths, stale-base-overlap and changelog-parity-bump on #5920, as reported by the authoring session (run IDs not re-checked here).

Every one of these is a deterministic script that needs only the checkout and origin/main.

Impact

Each CI-only failure costs one push, one ci run (PR median about 3.5 min after #5886) plus test-windows (about 11 to 13 min), and a fresh turn to read the log. While main merges about 3 PRs an hour, each extra round also raises the odds of a version collision and another base merge.

Proposed fix options

  1. scripts/run-hygiene-gates.sh [--base <ref>], called by CI as well. One script owns the gate list, runs each gate against the diff, prints <gate>: ok|FAIL plus the gate's own output for failures, and exits non-zero on any failure. CI's two lint jobs call it (or call it per half) instead of listing steps inline, so the local and CI lists cannot drift. Composite-action gates either get a local equivalent or are listed as "CI only" in the script's output.
  2. A local-only script that mirrors the inline steps. Less change to ci.yml, but it is a second list that drifts the first time someone adds a step.
  3. A pre-push hook. Only worth it on top of option 1, and opt-in, since some gates need origin/main fetched and Python with the pinned requirements.

Recommendation: option 1. One gate list serves both places, and the same script can print each failure's reason into the CI annotation (see the companion draft on aggregate-hygiene-results.sh). Basis: the eight CI-only failures above; ci.yml step count from grep -c 'continue-on-error: true' over the lint jobs.

Acceptance criteria

  • One command, documented in AGENTS.md, runs every scripts/ hygiene gate from both lint jobs against origin/main on a workstation and exits non-zero when any fails.
  • CI runs the same gate list from the same source, so adding a gate in one place adds it to both.
  • Gates that cannot run locally are named in the command's output, not skipped silently.
  • Re-running the eight failures above locally on their failing heads reproduces each one.

Activity

  1. added
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    on Oct 3, 2026
  2. kyle-sexton commented on Oct 4, 2026

    @kyle-sexton
    ContributorAuthor

    This was generated by AI during triage.

    Routing: human-gated, decision needed. Work class: structural (C4). Priority: medium.

    Why a human decides: the repo already decided to defer exactly this. README.md:145-163 ("Local pre-flight for hygiene gates", from the #3522 owner decision) maps every CI gate to its local command, and says "There is no single script that runs the whole hygiene set ... A pre-commit hook or a make target that wraps the runnable rows is a later tooling decision, not this record." This issue asks for that decision. The recommended option also moves the gate list out of the CI workflow and into a script that every hygiene job calls, which is a cross-cutting CI change (C4).

    Verification of the claims on main:

    • Confirmed: no runner exists (scripts/ has aggregate-hygiene-results.sh, which reads CI outcomes and runs no gates). There is no lefthook.yml, .pre-commit-config.yaml or .husky/.
    • Stale: lint and lint-2 no longer exist. The hygiene gates now run in four jobs, lint-repo, lint-shell, check-plugins and check-skills, each ending in the aggregator (.github/workflows/ci.yml:978, :1230, :1611, :1936). The ci.yml has 69 continue-on-error: true steps.
    • Partial existing coverage: the README mapping table names a local command for a subset of gates, so part of the discoverability gap is already closed. The single command is not.

    The decision:

    1. Recommended by the issue: scripts/run-hygiene-gates.sh [--base <ref>] owns the gate list, and every hygiene job calls it, so the local and CI lists cannot drift. Cost: restructures the four hygiene jobs' gate steps. Per-gate step names and outcomes then come from the script rather than from separate workflow steps.
    2. A local-only runner that mirrors the workflow steps: smaller, but a second list that drifts.
    3. A pre-push hook on top of option 1, opt-in.

    What the answer unblocks: the brief, and whether #6009's annotation work should wait for the runner (#6009 is defaulted to its smaller option 1 now; whichever lands second rebases onto the other's gate steps).

    Agent Brief (for after the decision, option 1):

    • Summary: One command runs every in-repo hygiene gate against a base ref on a workstation, and CI runs the same gate list from the same source.
    • Desired behavior: the command prints <gate>: ok|FAIL per gate plus the output of each failure, exits non-zero when any gate fails, and names each gate it cannot run locally (composite-action gates, missing tools) as "CI only" or "skipped: ". It never skips one silently. The hygiene jobs get their gate list from the same source, so adding a gate in one place adds it in both.
    • Acceptance criteria: documented in AGENTS.md; re-running the failing gates from the issue body's examples on their failing heads reproduces each failure; CI's hygiene results are unchanged on a clean PR.
    • Out of scope: changing what any gate checks; the ci-workflows composite actions themselves.
  3. added
    priority: mediumReal value, no hard deadline; normal backlog flow.
    needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.
    status: needs-decisionAwaiting a human or maintainer judgment call.
    work-class: structuralRefactors, migrations, contract changes; cross-cutting and hard to reverse.
    and removed
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    on Oct 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.priority: mediumReal value, no hard deadline; normal backlog flow.status: needs-decisionAwaiting a human or maintainer judgment call.work-class: structuralRefactors, migrations, contract changes; cross-cutting and hard to reverse.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions