Contract specification

Explicit Skips

When a capability is intentionally unavailable, a command says so with a skip: line and succeeds, while a check that actually fails still exits non-zero.

Applies at
scripts/
Status
Stable
Source · managed
scripts/README.md

01 Defines

The output and exit-code contract for commands that meet an absent capability: print a single skip: <reason> line, exit zero, and reserve non-zero exits for checks that ran and failed.

02 Applies when

  • A command covers a capability that may legitimately be absent — an uninstalled linter, a component the repository does not have, a step that needs the network.
  • The same command has to work in a bare checkout, in the dev container, and in CI.
  • A reader of the log needs to tell “did not run” apart from “ran and passed”.

03 Boundaries

  • A skip is never a way to hide a failing check; once a check runs, its failure is reported as a failure.
  • Diagnosing a missing tool is the job of make doctor, which reports missing: and exits non-zero.
  • Skips cover intentional unavailability, not misconfiguration of something the repository declares it needs.

Expected behaviour

  1. 01
    Every skipped step prints one line that starts with skip: and names what was not done and why.
  2. 02
    A skip exits zero and lets the surrounding command carry on.
  3. 03
    The reason is specific enough to act on, such as prettier is not installed (run npm ci).
  4. 04
    A check that runs and fails makes the overall command exit non-zero, even if other steps were skipped.
  5. 05
    Skipped and failed steps are never reported with the same word.

Behaviour#

Repository commands are written to run wherever the repository is checked out. Some of what they cover is optional: a linter that is only installed in the dev container, a link checker the repository may not have, Terraform roots that may not exist yet, a plugin download that needs the network. Rather than fail on the first absent piece, or silently do nothing, each step announces that it did not run.

The prefix is the contract. Anything that reads the log — a person, a CI summary, a grep — can find every step that was not exercised by looking for skip: at the start of a line, and can trust that the exit status still reflects every check that did run.

Three outcomes, three words#

OutcomeOutputExitWhere it appears
Capability intentionally absentskip: shellcheck is not installed0scripts/lint.sh, make modules
Required tool or file absentmissing: tflintnon-zeromake doctor, make format
Check ran and failedThe tool’s own output, then Lint FAILEDnon-zeroAny gate or validation target

missing: belongs to diagnostics and to commands that cannot do their job at all. make format exists to rewrite files, so without Prettier it prints missing: root npm dependencies; rebuild the devcontainer or run npm ci and exits 1. The lint gate, by contrast, treats an absent Prettier as a skip, because a gate in a bare checkout should still run every other linter.

Where skips come from#

  • Tools not installed. The shared run helper in scripts/lint.sh and scripts/security-scan.sh checks command -v first.
  • Components not present. Hugo and Terraform targets skip when none of the declared directories exist: skip: no Hugo sites (directories not found).
  • Optional configuration not set. An empty HUGO_PREPARE_COMMAND, a missing scripts/check-links.mjs, or no LINT_COMMAND each print their own line, such as skip: no link checker configured.
  • No input to work on. The docs stager prints skip: no docs/ directory; the Node restore hook prints skip: no root package.json.
  • Network deliberately off. Under Offline Validation , plugin and provider downloads skip and validation continues with what is cached.

Examples#

The lint gate’s runner is the whole pattern in a few lines:

scripts/lint.sh
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
}

The same shape appears outside shell scripts. The documentation stager exits cleanly when there is nothing to stage:

scripts/docs/prepare-site-content.mjs
try {
  readdirSync(DOCS_DIR);
} catch {
  console.log("skip: no docs/ directory");
  process.exit(0);
}