#!/bin/sh
# script/lint: run the linter.
#
# The linter always runs at the version pinned by the Dockerfile's lint
# stage, so a local run and a CI run of the same tree cannot disagree.
# That FROM line (image tag plus digest) is the single source of truth
# for the linter version in this repo: bump it there and nothing else
# needs editing.
#
# Normally that means running the pinned image with docker. The one
# exception is a golangci-lint on PATH whose version is exactly equal to
# the pin: that is the same linter, so it is run directly. This is what
# happens inside the lint container itself (Dockerfile runs `make lint`,
# and there is no docker daemon in there). A PATH binary at any other
# version is never used - that silent substitution is the bug this
# script exists to prevent.
#
# Extra arguments are passed through to `golangci-lint run`, before
# `./...` (see script/lint-fix).
set -eu

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

# The image reference of the Dockerfile's lint stage, tag and digest
# included, e.g.
# golangci/golangci-lint:v2.12.2-alpine@sha256:91b2...
lint_image() {
    awk '$1 == "FROM" && $3 == "AS" && $4 == "lint" { print $2; exit }' \
        "$DOCKERFILE"
}

# The bare version that image reference pins, e.g. 2.12.2
pinned_version() {
    lint_image | sed -e 's/@.*//' -e 's/.*://' -e 's/^v//' -e 's/-.*//'
}

# The version of the golangci-lint on PATH, if any, e.g. 2.12.2
installed_version() {
    command -v golangci-lint >/dev/null 2>&1 || return 0
    golangci-lint version 2>/dev/null | awk '
        {
            for (i = 1; i <= NF; i++) {
                if ($i ~ /^[0-9]+\.[0-9]+\.[0-9]+$/) {
                    print $i
                    exit
                }
            }
        }'
}

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

  pinned image: $image

Install docker, or install golangci-lint $(pinned_version) on PATH.
Linting with any other version is not supported: it is what lets a
local run pass while CI fails.
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.

  pinned image: $image

Start the daemon (and check DOCKER_HOST / your group membership), or
install golangci-lint $(pinned_version) on PATH. This script will not
fall back to a different linter version.
EOF
        exit 1
    fi
}

# Caches for the containerized linter. Keeping them out of the repo and
# persisting them between runs is what keeps the inner loop fast: a warm
# run costs about the same as a native one plus container startup.
cache_root() {
    echo "${XDG_CACHE_HOME:-${HOME:-/tmp}/.cache}/vaultik-lint"
}

run_in_docker() {
    image="$1"
    shift
    require_docker "$image"

    cache="$(cache_root)"
    mkdir -p "$cache/go-build" "$cache/go-mod" "$cache/golangci-lint"

    exec docker run --rm \
        --user "$(id -u):$(id -g)" \
        --env HOME=/tmp \
        --env GOFLAGS=-buildvcs=false \
        --env GOCACHE=/cache/go-build \
        --env GOMODCACHE=/cache/go-mod \
        --env GOLANGCI_LINT_CACHE=/cache/golangci-lint \
        --volume "$ROOT:/src" \
        --volume "$cache:/cache" \
        --workdir /src \
        "$image" \
        golangci-lint run "$@" ./...
}

main() {
    cd "$ROOT"

    image="$(lint_image)"
    if [ -z "$image" ]; then
        echo "lint: no lint stage found in $DOCKERFILE" >&2
        exit 1
    fi

    if [ "$(installed_version)" = "$(pinned_version)" ]; then
        exec golangci-lint run "$@" ./...
    fi

    run_in_docker "$image" "$@"
}

main "$@"
