#!/usr/bin/env bash # # The full static gate: yamllint, ansible-lint, and a syntax check of every # playbook. This is the definition of "done" for a change to this repository. # # scripts/check.sh # or: make check # # CI runs this same script, so a green run here means a green pipeline. Keep it # that way: a check that only exists in a workflow file cannot be run before # pushing, and one that only exists here does not gate anything. set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$repo_root" # Version specifications, single-sourced here and read by both CI configurations. # # Pinned to a minor series so that a deploy does not change because PyPI did, and # a lint result does not depend on the morning it ran, while patch releases still # arrive. Verified against ansible-core 2.21.2, ansible-lint 26.6.0 and yamllint # 1.38.0. Raise a ceiling deliberately, after running this script -- not to chase # a release. The Galaxy collections are floored separately in # ansible/requirements.yml. ANSIBLE_CORE_SPEC="${ANSIBLE_CORE_SPEC:-ansible-core>=2.21,<2.22}" ANSIBLE_LINT_SPEC="${ANSIBLE_LINT_SPEC:-ansible-lint>=26.6,<27}" YAMLLINT_SPEC="${YAMLLINT_SPEC:-yamllint>=1.38,<2}" export ANSIBLE_CORE_SPEC ANSIBLE_LINT_SPEC YAMLLINT_SPEC # CI installs the toolchain itself and then re-enters this script with # RUNNER=installed. It asks for the pins here so that they are defined in exactly # one place and a workflow can never drift from what runs locally. if [ "${1:-}" = "--print-specs" ]; then printf '%s\n%s\n%s\n' \ "$ANSIBLE_CORE_SPEC" "$ANSIBLE_LINT_SPEC" "$YAMLLINT_SPEC" exit 0 fi # In CI the tools are already installed and on PATH. Locally, run them through # uvx so this repository needs no virtualenv of its own. Set RUNNER=pip if you # would rather install them yourself. if [ "${RUNNER:-}" = "installed" ] || command -v ansible-lint >/dev/null 2>&1; then yamllint_cmd=(yamllint) ansible_lint_cmd=(ansible-lint) galaxy_cmd=(ansible-galaxy) syntax_cmd=(ansible-playbook) elif command -v uvx >/dev/null 2>&1; then yamllint_cmd=(uvx --from "$YAMLLINT_SPEC" yamllint) ansible_lint_cmd=(uvx --from "$ANSIBLE_LINT_SPEC" --with "$ANSIBLE_CORE_SPEC" ansible-lint) galaxy_cmd=(uvx --from "$ANSIBLE_CORE_SPEC" ansible-galaxy) syntax_cmd=(uvx --from "$ANSIBLE_CORE_SPEC" ansible-playbook) else echo "check: need either the tools on PATH or uv installed (https://docs.astral.sh/uv/)" >&2 exit 1 fi step() { printf '\n== %s ==\n' "$*"; } # From the repository root, where .yamllint and its ignore list live. ansible-lint # runs yamllint too, but only over files it recognises as Ansible content; this # covers the workflows, the CI configuration and everything else. step "yamllint" "${yamllint_cmd[@]}" . # Every ansible command runs from ansible/: ansible.cfg is only discovered in the # current directory, and it is what puts roles/ on the roles path. From the # repository root you get "the role 'compose_stack' was not found". cd ansible # Install the collections before linting, or community.docker and ansible.posix # resolve to nothing and the fully-qualified-name checks pass vacuously. step "ansible-galaxy install" "${galaxy_cmd[@]}" install -r requirements.yml step "ansible-lint" "${ansible_lint_cmd[@]}" . step "syntax check" for playbook in playbooks/*.yml; do printf ' %s\n' "$playbook" "${syntax_cmd[@]}" --syntax-check "$playbook" >/dev/null done printf '\nAll checks passed.\n'