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

Open
opened 2026-08-09 03:40:18 +02:00 by clawbot · 1 comment
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.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/mfer#68