FIX-030 — Local CI parity on macOS and pinned-tool version checks¶
Type: FIX (build/CI infrastructure).
Status: Implemented (2026-07-30; pending landing).
Date: 2026-07-30.
Author: Ruslan Gabitov.
Paired doc: none.
Upstream: operational backlog item “make ci cannot pass on macOS”; also
closes the pinned-tool-version finding recorded in
docs/audit/code-review-codex-second-pass-2026-06-29.md.
§1 Symptoms¶
The local make ci contract was weaker than the GitHub CI contract in two
ways:
run-examplesinvoked GNUtimeoutby its Linux name. macOS does not ship that binary; Homebrew coreutils installs the same command asgtimeout. The target had no preflight, so it failed late with a barecommand not found.require-toolchecked only whether a binary existed. An incompatible global install therefore passed the guard and failed later with misleading diagnostics — observed withcovercheck v0.1.2, which does not support the pinnedv0.2.0-exclude-pathsflag. The same risk applied to mock generation and lint configuration.
The Go tests themselves are portable; the failure was in their local CI orchestration.
§2 Root cause¶
The example gate assumed GNU userland even though local CI is documented for
all developer platforms. Separately, the tool pins in Makefile were used only
for installation, not validation, so they were not an executable parity
contract.
Parsing each tool's human-readable --version output is brittle because the
four tools format it differently. Go binaries already carry stable build
metadata containing their module path and version.
§3 Solution¶
§3.1 Portable example timeout¶
Makefile resolves EXAMPLE_TIMEOUT in this order:
timeout(GNU/Linux and any explicitly exposed GNU coreutils binary);gtimeout(Homebrew's non-conflicting macOS name);- literal
timeout, so the guard emits a deterministic install error.
run-examples uses require-command before starting the loop. On macOS the
error names the exact prerequisite:
brew install coreutils
EXAMPLE_TIMEOUT remains overrideable for nonstandard environments.
§3.2 Exact pinned versions¶
require-go-tool now validates all pinned Go development tools:
| Command | Required module | Pin |
|---|---|---|
mockery |
github.com/vektra/mockery/v3 |
v3.5.0 |
golangci-lint |
github.com/golangci/golangci-lint/v2 |
v2.11.4 |
govulncheck |
golang.org/x/vuln |
v1.6.0 |
covercheck |
github.com/dr-dobermann/covercheck |
v0.2.0 |
The guard resolves the executable from PATH, reads go version -m, matches
the expected module, and requires the exact version. Missing, stale,
wrong-module, and unversioned binaries all fail before the destructive or
expensive target body starts. make tools remains the single repair command.
Its golangci-lint installer is fetched from $(GOLANGCI_VERSION) rather than
the moving master branch, matching the workflow's supply-chain pin.
The guard was also added to the single-module lint, lint_fix, and
lint_all targets, which previously bypassed the repository's fail-loud rule.
§4 Documentation¶
README.mdandREADME.ru.mdlist all four tools installed bymake toolsand the one-time macOS coreutils prerequisite.CLAUDE.mdrecords the version-aware guard and thetimeout/gtimeoutplatform mapping.- The completed item leaves
docs/backlog.md; this FIX is its durable record.
§5 Verification¶
Verified on macOS on 2026-07-30:
- the installed pins passed their preflights;
COVERCHECK_VERSION=v9.9.9failed beforecovercheckran, reporting the installedv0.2.0and requiredv9.9.9;- a missing
EXAMPLE_TIMEOUTfailed before the loop and namedbrew install coreutils; - a PATH containing
gtimeoutbut notimeoutpassed therun-examplespreflight; make mock-checkregenerated the committed mocks with no drift;make lintpassed with zero issues;git diff --checkpassed;- a full isolated
make cipassed: race tests, 96.3% diff coverage (95% minimum), vulnerability scans, and the complete examples tidy/lint/build/run sweep.