d43c1d31ac8676457ca0fe0f41a2141693168154
7 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| d4eaf5fed2 |
Run all linting in Docker via Dockerfile.lint (closes #46)
All checks were successful
check / check (push) Successful in 1m38s
Per the owner ruling the linter runs in a container invoked through the script/ entrypoint, never installed on a host. Dockerfile.lint COPYs the repo into the digest-pinned golangci/golangci-lint:v2.12.2 image and runs `golangci-lint config verify` and `golangci-lint run` as build steps, so a successful build IS a clean lint. script/lint is reduced to building it, and works with a remote docker daemon, where bind mounts are impossible. script/bootstrap loses the `go install`, the pin constants, the version parser and verify_golangci_lint: with nothing linting on the host, the $GOPATH/bin versus PATH shadowing they diagnosed has no subject. It keeps the git/make/go presence checks and `go mod download`, and warns rather than fails when docker is absent. Traps for anyone changing this. A lint build on an unchanged tree exits 0 in under a second having run no linter -- #32 and #39 again. Caching is waived by ruling: Dockerfile.lint carries ARG CHECK_EPOCH referenced inside every gate RUN, because BuildKit hashes the expanded command and a declared but unreferenced ARG invalidates nothing. script/lint passes "$(date +%s)-$$"; the PID is there because two runs land in the same second easily and a bare epoch would cache the second. Nothing inside an image build may shell out to docker. The main Dockerfile's lint stage therefore invokes golangci-lint directly rather than `make lint`, and its build stage runs `make test` and `make fmt-check` rather than the `make check` aggregate, which reaches script/lint. Both stay `make` invocations rather than bare scripts because the Makefile's `export CGO_ENABLED = 0` only reaches what it invokes. COPY --from=lint /usr/bin/golangci-lint becomes COPY --from=lint /src/go.sum /dev/null. The copied binary was the only edge forcing BuildKit to finish linting before the build stage starts; dropping it without replacing the edge would have ended fail-fast linting silently under a still-green build. That no-op copy is the ordering edge canonical REPO_POLICIES.md prescribes. Nothing in the build stage runs the linter now, so ENV PATH=/home/builder/go/bin:$PATH goes with the `go install` that justified it. script/verify-linter-pin is retired with its README entry: it compared a linter binary against GOLANGCI_LINT_VERSION in script/bootstrap and neither subject still exists. The drift moved rather than went away -- the linter is pinned twice, as the FROM line of Dockerfile.lint and the FROM line of the Dockerfile lint stage, which is what #42 made a build failure. script/verify-lint-image-pin compares those two references to each other and restates neither pin; a hardcoded digest would be a third copy and the same drift one file further out. It runs as a gate in both files, and an unreadable reference is a hard failure rather than a vacuous pass. `golangci-lint config verify` is included per the ruling, and its unpinned live HTTPS schema fetch was measured rather than assumed: under --network none the pinned binary passes a valid config and rejects an invalid one with the jsonschema error, so it validates against a schema it embeds. That holds for the gate steps, none of which makes a network call, but not for the build around them -- Dockerfile.lint runs `go mod download` above the gates, so a cold cache needs the network and only a warm one lints offline, until go.mod or go.sum changes. Verified. `make lint` green with every PATH directory containing a golangci-lint removed and `command -v golangci-lint` empty. Two consecutive script/lint runs on an untouched tree both executed the linter, 27.7s and 28.7s under distinct epochs with the COPY layer CACHED above them. A planted unused variable failed script/lint with that finding, and failed `make docker` at the lint stage with the build stage stopped before its COPY --from=lint; reverted clean. The drift guard fails on tag-only, digest-only and unreadable-reference cases, naming both sides. `make check` green; `make docker` green in 5m35s with all six gates executing and the test gate reporting real coverage rather than a cached ok. In the builder image with the Go test cache off, --user 0:0 still fails TestScanHardlinkRunFailsTogether where the unprivileged user passes, so the non-root quirk is intact. |
|||
|
|
5ca68804ac |
Fail the Docker build when the lint stage's linter is not the pin
All checks were successful
check / check (push) Successful in 1m23s
The reordered COPY --from=lint did not make the two stages provably one toolchain, as the Dockerfile comment, the previous commit message and TODO.md all claimed. script/bootstrap compares its pin against whatever PATH resolves, and $GOPATH/bin sits ahead of /usr/local/bin, so any drift was absorbed: bootstrap rebuilt the pinned version from source, verified that, and the build went green with the lint stage having linted at one version and make check having run at another. Bumping the lint stage image without touching the pin was enough to produce it. New script/verify-linter-pin fails, naming both versions, unless a given golangci-lint binary is exactly the version script/bootstrap pins. The build stage runs it on the binary copied out of the lint stage, immediately after the copy and before bootstrap, so no reinstall can satisfy it. The pin is read out of script/bootstrap, which stays its single source of truth; a pin that cannot be read is a hard failure rather than a skip. The check takes no CHECK_EPOCH because its only inputs are the copied binary and script/, so Docker invalidates the layer exactly when a cached result would stop being true. The linter version is pinned independently in the lint stage's image digest and in GOLANGCI_LINT_VERSION, with nothing keeping them in sync; a half-applied bump is now a build failure instead of a silent split. ENV PATH keeps $GOPATH/bin, but its comment no longer claims a reinstall is the reason: bootstrap must be able to run and verify what it installs, and nothing in this image is shadowed by the entry. Verified: with the lint stage's linter faked to 2.11.0 after the gates had really run, the build fails at verify-linter-pin naming 2.11.0 and 2.12.2, with bootstrap and the check gate never reached; an unmodified make docker is green with all three gates run on a fresh epoch and real test results. A planted unused finding still fails at the lint stage with gate check absent from the log; the image still fails TestScanHardlinkRunFailsTogether under --user 0:0 and passes as uid 1000, both with the Go test cache disabled; and a second build serves bootstrap, the verify layer and the dependency layers CACHED while the gates go cold. |
||
|
|
964fc29ed3 |
Bust the Docker layer cache for the gate steps (closes #32)
All checks were successful
check / check (push) Successful in 1m50s
script/cibuild and script/docker were bare docker build invocations with no cache control, and the Dockerfile copies the tree before running its gates. On an unchanged tree Docker served those layers from cache, so the gates never executed and the build still exited 0. A merge commit here has a tree byte-identical to the branch head it merges, so every merge CI run was almost certainly a full cache hit, and PR #31's reviewer caught make docker returning success as a 17-layer cache hit that proved nothing. Declare ARG CHECK_EPOCH in both stages and have the scripts pass --build-arg CHECK_EPOCH="$(date +%s)". ARG is scoped per stage and this Dockerfile has three gates across two of them (make fmt-check and make lint in the lint stage, make check in the build stage), so one declaration would have left a stage silently cacheable. BuildKit hashes the expanded command rather than the declaration, so each gate RUN echoes the epoch: an unreferenced ARG invalidates nothing, and the echo doubles as evidence in the build log that the layer really ran. Both declarations sit below the dependency layers, so the pinned base images, go mod download, apk add and the source copies keep their cache and only the gates go cold. The build-stage declaration sits after USER, so the drop to the unprivileged builder user still happens before make check and the chmod(0) permission tests stay real. |
||
|
|
9d06c13777 |
Verify the golangci-lint install actually took effect
All checks were successful
check / check (push) Successful in 1m31s
`go install` writes into GOBIN (or GOPATH/bin), but the linter `make lint` runs is whichever golangci-lint PATH resolves first. On a host where a wrong-version binary sits ahead of that directory — a nix profile, apt, brew, apk, a tarball in /usr/local/bin, or the /usr/local/bin copy the Dockerfile builder stage makes — the install landed behind the shadow, changed nothing the gate uses, and bootstrap still printed "bootstrap complete" and exited 0. That leaves the local gate linting against a different ruleset than CI while affirmatively claiming otherwise, and every subsequent run reinstalls forever, so the second run is never a no-op. After installing, re-read the effective version. On a mismatch print the resolved binary, the install directory and both versions to stderr and exit non-zero. Do not reorder PATH or remove anyone's binary: diagnose and stop. Also: - stop discarding `golangci-lint --version` stderr, so a present but broken binary (missing shared library, wrong architecture) says why instead of silently yielding the empty string and reinstalling on every run forever. Only stdout is parsed, so the parse matrix is unchanged. - bound the `--version` call with timeout(1) where it exists, since bootstrap now executes a binary it previously only located and a wedged one would otherwise hang the script. Hosts without timeout(1) run it unbounded, as before. - use X.Y.Z in the parsing comment so the pinned version stays a single literal in the script. |
||
|
|
9e924721e6 |
Check the golangci-lint version in bootstrap, not just presence (closes #24)
All checks were successful
check / check (push) Successful in 1m48s
script/bootstrap installed the pinned linter only when the command was absent, so on any host that already had some golangci-lint the pin was never consulted and a version bump was inert forever. That is how a host running v2.10.1 against a v2.12.2 pin got a green `make check` while `make docker` rejected the same commit: the local gate was linting with a different ruleset than CI, and the disagreement only surfaced after a push. The version is now a single value, GOLANGCI_LINT_VERSION, with the `go install` module ref derived from it, so a future bump cannot half-apply. A golangci_lint_version helper parses the installed version out of `golangci-lint --version` (the field after the word "version", with an optional leading "v" stripped, since the module ref carries one and the binary's output does not) and yields the empty string when the tool is absent or unreadable. Any version that is not the pin -- older, newer, absent or unparseable -- is reinstalled, so a first run upgrades and a second is a no-op. git, make and go keep their presence-only checks: they come from the host package manager, the repo pins no system toolchain versions, and go.mod governs the language version. That is now stated in a comment next to them rather than left ambiguous beside a tool that is version-checked. |
||
| 814bdada2b |
Update golangci-lint to v2.12.2 with canonical config
All checks were successful
check / check (push) Successful in 1m5s
Bump the pinned golangci-lint from v2.12.1 to v2.12.2 in the Dockerfile lint stage (tagged, digest-pinned Debian image) and in script/bootstrap (go install ref). Replace .golangci.yml with the canonical config: linter settings (lll, funlen, cyclop, dupl) move under linters.settings per the v2 schema so they are actually applied, and the redundant issues.exclude-use-default key is dropped. No new lint findings surfaced; make check is green. |
|||
| 3abeacf8ee |
Add scripts-to-rule-them-all scaffold (refs #1)
All checks were successful
check / check (push) Successful in 6s
Bring the repo into conformance with the scripts-to-rule-them-all (STRTA) scaffold. The real logic that lived inline in the Makefile now lives in POSIX-sh entrypoints under script/, and the Makefile's standard targets are thin @script/NAME shims. - script/: bootstrap, setup, projectname, test, lint, fmt, fmt-check, check, docker, precommit, install-precommit, cibuild. All are executable #!/bin/sh entrypoints; the go mod tidy guard from the old inline hooks recipe moved into script/precommit. - Makefile: the nine standard targets (bootstrap, setup, test, lint, fmt, fmt-check, check, docker, hooks) are now thin shims; the repo-specific sfdupes/build/clean targets and the CGO_ENABLED export are preserved. - .gitea/workflows/check.yml: run script/cibuild instead of a bare docker build. - Dockerfile: run make check (and the build) as an unprivileged builder user rather than root. We should never build or run as root, and doing so also lets the permission-denied tests run legitimately: root bypasses the chmod(0) that TestScanHardlinkRunFailsTogether relies on, which made the in-image make check fail. HOME and the Go caches point at the user's home so go build/test and golangci-lint can write. make check passes locally and docker build . is green (the in-image non-root make check passes, including the hardlink permission test). |