Pin every developer tool install by hash; make local lint match CI #68

Closed
opened 2026-08-09 03:40:18 +02:00 by clawbot · 2 comments
Collaborator

Context

Policy: "ALL external references must be pinned by cryptographic hash...
Version tags are server-mutable and therefore remote code execution
vulnerabilities... There are zero exceptions to this rule."

The Dockerfile and the Gitea workflow comply. The developer-facing install
path does not:

  • script/bootstrap:141-143 installs golangci-lint from whatever the OS
    package manager offers — nix, apt, brew, or apk — with no version
    constraint at all
    . This is not merely unpinned, it is unversioned.
  • Makefile:36: go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28.1
    — mutable tag.
  • Makefile:51 (devprereqs): go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2
    — mutable tag, and a second, weaker source of truth competing with
    script/bootstrap.
  • script/bootstrap never installs gofumpt or prettier, both of which
    script/fmt requires. A developer who follows make setup cannot run
    make fmt.

This is not theoretical. On this machine script/bootstrap produced
golangci-lint v2.10.1 while the repo pins v2.12.2, and the two
disagree by 10 findings. That skew already caused a green PR to look red
locally, and it will keep costing review time until it is fixed.

Definition of done

  • script/bootstrap installs the exact pinned golangci-lint version the
    repo targets, verified by hash, on every supported platform — not
    "whatever the package manager has".
  • gofumpt and prettier are installed by script/bootstrap at pinned
    versions, so make fmt works on a fresh clone after make setup.
  • Makefile:36 and the devprereqs target either install by content hash
    or are deleted in favour of script/bootstrap being the single source of
    truth. Prefer deletion — two install paths is the actual defect.
  • Every pinned version carries a # tool vX.Y.Z, YYYY-MM-DD comment, per
    policy.
  • The pinned golangci-lint version appears in exactly one place that the
    Makefile, Dockerfile, and script/bootstrap all derive from, so they
    cannot drift again.
  • Running script/bootstrap on a machine with a different golangci-lint
    already on PATH results in the pinned version being used by
    script/lint — not the pre-existing one.
  • make check passes and docker build . succeeds. TODO.md updated in
    the same commit.

Implementation requirements

  • Never curl | sh. Download a specific release archive, verify a hardcoded
    sha256 with the existing verify_sha256 helper in script/bootstrap, then
    install. That helper already exists and is used for nvm; follow it.
  • go install pkg@v1.2.3 is not hash pinning — the tag is mutable. If
    you keep a go install path, it must resolve through a go.sum-verified
    mechanism (a tools.go with the tool as a module dependency, built with
    the repo's own go.sum), which is content-verified. Otherwise fetch a
    release archive and verify its hash.
  • script/bootstrap must stay POSIX sh, set -eu, no bashisms, and stay
    idempotent — re-running it must not reinstall or fail.
  • Per-platform hashes are required for a release-archive approach; detect
    OS/arch and select the right one. Do not pin only the Linux amd64 hash and
    silently fall through to an unverified install elsewhere.
  • If script/lint should assert the linter version matches the pin, add
    that check and make the mismatch message tell the developer to run
    make bootstrap.
  • Commit title must end with (closes #68).
## Context Policy: "ALL external references must be pinned by cryptographic hash... Version tags are server-mutable and therefore remote code execution vulnerabilities... There are zero exceptions to this rule." The `Dockerfile` and the Gitea workflow comply. The developer-facing install path does not: - `script/bootstrap:141-143` installs `golangci-lint` from whatever the OS package manager offers — nix, apt, brew, or apk — with **no version constraint at all**. This is not merely unpinned, it is unversioned. - `Makefile:36`: `go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28.1` — mutable tag. - `Makefile:51` (`devprereqs`): `go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2` — mutable tag, and a second, weaker source of truth competing with `script/bootstrap`. - `script/bootstrap` never installs `gofumpt` or `prettier`, both of which `script/fmt` requires. A developer who follows `make setup` cannot run `make fmt`. This is not theoretical. On this machine `script/bootstrap` produced golangci-lint **v2.10.1** while the repo pins **v2.12.2**, and the two disagree by 10 findings. That skew already caused a green PR to look red locally, and it will keep costing review time until it is fixed. ## Definition of done - `script/bootstrap` installs the exact pinned `golangci-lint` version the repo targets, verified by hash, on every supported platform — not "whatever the package manager has". - `gofumpt` and `prettier` are installed by `script/bootstrap` at pinned versions, so `make fmt` works on a fresh clone after `make setup`. - `Makefile:36` and the `devprereqs` target either install by content hash or are deleted in favour of `script/bootstrap` being the single source of truth. Prefer deletion — two install paths is the actual defect. - Every pinned version carries a `# tool vX.Y.Z, YYYY-MM-DD` comment, per policy. - The pinned `golangci-lint` version appears in exactly one place that the `Makefile`, `Dockerfile`, and `script/bootstrap` all derive from, so they cannot drift again. - Running `script/bootstrap` on a machine with a different `golangci-lint` already on `PATH` results in the pinned version being used by `script/lint` — not the pre-existing one. - `make check` passes and `docker build .` succeeds. `TODO.md` updated in the same commit. ## Implementation requirements - Never `curl | sh`. Download a specific release archive, verify a hardcoded sha256 with the existing `verify_sha256` helper in `script/bootstrap`, then install. That helper already exists and is used for nvm; follow it. - `go install pkg@v1.2.3` is **not** hash pinning — the tag is mutable. If you keep a `go install` path, it must resolve through a `go.sum`-verified mechanism (a `tools.go` with the tool as a module dependency, built with the repo's own `go.sum`), which is content-verified. Otherwise fetch a release archive and verify its hash. - `script/bootstrap` must stay POSIX sh, `set -eu`, no bashisms, and stay idempotent — re-running it must not reinstall or fail. - Per-platform hashes are required for a release-archive approach; detect OS/arch and select the right one. Do not pin only the Linux amd64 hash and silently fall through to an unverified install elsewhere. - If `script/lint` should assert the linter version matches the pin, add that check and make the mismatch message tell the developer to run `make bootstrap`. - Commit title must end with ` (closes #68)`.
clawbot added this to the 1.0.0 milestone 2026-08-09 03:40:18 +02:00
Author
Collaborator

Scope additions folded in from the PR #88 review, so they are fixed
alongside the bootstrap work they belong with rather than as a separate
issue.

The prettier half of this issue is already done. PR #88 added
package.json / yarn.lock pinning prettier 3.9.6 by sha512, and
script/bootstrap now installs node, yarn, and the locked JS deps. That
pin is verified enforced, not merely asserted — corrupting the lockfile
hash makes docker build . fail at Integrity check failed for "prettier".

So what remains here is: gofumpt, golangci-lint, and the
Makefile Go tool installs. Adjust the definition of done accordingly;
do not redo the prettier work.

Four additional items to fix as part of this issue:

  1. Make the prettier version check a hard failure. script/prettier
    currently prefers node_modules/.bin/prettier but falls back to a
    PATH prettier of unknown version with only a stderr warning. Different
    prettier versions format differently, so this is the same version-skew
    trap that already cost a review cycle when a local golangci-lint v2.10.1
    disagreed with the pinned v2.12.2 by ten findings. Compare the resolved
    binary's --version against the pin in package.json and fail hard on
    mismatch, naming script/bootstrap in the message.

    Apply the identical treatment to gofumpt and golangci-lint in
    script/fmt, script/fmt-check-go, and script/lint — those are
    currently invoked with no version check at all, which is worse. That
    consistency is the reason the prettier fallback was not treated as
    blocking on #88.

  2. Correct the --frozen-lockfile explanation. The comment in
    script/bootstrap (and the #88 commit message) states that
    --frozen-lockfile enforces the integrity hash. It does not — the flag
    only guards lockfile-versus-manifest consistency; integrity verification
    happens at fetch time regardless. The behaviour is right, the stated
    reason is wrong, and a wrong reason in a comment about supply-chain
    pinning will mislead the next person to touch it.

  3. Remove the dead make fmt-check-md target, or wire it up. It is
    currently unreachable.

  4. Record the Dockerfile lint-stage deviation in TODO.md. The lint
    stage runs make fmt-check-go rather than the canonical policy
    Dockerfile's make fmt-check, because the golangci/golangci-lint
    image has no node and the markdown check lives in its own mdfmt stage.
    That is deliberate and correct, but it is a visible divergence from the
    template — without a note, a future policy audit will "correct" it back
    and silently drop the markdown gate.

Also cosmetic, fix while you are in there: package.json invents
"version": "0.1.0" for a private manifest that has no version semantics,
and script/fmt-check prints its "Checking formatting" banner twice.

Scope additions folded in from the PR #88 review, so they are fixed alongside the bootstrap work they belong with rather than as a separate issue. **The prettier half of this issue is already done.** PR #88 added `package.json` / `yarn.lock` pinning prettier 3.9.6 by `sha512`, and `script/bootstrap` now installs node, yarn, and the locked JS deps. That pin is verified enforced, not merely asserted — corrupting the lockfile hash makes `docker build .` fail at `Integrity check failed for "prettier"`. So what remains here is: **gofumpt**, **golangci-lint**, and the **Makefile Go tool installs**. Adjust the definition of done accordingly; do not redo the prettier work. Four additional items to fix as part of this issue: 1. **Make the prettier version check a hard failure.** `script/prettier` currently prefers `node_modules/.bin/prettier` but falls back to a `PATH` prettier of unknown version with only a stderr warning. Different prettier versions format differently, so this is the same version-skew trap that already cost a review cycle when a local golangci-lint v2.10.1 disagreed with the pinned v2.12.2 by ten findings. Compare the resolved binary's `--version` against the pin in `package.json` and fail hard on mismatch, naming `script/bootstrap` in the message. Apply the identical treatment to `gofumpt` and `golangci-lint` in `script/fmt`, `script/fmt-check-go`, and `script/lint` — those are currently invoked with no version check at all, which is worse. That consistency is the reason the prettier fallback was not treated as blocking on #88. 2. **Correct the `--frozen-lockfile` explanation.** The comment in `script/bootstrap` (and the #88 commit message) states that `--frozen-lockfile` enforces the integrity hash. It does not — the flag only guards lockfile-versus-manifest consistency; integrity verification happens at fetch time regardless. The behaviour is right, the stated reason is wrong, and a wrong reason in a comment about supply-chain pinning will mislead the next person to touch it. 3. **Remove the dead `make fmt-check-md` target**, or wire it up. It is currently unreachable. 4. **Record the Dockerfile lint-stage deviation in `TODO.md`.** The lint stage runs `make fmt-check-go` rather than the canonical policy Dockerfile's `make fmt-check`, because the `golangci/golangci-lint` image has no node and the markdown check lives in its own `mdfmt` stage. That is deliberate and correct, but it is a visible divergence from the template — without a note, a future policy audit will "correct" it back and silently drop the markdown gate. Also cosmetic, fix while you are in there: `package.json` invents `"version": "0.1.0"` for a private manifest that has no version semantics, and `script/fmt-check` prints its "Checking formatting" banner twice.
Author
Collaborator

The items still open are fixed in #155: gofumpt is now checked against hashes in the repo, prettier refuses any version but the pinned one, and three comment and package.json fixes. The PR body lists every item as satisfied, moot or fixed.

Model: opus-5-5

The items still open are fixed in https://git.eeqj.de/sneak/mfer/pulls/155: gofumpt is now checked against hashes in the repo, prettier refuses any version but the pinned one, and three comment and `package.json` fixes. The PR body lists every item as satisfied, moot or fixed. Model: opus-5-5
Sign in to join this conversation.