Skip to content

lint runs on every leg, and gates over dead code and staticcheck's correctness checks - #105

Merged
enesismail merged 5 commits into
mainfrom
lint-runs-on-every-ci-leg
Oct 5, 2026
Merged

enesismail merged 5 commits into
mainfrom
lint-runs-on-every-ci-leg

Conversation

@enesismail

@enesismail enesismail commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

What was wrong. No CI leg had golangci-lint, so make lint printed "golangci-lint not installed; skipping lint" inside every make ci step, and lint ran nowhere. Nothing said so except that line.

What changes.

  • The workflow installs golangci-lint 2.14.0 on every leg before make ci, with go install at an exact version: the install is checked against the checksum database, and the linter is built by the same Go the leg tests with. The step prints the version it installed.
  • On CI, make lint treats an absent linter as a failure that names it; on a machine without it, it still skips, with the same line. It prints every finding: the linter's caps per linter and per message, and its rule of one finding per line, are off.
  • .golangci.yml enables unused and staticcheck's correctness checks, and nothing else.
  • The five real findings are fixed. A guard that holds every git invocation to a deadline read its package with the deprecated parser.ParseDir; it now parses each non-test file itself, so a file built only for another platform is still read. Four dead declarations are gone.
  • Two lines that exist to prove something carry //nolint:<linter> // <reason>: the %s of a whole config through an any in the unknown-fields leak test, and the stall-window floor for a leg with nothing recorded.
  • CLAUDE.md records the posture: lint gates, over those two classes, and why.
  • CLAUDE.md's record of the merge queue gains one observed fact: the queue refuses to enqueue a pull request whose head commit carries a failing run of a required check, even when another run of that check on the same commit is green.

The measurement behind the set. The linter's default set, run with every finding printed, reported the same 110 findings on all three legs:

class findings what they were
errcheck 93 all false: writes to a terminal or a report stream the program cannot act on, and closes of handles opened only for reading
staticcheck's style families 10 nine rewrites of code that already reads clearly; one would have removed the %s path from a redaction test's positive control
unused, and staticcheck's correctness checks 7 all signal: five fixed here, two annotated

With this configuration the tree reports none.

Rows and runs.

  • Locally, with no linter on PATH: with CI unset the target prints the skip line and exits 0; with CI=true it exits non-zero, naming the linter.
  • On CI, before the set was narrowed, every leg printed the installed version, never printed the skip line, and failed at lint on the 110.
  • A run with the install step removed on the Linux leg failed there at lint, with the message naming the missing linter, and not at the skip.

Mutations, each run and reverted.

  • make lint made to skip under CI=true: the local row's second half reds.
  • A second caller of the undeadlined git call, once in an ordinary file and once in a file only Windows builds: the deadline guard reds naming each, which is the per-file parse keeping the guard's field of view.

No leg of the matrix had golangci-lint, so the lint target printed its
skip line inside every make ci step and lint ran nowhere. Nothing said
so except that line.

The workflow now installs the linter on every leg before make ci, at an
exact version, through the Go toolchain: the install is checked against
the checksum database, and the linter is built by the same Go the leg
tests with. The step prints the version it installed.

On CI the target treats an absent linter as a failure that names it,
told apart by the same CI variable the branch guard reads. On a machine
without the linter it still skips, with the skip line unchanged word for
word, because that line is what a CI log is searched for. With the
linter present it prints every finding: its caps per linter and per
message, and its rule of one finding per line, are turned off.

With the linter present, lint fails ci on any finding, which is what
this target has always done when the linter was there. No row in the
suite holds the CI check; it was exercised both ways, and with the check
disabled, by running the target.
…by file

The linter's dead-code check and its correctness checks found five
things worth changing in this tree, and these are they.

A guard that holds every git invocation to a deadline read its own
package with parser.ParseDir, which is deprecated. It now lists the
directory and parses each non-test file, so every file is still read
whatever its build constraint. The replacement the deprecation names, a
package loader, would see only the host's build and would add a
dependency. The guard still reds on a second caller of the undeadlined
call, including one in a file only another platform builds.

Four declarations nothing used are gone: a citation-pattern loader the
citation guard stopped calling when it moved to the shared engine, an
ordering helper whose only call was replaced by a structured assertion,
a wrapper every caller went around (its one sentence of contract now
opens the doc of the function they call), and a struct field nothing
read or wrote.
Two of the findings in the set the linter now gates on are lines that
exist to prove something, and changing them as it asks would weaken the
proof. Each carries a //nolint for that one linter, with its reason:

- a %s of a whole config through an any, in the unknown-fields leak
  test. The verb is wrong for the type on purpose: a debug print through
  an any is how a secret would leak, and the row proves it does not.
- the stall-window floor for a leg with nothing recorded. Nothing uses
  it, by design: a refusal tells a caller to pass it, and a guard
  polices that name.
…hecks

.golangci.yml enables unused and staticcheck's correctness checks, and
nothing else. The linter's default set, with every finding printed,
reported the same 110 on all three legs. errcheck gave 93, and every one
was false: writes to a terminal or a report stream the program cannot
act on, and closes of handles opened only for reading. staticcheck's
style families gave 10: nine rewrites of code that already reads
clearly, and one that would have removed the %s path from a redaction
test's positive control. The two classes kept gave 7, all signal, fixed
or annotated in the two commits before this one, and with this
configuration the tree reports none.

CLAUDE.md records the posture: why these two, why a gate inside make ci
rather than advice, what the two annotations protect, and that an
unchecked error is not linted at all.
Observed 2026-10-05: a pull request's head commit carried two runs of
the required check ci (windows-latest), one from the push event and one
from the pull request, and with the first red and the second green the
queue refused to enqueue the pull request, naming that check as failing.
CLAUDE.md's record of the queue now says so, beside the way a pull
request is enqueued here.
@enesismail
enesismail added this pull request to the merge queue Oct 5, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 5, 2026
@enesismail
enesismail added this pull request to the merge queue Oct 5, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 5, 2026
@enesismail
enesismail added this pull request to the merge queue Oct 5, 2026
Merged via the queue into main with commit 81b8f53 Oct 5, 2026
24 of 25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant