Files
computer-playbook/scripts/lint/autoformat.sh
T
kevinveenbirkenbachandClaude Opus 5.5 d92375922d feat(roles): pull custom roles from inventory-declared sources
An inventory group web-app-cst-<name> with applications.<id>.custom: true and
a pinned source {url, version, path} is pulled into roles/ before the play,
read raw (Jinja, Ansible tags and credential URLs refused), placed through a
staging directory under an exclusive lock, and owned by a marker that names
its own directory, so a pull or roles-clean never replaces or removes a
directory it did not place and a failed pull leaves roles/ as it was.
Directory inventories, host lists and non-UTF-8 files behave as Ansible
reads them; generated test deploys pass --skip-pull. make roles-pull,
roles-clean and roles list, INFINITO_EXTRA_MOUNTS for local sources, the
web-app-cst category, the reserved prefix lint and a central
repository_files() that also yields the gitignored custom roles let every
lint scan them like in-tree roles. Supersedes the uncommitted 045 External
Roles draft and reuses its fetch, sync and mount design.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-29 00:02:58 +01:00

267 lines
7.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# shellcheck shell=bash
# Auto-format all source files using available tools.
# Missing tools are reported and skipped rather than aborting the run.
#
# Per-tool workers run concurrently by default. Set INFINITO_WORKER_ENABLED=0
# (also accepts `false`/`no`/`off`) to fall back to sequential execution.
# Tools that touch the same files are kept on the same worker (the
# shfmt+shellcheck pair both writes to scripts/*.sh, so they share a
# worker) regardless of the parallelism setting.
set -euo pipefail
# Default lives in default.env (SPOT); load.sh exports it.
PARALLEL="${INFINITO_WORKER_ENABLED}"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)"
cd "${REPO_ROOT}"
RUFF_CACHE_DIR="build/ruff-cache-$(id -u)"
export RUFF_CACHE_DIR
# shellcheck source=scripts/lint/diff/files.sh
source "${SCRIPT_DIR}/diff/files.sh"
# shellcheck source=scripts/lint/ansible/stamp.sh
source "${SCRIPT_DIR}/ansible/stamp.sh"
# Exception: no trap-delete on EXIT for status_dir -- that would race the
# background workers if the parent shell is interrupted mid-run (the trap
# fires, the dir disappears, an in-flight worker then tries to write its
# status file and crashes with "No such file or directory"). Cleanup happens
# explicitly after `wait` returns, see end of script.
status_dir="$(mktemp -d)"
# $1 = tool name, $2 = OK|SKIP
write_status() {
printf '%s %s\n' "$2" "$1" >"${status_dir}/$1"
}
# ── individual workers ───────────────────────────────────────────────────────
run_ruff() {
if ! command -v ruff &>/dev/null; then
write_status ruff SKIP
return 0
fi
local rc=0
local -a custom_roles=()
mapfile -t custom_roles < <(lint_custom_roles)
ruff format . "${custom_roles[@]}" || rc=$?
[[ $rc -eq 0 ]] && { ruff check . --fix "${custom_roles[@]}" || rc=$?; }
if [[ $rc -eq 0 ]]; then
write_status ruff OK
else
write_status ruff FAIL
fi
}
run_claude() {
if ! command -v python3 &>/dev/null; then
write_status claude SKIP
return 0
fi
if bash "${SCRIPT_DIR}/claude.sh"; then
write_status claude OK
else
write_status claude FAIL
fi
}
# shfmt + shellcheck both write to scripts/*.sh and therefore share a
# worker. They run sequentially within this function so their edits do
# not race.
run_shell_pair() {
if ! command -v shfmt &>/dev/null; then
write_status shfmt SKIP
elif shfmt -w scripts; then
write_status shfmt OK
else
write_status shfmt FAIL
fi
if command -v shellcheck &>/dev/null; then
if lint_diff_active shellcheck; then
mapfile -t shellcheck_files < <(lint_diff_files '\.sh$')
else
mapfile -t shellcheck_files < <(find . -type f -name '*.sh' 2>/dev/null | sort)
fi
if [[ "${#shellcheck_files[@]}" -gt 0 ]]; then
local diff_file rc=0
diff_file="$(mktemp)"
set +e
shellcheck -f diff -x "${shellcheck_files[@]}" >"${diff_file}" 2>/dev/null
set -e
if [[ -s "${diff_file}" ]]; then
printf 'Applying ShellCheck autofixes.\n'
git apply --whitespace=nowarn "${diff_file}" || rc=$?
fi
rm -f "${diff_file}"
if [[ $rc -eq 0 ]]; then
write_status shellcheck OK
else
write_status shellcheck FAIL
fi
else
write_status shellcheck OK
fi
else
write_status shellcheck SKIP
fi
}
run_markdownlint() {
if ! command -v markdownlint-cli2 &>/dev/null; then
write_status markdownlint-cli2 SKIP
return 0
fi
local rc=0
local -a args=()
if lint_diff_active markdown; then
mapfile -t args < <(lint_diff_files '\.md$' | sed 's/^/:/')
if [[ "${#args[@]}" -eq 0 ]]; then
write_status markdownlint-cli2 OK
return 0
fi
args=(--no-globs "${args[@]}")
fi
markdownlint-cli2 --fix "${args[@]}" >/dev/null 2>&1 || rc=$?
if [[ $rc -le 1 ]]; then
write_status markdownlint-cli2 OK
else
write_status markdownlint-cli2 FAIL
fi
}
run_ansible_lint() {
if ! command -v ansible-lint &>/dev/null; then
write_status ansible-lint SKIP
return 0
fi
local rc=0 out yaml_before
local -a targets=() custom_roles=()
if lint_diff_active ansible; then
mapfile -t targets < <(lint_diff_files '\.ya?ml$' | ansible_lint_included)
if [[ "${#targets[@]}" -eq 0 ]]; then
write_status ansible-lint OK
return 0
fi
else
mapfile -t custom_roles < <(lint_custom_roles)
fi
out="$(mktemp)"
yaml_before="$(ansible_lint_fingerprint yaml)"
ansible-lint --fix "${targets[@]}" >"${out}" 2>&1 || rc=$?
if [[ "${#custom_roles[@]}" -gt 0 && ($rc -eq 0 || $rc -eq 2 || $rc -eq 8) ]]; then
ansible-lint --fix "${custom_roles[@]}" >>"${out}" 2>&1 || rc=$?
fi
if [[ $rc -eq 0 && "${#targets[@]}" -eq 0 && "$(ansible_lint_fingerprint yaml)" == "${yaml_before}" ]]; then
touch "${status_dir}/.ansible-lint-clean"
fi
if [[ $rc -eq 0 || $rc -eq 2 || $rc -eq 8 ]]; then
write_status ansible-lint OK
else
printf 'ansible-lint exited %s:\n' "${rc}" >&2
cat "${out}" >&2
write_status ansible-lint FAIL
fi
rm -f "${out}"
}
run_mbake() {
if ! command -v mbake &>/dev/null; then
write_status mbake SKIP
return 0
fi
local rc=0
mbake format Makefile >/dev/null 2>&1 || rc=$?
[[ $rc -eq 0 ]] && { mbake format Makefile >/dev/null 2>&1 || rc=$?; }
if [[ $rc -eq 0 ]]; then
write_status mbake OK
else
write_status mbake FAIL
fi
}
run_eslint() {
if [[ ! -d "node_modules/eslint" ]]; then
write_status eslint SKIP
return 0
fi
local rc=0
local -a targets=('roles/**/files/**/*.js')
if lint_diff_active javascript; then
mapfile -t targets < <(lint_diff_files '^roles/[^/]+/files/.*\.js$')
if [[ "${#targets[@]}" -eq 0 ]]; then
write_status eslint OK
return 0
fi
fi
npx --no-install eslint --fix --no-warn-ignored "${targets[@]}" >/dev/null 2>&1 || rc=$?
if [[ $rc -le 1 ]]; then
write_status eslint OK
else
write_status eslint FAIL
fi
}
# ── dispatch ─────────────────────────────────────────────────────────────────
is_truthy() {
case "${1:-}" in
1 | true | TRUE | True | yes | YES | Yes | on | ON | On) return 0 ;;
*) return 1 ;;
esac
}
workers=(run_ruff run_claude run_shell_pair run_markdownlint run_ansible_lint run_mbake run_eslint)
if is_truthy "${PARALLEL}"; then
for w in "${workers[@]}"; do "${w}" & done
wait || true # nocheck: shell-or-true -- grandfathered: worked in practice; TODO: sharpen to catch only the exact tolerated error
else
for w in "${workers[@]}"; do "${w}" || true; done # nocheck: shell-or-true -- grandfathered: worked in practice; TODO: sharpen to catch only the exact tolerated error
fi
if [[ -f "${status_dir}/.ansible-lint-clean" ]]; then
ansible_lint_stamp_record "$(ansible_lint_fingerprint all)"
fi
ran=()
failed=()
skipped=()
for f in "${status_dir}"/*; do
[[ -f "${f}" ]] || continue
line="$(<"${f}")"
case "${line}" in
OK\ *) ran+=("${line#OK }") ;;
FAIL\ *) failed+=("${line#FAIL }") ;;
SKIP\ *) skipped+=("${line#SKIP }") ;;
esac
done
if [[ "${#ran[@]}" -gt 0 ]]; then
printf 'autoformat: ran %s\n' "$(
IFS=', '
echo "${ran[*]}"
)"
fi
if [[ "${#skipped[@]}" -gt 0 ]]; then
printf 'autoformat: skipped (not installed) %s\n' "$(
IFS=', '
echo "${skipped[*]}"
)"
fi
if [[ "${#failed[@]}" -gt 0 ]]; then
printf 'autoformat: FAILED %s\n' "$(
IFS=', '
echo "${failed[*]}"
)" >&2
fi
rm -rf "${status_dir}"
[[ "${#failed[@]}" -eq 0 ]] || exit 1