Description
CI runs golangci-lint as its own lint job, but there is no lint target in the Taskfile.yml — so it is not part of task test, task test-integration, or task ci. A developer can run the whole local suite green and still fail CI.
That happened on #298: 40/40 packages passed locally, CI failed on a one-line ineffassign. golangci-lint was already on the developer's PATH; nothing pointed at it.
Why it needs its own ticket
Adding the target is two lines, but making it pass is not. Two flags are needed to match CI rather than merely resemble it:
--build-tags=integration, or integration-tagged files are excluded and their findings only ever surface on the PR;
GOOS=linux, because the kernel-facing packages are linux-only — on a darwin host every file in them is excluded by build constraints and golangci-lint exits non-zero on the resulting typecheck error.
Resolved during the work — recorded because it is the interesting part. GOOS=linux on a darwin host looked like the obvious way to get the linux-only packages to typecheck, and it produced two findings CI does not report:
internal/neutron/resolve_test.go:218:5: SA5011(related information): this check suggests that the pointer can be nil
internal/neutron/resolve_test.go:221:9: SA5011: possible nil pointer dereference
Both are false positives. The deref sits after a t.Fatalf, which never returns — the code is correct. Cross-compiling loses staticcheck's fact that testing.T.Fatalf is terminal, so it flags a sound pattern. CI, running natively, has the fact and stays quiet.
So the discrepancy was cross-compilation over-reporting, not CI under-reporting, and the GOOS=linux approach is unusable: a lint target that invents failures is worse than no target. The faithful option is to run it where CI runs it — natively, inside the existing Docker builder.
Second requirement found the same way: the target must depend on generate. Without the bpf2go-generated types internal/bpf does not typecheck and every dependent package is analysed with degraded facts — silently, with no error that says so. CI already runs task generate before lint for exactly this reason.
Definition of Done (DoD)
Technical notes
Verified by mutation: reintroducing the exact ineffassign that failed CI on #298 makes task lint report 1 issues; reverting returns 0 issues.
One cost to know about: golangci-lint now lives in the builder image, so task setup must be re-run after pulling this or task lint fails on a missing binary. Every other real task in this repo (generate, binary, test-integration) already goes through the builder, so this follows the grain rather than adding a new mechanism.
Output artifacts (Definition of Done)
Beyond code, docs, and config changes, completing this issue must also deliver:
Description
CI runs
golangci-lintas its ownlintjob, but there is nolinttarget in theTaskfile.yml— so it is not part oftask test,task test-integration, ortask ci. A developer can run the whole local suite green and still fail CI.That happened on #298: 40/40 packages passed locally, CI failed on a one-line
ineffassign.golangci-lintwas already on the developer's PATH; nothing pointed at it.Why it needs its own ticket
Adding the target is two lines, but making it pass is not. Two flags are needed to match CI rather than merely resemble it:
--build-tags=integration, or integration-tagged files are excluded and their findings only ever surface on the PR;GOOS=linux, because the kernel-facing packages are linux-only — on a darwin host every file in them is excluded by build constraints andgolangci-lintexits non-zero on the resulting typecheck error.Resolved during the work — recorded because it is the interesting part.
GOOS=linuxon a darwin host looked like the obvious way to get the linux-only packages to typecheck, and it produced two findings CI does not report:Both are false positives. The deref sits after a
t.Fatalf, which never returns — the code is correct. Cross-compiling loses staticcheck's fact thattesting.T.Fatalfis terminal, so it flags a sound pattern. CI, running natively, has the fact and stays quiet.So the discrepancy was cross-compilation over-reporting, not CI under-reporting, and the
GOOS=linuxapproach is unusable: a lint target that invents failures is worse than no target. The faithful option is to run it where CI runs it — natively, inside the existing Docker builder.Second requirement found the same way: the target must depend on
generate. Without the bpf2go-generated typesinternal/bpfdoes not typecheck and every dependent package is analysed with degraded facts — silently, with no error that says so. CI already runstask generatebefore lint for exactly this reason.Definition of Done (DoD)
linttarget inTaskfile.ymlrunning the same command and flags as the CI job — in the builder,deps: [generate]task cialongsidetestandbench-gatetask lintexits 0 on a clean tree, from any host — it runs in the linux builder, so the host OS stops matteringt.Fatalf. Not excluded either; running natively means they never appeargolangci-lintpinned in the builder image to the version the workflow installs (2.12.2), with a note to bump both togetherTechnical notes
Verified by mutation: reintroducing the exact
ineffassignthat failed CI on #298 makestask lintreport1 issues; reverting returns0 issues.One cost to know about:
golangci-lintnow lives in the builder image, sotask setupmust be re-run after pulling this ortask lintfails on a missing binary. Every other real task in this repo (generate,binary,test-integration) already goes through the builder, so this follows the grain rather than adding a new mechanism.Output artifacts (Definition of Done)
generally useful (e.g. "how to run this repo's CI checks locally"); a
two-line Taskfile target on its own does not need a kb entry.