All checks were successful
check / check (pull_request) Successful in 3m11s
Closes #80. script/lint decided whether it could skip the pinned image by asking what version was on PATH rather than where it was running, and cache isolation is part of that same question. Both issues are that one defect. The cache was one directory per repo, shared by every worktree on the host. Two checkouts of this repo have identical Go file contents, so their cache keys collide and golangci-lint replays the stored analysis, including the paths recorded when it was produced. The loud direction of that failure - a clean tree failed by a dirty sibling - is the harmless one. The silent direction, a dirty tree passed by a clean sibling, is another way for a gate here to report a green it did not earn. The cache is now keyed on a digest of the worktree path, so a collision is not possible, and it stays persistent per worktree: a warm run is still seconds. Each cache records the worktree it belongs to and is collected when that worktree is gone, so throwaway worktrees do not accumulate caches; the tree lives under XDG_CACHE_HOME and is disposable by definition. script/lint-audit is the backstop, and runs on every lint: it rejects output citing any file that is not in the tree being linted, so a result built out of another checkout's analysis is a hard error instead of a silent pass. It runs on clean output too, because that is the case nobody investigates. It never certifies that a run passed - it does not look at whether there were findings - so it cannot itself become a gate that reports a green. golangci-lint's "parallel golangci-lint is running" refusal is now a bounded retry rather than a verdict. It is not a lint result, and exiting non-zero on it is indistinguishable to a caller from real findings; issue #88 measured that a private cache does not remove the contention. Exhausting the retries fails with a message that says the tree was never analysed. The native path now requires VAULTIK_LINT_IN_CONTAINER=1, which only the Dockerfile's lint stage sets, in addition to matching the pin. A developer's locally installed 2.12.2 is a different build reached by a different code path and no longer bypasses the digest pin. /.dockerenv was considered and rejected as the signal: dockerd creates it for `docker run`, but it is not reliably present during a BuildKit `docker build`, which is exactly the case the exception exists for. Inside the container a version mismatch is now a hard error rather than a fall-through, since there is no daemon there to fall through to. Version detection uses `golangci-lint version --short`, the interface meant for it, keeping the banner scrape only as a fallback. script/bootstrap no longer prints "bootstrap complete" on a machine that cannot run the gate. Docker missing, or present with an unreachable daemon, is a hard failure naming exactly what breaks. Installing docker from bootstrap was rejected: it needs root, a daemon, and on macOS a GUI cask, so the attempt would itself fail in the common case and trade one false success for a second failure mode. TODO.md's claim that `make check` became "as trustworthy as script/cibuild" is corrected to what README.md already said: only the lint leg is equivalent, while tests and gofmt still run against the host toolchain. README.md's requirements section gains docker and sqlite3. Verified by reproduction, not inspection: two concurrent lints from two worktrees of differing cleanliness each reported only their own findings with no lock error; a real run made to report paths outside its tree exits 1; a matching linter shimmed onto PATH is never invoked while the pinned image runs; a PATH without docker makes bootstrap fail. script/ cibuild exits 0 with the lint layer executing in the pinned image, which is what proves the in-container path still works.
109 lines
4.3 KiB
Docker
109 lines
4.3 KiB
Docker
# Lint stage
|
|
#
|
|
# This FROM line is the single source of truth for the linter version:
|
|
# script/lint parses the image reference out of it and runs that exact
|
|
# image, so a local `make lint` and CI use the same linter. Bump the
|
|
# linter here (tag AND digest) and nowhere else.
|
|
#
|
|
# golangci/golangci-lint:v2.12.2-alpine, 2026-08-07
|
|
FROM golangci/golangci-lint:v2.12.2-alpine@sha256:91b27804074a0bacea298707f016911e60cf0cdbc6c7bf5ccacb5f0606d18d60 AS lint
|
|
|
|
RUN apk add --no-cache make build-base
|
|
|
|
# The context signal for script/lint's native path. This stage runs
|
|
# `make lint` with no docker daemon available, so it is the one place
|
|
# that must run the golangci-lint on PATH directly. script/lint takes
|
|
# that path only when this is set AND the version matches the pin above;
|
|
# version equality alone would also admit a developer's locally
|
|
# installed copy on a host, bypassing the digest pin (issue #80).
|
|
# Nothing outside this stage sets it.
|
|
ENV VAULTIK_LINT_IN_CONTAINER=1
|
|
|
|
WORKDIR /src
|
|
|
|
# Copy go mod files first for better layer caching
|
|
COPY go.mod go.sum ./
|
|
RUN go mod download
|
|
|
|
# Copy source code
|
|
COPY . .
|
|
|
|
# Run formatting check and linter.
|
|
#
|
|
# CHECK_EPOCH must stay immediately above these RUNs. These layers are
|
|
# keyed on its value, so they are cache-eligible only for a value
|
|
# already built against this same tree. script/cibuild and script/docker
|
|
# each pass a fresh value on every invocation, which is what makes their
|
|
# green mean the checks really executed.
|
|
#
|
|
# The value is expanded into each check command rather than left to a
|
|
# bare declaration, so the cache miss does not depend on BuildKit's
|
|
# unreferenced-ARG handling staying as it is. It also puts the epoch in
|
|
# the build log, where a reader can see the layer was keyed fresh.
|
|
#
|
|
# The guard is what makes a build that omits --build-arg fail instead of
|
|
# lie. An unset ARG is an empty string, and an empty string is a
|
|
# perfectly stable cache key: without the guard the first such build
|
|
# runs the checks and every one after it on an unchanged tree replays
|
|
# these layers from cache, executes nothing, and still exits 0. Failed
|
|
# steps are never cached, so the guard fails on EVERY invocation rather
|
|
# than once -- a bare `docker build .` is now a loud error, not a quiet
|
|
# green. Do not give CHECK_EPOCH a default value; a default would
|
|
# satisfy the guard with a constant and restore the hole.
|
|
#
|
|
# ARG scope is per-stage, so the builder stage declares its own.
|
|
# Everything above this line (apk, go.mod, `go mod download`) is
|
|
# deliberately outside the busted range and keeps caching.
|
|
ARG CHECK_EPOCH
|
|
RUN [ -n "$CHECK_EPOCH" ] || exit 1
|
|
RUN echo "check epoch: ${CHECK_EPOCH}" && make fmt-check
|
|
RUN echo "check epoch: ${CHECK_EPOCH}" && make lint
|
|
|
|
# Build stage
|
|
# golang:1.26.1-alpine, 2026-03-17
|
|
FROM golang:1.26.1-alpine@sha256:2389ebfa5b7f43eeafbd6be0c3700cc46690ef842ad962f6c5bd6be49ed82039 AS builder
|
|
|
|
# Depend on lint stage passing
|
|
COPY --from=lint /src/go.sum /dev/null
|
|
|
|
ARG VERSION=dev
|
|
|
|
# Install build dependencies for CGO (mattn/go-sqlite3) and sqlite3 CLI (tests)
|
|
RUN apk add --no-cache make build-base sqlite
|
|
|
|
WORKDIR /src
|
|
|
|
# Copy go mod files first for better layer caching
|
|
COPY go.mod go.sum ./
|
|
RUN go mod download
|
|
|
|
# Copy source code
|
|
COPY . .
|
|
|
|
# Run tests. See the CHECK_EPOCH comment in the lint stage for the
|
|
# mechanism; ARG scope is per-stage, so this stage needs its own
|
|
# declaration, its own guard, and its own expansion, and they must stay
|
|
# immediately above the check RUN.
|
|
ARG CHECK_EPOCH
|
|
RUN [ -n "$CHECK_EPOCH" ] || exit 1
|
|
RUN echo "check epoch: ${CHECK_EPOCH}" && make test
|
|
|
|
# Build (pure Go, no CGO required since we use modernc.org/sqlite)
|
|
RUN CGO_ENABLED=0 go build -ldflags "-X 'sneak.berlin/go/vaultik/internal/globals.Version=${VERSION}' -X 'sneak.berlin/go/vaultik/internal/globals.Commit=$(git rev-parse HEAD 2>/dev/null || echo unknown)' -X 'sneak.berlin/go/vaultik/internal/globals.CommitDate=$(git show -s --format=%cs HEAD 2>/dev/null || echo unknown)'" -o /vaultik ./cmd/vaultik
|
|
|
|
# Runtime stage
|
|
# alpine:3.21, 2026-02-25
|
|
FROM alpine:3.21@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709
|
|
|
|
RUN apk add --no-cache ca-certificates sqlite
|
|
|
|
# Copy binary from builder
|
|
COPY --from=builder /vaultik /usr/local/bin/vaultik
|
|
|
|
# Create non-root user
|
|
RUN adduser -D -H -s /sbin/nologin vaultik
|
|
|
|
USER vaultik
|
|
|
|
ENTRYPOINT ["/usr/local/bin/vaultik"]
|