Adopt the org-standard .golangci.yml and golangci-lint v2.12.2 #14

Open
opened 2026-08-09 03:36:49 +02:00 by clawbot · 4 comments
Collaborator

Problem

This repo is not on the org-standard Go lint configuration, and the linter version pinned in the backend Docker build is two minor versions behind what is now deployed across the org's Go repos.

Verified on main at fbfe1df:

  1. .golangci.yml content mismatch.

    sha256(backend/.golangci.yml)                = 33ba2bf7fe4a44779d09b0fb31d6daf03685f8dc9d2bc417f963d7aabb0d17dc
    sha256(org standard, prompts/.golangci.yml)  = 021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb
    

    REPO_POLICIES.md states: ".golangci.yml is standardized and must NEVER be modified by an agent, only manually by the user. Fetch from https://git.eeqj.de/sneak/prompts/raw/branch/main/.golangci.yml."

  2. Linter version mismatch. Dockerfile.backend pins golangci-lint at commit 9f61b0f53f80672872fced07b6874397c3ed197b (v2.7.2, dated 2026-02-27). The org standard is v2.12.2, commit c0d3ddc9cf3faa61a4e378e879ece580256d76e5.

make check in backend/ currently reports 0 issues. against the non-standard config. Moving to the stricter standard config plus a newer linter is expected to surface new findings; fixing those findings is part of this work unit.

Definition of done

  • backend/.golangci.yml is byte-identical to the org standard — sha256sum backend/.golangci.yml outputs 021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb. Copy it verbatim; do not hand-edit, tune, or add exclusions to it.
  • Dockerfile.backend pins golangci-lint to commit c0d3ddc9cf3faa61a4e378e879ece580256d76e5, with a comment above the reference reading the version and date in the form # golangci-lint v2.12.2 (YYYY-MM-DD) per the hash-pinning policy.
  • Every new lint finding surfaced by the standard config and the newer linter is fixed in the Go source. Suppressing findings by editing .golangci.yml is explicitly forbidden. A targeted //nolint:linter // reason on an individual line is acceptable only where the finding is genuinely a false positive, and each one must carry a written justification comment.
  • cd backend && make check passes and reports 0 issues.
  • make check at the repo root still passes.
  • docker build -f Dockerfile.backend . succeeds.
  • TODO.md is updated in the same commit as the work.
  • Commit title ends with (closes #N) for this issue number.

Implementation requirements

  • Use make targets and script/ entrypoints only. Never invoke go, gofmt, or golangci-lint directly.
  • Do not change the .golangci.yml in any way after copying it. If the standard config disagrees with the code, the code changes.
  • Keep this commit scoped to lint configuration, the linter version pin, and the source fixes required to make them pass. Do not fold in unrelated compliance work — those are separate issues.
  • No attribution trailers in the commit message.
## Problem This repo is not on the org-standard Go lint configuration, and the linter version pinned in the backend Docker build is two minor versions behind what is now deployed across the org's Go repos. Verified on `main` at `fbfe1df`: 1. **`.golangci.yml` content mismatch.** ``` sha256(backend/.golangci.yml) = 33ba2bf7fe4a44779d09b0fb31d6daf03685f8dc9d2bc417f963d7aabb0d17dc sha256(org standard, prompts/.golangci.yml) = 021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb ``` `REPO_POLICIES.md` states: "`.golangci.yml` is standardized and must _NEVER_ be modified by an agent, only manually by the user. Fetch from `https://git.eeqj.de/sneak/prompts/raw/branch/main/.golangci.yml`." 2. **Linter version mismatch.** `Dockerfile.backend` pins golangci-lint at commit `9f61b0f53f80672872fced07b6874397c3ed197b` (v2.7.2, dated 2026-02-27). The org standard is **v2.12.2**, commit `c0d3ddc9cf3faa61a4e378e879ece580256d76e5`. `make check` in `backend/` currently reports `0 issues.` against the non-standard config. Moving to the stricter standard config plus a newer linter is expected to surface new findings; fixing those findings is part of this work unit. ## Definition of done - [ ] `backend/.golangci.yml` is byte-identical to the org standard — `sha256sum backend/.golangci.yml` outputs `021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb`. Copy it verbatim; do **not** hand-edit, tune, or add exclusions to it. - [ ] `Dockerfile.backend` pins golangci-lint to commit `c0d3ddc9cf3faa61a4e378e879ece580256d76e5`, with a comment above the reference reading the version and date in the form `# golangci-lint v2.12.2 (YYYY-MM-DD)` per the hash-pinning policy. - [ ] Every new lint finding surfaced by the standard config and the newer linter is **fixed in the Go source**. Suppressing findings by editing `.golangci.yml` is explicitly forbidden. A targeted `//nolint:linter // reason` on an individual line is acceptable only where the finding is genuinely a false positive, and each one must carry a written justification comment. - [ ] `cd backend && make check` passes and reports `0 issues.` - [ ] `make check` at the repo root still passes. - [ ] `docker build -f Dockerfile.backend .` succeeds. - [ ] `TODO.md` is updated in the same commit as the work. - [ ] Commit title ends with ` (closes #N)` for this issue number. ## Implementation requirements - Use `make` targets and `script/` entrypoints only. Never invoke `go`, `gofmt`, or `golangci-lint` directly. - Do not change the `.golangci.yml` in any way after copying it. If the standard config disagrees with the code, the code changes. - Keep this commit scoped to lint configuration, the linter version pin, and the source fixes required to make them pass. Do not fold in unrelated compliance work — those are separate issues. - No attribution trailers in the commit message.
clawbot added this to the 1.0.0 milestone 2026-08-09 03:36:49 +02:00
Author
Collaborator

Escalation: the current config is not merely non-standard, it is invalid and silently inert

Follow-up audit found the problem is worse than a hash mismatch. backend/.golangci.yml declares version: "2" on line 1 but uses golangci-lint v1 schema below it:

  • Line 18 uses a top-level linters-settings: key. In v2 this moved to linters.settings:.
  • Line 30 adds issues.exclude-use-default: false, which does not exist in v2 and is absent from the canonical file.
  • The canonical file's explanatory header comment — which exists specifically to prevent this edit — was stripped.

Verified empirically with golangci-lint v2.10.1:

$ golangci-lint config verify
jsonschema: "issues" does not validate ... additional properties 'exclude-use-default' not allowed
jsonschema: "" does not validate ... additional properties 'linters-settings' not allowed
The command is terminated due to an error: the configuration contains invalid elements

Why this matters more than the version bump

Every threshold in the file is inert: lll: 88, funlen: 80/50, cyclop: 15, dupl: 100 are all ignored. lll falls back to its default of 120 columns.

So when cd backend && make check prints 0 issues., that is not evidence the code is clean — it is evidence the linter ran with defaults and found nothing at defaults. The green result we have been relying on is meaningless.

Concretely, three lines already exceed the 77-character hard wrap required by CODE_STYLEGUIDE_GO.md, and go unreported today:

  • internal/server/server.go:65 — 93 chars
  • internal/server/server.go:97 — 81 chars
  • internal/reportbuf/reportbuf.go:166 — 88 chars

All three are long //nolint justification comments; lll would catch them once the config is valid.

Consequences for this issue

  • Expect substantially more than a handful of new findings once the canonical config actually loads. Budget accordingly; this may not fit one commit. If it does not, split into "adopt config + linter version" and "fix the findings" and note the split here.
  • The definition of done already requires sha256sum backend/.golangci.yml to equal 021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb. Add one verification step: after copying, run the linter's own config verification and confirm it reports the config valid. A config that fails to verify must never be committed again.
  • The three lll violations above are in scope.

This also raises the priority of this issue above the other compliance items: until the config is valid, no other issue's "lint passes" claim can be trusted.

## Escalation: the current config is not merely non-standard, it is invalid and silently inert Follow-up audit found the problem is worse than a hash mismatch. `backend/.golangci.yml` declares `version: "2"` on line 1 but uses **golangci-lint v1 schema** below it: - Line 18 uses a top-level `linters-settings:` key. In v2 this moved to `linters.settings:`. - Line 30 adds `issues.exclude-use-default: false`, which does not exist in v2 and is absent from the canonical file. - The canonical file's explanatory header comment — which exists specifically to prevent this edit — was stripped. Verified empirically with golangci-lint v2.10.1: ``` $ golangci-lint config verify jsonschema: "issues" does not validate ... additional properties 'exclude-use-default' not allowed jsonschema: "" does not validate ... additional properties 'linters-settings' not allowed The command is terminated due to an error: the configuration contains invalid elements ``` ### Why this matters more than the version bump Every threshold in the file is **inert**: `lll: 88`, `funlen: 80/50`, `cyclop: 15`, `dupl: 100` are all ignored. `lll` falls back to its default of 120 columns. So when `cd backend && make check` prints `0 issues.`, that is **not evidence the code is clean** — it is evidence the linter ran with defaults and found nothing at defaults. The green result we have been relying on is meaningless. Concretely, three lines already exceed the 77-character hard wrap required by `CODE_STYLEGUIDE_GO.md`, and go unreported today: - `internal/server/server.go:65` — 93 chars - `internal/server/server.go:97` — 81 chars - `internal/reportbuf/reportbuf.go:166` — 88 chars All three are long `//nolint` justification comments; `lll` would catch them once the config is valid. ### Consequences for this issue - Expect **substantially more** than a handful of new findings once the canonical config actually loads. Budget accordingly; this may not fit one commit. If it does not, split into "adopt config + linter version" and "fix the findings" and note the split here. - The definition of done already requires `sha256sum backend/.golangci.yml` to equal `021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb`. Add one verification step: after copying, run the linter's own config verification and confirm it reports the config valid. A config that fails to verify must never be committed again. - The three `lll` violations above are in scope. This also raises the priority of this issue above the other compliance items: until the config is valid, no other issue's "lint passes" claim can be trusted.
Author
Collaborator

Implementation plan

Branch feat/golangci-standard-config off main at fbfe1df.

  1. Copy the org standard .golangci.yml verbatim over backend/.golangci.yml
    (target sha256 021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb).
    No edits, no added exclusions.

  2. Repoint the golangci-lint pin in Dockerfile.backend from
    9f61b0f53f80672872fced07b6874397c3ed197b to
    c0d3ddc9cf3faa61a4e378e879ece580256d76e5, and update the comment above it
    to # golangci-lint v2.12.2 (2026-08-09).

  3. Make the config-validity check permanent rather than a one-off manual step.
    The escalation asks for golangci-lint config verify to be run after
    copying; a one-time run does not stop the schema from rotting again. So
    backend/Makefile's lint target gains a golangci-lint config verify
    line ahead of golangci-lint run ./.... From then on make lint — and
    therefore make check, the pre-commit hook, and the Docker build — fails on
    an invalid config instead of silently falling back to defaults. This also
    keeps the verification reachable through a make target, so nothing has to
    invoke the linter by hand.

  4. Fix every surfaced finding in the Go source. No .golangci.yml edits.

  5. Update TODO.md in the same commit, correcting the stale Status/Next Step
    (it still describes feat/reportbuf-storage as unmerged; it is merged).

  6. Gate on cd backend && make check, root make check, and
    docker build -f Dockerfile.backend . (via make docker, which is what
    exercises the pinned v2.12.2 rather than whatever is on the local PATH).

Scope note on the three lines called out in the escalation

The canonical config sets lll.line-length: 88, so of the three lines listed
only one is an actual lint finding:

  • internal/server/server.go:65 — 93 chars, is an lll finding.
  • internal/server/server.go:97 — 81 chars, under 88, not reported.
  • internal/reportbuf/reportbuf.go:166 — 88 chars, exactly at the limit, not
    reported.

All three do violate the 77-column hard wrap in CODE_STYLEGUIDE_GO.md, and
the escalation puts all three in scope, so all three get wrapped. In each case
the length comes from a long //nolint justification on the code line; the fix
is to move the justification to a preceding comment block and leave a short
//nolint:linter // see comment above on the line itself. The suppressions
themselves are unchanged in meaning and are not being widened.

Expected outcome: one commit, no split needed. Will report back if the v2.12.2
run inside the Docker build surfaces materially more than the local v2.10.1 run
did.

## Implementation plan Branch `feat/golangci-standard-config` off `main` at `fbfe1df`. 1. Copy the org standard `.golangci.yml` verbatim over `backend/.golangci.yml` (target sha256 `021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb`). No edits, no added exclusions. 2. Repoint the golangci-lint pin in `Dockerfile.backend` from `9f61b0f53f80672872fced07b6874397c3ed197b` to `c0d3ddc9cf3faa61a4e378e879ece580256d76e5`, and update the comment above it to `# golangci-lint v2.12.2 (2026-08-09)`. 3. Make the config-validity check permanent rather than a one-off manual step. The escalation asks for `golangci-lint config verify` to be run after copying; a one-time run does not stop the schema from rotting again. So `backend/Makefile`'s `lint` target gains a `golangci-lint config verify` line ahead of `golangci-lint run ./...`. From then on `make lint` — and therefore `make check`, the pre-commit hook, and the Docker build — fails on an invalid config instead of silently falling back to defaults. This also keeps the verification reachable through a `make` target, so nothing has to invoke the linter by hand. 4. Fix every surfaced finding in the Go source. No `.golangci.yml` edits. 5. Update `TODO.md` in the same commit, correcting the stale Status/Next Step (it still describes `feat/reportbuf-storage` as unmerged; it is merged). 6. Gate on `cd backend && make check`, root `make check`, and `docker build -f Dockerfile.backend .` (via `make docker`, which is what exercises the pinned v2.12.2 rather than whatever is on the local PATH). ### Scope note on the three lines called out in the escalation The canonical config sets `lll.line-length: 88`, so of the three lines listed only one is an actual lint finding: - `internal/server/server.go:65` — 93 chars, **is** an `lll` finding. - `internal/server/server.go:97` — 81 chars, under 88, not reported. - `internal/reportbuf/reportbuf.go:166` — 88 chars, exactly at the limit, not reported. All three do violate the 77-column hard wrap in `CODE_STYLEGUIDE_GO.md`, and the escalation puts all three in scope, so all three get wrapped. In each case the length comes from a long `//nolint` justification on the code line; the fix is to move the justification to a preceding comment block and leave a short `//nolint:linter // see comment above` on the line itself. The suppressions themselves are unchanged in meaning and are not being widened. Expected outcome: one commit, no split needed. Will report back if the v2.12.2 run inside the Docker build surfaces materially more than the local v2.10.1 run did.
Author
Collaborator

No split needed — delivered as one PR

PR #31 on branch feat/golangci-standard-config.

The escalation warned to budget for "substantially more" findings once the
canonical config actually loaded, and to split into "adopt config + linter
version" and "fix the findings" if it did not fit one commit. It fit: with the
standard config loading correctly under v2.12.2 the backend produced exactly
one finding, lll on internal/server/server.go:65.

The linters that had been silently inert under the broken config — funlen,
cyclop, dupl, and lll at its real 88-column threshold instead of the
120-column default — had nothing further to report. The backend is small
enough that the defaults it had been accidentally linted against were not far
off the intended thresholds.

Two corrections to the escalation's line inventory, for the record:

  • internal/server/server.go:97 (81 cols) and
    internal/reportbuf/reportbuf.go:166 (88 cols) are not lll findings.
    The canonical config sets lll.line-length: 88, and lll reports lines
    strictly over the limit, so 81 passes and 88 sits exactly at it.
  • They do violate the 77-column hard wrap in CODE_STYLEGUIDE_GO.md, and the
    escalation explicitly scoped them in, so both were wrapped alongside the
    genuine finding.

All three were long //nolint justifications on the code line; each
justification moved to a comment block above, leaving a short directive behind.
No suppression added or widened, and .golangci.yml untouched after the
verbatim copy.

On the verification requirement: rather than running golangci-lint config verify once by hand, it is now the first step of the backend lint target, so
it runs on every make check, every pre-commit, and every Docker build. An
invalid config fails the build from now on instead of silently degrading to
defaults — which is the failure this issue exists to close out.

## No split needed — delivered as one PR PR #31 on branch `feat/golangci-standard-config`. The escalation warned to budget for "substantially more" findings once the canonical config actually loaded, and to split into "adopt config + linter version" and "fix the findings" if it did not fit one commit. It fit: with the standard config loading correctly under v2.12.2 the backend produced exactly **one** finding, `lll` on `internal/server/server.go:65`. The linters that had been silently inert under the broken config — `funlen`, `cyclop`, `dupl`, and `lll` at its real 88-column threshold instead of the 120-column default — had nothing further to report. The backend is small enough that the defaults it had been accidentally linted against were not far off the intended thresholds. Two corrections to the escalation's line inventory, for the record: - `internal/server/server.go:97` (81 cols) and `internal/reportbuf/reportbuf.go:166` (88 cols) are **not** `lll` findings. The canonical config sets `lll.line-length: 88`, and `lll` reports lines strictly over the limit, so 81 passes and 88 sits exactly at it. - They do violate the 77-column hard wrap in `CODE_STYLEGUIDE_GO.md`, and the escalation explicitly scoped them in, so both were wrapped alongside the genuine finding. All three were long `//nolint` justifications on the code line; each justification moved to a comment block above, leaving a short directive behind. No suppression added or widened, and `.golangci.yml` untouched after the verbatim copy. On the verification requirement: rather than running `golangci-lint config verify` once by hand, it is now the first step of the backend `lint` target, so it runs on every `make check`, every pre-commit, and every Docker build. An invalid config fails the build from now on instead of silently degrading to defaults — which is the failure this issue exists to close out.
Author
Collaborator

Correction to my earlier escalation comment

My escalation listed three lines as lll violations that would surface once the canonical config loaded. Two of those three were wrong, and the implementer was right to push back rather than accept the list as given.

Verified directly against the canonical config at /home/user/dev/prompts/.golangci.yml:

    lll:
      line-length: 88

lll reports lines strictly over 88. So:

Line Length lll finding?
internal/server/server.go:65 93 Yes — correctly predicted
internal/server/server.go:97 81 No — under the limit
internal/reportbuf/reportbuf.go:166 88 No — at the limit, not over it

Where my escalation went wrong: I carried over the audit's framing, which measured those lines against the 77-column hard-wrap guidance in CODE_STYLEGUIDE_GO.md ("Try to hard wrap long lines at 77 characters or less") and then described them as things lll would catch. Those are two different standards. 77 is styleguide guidance; 88 is what the linter actually enforces. Conflating them produced a false prediction, and the "expect substantially more findings" warning that followed was built on the same overreach.

The implementer wrapped all three anyway, which is fine — the escalation explicitly scoped them in, and they do violate the 77-column guidance even though only one violates lll. But the single real lll finding is the honest count, and the anticipated second PR was correctly not created.

Recording this because the wrong claim is durable once it is in the tracker, and because the general lesson applies to the rest of this backlog: several other issues I filed cite audit findings I did not personally re-derive. Implementers should verify the specific line numbers and thresholds in an issue against the source before treating them as fact — and say so when they do not hold up, as happened here.

None of this changes the definition of done or the core finding: the previous config was genuinely schema-invalid and genuinely inert, and that part stands.

## Correction to my earlier escalation comment My escalation listed three lines as `lll` violations that would surface once the canonical config loaded. **Two of those three were wrong**, and the implementer was right to push back rather than accept the list as given. Verified directly against the canonical config at `/home/user/dev/prompts/.golangci.yml`: ```yaml lll: line-length: 88 ``` `lll` reports lines strictly over 88. So: | Line | Length | `lll` finding? | | --- | --- | --- | | `internal/server/server.go:65` | 93 | **Yes** — correctly predicted | | `internal/server/server.go:97` | 81 | **No** — under the limit | | `internal/reportbuf/reportbuf.go:166` | 88 | **No** — at the limit, not over it | Where my escalation went wrong: I carried over the audit's framing, which measured those lines against the **77-column hard-wrap guidance in `CODE_STYLEGUIDE_GO.md`** ("Try to hard wrap long lines at 77 characters or less") and then described them as things `lll` would catch. Those are two different standards. 77 is styleguide guidance; 88 is what the linter actually enforces. Conflating them produced a false prediction, and the "expect substantially more findings" warning that followed was built on the same overreach. The implementer wrapped all three anyway, which is fine — the escalation explicitly scoped them in, and they do violate the 77-column guidance even though only one violates `lll`. But the single real `lll` finding is the honest count, and the anticipated second PR was correctly not created. Recording this because the wrong claim is durable once it is in the tracker, and because the general lesson applies to the rest of this backlog: several other issues I filed cite audit findings I did not personally re-derive. Implementers should verify the specific line numbers and thresholds in an issue against the source before treating them as fact — and say so when they do not hold up, as happened here. None of this changes the definition of done or the core finding: the previous config was genuinely schema-invalid and genuinely inert, and that part stands.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/netwatch#14