1 Commits

Author SHA1 Message Date
clawbot
07129f0ec1 Make the pinned golangci-lint actually reach the host (closes #28)
All checks were successful
check / check (push) Successful in 7s
REPO_POLICIES.md now carries the canonical script/bootstrap snippet for Go
repos alongside the .golangci.yml bullet, where the pinned linter version
already lives.

The guard it replaces, `if missing golangci-lint; then go install ...; fi`,
tests PATH presence and never version, so on any already-provisioned machine
the pin is inert and a version bump is a no-op. The Dockerfile installs
unconditionally into a clean image, so CI and local then disagree about what
the linter is: a local `make check` green while `make docker` rejects the same
commit, and a container run surfacing findings the host run cannot see.

Comparing versions alone is not enough. `go install` writes to GOBIN (or
GOPATH/bin) while callers resolve through PATH, so a shadowing binary earlier
in PATH lets the install succeed and change nothing a caller ever sees, while
bootstrap prints success. The canonical form therefore compares the installed
version against the pin, re-resolves through PATH after installing and asserts
the pin, failing non-zero and naming the shadowing path when it does not, and
treats any unparseable --version output as a mismatch so the failure direction
is a redundant install rather than a skipped one.

The policy text states each of those as a requirement rather than leaving them
implicit in the code, records why the commit-pinned `go install` ref satisfies
the hash-pinning rule (a commit hash is not a mutable tag, and the go command
verifies the module against the checksum database), and requires that any
change to this logic be validated with a negative control run against a
shadowing binary, because a control without one passes against the naive
implementation too.

The node and yarn handling described earlier in the document is untouched.

Verified by extracting the snippet to a scratch harness with fake `go` and both
fake and real golangci-lint binaries: shadowing fails loudly and names the
path while the naive compare-then-install form reports success with the stale
2.7.2 still resolved; a wrong version at the install target is replaced;
garbage, empty and non-zero --version output all reinstall; the matching case
runs zero installs. The block in the document is byte-identical to the one
exercised.
2026-08-09 15:23:50 +00:00

View File

@@ -381,21 +381,17 @@ style conventions are in separate documents:
The canonical form, placed in `script/bootstrap` after Go itself is present:
```sh
# golangci-lint v2.12.2, 2026-05-06. The ref must resolve to a TAGGED
# release; see the note on untagged pins below.
# golangci-lint v2.12.2, 2026-05-06
GOLANGCI_LINT_VERSION="2.12.2"
GOLANGCI_LINT_REF="github.com/golangci/golangci-lint/v2/cmd/golangci-lint@c0d3ddc9cf3faa61a4e378e879ece580256d76e5"
# The version golangci-lint reports, resolved the way callers resolve it.
# Prints nothing when the binary is absent, exits non-zero, or prints
# something unparseable: all of those must read as "does not match".
# The trailing `|| true` is required, not tidiness. Under `set -o pipefail`
# a non-zero --version would otherwise propagate out of the pipeline and
# kill the script through `set -e` before the diagnostic below is printed.
# something unparseable all of which must read as "does not match".
golangci_lint_version() {
command -v golangci-lint >/dev/null 2>&1 || return 0
golangci-lint --version 2>/dev/null | head -n 1 |
sed -n 's/.*has version v\{0,1\}\([0-9][0-9.]*\).*/\1/p' || true
sed -n 's/.*has version v\{0,1\}\([0-9][0-9.]*\).*/\1/p'
}
ensure_golangci_lint() {
@@ -407,98 +403,57 @@ style conventions are in separate documents:
# go install writes to GOBIN (or GOPATH/bin); callers resolve through
# PATH. Re-resolve through PATH and assert the install took effect.
# `hash -r` is load-bearing: without it a shell that already resolved
# a stale golangci-lint answers from its own lookup cache, and this
# check false-fails with the shadowing message below.
hash -r 2>/dev/null || true
gcl_got="$(golangci_lint_version)"
if [ "$gcl_got" = "$GOLANGCI_LINT_VERSION" ]; then
got="$(golangci_lint_version)"
if [ "$got" = "$GOLANGCI_LINT_VERSION" ]; then
return 0
fi
gcl_bin="$(go env GOBIN)"
[ -n "$gcl_bin" ] || gcl_bin="$(go env GOPATH)/bin"
gcl_found="$(command -v golangci-lint 2>/dev/null || true)"
gobin="$(go env GOBIN)"
[ -n "$gobin" ] || gobin="$(go env GOPATH)/bin"
found="$(command -v golangci-lint 2>/dev/null || true)"
echo "bootstrap: installed golangci-lint $GOLANGCI_LINT_VERSION into" \
"$gcl_bin, but that is not what callers will get." >&2
case "$gcl_found" in
"")
echo "bootstrap: PATH resolves no golangci-lint at all." \
"Add $gcl_bin to PATH, then re-run bootstrap." >&2
;;
"$gcl_bin"/*)
echo "bootstrap: PATH resolves $gcl_found, inside that same" \
"directory, reporting version ${gcl_got:-unparseable}." \
"Nothing is shadowing it, so the install itself did not" \
"produce the pinned version: check that" \
"GOLANGCI_LINT_VERSION matches GOLANGCI_LINT_REF." >&2
;;
*)
echo "bootstrap: PATH resolves $gcl_found instead, reporting" \
"version ${gcl_got:-unparseable}. Remove that binary or" \
"put $gcl_bin earlier in PATH, then re-run bootstrap." >&2
;;
esac
"$gobin, but PATH resolves golangci-lint to ${found:-nothing}," \
"reporting version ${got:-unparseable}." >&2
echo "bootstrap: remove that binary or put $gobin earlier in PATH," \
"then re-run bootstrap." >&2
exit 1
}
# The definitions above are inert on their own; the call site is part of
# the canonical form. In a script/bootstrap that follows the "define all
# functions, then call main" convention, this line belongs inside main()
# next to the other ensure_* steps.
ensure_golangci_lint
```
Four properties are load-bearing; each guards a failure mode that otherwise
Three properties are load-bearing; each guards a failure mode that otherwise
fails green:
- **Compare the installed version against the pin**, never test presence.
This is what makes a version bump propagate to machines that already have
some golangci-lint.
- **After installing, re-resolve the binary the way callers resolve it** —
through `PATH`, not the path `go install` wrote to — and assert
`--version` reports the pin. When it does not, fail non-zero and name the
path `command -v` actually found, the version it reports, and the
directory the install wrote to. That is a condition a human has to fix by
hand, so bootstrap must not print success in it. Use `hash -r` first so
the shell does not answer from its own lookup cache. Diagnose the cause
from the resolved path rather than asserting one: only a path **outside**
the install directory is shadowing. When the resolved path is inside it,
nothing is shadowing and telling the operator to delete that binary or
reorder `PATH` sends them after a fault that does not exist.
`--version` reports the pin. When it does not, fail non-zero and **name
the shadowing path** `command -v` actually found, the version it reports,
and the directory the install wrote to. That is a condition a human has to
fix by hand, so bootstrap must not print success in it. Use `hash -r`
first so the shell does not answer from its own lookup cache.
- **A mis-parse must fall through to reinstall, never to a false match.**
Absent binary, non-zero exit, empty output, and unrecognised output all
yield an empty string, which compares unequal to the pin. The failure
direction is always a redundant install, never a skipped one.
- **Call it.** Two function definitions with no call site are a silent no-op
that reproduces the original defect exactly: exit 0, nothing installed, no
output, stale linter still resolved.
Keep it POSIX sh: no bashisms, no arrays, no `[[`, no `grep -P`.
**On the hash-pinning rule.** `@c0d3ddc9cf3faa61a4e378e879ece580256d76e5` is
a commit hash, not a server-mutable version tag, and the go command verifies
the fetched module against the checksum database — the mechanism the
hash-pinning rule at the top of this document already names as acceptable
for Go modules. Note that `go install pkg@version` runs in module-aware mode
ignoring the `go.mod` in the current directory or any parent, so no repo
`go.sum` is consulted for this install; the checksum database is what
verifies it. The linter is a bootstrap prerequisite rather than part of any
repo's module graph, which is why the canonical form installs it by
commit-pinned ref instead of declaring it in `go.mod`.
**The pin must be a tagged release commit.** `GOLANGCI_LINT_VERSION` is a
separate string because the ref is a hash and carries no readable version,
so the two must be updated together. It works out to a clean `2.12.2` only
because that commit is the `v2.12.2` tag commit: the go command resolves a
commit query to a semver tag on that commit when one exists, so the built
binary reports `2.12.2`. Pin to an **untagged** commit and the go command
falls back to a pseudo-version — the binary then reports something like
`2.12.3-0.20260506110758-c0d3ddc9cf3f`, and this comparison cannot express
it: the parser above stops at the first `-`, so the value to match would
have to be the truncated numeric prefix `2.12.3`, which is also the number
of a real future release. Setting `GOLANGCI_LINT_VERSION` to the full string
`--version` prints can never match, and bootstrap would then reinstall on
every run and exit 1 blaming a shadowing binary that does not exist. Keep
pins on tagged releases.
the fetched module against the checksum database and `go.sum` — the
mechanism the hash-pinning rule at the top of this document already names as
acceptable for Go modules. So the ref stays a bare `go install` of a
commit-pinned module rather than a `go.mod` tool dependency; the linter is a
bootstrap prerequisite rather than part of the module graph, and tracking it
as a tool dependency would pull its whole dependency tree into every
consuming repo's `go.mod` and `go.sum`. `GOLANGCI_LINT_VERSION` is a
separate string because the ref is a hash and carries no readable version;
it must be updated with the ref. That commit is the `v2.12.2` tag commit, so
the go command resolves it to `v2.12.2` and the built binary reports
`2.12.2`. If a pin is ever moved to a commit that carries no release tag,
the binary will report a pseudo-version instead and `GOLANGCI_LINT_VERSION`
must be set to whatever `--version` then prints.
**Verifying a change to this logic requires a negative control run in an
environment where a shadowing binary exists earlier in `PATH` than the
@@ -507,13 +462,6 @@ style conventions are in separate documents:
the mis-parse direction by feeding it unparseable `--version` output and
confirming it reinstalls rather than reporting a match.
**Run those controls against the block as a consuming repo would adopt it**
— pasted into a `script/bootstrap`-shaped file that is then executed — not
by sourcing it and invoking the function yourself. Driving the function
directly tests something the artifact does not do, and it is exactly how a
missing call site passes every control while the adopted snippet does
nothing.
- When pinning images or packages by hash, add a comment above the reference
with the version and date (YYYY-MM-DD).