Operational scripts
This document describes the operational scripts in the
scripts
directory. These scripts support developing, validating, provisioning, and
maintaining the home lab.
Run every script from the repository root: the scripts source
scripts/common.sh and resolve configuration files using paths relative to the
repository root. Most scripts wrap containerized tools, so they require a
working Docker installation.
Linting and formatting
scripts/lint.sh runs
super-linter against the whole
repository, using the same container image version and configuration as the
Lint CI workflow (.github/workflows/lint.yaml and
config/lint/super-linter.env), so a clean local run closely predicts a clean
CI run:
scripts/lint.sh
The script also runs from a Git worktree: a worktree holds only the working files, so the script mounts the main repository's metadata directory into the container at the same absolute path, as the super-linter local run documentation describes.
The script supports the following environment variables:
-
LINTER_CONTAINER_FIX_MODE: set totrueto run the linters in fix mode (loadsconfig/lint/super-linter-fix-mode.env), letting formatters apply fixes to the working tree instead of only reporting issues:LINTER_CONTAINER_FIX_MODE=true scripts/lint.sh -
LINTER_CONTAINER_OPEN_SHELL: set totrueto open an interactive Bash shell inside the linter container instead of running the linters, useful to debug linter configuration. -
LINTER_CONTAINER_LINT_COMMIT_MESSAGE: set totrueto validate a commit message from the standard input instead of running the linters, with the same configuration (commitlint.config.js) and strictness as the lint run:LINTER_CONTAINER_LINT_COMMIT_MESSAGE=true scripts/lint.sh < <message file>Validate commit messages before committing: the lint run only validates the message of the last commit, while the
LintCI workflow validates the messages of all the pushed commits. You can run this check while a lint run is in progress. -
LINTER_CONTAINER_IMAGE_VERSION: override the linter container image version. Defaults to the version that theLintCI workflow pins. LOG_LEVEL: super-linter log level. Defaults toINFO.
Super-linter writes its results to the gitignored super-linter-output
directory in the repository root:
super-linter-output/super-linter-summary.md: the per-linter pass/fail summary table. Check this first to see which linters failed.super-linter-output/super-linter/: the detailed outputs, onesuper-linter-parallel-stdout-<LINTER>(and, when produced,super-linter-parallel-stderr-<LINTER>) file per linter, along with the per-linter exit codes and the lists of files each linter processed. Read the files for the linters the summary marks as failed to get the actual findings.
Prefer these files over scrolling the console output: they persist after the run and separate each linter's findings.
The linter container runs as root and rewrites the Git index (.git/index),
which then belongs to root. A lint run can also unstage staged changes, so
stage files after linting, right before committing. Committing still works
because Git replaces the index file instead of writing to it.
scripts/format.sh formats the given paths (default: the whole repository) with
the formatters that super-linter validates in check mode, running them from the
same pinned super-linter container image so results match what CI expects:
scripts/format.sh [path ...]
The script runs:
- Prettier on Markdown, YAML, JSON, JavaScript, HTML,
and CSS files, excluding the generated
docs/site output and thesuper-linter-output/directory. - markdownlint with the repository
configuration (
config/lint/.markdown-lint.yaml) on Markdown files. Issues that--fixcannot resolve do not stop the run: the ones Prettier fixes (for example, table pipe alignment) are cleared by a second markdownlint pass that runs after the other formatters, and the script fails only when that pass still reports issues — those need manual fixes. - shfmt on shell scripts.
- textlint with super-linter's default configuration on Markdown and text files.
terraform fmt -recursiveon Terraform files.
The script runs textlint and markdownlint before Prettier, so that Prettier formats the results of their fixes.
Directories go through every formatter; single files only go through the
formatters that support their file type. Use it as the fast inner loop when
editing: the check-mode scripts/lint.sh run remains the authoritative verdict.
The fixers that scripts/format.sh covers, and the ones it knowingly delegates
to super-linter fix mode, are listed in scripts/common.sh. scripts/lint.sh
fails when config/lint/super-linter-fix-mode.env enables a fixer that appears
in neither list, forcing a deliberate decision for every new fixer: implement it
in scripts/format.sh, or explicitly delegate it.
scripts/run-pre-commit.sh runs the configured
pre-commit hooks
(config/pre-commit/.pre-commit-config.yaml) against all files, creating a
dedicated Python virtual environment on first run. Pass an argument to run a
different command inside that environment instead.
Provisioning and configuration
scripts/bootstrap-host.sh and scripts/run-terraform.sh depend on tools that
only the operations Nix development shell provides, so run them inside that
shell. See the Nix development shells guide.
scripts/bootstrap-host.sh <hostname> <expected_mac>: bootstraps a home lab host, validating that the target machine's MAC address matches the expected one before deploying. Hosts with adisko.nixare installed withnixos-anywhere; hosts without one (LXC containers) receive their configuration vianixos-rebuild switch --flake --target-host.-
scripts/run-ansible.sh: runs Ansible playbooks fromconfig/ansibleinside a purpose-built container. Select the playbook withANSIBLE_PLAYBOOK_FILE_NAMEand pass extra flags viaADDITIONAL_ANSIBLE_FLAGS. Pick the narrowest playbook for the change first: per-purpose playbooks underconfig/ansible/playbooks/exist alongsidehome-lab-node.yaml;restore-workloads.yamlrestores a stack's state from a restic repository and is documented in the backup operations guide. The others (for example,setup-disks.yamlconverges pools, datasets, mounts, and thedirectories_to_createlist), andmain.yamlchains all the playbooks, so tag-scoping it also runs the untagged tasks of every other role — on a host outside those roles' normal scope, the check run then predicts unrelated, possibly destructive changes. To scope a run to one workload stack on one host, combine the stack's tag with theuntaggedpseudo-tag (global, untagged initialization tasks must always run) and a host limit:ADDITIONAL_ANSIBLE_FLAGS="--check --diff --tags='monitoring-apt' --tags untagged --limit raspberrypi2.edge.lab.ferrari.how" \ ANSIBLE_PLAYBOOK_FILE_NAME="home-lab-node.yaml" \ scripts/run-ansible.shRestic repositories are initialized by the node role's read-then-act task only, never by the backup job: the task runs
restic initthrough the job container withRESTIC_ALLOW_INIT=truewhen the repository'sconfigfile is absent andfindmnt --targetshows the repository path backed by a filesystem other than the root one; the job's unit refuses to run on the root filesystem for the same reason, and the entrypoint refuses an implicit initialization. A replacement host whose repository already exists on the share is therefore never re-initialized.Stack tags are the
fact_categorynames declared in theferrarimarco_home_lab_noderole'sinclude-variables.yaml. See the Ansible development guide for how the role's tagging and enablement machinery works. The Docker Compose lifecycle tasks (image pulls, service starts, and the restarts that configuration changes trigger) carry thealwaystag so tag-scoped runs still execute them — theirmolecule-notesttag alone would deselect them, deploying changed configuration without restarting the services that read it; Molecule still skips them via--skip-tags.Always run with
--check --difffirst and review the predicted changes (watching for unexpectedstate: absentteardowns) before repeating the same command without those flags to apply.The script requires a running SSH agent (
SSH_AUTH_SOCK) holding the keys to connect to the nodes; it forwards the agent socket into the Ansible container. Shells that do not inherit the session environment (for example AI agent shells) must discover the socket and pass it explicitly: find it withls /tmp/ssh-*/agent.*, verify the loaded keys withSSH_AUTH_SOCK=<socket> ssh-add -l, and prefix the invocation withSSH_AUTH_SOCK=<socket>. The socket path changes across reboots.The script also wraps
ansible-vaultfor vault file maintenance: neither the host nor the Nix dev shells provideansible-vault. SetANSIBLE_EDIT_VAULT_FILE=trueto edit a vault file, orANSIBLE_VIEW_VAULT_FILE=trueto view one read-only. Both modes default toinventory/group_vars/all/vault.yaml; select a different file withANSIBLE_VAULT_FILE_PATH(relative toconfig/ansible). For example, to list the key names a vault file defines without exposing secret values:ANSIBLE_VIEW_VAULT_FILE=true scripts/run-ansible.sh \ | grep -oE '^vault[a-zA-Z_0-9-]*'Never print decrypted vault values: pipe
ansible-vault viewoutput through a filter (as above) that only surfaces key names. The filter anchors on thevaultprefix because all vaulted variables follow thevault_*naming convention, and the script's own log output shares the stream with the decrypted content.The script builds the Ansible container image on each run. To run an existing container image instead, set
ANSIBLE_CONTAINER_IMAGE_IDto the container image to run, andANSIBLE_CONTAINER_IMAGE_SKIP_BUILD=trueto skip the build. TheTest Ansible collections, roles, playbooksCI workflow uses these variables to build the container image once per job with the build cache that all the test jobs share.To test a playbook with Molecule, set
ANSIBLE_TEST_DISTROto the container image of the test instance,ANSIBLE_TEST_PLAYBOOK_NAMEto the name of the playbook to test, and pass the Molecule command to run:ANSIBLE_TEST_DISTRO="geerlingguy/docker-debian12-ansible:latest" \ ANSIBLE_TEST_PLAYBOOK_NAME="bootstrap" \ scripts/run-ansible.sh "molecule test"The test starts a privileged container for the test instance, and removes it when the test completes. Molecule skips the tasks that have the
molecule-notesttag: tests don't pull container images, and don't start containerized services.Both variables are required:
ANSIBLE_TEST_DISTROalso switches the container image build to the target that contains Molecule, so a barescripts/run-ansible.sh "molecule test"fails withexec: "molecule": executable file not found. The SSH agent socket is required for Molecule runs too, because the script forwards it before checking the command: setSSH_AUTH_SOCKas described above. -
scripts/run-terraform.sh: iterates over the numbered Terraform service directories inconfig/terraformand runsterraform initandterraform applyfor each one. Runscripts/run-terraform.sh output <service> [<output-name>]to print a service's outputs as JSON instead. The script needsterraformfrom theoperationsNix shell. Reviewing and applying in two passes: with stdin closed (</dev/null) the stacks without changes complete and the first stack with changes prints its plan and aborts at the approval prompt; after reviewing that plan, a run with a single approval on stdin (printf 'yes\n' | nix develop ./config/nix#operations --command scripts/run-terraform.sh) applies that stack and still aborts at the next stack with changes, so nothing later is applied unreviewed. The approved run plans again, so compare its plan with the reviewed one before trusting the apply.
Documentation site
scripts/run-mkdocs.sh builds or serves the documentation sites with Material
for MkDocs. See Develop and build the Home Lab site.
CI/CD tooling
scripts/open-shell-ci-cd-tools-container.sh: opens an interactive shell in the CI/CD tools container (docker/ci-cd-tools).scripts/release-please-dry-run.sh: runs a release-pleaserelease-prdry run against the current branch to preview release notes and version bumps.scripts/run-renovate.sh: runs Renovate locally.
Miscellaneous
scripts/build-arduino-project.sh: compiles an Arduino project for a given fully qualified board name (FQBN).scripts/copy-data.sh <source-host> <source-directory> <target-directory>: copies data from a remote host with rsync. SetENABLE_DRY_RUN=truefor a dry run andENABLE_CHECKSUM=trueto compare files by checksum.scripts/common.sh: shared helper functions and variables sourced by the other scripts; not meant to be invoked directly.