ci, build: modernize toolchain — goreleaser, golangci-lint v2, Makefile-only tooling#254
Open
l-hellmann wants to merge 17 commits into
Open
ci, build: modernize toolchain — goreleaser, golangci-lint v2, Makefile-only tooling#254l-hellmann wants to merge 17 commits into
l-hellmann wants to merge 17 commits into
Conversation
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).
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.
- 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A scoped modernization of the CI, release, and local development tooling. Reviewer-friendly: every commit is independently buildable.
go.mod;setup-goreadsgo-version-fileso workflows track automatically).pre-release.yml.tools/install.sh,tools/gomodrun.go, andtools/build.shall deleted. Their logic lives in theMakefilenow, with pinned tool versions at the top of the file and stamp-file installs into./bin(auto-install on firstmake lint/make goreleaser-*/make build-dev).Release pipeline
.goreleaser.yamlproduces every asset shape the previous workflows produced. The release workflow now handles bothreleasedandprereleasedevents; 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 -tzfandgoreleaser release --snapshot:zcli-linux-amd64,zcli-linux-i386,zcli-darwin-amd64,zcli-darwin-arm64,zcli-win-x64.exezcli-<platform>-npm.tar.gzcontainingbuilds/<binary>(the npm consumer'star.x({ strip:1 })still finds the binary)zcli_<tag>_<arch>.deb(amd64, i386)Deliberate behavioural changes vs the old workflows:
pre-release.ymlskipped both — simplification, pre-release artifacts now match release artifacts. npm publish + Discord still release-only.gomodrun goreleaseris gone; usemake goreleaser-check/make goreleaser-snapshotor./bin/goreleaserdirectly.Main CI workflow
main.ymlhad 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 dedicatedlint(viagolangci-lint-action@v7) +testjobs plus a compile-only build matrix. Added concurrency cancellation, per-job timeouts,fail-fast: false, and agoreleaser checkjob 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 pinnedgolangci-lintandgoreleaserinto./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 fromgit rev-parse --abbrev-ref+git describe+ author).make install— production install to~/.local/bin/zcli(matches the publicinstall.sh).make install-dev— installszcli-devinto$GOBIN/$GOPATH/binso it coexists with a productionzclion PATH.make all/make windows-amd/…— cross-builds reuse the sameDEV_BUILDrecipe.make helpreads## descriptioncomments next to each target, grouped under##@ Sectionheaders.Latent bug fixed along the way:
tools/build.shwas injecting version via-X github.com/zeropsio/zcli/src/cmd.version=..., but the actual variable lives insrc/version.version. Dev binaries have been reportinglocalindefinitely. The Makefile recipe (and.goreleaser.yaml) use the correct symbol.Lint config
The v1 → v2 bump surfaced 84 findings; triaged rather than fixed wholesale.
ginkgolinter,sqlclosecheck,zerologlint,protogetter,promlinter,loggercheck,rowserrcheck,gochecksumtype,goheader,gomodguard,importas,sloglint) or are purely stylistic (asciicheck,bidichk,decorder,grouper,nosprintfhostport,tagliatelle).errcheck; the rest (G204 subprocess, G304 file inclusion, G301 directory permissions) are inherent to a CLI tool — every finding was a false positive needingnolint.errcheckexcludes never-erroringBuilder/Bufferwrites,fmt.Fprint*, and deferredClose(); staticcheck QF1008 (cosmetic) and ST1001 (intentional dot import) silenced; ST1005 silenced for the wireguard/ip stderr-matching sentinels insrc/cmdRunner/run.go._receiver names, 1×noctx(useNewRequestWithContext), 1×testifylint(require.Empty).// nolint:tagliatelleand// nolint:gosecdirectives left orphaned by the linter drops.make lintnow passes 0 issues across darwin/arm64, linux/amd64, windows/amd64.Test plan
make lintclean across all three GOOSes locallymake testpasses locallymake build-devproduces a working binary with version metadata embeddedmake install/make install-devinstall to the right paths with the right flagsmake goreleaser-checkvalidates the configmake goreleaser-snapshotproduces all expected assets with correct names/tarball structurelint,test,buildmatrix, andgoreleaser-checkjobs all pass on this PRrelease.ymlend-to-end before relying on it for a stable release