Skip to content

ci, build: modernize toolchain — goreleaser, golangci-lint v2, Makefile-only tooling#254

Open
l-hellmann wants to merge 17 commits into
mainfrom
ci-improvements
Open

ci, build: modernize toolchain — goreleaser, golangci-lint v2, Makefile-only tooling#254
l-hellmann wants to merge 17 commits into
mainfrom
ci-improvements

Conversation

@l-hellmann
Copy link
Copy Markdown
Collaborator

@l-hellmann l-hellmann commented May 15, 2026

Summary

A scoped modernization of the CI, release, and local development tooling. Reviewer-friendly: every commit is independently buildable.

  • Go: 1.24 → 1.26 (go.mod; setup-go reads go-version-file so workflows track automatically).
  • golangci-lint: v1.64.7 → v2.12.0. Config migrated to v2 schema; v2.12 is the first build compiled with Go 1.26.
  • goreleaser: introduced at v2.15.4. Replaces ~140 lines of hand-rolled per-platform release steps and the entire pre-release.yml.
  • Tooling scripts removed: tools/install.sh, tools/gomodrun.go, and tools/build.sh all deleted. Their logic lives in the Makefile now, with pinned tool versions at the top of the file and stamp-file installs into ./bin (auto-install on first make lint / make goreleaser-* / make build-dev).
  • README: new "Development setup" section covering prerequisites and the make-driven workflow.

Release pipeline

.goreleaser.yaml produces every asset shape the previous workflows produced. The release workflow now handles both released and prereleased events; npm publish + Discord notify are gated to full releases (github.event.action == 'released'). UPX, .deb (via nfpm), and GitHub release append are all delegated to goreleaser.

Asset compatibility — verified with tar -tzf and goreleaser release --snapshot:

  • zcli-linux-amd64, zcli-linux-i386, zcli-darwin-amd64, zcli-darwin-arm64, zcli-win-x64.exe
  • zcli-<platform>-npm.tar.gz containing builds/<binary> (the npm consumer's tar.x({ strip:1 }) still finds the binary)
  • zcli_<tag>_<arch>.deb (amd64, i386)

Deliberate behavioural changes vs the old workflows:

  • Prereleases now also get UPX-compressed Linux binaries and .deb packages. Old pre-release.yml skipped both — simplification, pre-release artifacts now match release artifacts. npm publish + Discord still release-only.
  • gomodrun goreleaser is gone; use make goreleaser-check / make goreleaser-snapshot or ./bin/goreleaser directly.

Main CI workflow

main.yml had a long-standing bug: lint and test were running 5× across the build matrix but always as linux/amd64 because the matrix referenced an undefined ${{ matrix.osEnv }}. Restructured into dedicated lint (via golangci-lint-action@v7) + test jobs plus a compile-only build matrix. Added concurrency cancellation, per-job timeouts, fail-fast: false, and a goreleaser check job so config errors surface on PRs instead of at release time.

Local dev workflow (Makefile)

Single source of truth for tool versions, build flags, and install paths.

  • make tools / make clean-tools — installs pinned golangci-lint and goreleaser into ./bin. Stamp files (./bin/.<tool>-<version>) drive reinstall when a version is bumped.
  • make build-dev — host-platform dev binary into ./bin/zcli (devel tag, -gcflags='all=-l -N', version composed from git rev-parse --abbrev-ref + git describe + author).
  • make install — production install to ~/.local/bin/zcli (matches the public install.sh).
  • make install-dev — installs zcli-dev into $GOBIN / $GOPATH/bin so it coexists with a production zcli on PATH.
  • make all / make windows-amd / — cross-builds reuse the same DEV_BUILD recipe.
  • Self-documenting help: make help reads ## description comments next to each target, grouped under ##@ Section headers.

Latent bug fixed along the way: tools/build.sh was injecting version via -X github.com/zeropsio/zcli/src/cmd.version=..., but the actual variable lives in src/version.version. Dev binaries have been reporting local indefinitely. The Makefile recipe (and .goreleaser.yaml) use the correct symbol.

Lint config

The v1 → v2 bump surfaced 84 findings; triaged rather than fixed wholesale.

  • Dropped 18 linters that target frameworks the project doesn't use (ginkgolinter, sqlclosecheck, zerologlint, protogetter, promlinter, loggercheck, rowserrcheck, gochecksumtype, goheader, gomodguard, importas, sloglint) or are purely stylistic (asciicheck, bidichk, decorder, grouper, nosprintfhostport, tagliatelle).
  • gosec dropped: every real signal (G104) duplicates errcheck; the rest (G204 subprocess, G304 file inclusion, G301 directory permissions) are inherent to a CLI tool — every finding was a false positive needing nolint.
  • Configured exclusions: errcheck excludes never-erroring Builder/Buffer writes, fmt.Fprint*, and deferred Close(); staticcheck QF1008 (cosmetic) and ST1001 (intentional dot import) silenced; ST1005 silenced for the wireguard/ip stderr-matching sentinels in src/cmdRunner/run.go.
  • Fixed in code: 3× ST1006 _ receiver names, 1× noctx (use NewRequestWithContext), 1× testifylint (require.Empty).
  • Removed dead // nolint:tagliatelle and // nolint:gosec directives left orphaned by the linter drops.

make lint now passes 0 issues across darwin/arm64, linux/amd64, windows/amd64.

Test plan

  • make lint clean across all three GOOSes locally
  • make test passes locally
  • make build-dev produces a working binary with version metadata embedded
  • make install / make install-dev install to the right paths with the right flags
  • make goreleaser-check validates the config
  • make goreleaser-snapshot produces all expected assets with correct names/tarball structure
  • CI: lint, test, build matrix, and goreleaser-check jobs all pass on this PR
  • First real prerelease tag exercises release.yml end-to-end before relying on it for a stable release

v2 introduces a new config schema: 'version: "2"' is required, linters
move under 'linters.default/enable', settings live in 'linters.settings',
and formatters (gofmt, goimports) are split into a 'formatters' section.
gosimple and typecheck are folded into staticcheck and removed.
.goreleaser.yaml replaces the hand-rolled per-platform build/upload
steps. Asset names are preserved exactly (zcli-<platform>, the
zcli-<platform>-npm.tar.gz tarball with builds/<binary> inside used by
@zerops/zcli npm package, and zcli_<tag>_<arch>.deb) so consumers are
unaffected.

Pinned to v2.5.0. Local invocation via 'gomodrun goreleaser'
(installed by tools/install.sh). Makefile targets:
- make goreleaser-check    -> validate config
- make goreleaser-snapshot -> dry-run full release build to ./dist
main.yml: lint and test now run once each in dedicated jobs instead of
5x duplicated across the build matrix (previous setup referenced an
undefined ${{ matrix.osEnv }}, so all lint/test invocations actually
ran as linux/amd64). Build matrix is preserved as a compile-only check
for each target platform. Adds concurrency cancellation, per-job
timeouts, fail-fast: false, and a goreleaser check job.

release.yml: full pipeline now driven by goreleaser, replacing ~140
lines of upload/packaging steps. Released and prereleased events share
one workflow; npm publish and the Discord notification are gated to
full releases via 'github.event.action == 'released''. Pre-release UPX
compression and .deb packaging now happen for prereleases too, which
is a deliberate simplification.

pre-release.yml: deleted, folded into release.yml.
Replace the hand-maintained help message with awk that reads '## desc'
comments from the target lines themselves, so help can't drift from the
real targets. Group targets under '##@ Section' headers, declare every
phony target in .PHONY, and add inline comments for the non-obvious
mechanics (build.sh ldflags, per-GOOS lint pass, gomodrun indirection).
@l-hellmann l-hellmann self-assigned this May 15, 2026
l-hellmann added 11 commits May 15, 2026 10:52
CI uses setup-go with go-version-file, so it auto-tracks go.mod.
Updates the version mention in CLAUDE.md too.
install.sh assumes $GOPATH is set and writes binaries to $GOPATH/bin.
In the runner $GOPATH is empty, so it expands to /bin and the gomodrun
install fails with permission denied. The lint job now installs
golangci-lint via golangci/golangci-lint-action, and the test job
doesn't need the wrapper at all. install.sh stays as the local
developer entrypoint.
The install.sh script assumed $GOPATH was set and used a wrapper binary
(gomodrun) to find tools in ./bin from arbitrary CWDs. CI didn't set
$GOPATH so install.sh expanded GOBIN to /bin and failed with permission
denied.

Replaced both with Makefile rules. Versions are pinned at the top of
the Makefile and a stamp file (./bin/.<tool>-<version>) tracks each
install — bumping a version retargets the dependency, the old stamp
is removed in the recipe, and the tool gets reinstalled. lint and the
goreleaser-* targets depend on the tool stamps, so a first invocation
installs automatically.

gomodrun is gone because Make recipes always run from the repo root,
so plain ./bin/<tool> works without a path-discovery wrapper.
golangci-lint v2.1.6 is built with Go 1.24 and refuses to lint
projects targeting Go 1.26 ('language version used to build is lower
than targeted'). v2.12.0 is built with Go 1.26.

The master install.sh checksum verification kept failing for v2.12.0
and v2.12.2 (computed vs. expected mismatch), so the Makefile now
fetches the release tarball directly via the github.com/.../releases
URL, mirroring the goreleaser install pattern. Both the Makefile pin
and the CI golangci-lint-action version are bumped in lockstep.
The v1 -> v2 golangci-lint bump exposed ~84 findings. Triaged:

Silenced in .golangci.yaml:
- gosec dropped entirely — every meaningful check (G104) duplicated
  errcheck, and the rest (G204/G304/G301) are inherent to a CLI tool
  that runs subprocesses, opens user-supplied paths, and writes to a
  ~/.zcli config dir.
- prealloc dropped — slice prealloc nudges are noise where the slice
  length is tiny or driven by control flow.
- errcheck excludes: never-erroring Builder/Buffer writes, fmt.Fprint*
  to printer wrappers, deferred Close (matched by source regex since
  errcheck's (io.Closer).Close exclude doesn't match concrete types).
- staticcheck: QF1008 (cosmetic embedded-field selector) and ST1001
  (intentional dot import in showcase/main.go) excluded by text rule.
  ST1005 silenced on src/cmdRunner/run.go — those error strings match
  exact stderr output from wireguard/ip and can't be lowered without
  breaking string comparisons.

Fixed in code:
- 3x ST1006 in src/version/message.go: drop '_' receiver names.
- 1x noctx in src/serviceLogs/handler_getLogs.go: use NewRequestWithContext.
- 1x testifylint in src/storage/handler_test.go: require.Empty for
  empty-string check.

make lint now passes for all three target GOOSes.
Disabled 18 linters that either target frameworks the project doesn't
use or are purely stylistic:

Framework/library mismatch (no-op for this codebase):
- ginkgolinter, gochecksumtype, goheader, gomodguard, importas,
  loggercheck, promlinter, protogetter, rowserrcheck, sloglint,
  sqlclosecheck, zerologlint

Low signal / stylistic:
- asciicheck, bidichk, decorder, grouper, nosprintfhostport,
  tagliatelle (the last was already being papered over by a
  // nolint:tagliatelle on src/version/apiDto.go).

Kept: gosmopolitan, godox, mirror, maintidx — small but real signal.

Also removed nolint directives left dead by this PR's earlier drops
(tagliatelle, gosec).
Migrates .goreleaser.yaml off the deprecated 'builds:' / 'format:'
keys (archives + nfpms) onto the modern 'ids:' / 'formats:' schema,
which v2.15 requires. Locally verified with 'goreleaser check' and a
full --snapshot build: all asset names, tarball structure
(builds/<binary>), and .deb file names are identical to the v2.5.0
output.
Adds a 'make build-dev' target that builds a host-platform dev binary
into ./bin/zcli with the devel build tag, -gcflags='all=-l -N' for
dlv-friendly debugging, and version metadata composed from git
(branch:tag-(name:<email>)) — same shape build.sh produced.

windows-amd/linux-amd/darwin-amd/darwin-arm now share the same
DEV_BUILD recipe via the Makefile variable instead of shelling out.

Also fixes a latent bug build.sh has been carrying: -X was targeting
github.com/zeropsio/zcli/src/cmd.version which has no such symbol,
so the dev binaries always reported 'local'. The correct symbol is
src/version.version (the same one .goreleaser.yaml uses).
- 'make install' builds an optimized, stripped zcli binary (using the
  same flag set goreleaser uses for releases — -trimpath, -s -w, version
  from git describe) and writes it to $GOBIN (falling back to
  $GOPATH/bin).
- 'make install-dev' uses the existing DEV_BUILD recipe (devel tag,
  -gcflags='all=-l -N', verbose version metadata) and writes a
  zcli-dev binary, so devs can keep a production zcli alongside the
  dlv-friendly dev build on the same PATH.
The public install.sh script puts zcli at $HOME/.local/bin/zcli, so
'make install' should land on the same PATH entry — anyone who has
the binary on their PATH from the public install script will get a
local make-installed binary picking up the same way.

'make install-dev' stays on $GOBIN/$GOPATH/bin (Go's convention for
dev tooling) so the dev build sits alongside other go-installed tools
and doesn't shadow the user-facing production zcli.
Documents the make-driven workflow (tools, build-dev, install,
install-dev, test, lint, goreleaser-snapshot) plus the version-pinning
contract for tooling, so a new contributor can go from clone to working
build without spelunking through the Makefile.
@l-hellmann l-hellmann changed the title ci: modernize pipeline with goreleaser and golangci-lint v2 ci, build: modernize toolchain — goreleaser, golangci-lint v2, Makefile-only tooling May 15, 2026
- actions/checkout v4 -> v6
- actions/setup-go v5 -> v6
- actions/setup-node v4 -> v6
- golangci/golangci-lint-action v7 -> v9 (requires golangci-lint
  >= v2.1.0; we pin v2.12.0)
- goreleaser/goreleaser-action v6 -> v7
- sarisia/actions-status-discord v1.15.0 -> v1.16.0

The major bumps are all Node runtime updates (Node 20 -> Node 24) plus
the golangci-lint-action v8 'use absolute paths by default' change,
which doesn't affect our run. No input/argument schemas changed for
how we invoke them.
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