Files
vaultik/script/bootstrap
sneak 739de1e101
All checks were successful
check / check (pull_request) Successful in 4m0s
Lint in a container as a build step, via Dockerfile.lint (closes #113)
Every lint run now happens inside its own container, invoked through
script/lint, and linting is a build step rather than a container
command: a successful build of the new root Dockerfile.lint IS a clean
lint. That shape also works where the docker daemon is remote and bind
mounts are impossible.

Its FROM line -- golangci/golangci-lint:v2.12.2, pinned by digest -- is
now the only pin of the linter version in this repo.

A container per run has its own lint cache and its own golangci-lint
lock, both discarded with it, so neither cross-worktree contamination
nor lock contention exists any more. The machinery that defended
against them is therefore gone: the per-worktree cache directories, the
lock-retry loop, and script/lint-audit, which existed to catch findings
replayed from a cache that no longer exists. So is the host lint path
in its entirety -- the native escape hatch, its version detection, and
VAULTIK_LINT_IN_CONTAINER in both script/lint and the Dockerfile.
Nothing lints on the host, at any version.

A cached build lints nothing, so the CHECK_EPOCH mechanism the product
Dockerfile already used is what makes a green mean something:
ARG CHECK_EPOCH with no default, placed below the module layers so
dependency caching survives, a `RUN [ -n "$CHECK_EPOCH" ] || exit 1`
guard so a build that withholds the arg fails instead of replaying, and
the value expanded into the lint command itself. script/lint computes
`epoch="$(date +%s%N)$$"` as a bare assignment on its own line, because
inline in the argument a failing substitution does not abort under
`set -eu` and yields a constant empty epoch -- which is exactly the
false green being prevented.

The product Dockerfile loses its lint stage rather than gaining a
second linter pin. That stage ran `make lint`, which is now
`docker build`: docker-in-docker inside a BuildKit step with no daemon.
Calling golangci-lint directly there instead would have meant two
independently bumpable digests for one tool. `make fmt-check` moves
beside `make test` in the builder stage, and script/cibuild now builds
Dockerfile.lint and then Dockerfile, each with its own fresh epoch,
failing on either. Consequence, stated in comments rather than left to
be discovered: script/docker builds the product image only and no
longer lints; script/check and script/cibuild are the gates.

Two decisions taken deliberately and documented where they apply.
`golangci-lint config verify` is omitted: it fetches its JSON schema
over an unpinned live HTTPS call, which would make the gate depend on a
remote resource outside this repo's hash-pinning discipline and turn an
upstream outage or an egress-less runner into a red that is not a lint
verdict. script/lint-fix is kept, reimplemented as a bind-mounted
docker run against the image parsed out of Dockerfile.lint -- a build
step cannot write fixes back to the worktree -- and its header states
outright that it is a developer convenience, never a gate, and needs a
local daemon.

cmd/vaultik/lintdocker_test.go parses both Dockerfiles and both scripts
and fails if any part of the mechanism is dropped: the digest pin, the
defaultless ARG below `go mod download`, the emptiness guard, the
expansion of the epoch into each check command, the bare per-invocation
epoch assignment in both scripts, cibuild building both files, and the
absence of any host-lint escape hatch. Every one of those losses is
silent -- the build still exits 0 and nothing is checked -- which is
why they are asserted rather than trusted.

script/lint takes no arguments now, and says so instead of dropping
them: a build step has no command line to pass linter flags to.
2026-08-10 12:54:46 +00:00

138 lines
4.6 KiB
Bash
Executable File

#!/bin/sh
# script/bootstrap: install all dependencies needed to build and develop
# this repo. Idempotent: every install is guarded by a check so already
# installed tools are skipped. Base tooling comes from nix, apt, brew,
# or apk (detected in that order); assumes NOTHING is present (not git,
# make, or go).
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
PKGMGR=""
SUDO=""
detect_pkgmgr() {
[ -n "$PKGMGR" ] && return 0
if command -v nix-env >/dev/null 2>&1; then
PKGMGR="nix"
elif command -v apt-get >/dev/null 2>&1; then
PKGMGR="apt"
elif command -v brew >/dev/null 2>&1; then
PKGMGR="brew"
elif command -v apk >/dev/null 2>&1; then
PKGMGR="apk"
else
echo "bootstrap: no supported package manager (nix, apt, brew, apk)" >&2
exit 1
fi
if [ "$PKGMGR" = "apt" ]; then
export DEBIAN_FRONTEND=noninteractive
if [ "$(id -u)" != "0" ]; then
SUDO="sudo"
fi
fi
}
# pkg_install <nix-attr> <apt-pkg> <brew-formula> <apk-pkg>
pkg_install() {
detect_pkgmgr
case "$PKGMGR" in
nix) nix-env -iA "nixpkgs.$1" ;;
apt) $SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y "$2" ;;
brew) brew install "$3" ;;
apk) apk add --no-cache "$4" ;;
esac
}
missing() {
! command -v "$1" >/dev/null 2>&1
}
# Docker is a hard requirement, not a nice-to-have: script/lint lints by
# building Dockerfile.lint, whose digest-pinned golangci-lint image is
# the only place the linter runs, and script/check and script/precommit
# both run script/lint. A bootstrap that prints "bootstrap complete" on a
# machine where `make check` cannot run is a false success, so this fails
# instead.
#
# Installing docker from here was considered and rejected: it needs root,
# a running daemon, and on macOS a GUI cask, so an attempt would itself
# fail in the common case - trading one false success for a second
# failure mode. Naming exactly what breaks is more useful.
# Prints the problem and returns 0 when docker cannot be used; returns
# 1 (and prints nothing) when it can.
docker_problem() {
if missing docker; then
echo "docker is not installed"
return 0
fi
if ! docker info >/dev/null 2>&1; then
echo "the docker daemon is not reachable"
return 0
fi
return 1
}
require_docker() {
reason="$(docker_problem)" || return 0
cat >&2 <<EOF
bootstrap: FAILED - $reason.
Docker is required to develop this repo. Without it these do not work:
script/lint builds Dockerfile.lint, which runs the linter as a
build step in a digest-pinned golangci-lint image.
That FROM line is the single source of truth for the
linter version
script/check runs script/lint
script/precommit runs script/check, so commits are blocked by the
pre-commit hook installed by script/setup
script/cibuild builds Dockerfile.lint and Dockerfile, which is what
CI runs
Install docker (and start the daemon, checking DOCKER_HOST and your
group membership), then re-run script/bootstrap. golangci-lint on PATH
is deliberately not a substitute: script/lint will not use it.
EOF
exit 1
}
main() {
cd "$ROOT"
# Base tooling (every repo)
if missing git; then pkg_install git git git git; fi
if missing make; then pkg_install gnumake make make make; fi
# Go toolchain
if missing go; then pkg_install go golang go go; fi
# golangci-lint is deliberately NOT installed: script/lint lints by
# building Dockerfile.lint, whose digest-pinned image is the only
# place the linter runs, so whatever a package manager happens to
# ship would only be a shadow of the pinned version that could drift
# from CI. Nothing on the host is ever used as a linter, at any
# version, so installing one here would buy nothing.
# sqlite3 CLI: the test suite shells out to it (VACUUM).
if missing sqlite3; then pkg_install sqlite sqlite3 sqlite sqlite; fi
# goreleaser, at the version pinned by script/install-goreleaser and
# verified against a hardcoded sha256. Package managers are not used
# for it: they ship whatever version they happen to carry, and the
# tool that builds a release has to be a known one. The install is
# its own script because the release workflow needs goreleaser
# without needing the Docker requirement below.
"$ROOT/script/install-goreleaser"
go mod download
# Last, so that everything installable is installed before the one
# thing this script cannot install decides the outcome.
require_docker
echo "bootstrap complete"
}
main "$@"