Skip to content

Scan for vulnerabilities in CI, and cache the compile that pays for it - #29

Merged
rethab merged 5 commits into
mainfrom
worktree-ci-static-analysis-and-caching
Jul 12, 2026
Merged

Scan for vulnerabilities in CI, and cache the compile that pays for it#29
rethab merged 5 commits into
mainfrom
worktree-ci-static-analysis-and-caching

Conversation

@rethab

@rethab rethab commented Jul 12, 2026

Copy link
Copy Markdown
Owner

No description provided.

@rethab
rethab force-pushed the worktree-ci-static-analysis-and-caching branch 2 times, most recently from 385acfd to 799e468 Compare July 12, 2026 09:19
rethab added 5 commits July 12, 2026 14:01
The lint target only installed the pinned linter when no golangci-lint was
found on $PATH, so anyone who already had one linted against that instead.
This is not a theoretical drift: a v2.12.2 on the author's machine reports
findings that v2.6.2 in CI never sees, and would equally hide findings that
CI does gate on. Check the version of the binary we have and reinstall when
it differs, and invoke it by absolute path so $PATH cannot pick another.

Expose the pinned version as a target so CI can key a cache on it without
restating the number, and let the test target take extra go test flags so CI
can ask for a coverage profile without duplicating the command.
The linter set caught style and obvious bugs but nothing about security or
error handling. Adds gosec, errorlint, noctx, testifylint, nolintlint and a
dozen quieter ratchets that currently report nothing but will hold the line.

The findings worth calling out:

- 34 assertions in pkg/jira read assert.Error(t, &ErrUnexpectedResponse{}, err).
  The second argument of assert.Error is the error under test, not an expected
  value, so these asserted that a freshly built error is non-nil, which it
  always is. They passed no matter what the client returned. They now check
  the error with ErrorAs, and still pass, so nothing was hiding behind them.
- errorlint found four type switches and assertions on errors that would stop
  recognising a jira error the moment anyone wrapped one.
- gosec found `jira man` creating its output directory 0777, under /tmp by
  default, where anyone could then tamper with the generated pages.
- TestGetConfigHome called os.Clearenv and os.Setenv while marked parallel,
  racing every other parallel test that reads the environment.

Also lifts golangci-lint's default issue caps. It reports at most 3 copies of
an identical finding, which is how the 34 vacuous assertions above showed up
as 3, and reads as a clean tail rather than a truncated one.

The gosec suppressions are deliberate and narrow: a CLI shells out to $EDITOR
and reads paths the user hands it, and --insecure is a documented flag.

testifylint's require-error check is left off. It is a good change and a
mechanical one, rewriting ~100 error assertions across 27 test files, which
has nothing to do with the above and belongs in its own commit.
Nothing watched the dependencies. Dependabot only tracked GitHub Actions, so
Go modules moved only when someone bumped them by hand, and no advisory ever
raised an alarm. Adds govulncheck (call-graph aware, so a finding is reachable
rather than merely present), CodeQL for the cross-function paths gosec cannot
see, and gomod and docker to Dependabot. The workflows are the most privileged
code here and nothing checked them either: actionlint and zizmor now do.

govulncheck and CodeQL also run weekly. A dependency becomes vulnerable when
somebody else publishes an advisory, not when we push.

On caching: the Makefile compiles into $(CURDIR)/.gocache, while setup-go asks
the toolchain where GOCACHE is and dutifully caches ~/.cache/go-build, which
the build never writes to. Every run recompiled the whole tree from scratch,
twice, the race-enabled test build included. Naming that same path in the
workflow env is the entire fix. The linter binary and its analysis cache are
now cached too, keyed on the pinned version.

The deliberate no-cache in release, snapshot and docker stays as it is. Those
publish artifacts, and a cache any branch can write to is a way into them.

Also adds a concurrency group so a superseded push stops burning a runner, and
a coverage profile (34.3% today, reported, not gated).

The cross-compile job covers the two release targets that can fail on their
own. There is no OS-specific source in the tree and cgo is off, so every
64-bit unix target type-checks exactly like the linux/amd64 that lint and test
already build. What is left is linux/386, where an int is 32 bits wide and a
narrowing conversion that is harmless on amd64 is a real bug, and windows, the
one GOOS that is not unix. Nothing compiled either until a tag was pushed.
Pinning the version means reinstalling on mismatch, and doing that in
GOPATH/bin silently downgrades whatever golangci-lint the developer keeps
there for other projects. bin/ is already gitignored and nothing else
competes for it.
setup-go's cache key has no job discriminator, so every job in this workflow
raced to save one entry per go.sum hash and the first to finish won. That was
always a fast cross-compile: the saved entry held GOARCH=arm artifacts, the
test job restored them, recompiled its race-enabled build from scratch every
run, and could never save the result. The one compile the caching was meant
to pay for was the one compile it never cached.

Each job now saves its own entry, keyed by what it builds, and a trailing SHA
in the key makes every run save so the cache tracks source changes instead of
freezing at whatever the first run compiled.
@rethab
rethab force-pushed the worktree-ci-static-analysis-and-caching branch from d3e90b3 to f26b335 Compare July 12, 2026 12:01
@rethab
rethab merged commit d685dbf into main Jul 12, 2026
8 checks passed
@rethab
rethab deleted the worktree-ci-static-analysis-and-caching branch July 12, 2026 12:06
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