#!/bin/sh # script/lint: run the linter. golangci-lint is never installed locally: # it runs via docker only, one way, everywhere — script/lint builds # Dockerfile.lint, which COPYs the repo into the pinned golangci-lint # image and lints as a build step. This works even when the docker daemon # is remote and bind mounts are impossible. # # --no-cache-filter forces the lint stage to re-execute every run, so an # unchanged tree is still actually linted; the deps stage keeps its cache, # so the module download is not repeated. It and --target must both stay, # for the reason in the next paragraph: do not "simplify" either of those # two away. # # The stage name is written ONCE, in $stage, and passed to both --target # and --no-cache-filter, so the two flags cannot come to name different # stages. That is the whole point of the variable. BuildKit silently # ignores --no-cache-filter when no stage matches its argument: a filter # naming a stage that does not exist is a no-op, the lint layer is served # from cache, and script/lint reports green having linted nothing — the # false green this setup exists to prevent. --target, by contrast, fails # loudly on a name that is not in the file. With a single shared name, a # typo or a stale rename therefore becomes a hard error instead of a # silent skip, because the one name reaches both flags. # # What the tooling does NOT check, and is left to whoever edits this: # # 1. $stage must name the stage in Dockerfile.lint that actually runs # golangci-lint. --target verifies that the name exists, not that it is # the right stage, and it stops the build at that stage — so moving the # lint step into a different stage, or adding a stage after this one, # would not be caught here. Keep this file and Dockerfile.lint in sync. # # 2. .dockerignore decides what reaches the container, and only what # reaches it gets linted. Excluding a Go file there removes it from the # lint with no warning: verified by planting a real violation and adding # just that file's path to .dockerignore, which produced `0 issues.` at # exit 0 with the violation still sitting in the working tree. It shows # up only if the rest of the package still references the excluded file, # in which case the build fails loudly on `undefined:` typecheck errors; # a self-contained file drops out silently. So .dockerignore is part of # this gate, not housekeeping — keep it to build inputs the lint does # not read, and never exclude Go sources, go.mod/go.sum or # .golangci.yml. # # --output=type=cacheonly skips the image export. Nothing consumes the # image — the deliverable of this build is an exit code — and exporting it # costs seconds per run and leaves a dangling image behind every time. The # lint stage still executes and a lint failure still exits non-zero. set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" # Must match the stage name in Dockerfile.lint. stage=lint main() { cd "$ROOT" docker build \ --target "$stage" \ --no-cache-filter="$stage" \ --output=type=cacheonly \ -f Dockerfile.lint . } main "$@"