Add SARIF-based lint regression ratchet - #287
Conversation
Lay the CLI groundwork for the #38 lint regression gate: - lint:eslint loads a bundled wrapper formatter that writes dist/eslint.sarif (via @microsoft/eslint-formatter-sarif) and returns compact console output; the file is a turbo task output so cached runs restore it - gtb lint:eslint:compare walks each lint cwd, runs sarif-multitool match-results-forward against dist/eslint-base.sarif, and fails only on results the baseliner classifies as new — fingerprint and content-based matching absorbs line shifts without any git-diff offset math - a pnpm override drops the formatter package''s wrongly-declared eslint v8 dependency, whose bin otherwise shadows the workspace ESLint inside packages/cli CI wiring (base-SHA lint via turbo remote cache, PR comment, label override) is follow-up work tracked in #38. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PRs now fail only on lint violations that are new relative to their merge base; pre-existing (accepted) violations never block. - lint:eslint drops --max-warnings=0 and becomes a reporter: warnings never fail it (fatals still do), so every commit produces a cacheable SARIF baseline. Local changed-file enforcement stays in the hk pre-commit eslint step and the IDE. - gtb lint:eslint:compare gains --base <ref>: it lints the merge base of that ref and HEAD in a throwaway git worktree to produce the baselines itself, so the same invocation works locally and in CI. A failing base lint is tolerated (pre-ratchet gtb wrote the SARIF log before exiting non-zero), and base commits that predate SARIF output just skip, which makes rollout graceful. - New lint-regression.yml reusable workflow wired into pr.yml: lints HEAD, compares against the merge base, posts an idempotent PR comment on failure, and honors a maintainer-applied override label (dismissed on every new push) as a run-but-neutral acceptance. - E2E coverage runs the real multitool against fixture SARIF logs. Per-test fixtures, not the file-scoped one: two compares sharing a project dir race on the matched-output write. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The compare machinery never inspects anything ESLint-specific, so stop naming it that way, and stop scattering loose artifacts in dist/. - SARIF artifacts move to a dedicated dist/sarif/ tree: <tool>.sarif from reporters, base/<tool>.sarif baselines, matched/<tool>.sarif multitool output, and base.ref (the merge-base stamp, root only). The compare pairs every dist/sarif/*.sarif with its baseline by filename, so a future reporter is gated by dropping a file there. - Commands become a `gtb sarif` hub (compare/baseline), reserving colon names for turbo tasks. `sarif compare --base` skips baseline production when the base.ref stamp already records the merge base — repeat local runs drop from ~35s to multitool time, and CI restores the stamped baselines from an actions/cache keyed on the merge-base SHA. release.yml seeds that cache cross-PR from each main commit via `gtb sarif baseline` (merge base == HEAD, so the baseline is the commit's own reporter output; pure file copy). - A missing baseline is an empty baseline, not a pass: every finding in an unbaselined log is new and needs acceptance, so the bootstrap PR and newly added reporters can't slip findings in silently. Findings carrying in-source suppressions are exempt — a reasoned suppression is already the accepted mechanism — but stay in the logs for visibility. - The multitool won't create its output file's parent directory; ensure dist/sarif/matched/ exists before matching. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the full-history checkout (fetch-depth: 0) with the merge-ref first-parent technique: on the PR merge commit both CI providers check out, HEAD^1 is the target branch head — which is the merge base of the merged checkout — so a fetch-depth: 2 checkout resolves it with no branch fetch and no merge-base computation. A guard fails fast if the job is not on a two-parent merge ref, where HEAD^1 would silently name the wrong commit. - `gtb sarif compare` gains --base-sha <sha> (exact commit, mutually exclusive with --base); CI passes `git rev-parse HEAD^1`. Local runs keep --base <ref> merge-base semantics, where full history is free. - Baseline production now ensures the commit exists locally, fetching it by SHA at depth 1 when absent — which also fixes local shallow clones. - Cache keys become base-tip SHAs, matching exactly what the default-branch seed job stamps, so cross-PR baseline cache hits stop depending on the branch being up to date with main. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Enforce step read labels from the event payload, but a re-run replays the original payload — so a label applied after the failure could never take effect, and no automatic path re-evaluated the check either (label events don't trigger the pipeline, deliberately: that would rebuild everything on any label change). Accept flow is now: apply the label, re-run the failed job. Enforce queries the live label state, so the stale payload doesn't matter. Dismissal only runs on the first attempt of a synchronize — a re-run of the replayed push event must not strip a label applied after it — and skips fork PRs, whose read-only token can't edit labels (their labels are still honored on re-runs, which only read). The PR comment now spells out the label + re-run ceremony. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GitHub has no per-label permission rules — anyone with triage can apply any label, and a PR author with write could self-accept their own regressions. The Enforce step now resolves the label's most recent applier from the issue events API and honors it only when their repository role meets the new `override-role` input (default `maintain`, so ordinary committers can't self-accept); unresolvable appliers or roles fail closed. Also reframe the docs and PR comment: the routine response to new findings is fixing or suppressing them in-source (suppressed findings are already gate-exempt); the label is the escape hatch for bulk introductions like a dependency bump shipping a new rule, where per-instance suppression is noise and disabling the rule would let new violations creep in while it's off. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Labels survive PR closure, and pushes to a closed PR emit no synchronize events — so a reopened head may have moved under a stale label with nothing dismissing it. Treat reopened like synchronize in both the dismissal step and Enforce: the label is dropped and discounted on the first attempt, so acceptance always postdates the code it vouches for. Also record the known residual: cancelling a run before its dismissal step and re-running preserves a stale label, which needs write access and deliberate timing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
Dependency ReviewThe following issues were found:
License Issuespnpm-lock.yaml
.github/workflows/lint-regression.yml
packages/cli/package.json
OpenSSF ScorecardScorecard details
Scanned Files
|
Two packaging consequences of the new SARIF dependencies surfaced on the first PR run: - pnpm blocks the multitool platform packages' build scripts (they chmod the bundled self-contained binary), failing every job at install on Linux. Allow all three; entries are consulted only when a script exists, so the scriptless win32 one is inert. - jschardet (LGPL-2.1-or-later) rides in via the formatter's SARIF_ESLINT_EMBED feature, which the lint pipeline never enables. Every consumer of @gtbuchanan/cli inherits it, so the name-scoped exception belongs in the shared dependency-review config — same pattern as the lightningcss MPL carve-out. Retire it if sarif-sdk makes the embed deps optional. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
actionlint's shellcheck pass flags `${{ }}` inside single-quoted
script strings (SC2016) — only on Linux, since the shared preset
disables shellcheck under actionlint on Windows, which is why local
pre-commit missed it. Env-var passing is the safer pattern anyway:
the label value never touches shell parsing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codecov flagged the real I/O layer as untested: the unit suites inject stubs and the e2e suites collect no source coverage by convention. Export the default deps and test each closure against a temp sandbox (list filtering, recursive removal, mkdir-before-write, multitool resolution, single-package workspace fallback), and test the formatter's SARIF write end to end. The formatter now resolves its output under the ESLint-provided context cwd instead of the process cwd — what it should have done anyway, and what makes the write testable without cwd games. The citty command wrappers stay uncovered like every other command file: they only delegate to the tested executors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The changeset recapped the full architecture; the deep detail lives in AGENTS.md and the README. Keep only what a consumer reading the CHANGELOG needs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With both modes in lint-regression.yml behind a seed input, every PR run showed a permanently skipped Lint Baseline Seed job and every release run a skipped Lint Regression job. A dedicated lint-baseline reusable removes the skipped peers; release.yml calls it directly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The PR-comment report could go stale after a green re-run (nothing edited it) and gh's --edit-last targets the bot's last comment, which another workflow (dependency review) may own. A job summary is pinned to its run, so neither failure mode exists, and it needs no write token, so fork PRs get the full report too. Also fail the merge-ref guard with an explicit ::error:: instead of git's bare "Needed a single revision", and remove the seed job now that lint-baseline.yml owns seeding. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The duplicated 'internal' literal covers ESLint's null ruleId (fatal parse/config errors); hoist it into a shared, documented module. Also record why formatConsole is hand-rolled (ESLint v9 exposes no importable formatter, and the line shape matches the compare report) and that the baseline worktree bootstrap assumes a node-only lint graph (mise never activates outside the repo). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ratchet documentation lived only in AGENTS.md while the README -- the consumer-facing surface -- never mentioned the new workflows. README gains the Lint jobs in both pipeline examples, the reusables table rows, and a lint regression section (ratchet semantics, override label flow, inputs, permissions). AGENTS.md keeps a condensed section scoped to the invariants agents need and points to the README for usage; its workflow bullets now reflect the job-summary report and the lint-baseline split. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Only pnpm-lock.yaml conflicted; regenerated via pnpm install (which merges the lockfile from the combined manifests). Everything else auto-merged; build and gtb verify pass on the result. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes #38.
Implements the lint regression gate with SARIF baselining (the standard +
sarif-multitoolthe issue's follow-up comment pointed at) instead of a hand-rolled cross-ref diff: fingerprint/content-based matching absorbs line shifts natively, so there's no git-diff offset math anywhere.Design
Lint enforcement becomes a ratchet — PRs may not introduce new findings, while pre-existing (accepted) ones never block:
lint:eslintdrops--max-warnings=0and writesdist/sarif/eslint.sarifvia a bundled formatter (compact console output preserved). Warnings never fail it; fatals still do. The layout is tool-agnostic: any reporter that drops a<tool>.sarifintodist/sarif/is gated with no extra wiring.gtb sarif compareis the gate. Pairs everydist/sarif/*.sarifwithdist/sarif/base/<name>.sarifand classifies viasarif-multitool match-results-forward, failing only onnewresults. A missing baseline counts as an empty baseline (all findings new — a new reporter or the bootstrap PR can't slip findings in silently), and findings with reasoned in-source suppressions are exempt (the suppression is already the accepted mechanism).--base <ref>(local: merge-base semantics) or--base-sha <sha>(CI: the PR merge ref's first parent, which is the merge base of the merged checkout — no history fetch needed,fetch-depth: 2suffices) lints the baseline commit in a throwaway git worktree. A stamp (dist/sarif/base.ref) skips production when baselines are current; missing commits are fetched by SHA at depth 1, so shallow clones work.actions/cachekeyed on the baseline SHA;release.ymlseeds the cache cross-PR from every main commit viagtb sarif baseline(merge base == HEAD there, so it's a pure file copy of the commit's own reporter output).gtb sarif compare --base origin/maingives the full ratchet verdict locally (~seconds when the stamp is warm).Acceptance model
Routine response to new findings: fix them, or suppress in-source with a reason (gate-exempt, stays visible in the logs). The
accepted-lint-regressionlabel is the escape hatch for bulk introductions (e.g. a dependency bump shipping a new rule, where per-instance suppression is noise and disabling the rule would let new violations creep in while off): apply the label, re-run the failed job. Labels are read live (re-runs replay stale payloads), dismissed on every push and on reopen, and honored only when applied by someone with theoverride-role(defaultmaintain) — GitHub has no per-label permission rules, so the workflow implements its own.Rollout (post-merge)
accepted-lint-regressionlabelLint Regression(PR) to required checks;Lint Baseline Seedruns on main pushesFollow-ups (non-blocking)
gtb-build-pipelineskill + README coverage of thesarifcommand family--base/--base-shaworktree flows (unit-covered today)🤖 Generated with Claude Code