Contract specification

Lint Gate

One aggregate lint command runs every linter that is installed, skips the rest with a clear message, and fails only on real findings.

Applies at
scripts/lint.sh
Status
Stable

01 Defines

The repository’s single lint entry point: make lint runs the configured LINT_COMMAND, here scripts/lint.sh, which runs each linter against the tracked files it applies to and reports one overall result.

02 Applies when

  • Several linters cover different file types and nobody should need to know them all.
  • The same command runs in a bare checkout, in the dev container, and in CI.
  • A tool is legitimately absent in some environments and its absence should not look like a failure.

03 Boundaries

  • The gate checks; rewriting files is the separate, deliberate make format.
  • It does not install linters; the environment provides them and make doctor reports what is missing.
  • Terraform checks are delegated to the make targets that know which roots exist.

Expected behaviour

  1. 01
    make lint delegates to LINT_COMMAND from repository.mk, or prints skip: no lint command configured.
  2. 02
    A linter that is not installed prints skip: <tool> is not installed and the run continues.
  3. 03
    Every installed linter runs even after an earlier one fails; the result is reported once at the end.
  4. 04
    Linters read their configuration from .linter/ by explicit path.
  5. 05
    File lists come from Git, so ignored and generated files are not linted.
  6. 06
    The run ends with Lint PASSED or Lint FAILED and the matching exit status.

Behaviour#

The gate is written so that it works anywhere. A small run helper checks whether the linter’s command exists; if not, it prints a skip: line and moves on. If it does, the linter runs and any failure is recorded, not raised, so the remaining linters still report.

FlowOne run of the lint gate
  1. 01 Locate cd to the Git top level
  2. 02 Collect files Tracked and untracked-but-not-ignored *.sh, *.md, *Dockerfile*
  3. 03 Run linters ShellCheck, MarkdownLint, Hadolint, shfmt, Actionlint, EditorConfig, Codespell Explicit Skips
  4. 04 Prettier npm run format:check when node_modules/.bin/prettier exists
  5. 05 Terraform make check.terraform.fmt check.terraform.lint
  6. 06 Report Lint PASSED, or Lint FAILED and exit 1

A linter is skipped only when it is missing. When there are no files of a type — no Dockerfiles, say — that linter is not invoked at all.

Skipping versus failing#

Lint gate

  • Missing tool: skip: and carry on
  • Exit status reflects lint findings only
  • Answers “is this code clean?”

Environment doctor

  • Missing tool: missing: and a non-zero exit
  • Exit status reflects the environment only
  • Answers “is this environment complete?”

The two are complementary. The gate stays usable in a bare checkout, and the Environment Doctor is where a missing linter shows up as a problem.

Examples#

scripts/lint.sh (excerpt)
run() {
	local name="$1"
	shift
	if ! command -v "$1" > /dev/null 2>&1; then
		printf 'skip: %s is not installed\n' "$1"
		return 0
	fi
	printf '\n--- %s ---\n' "$name"
	"$@" || failed=1
}

[ "${#markdown_files[@]}" -eq 0 ] \
	|| run MarkdownLint markdownlint --config .linter/.markdownlint.json "${markdown_files[@]}"

The wiring through the command surface is one line of repository configuration:

scripts/build/repository.mk
LINT_COMMAND := bash "$(REPO_ROOT)/scripts/lint.sh"