Skip to content

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:

Terminal window
brew install actionlint
actionlint

Run 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.

Terminal window
./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 only

The 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:

Terminal window
mkdir -p .ci-validation
docs_tmp=$(mktemp -d "$PWD/.ci-validation/tmp.XXXXXX")
TMPDIR="$docs_tmp" DF_TEST_TMPDIR=/var/tmp ./tests/ci.sh full

Use 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.

Once per Git checkout:

Terminal window
./tests/install-hooks.sh

This 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.

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:

Terminal window
gh run list --commit "$(git rev-parse HEAD)"
gh run watch RUN_ID --exit-status

Git 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.