Validation before publishing
The Docker bootstrap suite is only one part of CI. ShellCheck, fast fixtures,
documentation, secrets, workflow linting, macOS smoke, and infrastructure checks
also need to pass. Their commands live in tests/ci.sh, which both local checks
and GitHub Actions invoke.
actionlint checks GitHub Actions workflow structure, expressions, and embedded
shell commands (through ShellCheck). Homebrew owns both tools in
packages/Brewfile on macOS and Linux; normal
bootstrap reconciles them. To install just the missing workflow checker on an
already configured Homebrew host, run:
brew install actionlintactionlintRun the second command from this checkout. Exit zero with no diagnostics means the local workflow checks passed; it does not execute the workflows or validate hosted credentials and runner availability. The isolated GitHub Quality job installs its own pinned version. See actionlint’s checks and the Homebrew formula.
./tests/ci.sh full # complete local gate./tests/ci.sh quality # shell, fixtures, docs, secrets, workflow lint./tests/ci.sh shell # syntax and ShellCheck./tests/ci.sh fast # fixture-based tests./tests/ci.sh macos # constrained-PATH smoke; requires macOS./tests/ci.sh infrastructure # backend-free OpenTofu validation./tests/run.sh # Docker bootstrap onlyThe full gate runs quality and infrastructure, macOS smoke on macOS, then the
Docker bootstrap suite. It requires a running Docker daemon and the declared
validation tools. Missing tools and failed checks stop the gate; it does not
silently install dependencies or skip unavailable checks. Temporary homes,
documentation output, and provider data keep checks away from live settings.
The macOS mode parses shell scripts with /bin/bash explicitly and exercises
Mesa and systemd formula fixtures under the constrained system PATH, preserving
compatibility with macOS’s Bash 3.2 even when Homebrew provides a newer Bash.
The Bats entrypoints also clear inherited managed runtime roots and build flags
before creating fixtures. A fake HOME alone is insufficient on hosts that
export DF_TOOLS_ROOT, CODEX_HOME, or language-specific runtime directories.
Fixtures that write tools must explicitly select their temporary roots when
run directly as well.
DF_TEST_TMPDIR selects a separate temporary directory for Bats; it defaults to
TMPDIR (or /tmp). On an NFS checkout, documentation output may need the same
filesystem as the checkout, while Codex runtime fixtures require local storage:
mkdir -p .ci-validationdocs_tmp=$(mktemp -d "$PWD/.ci-validation/tmp.XXXXXX")TMPDIR="$docs_tmp" DF_TEST_TMPDIR=/var/tmp ./tests/ci.sh fullUse a local, trusted temporary directory for DF_TEST_TMPDIR; /var/tmp must be
a root-owned sticky directory. This separation preserves the runtime filesystem
checks and avoids cross-filesystem documentation renames.
The full gate always rebuilds the test image from the committed Dockerfile;
DOCKER_BUILD=0 remains available only for focused tests/run.sh iterations.
Install the push gate
Section titled “Install the push gate”Once per Git checkout:
./tests/install-hooks.shThis selects the versioned .githooks directory in this repository’s local Git
configuration. It refuses to replace an existing hook configuration or
executable hooks; compose existing hooks explicitly first. It does not change
global Git settings. Git does not clone local configuration, so a fresh clone
needs this installation too.
Normal git push now validates every distinct outgoing commit in an independent
temporary clone with its history. It calls that commit’s tests/ci.sh full and
blocks the entire push if validation fails or the entrypoint is missing.
Secret scanning covers HEAD and its complete ancestry, including credentials
removed in later commits. Unrelated local branches are not part of that snapshot;
they are scanned separately if pushed.
Clones live under the ignored .ci-validation/ directory so Docker uses the
same shared host mount as the original checkout; each clone is removed on exit.
The gate makes its snapshot ancestors traversable and creates a readable
checkout so a Docker daemon subject to NFS root squashing can mount it. It does
not grant other users write access or change permissions on the working tree.
The checkout’s existing parent directories must already permit Docker access.
Uncommitted fixes cannot conceal committed failures; unrelated working changes
are left alone. Branch and annotated-tag updates to the same commit share one
validation within the push. Ref deletion publishes no new code and needs no
validation. No success receipt from an earlier commit or environment is reused.
The validation runs once as part of the push, so there is no need to manually run
the full gate immediately before pushing. Use focused modes while editing.
After a failed check, fix and commit the correction before retrying.
The implementation lives in tests/pre-push.sh;
see Git’s pre-push hook contract
and Docker’s bind mount requirements.
What a passing gate establishes
Section titled “What a passing gate establishes”A pass establishes the tested commit on this host and its Docker architecture. It does not establish the other hosted architecture, other operating systems, or the availability of remote registries at a later time. Check the exact pushed SHA’s GitHub Actions run before reporting CI success:
gh run list --commit "$(git rev-parse HEAD)"gh run watch RUN_ID --exit-statusGit hooks are local safeguards and can be bypassed by Git options or by a clone
where they are not installed. GitHub required checks on main provide the
server-side boundary when that publishing policy is enabled. A hook alone
cannot guarantee that failing code never reaches main.