1
0
forked from sneak/dcf

bring the repo up to org standards

Adopt the standard tooling: a Makefile of thin shims over a full
scripts-to-rule-them-all `script/` set, the canonical `.golangci.yml`
vendored byte-identical from `sneak/prompts`, docker-only linting via
`Dockerfile.lint`, a `Dockerfile` and Gitea workflow that gate every
push, and prettier/editorconfig/dockerignore config.

`make build` pointed at a `cmd/dcfinfo` that is not in the tree and
could never have succeeded; this repo is a library, so `build` is now
the compile check over every package.

Clear the 57 findings the canonical linter config reports on `pkg/dcf`.
Two were real: `findDCFMountPoints` checked a never-assigned `erro`
instead of the error from `findAllMountPoints`, discarding it, and
`privatePath` was computed twice so the first computation was dead.
Mountpoint selection otherwise behaves exactly as before; the defects
that survive are filed as issue #6, not fixed here.

Rename `DCFStore` to `Store` and `DCFObject` to `Object` (with its
`DCFStoreRoot` field to `StoreRoot`), which revive's stutter rule
requires and which the `fs.FS` rework in issue #4 will build on.

Replace the placeholder test with tests over the exported surface.
The filesystem walk stays uncovered: it is reachable only through
`GetDCFStores`, which needs real mounted media.

README keeps its content, reorganised into the required sections and
gaining Entrypoints.

(closes #1)
This commit is contained in:
2026-08-30 11:36:58 +00:00
parent f585e8fa34
commit ed95e66f7d
32 changed files with 1622 additions and 152 deletions

166
script/assert-context-complete Executable file
View File

@@ -0,0 +1,166 @@
#!/bin/sh
# script/assert-context-complete: fail unless the build context a stage
# was given contained every source file the repository has.
#
# Usage: script/assert-context-complete LOG STAGE EXPECTED
#
# LOG must be `docker build --progress=plain` output. EXPECTED is a list
# of repo-relative paths, one per line, as script/repo-source-manifest
# prints it.
#
# script/assert-step-ran answers "did the tool execute". This answers
# the other half: "was the tool given the tree". They are different
# questions, and a green build can satisfy the first while failing the
# second — a .dockerignore entry, or a COPY that brings in less than the
# whole tree, removes files from the context silently, and golangci-lint
# then genuinely runs, genuinely examines what it was handed, and
# genuinely reports `0 issues.` over a repo that has a violation in it.
# `go test ./...` likewise never runs a package that did not arrive.
#
# How it knows: each source-consuming stage emits, right after its
# `COPY . .`, an inventory of the Go files actually present —
#
# context-manifest-begin
# context-file: cmd/gotemplate/main.go
# ...
# context-manifest-end
#
# — and this compares that against EXPECTED, which comes from the git
# index rather than from the same build. The two sources are
# independent: .dockerignore decides the first and cannot touch the
# second. An expectation read from the evidence would prove nothing.
#
# The manifest lines are only believed under the same discipline
# script/assert-step-ran applies to a tool's success line: they must be
# attributed to the id of a step whose header is "#ID [STAGE n/m] CMD"
# with CMD matching the manifest command, which BuildKit reported DONE,
# and both sentinels must be present. So another step's output does not
# count, a header does not count, a cached step (which writes nothing)
# does not count, and a truncated log fails rather than passing with a
# short list.
#
# Only one direction is checked: everything the repo has must have
# arrived. Files in the context that git does not track are not a
# failure — untracked local work is normal and hides nothing.
#
# What this does not cover: a Dockerfile edit to the manifest step
# itself, and evidence forged inside a matched command. Both are visible
# in the Dockerfile in plain sight, and both are equally outside
# script/assert-step-ran. This is not an exhaustive list of ways a green
# run can be untrue; it is the ones known.
set -eu
die() {
echo "script/assert-context-complete: $*" >&2
exit 1
}
[ $# -eq 3 ] || die "usage: $0 LOG STAGE EXPECTED"
[ -r "$1" ] || die "cannot read $1"
[ -r "$3" ] || die "cannot read $3"
# The stage and the expected-list path travel in the environment, not in
# -v: awk expands escape sequences in a -v assignment.
ACC_STAGE="$2" ACC_EXPECTED="$3" awk '
function fail(msg) {
printf "script/assert-context-complete: %s\n", msg | "cat 1>&2"
close("cat 1>&2")
exit 1
}
# The package a missing file belongs to — its directory, which is what a
# reader needs named. A bare "." for a root-level file reads as a typo
# next to the sentence punctuation, so it is spelled out.
function pkgof(path, i) {
i = length(path)
while (i > 0 && substr(path, i, 1) != "/") i--
return i == 0 ? "the repository root" : substr(path, 1, i - 1)
}
function preview(list, n, limit, i, out) {
for (i = 1; i <= n && i <= limit; i++)
out = out (i > 1 ? ", " : "") list[i]
if (n > limit)
out = out sprintf(", and %d more", n - limit)
return out
}
BEGIN {
stage = ENVIRON["ACC_STAGE"]
# Fixed by the protocol the Dockerfile emits, so callers cannot
# drift from it.
step = "^RUN echo context-manifest-begin"
beginmark = "context-manifest-begin"
endmark = "context-manifest-end"
prefix = "context-file: "
expected_path = ENVIRON["ACC_EXPECTED"]
while ((getline line < expected_path) > 0)
if (line != "") expected[++nexpected] = line
close(expected_path)
if (nexpected == 0)
fail(sprintf("the expected-file list (%s) is empty, so any build context would satisfy this check", expected_path))
}
# Every progress line is "#ID " and then a step header, a status, or one
# line the step itself wrote.
/^#[0-9]+ / {
id = substr($1, 2)
rest = substr($0, length($1) + 2)
if (rest == "CACHED") { cached[id] = 1; next }
if (rest ~ /^DONE /) { done[id] = 1; next }
# Header: "[STAGE n/m] CMD", or "[PLATFORM STAGE n/m] CMD" when the
# build names a platform. Bracketed spans with no n/m are BuildKit
# internals, not steps.
if (substr(rest, 1, 1) == "[") {
p = index(rest, "] ")
if (p == 0) next
n = split(substr(rest, 2, p - 2), part, " ")
if (n < 2 || part[n] !~ /^[0-9]+\/[0-9]+$/) next
if (part[n - 1] != stage) next
if (substr(rest, p + 2) ~ step) matched[id] = 1
next
}
# Output: "#ID 0.31 <the line the step wrote>".
if (rest ~ /^[0-9]+[.][0-9]+ /) {
sub(/^[0-9]+[.][0-9]+ /, "", rest)
if (rest == beginmark) begun[id] = 1
else if (rest == endmark) ended[id] = 1
else if (index(rest, prefix) == 1)
arrived[id SUBSEP substr(rest, length(prefix) + 1)] = 1
}
}
END {
for (id in matched) {
nmatched++
if (cached[id]) ncached++
if (done[id] && begun[id] && ended[id]) chosen = id
}
if (nmatched == 0)
fail(sprintf("the build ran no step matching /%s/ in stage \"%s\", so the build context was never inventoried. Either the stage is not in the build graph — renamed, deleted, or nothing the final stage builds depends on it any more — or the manifest step was taken out of it", step, stage))
if (chosen == "" && ncached == nmatched)
fail(sprintf("every step matching /%s/ in stage \"%s\" was served from cache, so its inventory describes an earlier tree rather than this one; the cache bust for that stage is not taking effect", step, stage))
if (chosen == "")
fail(sprintf("a step matching /%s/ ran in stage \"%s\" but wrote no complete inventory between %s and %s. The log is truncated, the build was not run with --progress=plain, or that step no longer emits the manifest", step, stage, beginmark, endmark))
for (i = 1; i <= nexpected; i++) {
if (arrived[chosen SUBSEP expected[i]]) continue
missing[++nmissing] = expected[i]
pkg = pkgof(expected[i])
if (!(pkg in seenpkg)) { seenpkg[pkg] = 1; pkgs[++npkgs] = pkg }
}
if (nmissing == 0) exit 0
fail(sprintf("%d of the %d source files this repository tracks never reached the build context of stage \"%s\", so nothing examined them. Missing package(s): %s. Missing file(s): %s. A .dockerignore entry, or a COPY that brings in less than the whole tree, drops files silently: the tool still runs, still finds nothing wrong with what it was handed, and still reports success", nmissing, nexpected, stage, preview(pkgs, npkgs, 10), preview(missing, nmissing, 10)))
}
' "$1"

125
script/assert-step-ran Executable file
View File

@@ -0,0 +1,125 @@
#!/bin/sh
# script/assert-step-ran: fail unless a docker build actually executed a
# named step, proved by output the step's own tool wrote.
#
# Usage: script/assert-step-ran LOG STAGE STEP_REGEX EVIDENCE_REGEX WHAT
#
# LOG must be `docker build --progress=plain` output. The default tty
# renderer rewrites lines in place and keeps only the tail of a step's
# output, so the flag is load-bearing wherever this is used.
#
# It passes only if some step in LOG satisfies all three of:
#
# 1. its header is "#ID [STAGE n/m] CMD" with CMD matching STEP_REGEX,
# 2. one of the lines that step wrote matches EVIDENCE_REGEX,
# 3. BuildKit reported "#ID DONE".
#
# This observes the build that happened; it parses no Dockerfile. A
# stage renamed, moved, made unreachable, deleted, split by a whitespace
# byte BuildKit treats as a separator, or an instruction rewritten —
# none of it can make this pass a build in which the step did not run,
# because a step that did not run wrote no output, and a step served
# from cache writes none either ("#ID CACHED", no output lines).
#
# The stage name in a header is BuildKit's label for that vertex, not a
# reading of the file being built. A vertex shared with an earlier build
# of a DIFFERENT Dockerfile is replayed under the label it was first
# recorded with, so a `Dockerfile.lint` run can print `[lint 1/8]` for
# the main `Dockerfile`'s lint stage. A borrowed label cannot produce a
# pass — a replayed vertex prints CACHED and writes no evidence, and a
# vertex that really executes really ran the command in the header — but
# it does mean a header alone proves nothing about which stages the file
# declares. Hence one failure message naming both causes rather than a
# split that would claim to know which.
#
# What this guard cannot see — not an exhaustive list of ways a green can
# be untrue, only the ones known:
#
# - Evidence forged inside the matched step, e.g.
# `RUN golangci-lint run ... || echo "0 issues."`. Condition 1 ties
# the evidence to a step whose own command is in the log, so the
# forgery has to be written into that command in the Dockerfile, in
# plain sight.
# This guards against a step falling silently out of the build. It does
# not certify that what ran examined everything it should have — a
# `.dockerignore` entry excluding a package makes the linter genuinely
# run and genuinely print `0 issues.` while a real violation sits
# unexamined in the repo. That is a different question, asked separately
# by script/assert-context-complete, which every caller of this script
# also runs.
#
# What it depends on — BuildKit's plain progress format and the tool's
# own success wording — fails the caller loudly if it drifts, because
# drift removes a match rather than creating one.
set -eu
die() {
echo "script/assert-step-ran: $*" >&2
exit 1
}
[ $# -eq 5 ] || die "usage: $0 LOG STAGE STEP_REGEX EVIDENCE_REGEX WHAT"
[ -r "$1" ] || die "cannot read $1"
# The regexes travel in the environment, not in -v: awk expands escape
# sequences in a -v assignment, which would eat a backslash before the
# regex ever sees it.
ASR_STAGE="$2" ASR_STEP="$3" ASR_EVIDENCE="$4" ASR_WHAT="$5" awk '
function fail(msg) {
printf "script/assert-step-ran: %s\n", msg | "cat 1>&2"
close("cat 1>&2")
exit 1
}
BEGIN {
stage = ENVIRON["ASR_STAGE"]
step = ENVIRON["ASR_STEP"]
evidence = ENVIRON["ASR_EVIDENCE"]
what = ENVIRON["ASR_WHAT"]
}
# Every progress line is "#ID " and then a step header, a status, or one
# line the step itself wrote.
/^#[0-9]+ / {
id = substr($1, 2)
rest = substr($0, length($1) + 2)
if (rest == "CACHED") { cached[id] = 1; next }
if (rest ~ /^DONE /) { done[id] = 1; next }
# Header: "[STAGE n/m] CMD", or "[PLATFORM STAGE n/m] CMD" when the
# build names a platform. Bracketed spans with no n/m are BuildKit
# internals ("[internal] load build context"), not steps.
if (substr(rest, 1, 1) == "[") {
p = index(rest, "] ")
if (p == 0) next
n = split(substr(rest, 2, p - 2), part, " ")
if (n < 2 || part[n] !~ /^[0-9]+\/[0-9]+$/) next
if (part[n - 1] != stage) next
if (substr(rest, p + 2) ~ step) matched[id] = 1
next
}
# Output: "#ID 41.80 <the line the step wrote>".
if (rest ~ /^[0-9]+[.][0-9]+ /) {
sub(/^[0-9]+[.][0-9]+ /, "", rest)
if (rest ~ evidence) emitted[id] = 1
}
}
END {
for (id in matched) {
nmatched++
if (done[id] && emitted[id]) exit 0
if (cached[id]) ncached++
}
if (nmatched == 0)
fail(sprintf("the build ran no step matching /%s/ in stage \"%s\", so %s did not run. Either the stage is not in the build graph — renamed, deleted, or nothing the final stage builds depends on it any more — or it was built without that command among its steps", step, stage, what))
if (ncached == nmatched)
fail(sprintf("every step matching /%s/ in stage \"%s\" was served from cache, so %s did not run on this tree; the cache bust for that stage is not taking effect", step, stage, what))
fail(sprintf("a step matching /%s/ ran in stage \"%s\" but never wrote a line matching /%s/, so there is no evidence %s did the work; the command or the tool that produces that line has changed", step, stage, evidence, what))
}
' "$1"

84
script/bootstrap Executable file
View File

@@ -0,0 +1,84 @@
#!/bin/sh
# script/bootstrap: install all dependencies needed to build and develop
# this repo. Idempotent: every install is guarded by a check so already
# installed tools are skipped. Base tooling comes from nix, apt, brew,
# or apk (detected in that order); assumes NOTHING is present (not git,
# make, or go). The linter is NOT installed locally: golangci-lint runs
# via docker only (script/lint), pinned by image digest, so the only
# lint prerequisite is a working docker.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
PKGMGR=""
SUDO=""
detect_pkgmgr() {
[ -n "$PKGMGR" ] && return 0
if command -v nix-env >/dev/null 2>&1; then
PKGMGR="nix"
elif command -v apt-get >/dev/null 2>&1; then
PKGMGR="apt"
elif command -v brew >/dev/null 2>&1; then
PKGMGR="brew"
elif command -v apk >/dev/null 2>&1; then
PKGMGR="apk"
else
echo "bootstrap: no supported package manager (nix, apt, brew, apk)" >&2
exit 1
fi
if [ "$PKGMGR" = "apt" ]; then
export DEBIAN_FRONTEND=noninteractive
if [ "$(id -u)" != "0" ]; then
SUDO="sudo"
fi
fi
}
# pkg_install <nix-attr> <apt-pkg> <brew-formula> <apk-pkg>
pkg_install() {
detect_pkgmgr
case "$PKGMGR" in
nix) nix-env -iA "nixpkgs.$1" ;;
apt) $SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y "$2" ;;
brew) brew install "$3" ;;
apk) apk add --no-cache "$4" ;;
esac
}
missing() {
! command -v "$1" >/dev/null 2>&1
}
main() {
cd "$ROOT"
# Base tooling
if missing git; then pkg_install git git git git; fi
if missing make; then pkg_install gnumake make make make; fi
# Go toolchain
if missing go; then pkg_install go golang go go; fi
# Linting runs via docker only (script/lint). Warn, don't fail:
# everything except `make lint` works without it.
if missing docker; then
echo "bootstrap: WARNING: docker not found; make lint and" >&2
echo "bootstrap: make docker require it. Install docker to" >&2
echo "bootstrap: run the linter." >&2
fi
# Markdown/CSS/JS formatting uses prettier. script/prettier falls
# back to a pinned docker image when it is not on PATH, so this is
# a convenience, not a requirement.
if missing prettier && missing docker; then
echo "bootstrap: WARNING: neither prettier nor docker found;" >&2
echo "bootstrap: markdown formatting will be skipped." >&2
fi
go mod download
echo "bootstrap complete"
}
main "$@"

15
script/check Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/sh
# script/check: run all checks (test, lint, fmt-check). Our own
# extension to scripts-to-rule-them-all. Must not modify any files.
# Generic: usually needs no adaptation.
set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
main() {
"$SCRIPT_DIR/test"
"$SCRIPT_DIR/lint"
"$SCRIPT_DIR/fmt-check"
}
main "$@"

89
script/cibuild Executable file
View File

@@ -0,0 +1,89 @@
#!/bin/sh
# script/cibuild: run the CI build. The Dockerfile runs the checks
# (gofmt, config verify, lint, test), so a successful build implies a
# green repo. The Gitea workflow runs this on push.
#
# Only if the checks actually ran, though, and a green `docker build` is
# no evidence that they did:
#
# 1. On an unchanged tree every layer comes from cache and the build
# exits 0 in under a second having executed nothing. Hence the two
# --no-cache-filter flags.
#
# 2. BuildKit silently ignores a --no-cache-filter naming a stage that
# does not exist, so a rename restores that green no-op.
#
# 3. BuildKit builds only the final stage's dependency graph. A stage
# nothing references is never built, and with no --target the final
# stage is whichever is last in the file — so appending a stage, or
# moving a reference into a stage that is itself unreachable, drops
# the checks out of the build while every reference to them is still
# there to read.
#
# Rather than predict any of that from the Dockerfile's text, this asks
# the finished build what it did: script/assert-step-ran requires that
# the log contain a lint step and a test step that ran, and that each
# wrote the line its own tool writes on success. Every trap above ends
# with the step absent from the log or served from cache, and both fail
# that assertion. See that script for what it does not cover.
#
# 4. A step that ran is not a step that saw the repo. .dockerignore, or
# a COPY narrower than the tree, removes files from the build
# context, and the linter then reports `0 issues.` over what is left
# while `go test ./...` never compiles the package that went missing.
# So each source-consuming stage inventories what reached it, and
# script/assert-context-complete compares that inventory against the
# git index — a source .dockerignore cannot reach.
set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
dockerfile=Dockerfile
lint_stage=lint
test_stage=builder
# `ok <pkg> 1.234s`: go test prints `(cached)` in place of the duration
# when it replays a result, so requiring the duration requires a package
# that was really exercised.
test_ran='^ok[[:space:]]+[^[:space:]]+[[:space:]]+[0-9]+[.][0-9]+s'
main() {
cd "$ROOT"
tmp="$(mktemp -d "${TMPDIR:-/tmp}/$("$SCRIPT_DIR/projectname")-cibuild.XXXXXX")"
trap 'rm -rf "$tmp"' EXIT INT TERM
# --progress=plain is load-bearing: the assertions below read the
# steps' own output out of this log, and the tty renderer discards
# all but the tail of it. The exit status travels through a file
# because a pipeline's status is the last command's; `set +e` is
# what lets the recording line run at all, since errexit would
# otherwise abandon the subshell on a failing build. A missing file
# reads as failure.
(
set +e
docker build \
--progress=plain \
--no-cache-filter="$lint_stage" \
--no-cache-filter="$test_stage" \
-f "$dockerfile" . 2>&1
echo "$?" >"$tmp/status"
) | tee "$tmp/build.log"
status="$(cat "$tmp/status" 2>/dev/null || echo 1)"
[ "${status:-1}" -eq 0 ] || exit "${status:-1}"
script/assert-step-ran "$tmp/build.log" "$lint_stage" \
'^RUN golangci-lint run' '^0 issues[.]$' 'the linter'
script/assert-step-ran "$tmp/build.log" "$test_stage" \
'^RUN go test' "$test_ran" 'the tests'
# Both stages, separately: each has its own COPY, so an intact
# context in one is no evidence about the other.
script/repo-source-manifest >"$tmp/expected"
script/assert-context-complete "$tmp/build.log" "$lint_stage" "$tmp/expected"
script/assert-context-complete "$tmp/build.log" "$test_stage" "$tmp/expected"
}
main "$@"

15
script/docker Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/sh
# script/docker: build the Docker image tagged with the project name.
# Identical in all repos; the tag comes from script/projectname.
# Generic: needs no adaptation.
set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
main() {
cd "$ROOT"
docker build -t "$("$SCRIPT_DIR/projectname")" .
}
main "$@"

26
script/fmt Executable file
View File

@@ -0,0 +1,26 @@
#!/bin/sh
# script/fmt: format all files (writes). Go via gofmt, everything else
# (markdown, CSS, JS, YAML, JSON) via prettier with the repo's
# .prettierrc. Never hand-rolled substitutions: this script is the only
# sanctioned way to reformat the tree.
set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
main() {
cd "$ROOT"
gofmt -s -w .
if command -v goimports >/dev/null 2>&1; then
goimports -w .
fi
# A missing runner (exit 2) is a warning here: script/prettier has
# already said so on stderr, and failing `make fmt` over it would
# block work that has nothing to do with markdown.
"$SCRIPT_DIR/prettier" --write . || [ $? -eq 2 ]
}
main "$@"

25
script/fmt-check Executable file
View File

@@ -0,0 +1,25 @@
#!/bin/sh
# script/fmt-check: check formatting (read-only). Same scope as
# script/fmt, but fails instead of writing.
set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
main() {
cd "$ROOT"
if [ -n "$(gofmt -s -l .)" ]; then
echo "gofmt needed on:"
gofmt -s -l .
exit 1
fi
# Exit 2 means no prettier and no docker: reported by
# script/prettier on stderr and not treated as a failure, so this
# check still runs on a machine that has neither. The container
# build is the gate that always has both.
"$SCRIPT_DIR/prettier" --check . || [ $? -eq 2 ]
}
main "$@"

16
script/install-precommit Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
# script/install-precommit: install the git pre-commit hook that runs
# script/precommit. Our own extension to scripts-to-rule-them-all.
# Generic: needs no adaptation.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
main() {
cd "$ROOT"
printf '#!/bin/sh\nset -e\nscript/precommit\n' > .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
echo "pre-commit hook installed: runs script/precommit"
}
main "$@"

71
script/lint Executable file
View File

@@ -0,0 +1,71 @@
#!/bin/sh
# script/lint: run the linter. golangci-lint is never installed locally: it
# runs via docker only, one way, everywhere.
#
# Traps, each of which yields a green run over an unlinted or partly linted
# tree:
#
# 1. A bare `docker build -f Dockerfile.lint .` serves the lint layer from
# cache on an unchanged tree and exits 0 having linted nothing, and
# BuildKit ignores a --no-cache-filter whose stage name matches
# nothing, restoring that no-op after a rename. So the run is not
# trusted: script/assert-step-ran requires the log to show the lint
# step executing and golangci-lint's own success line coming out of
# it. A cached or absent step fails that.
#
# 2. .dockerignore decides what reaches the container, and only what
# reaches it is linted, so excluding a Go file drops it from the lint
# with the linter still reporting `0 issues.` over what it was
# handed. So the context is not trusted either: the lint stage
# inventories the sources that reached it, and
# script/assert-context-complete compares that against the git index,
# which .dockerignore cannot touch. Never exclude Go sources,
# go.mod/go.sum or .golangci.yml — and now nothing silently does.
set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
dockerfile=Dockerfile.lint
# Must match the stage name in Dockerfile.lint.
stage=lint
main() {
cd "$ROOT"
tmp="$(mktemp -d "${TMPDIR:-/tmp}/$("$SCRIPT_DIR/projectname")-lint.XXXXXX")"
trap 'rm -rf "$tmp"' EXIT INT TERM
# No --target: whatever stage is last is the one BuildKit builds, and
# the assertion below is what decides whether the linter was in it.
# --progress=plain is load-bearing — the tty renderer discards all but
# the tail of a step's output. The exit status travels through a file
# because a pipeline's status is the last command's; `set +e` is what
# lets the recording line run at all, since errexit would otherwise
# abandon the subshell on a failing build. A missing file reads as
# failure.
(
set +e
docker build \
--progress=plain \
--no-cache-filter="$stage" \
--output=type=cacheonly \
-f "$dockerfile" . 2>&1
echo "$?" >"$tmp/status"
) | tee "$tmp/build.log"
status="$(cat "$tmp/status" 2>/dev/null || echo 1)"
[ "${status:-1}" -eq 0 ] || exit "${status:-1}"
script/assert-step-ran "$tmp/build.log" "$stage" \
'^RUN golangci-lint run' '^0 issues[.]$' 'the linter'
# That the linter ran says nothing about what it was given. The
# expectation comes from git and the evidence from the build, which
# is the only reason comparing them means anything.
script/repo-source-manifest >"$tmp/expected"
script/assert-context-complete "$tmp/build.log" "$stage" "$tmp/expected"
}
main "$@"

21
script/precommit Executable file
View File

@@ -0,0 +1,21 @@
#!/bin/sh
# script/precommit: run by the git pre-commit hook; fails the commit if
# checks fail. Our own extension to scripts-to-rule-them-all. Go repo
# extras: go mod tidy must not change go.mod/go.sum.
set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
main() {
cd "$ROOT"
go mod tidy
git diff --exit-code -- go.mod go.sum || {
echo "precommit: go mod tidy changed go.mod/go.sum;" \
"stage the changes and retry" >&2
exit 1
}
"$SCRIPT_DIR/check"
}
main "$@"

52
script/prettier Executable file
View File

@@ -0,0 +1,52 @@
#!/bin/sh
# script/prettier: run prettier over this repo with the repo's own
# .prettierrc. Helper, not an entrypoint: script/fmt and
# script/fmt-check call it.
#
# Prettier from PATH if it is there, otherwise a hash-pinned node image
# via docker, so a checkout with neither node nor a global prettier
# still formats identically. If neither is available the caller is told
# and the markdown pass is skipped rather than silently passing — a
# formatter that is not present cannot certify formatting.
#
# Exit status: prettier's own, 2 when it was skipped for lack of a
# runner. script/fmt-check treats 2 as a warning, not a failure, so
# that `make check` still works on a machine without docker.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# node:24-alpine, 2026-08-22. Bumping the pin is a deliberate edit.
NODE_IMAGE="node:24-alpine@sha256:d32cdf619f63fe0471182d08996dd516c6275bb5fd31ae06e55a570bd9e1ad43"
# Pinned exactly: prettier's default formatting changes between minor
# releases, so an unpinned version turns fmt-check into a coin flip.
PRETTIER_VERSION="3.9.6"
main() {
cd "$ROOT"
if command -v prettier >/dev/null 2>&1; then
prettier "$@"
return $?
fi
if command -v docker >/dev/null 2>&1; then
# --user keeps written files owned by the invoking user rather
# than root; the container only ever touches the bind mount.
docker run --rm \
--user "$(id -u):$(id -g)" \
-v "$ROOT:/work" \
-w /work \
"$NODE_IMAGE" \
npx --yes "prettier@$PRETTIER_VERSION" "$@"
return $?
fi
echo "script/prettier: neither prettier nor docker found;" >&2
echo "script/prettier: skipping markdown/CSS/JS formatting." >&2
return 2
}
main "$@"

12
script/projectname Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/sh
# script/projectname: output the name of this project. Our own
# extension to scripts-to-rule-them-all. Other scripts that need the
# name (e.g. script/docker) call this, so they can stay identical
# across all repos.
set -eu
main() {
echo "dcf"
}
main "$@"

60
script/repo-source-manifest Executable file
View File

@@ -0,0 +1,60 @@
#!/bin/sh
# script/repo-source-manifest: print every file this repository contains
# whose absence from a docker build context would go unnoticed — one
# repo-relative path per line, LC_ALL=C-sorted.
#
# That is the Go sources. A missing go.mod, go.sum or .golangci.yml
# fails the build loudly (the COPY errors, or golangci-lint refuses to
# start), so they cannot hide anything; they are listed anyway because
# it costs two lines and makes the manifest the linter's whole input
# rather than most of it.
#
# The list comes from the git index, never from a walk of the working
# tree, and that is the point: script/assert-context-complete compares
# it against the inventory the build itself emitted, and a check that
# reads its expectation from the same place it reads its evidence proves
# nothing. .dockerignore governs what docker sends; it cannot touch what
# git tracks.
#
# Tracked files only, and only those present in the worktree — a file
# staged for deletion is not something the build context is missing.
# Untracked files are not expected either, so local scratch work in the
# tree is not a failure.
#
# A path containing a newline or a quote is quoted by git and will not
# match the plain path the build emits, so it fails loudly rather than
# passing silently. No such path exists here, and none should.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
die() {
echo "script/repo-source-manifest: $*" >&2
exit 1
}
main() {
cd "$ROOT"
git rev-parse --is-inside-work-tree >/dev/null 2>&1 ||
die "not a git work tree, so there is nothing to compare a build context against"
# Captured before the loop so that a git failure is the script's
# exit status: in a pipeline only the last command's status counts.
tracked="$(git ls-files -- '*.go' go.mod go.sum .golangci.yml .golangci.yaml)"
manifest="$(
printf '%s\n' "$tracked" | while IFS= read -r f; do
if [ -n "$f" ] && [ -f "$f" ]; then
printf '%s\n' "$f"
fi
done | LC_ALL=C sort
)"
[ -n "$manifest" ] ||
die "the git index lists no Go sources, so any build context would satisfy the check"
printf '%s\n' "$manifest"
}
main "$@"

14
script/setup Executable file
View File

@@ -0,0 +1,14 @@
#!/bin/sh
# script/setup: set up the repo for development after a fresh clone:
# installs dependencies (script/bootstrap) and the git pre-commit hook.
# Add any repo-specific initialization (db init, .env template) here.
set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
main() {
"$SCRIPT_DIR/bootstrap"
"$SCRIPT_DIR/install-precommit"
}
main "$@"

21
script/test Executable file
View File

@@ -0,0 +1,21 @@
#!/bin/sh
# script/test: run the test suite. Quiet on success; on failure, rerun
# with -v for full diagnostic output (and still fail — the first run
# already proved the tests are broken).
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
main() {
cd "$ROOT"
# -count=1 disables the test result cache. Without it a repeat run on
# an unchanged tree reports every package `ok ... (cached)` and exits
# 0 having executed nothing — a gate that cannot fail.
go test -count=1 -timeout 90s -race -cover ./... || {
echo "--- Rerunning with -v for details ---"
go test -count=1 -timeout 90s -race -v ./...
exit 1
}
}
main "$@"