#!/bin/sh
# script/lint: run the linter.
#
# The linter runs inside the image built by Dockerfile.lint, and it runs
# there as a BUILD STEP: a successful build of that file IS a clean
# lint. Nothing lints on the host, at any version, ever. That FROM line
# is the single source of truth for the linter version in this repo, so
# a local run and a CI run of the same tree cannot disagree.
#
# One container per run means one lint cache and one golangci-lint lock
# per run, both private to that run and thrown away with it. That is
# what makes concurrent runs on a shared host safe, and it is why this
# script no longer carries per-worktree cache directories, a lock-retry
# loop, or an output audit: there is no shared state left for them to
# defend (issue https://git.eeqj.de/sneak/vaultik/issues/113).
#
# To watch the linter execute, set BUILDKIT_PROGRESS=plain, which docker
# honours directly:
#
#   BUILDKIT_PROGRESS=plain script/lint
#
# The lint layer must appear as executing rather than CACHED on every
# run; see the CHECK_EPOCH comment in Dockerfile.lint.
set -eu

ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
DOCKERFILE="$ROOT/Dockerfile.lint"

require_docker() {
    if ! command -v docker >/dev/null 2>&1; then
        cat >&2 <<EOF
lint: docker is required to run the pinned linter.

  lint image declared by: $DOCKERFILE

Install docker. Linting with any other golangci-lint is not supported:
it is what lets a local run pass while CI fails. A golangci-lint on
PATH is never used, whatever its version.
EOF
        exit 1
    fi
    if ! docker info >/dev/null 2>&1; then
        cat >&2 <<EOF
lint: the docker daemon is not reachable, so the pinned linter cannot
run.

  lint image declared by: $DOCKERFILE

Start the daemon (and check DOCKER_HOST / your group membership). This
script will not fall back to a different linter version or to an
unpinned binary on PATH.
EOF
        exit 1
    fi
}

usage() {
    cat >&2 <<EOF
usage: $(basename "$0")

script/lint takes no arguments. The linter runs as a build step, so
there is no command line to pass flags to; anything accepted here would
have to be silently dropped. To apply autofixes, use script/lint-fix,
which runs the same pinned image as a container for exactly this
reason.
EOF
    exit 2
}

main() {
    [ "$#" -eq 0 ] || usage

    cd "$ROOT"
    require_docker

    # A fresh epoch per invocation is what forces the lint layer to
    # execute; the layers above the ARG in Dockerfile.lint still cache,
    # so a run is not cold. The value must be unique per invocation, not
    # per second: `date +%s` is second-granular, so two concurrent
    # invocations in the same second would get identical epochs and the
    # later one could be served from cache -- the false green in
    # miniature. `%N` alone does not fix it either, because busybox
    # silently drops %N, exits 0, and hands back second granularity with
    # no warning. `$$` is what makes this correct regardless, since
    # concurrent invocations have different pids.
    #
    # Assign it on its own line rather than inline in the argument.
    # Under `set -eu` a command substitution that fails inside an
    # argument does NOT abort the script: CHECK_EPOCH would become an
    # empty string, an empty string is a constant, and a constant epoch
    # is exactly the cached-lint false green this guards against. As a
    # bare assignment, `set -e` catches a failing `date` and no build
    # starts.
    epoch="$(date +%s%N)$$"

    # cacheonly: the lint verdict is the build's exit status, and the
    # image it would otherwise produce is never run. Exporting it costs
    # most of the wall time of a warm run and leaves a dangling image
    # behind on every invocation, on a host that may be running many.
    docker build \
        --output=type=cacheonly \
        --build-arg CHECK_EPOCH="$epoch" \
        -f "$DOCKERFILE" \
        "$ROOT"
}

main "$@"
