All checks were successful
check / check (push) Successful in 2m23s
Closes #80. script/lint pointed GOLANGCI_LINT_CACHE at a path shared by every worktree of this repo. Two worktrees have identical Go file contents, so their cache keys collided and one tree's stored findings replayed for another, paths included - observed as 231 findings all citing another session's worktree, with no parallel-runner message to signal it. The failure is symmetric and only one direction is loud: a clean tree failed by a dirty sibling gets investigated, a dirty tree passed by a clean sibling does not. The cache is now keyed per worktree on a digest of $ROOT, and remains persistent. Independently of that, script/lint-audit inspects every run's output and fails the run if any finding cites a path outside the tree being linted. That guard is the load-bearing part: it converts a silent unearned green into a hard error regardless of how the cache is keyed. It is deliberately built so it can never certify a pass, only reject, so it cannot itself become a gate that reports green. The native path was gated on version equality alone, which admitted a locally installed matching binary and bypassed the digest pin. It now requires VAULTIK_LINT_IN_CONTAINER=1, set only by the Dockerfile lint stage, in addition to version equality. /.dockerenv was rejected as the signal because dockerd creates it for `docker run` but not reliably during a BuildKit `docker build`, which is the case the exception exists for. A version mismatch inside the container is now a hard error rather than a fall-through. This mattered more than the issue supposed: on this host a matching golangci-lint exists on PATH, so script/lint was taking the native path and linting against the global cache without ever running the pinned image. That is the likely root of the observed contamination, and it is closed here rather than mitigated. The parallel-runner error is retried rather than reported. It is not a lint result, and surfacing it as a non-zero exit is indistinguishable to a caller from real findings; exhausted retries fail saying the tree was never analysed. Note that a private cache alone does not remove lock contention - measured with two concurrent runs using separate cache directories. script/bootstrap no longer reports success on a machine that cannot run the gate: docker is now required by lint, check and precommit, so a missing binary or unreachable daemon is a hard failure naming what will not work.
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"]
|