Archived
Check NixOS configurations / eval-hosts (pull_request) Successful in 10m20s
codex-maintenance.sh evaluated each affected host/package one at a time, even though those calls are independent. Added scripts/lib/nix-parallel.sh (run_nix_parallel) and wired it into the host-eval, package-eval, and dry-run-build loops. Concurrency defaults to core count capped by available memory (~1GB/job) rather than plain nproc: empirically, nproc-many concurrent full-flake evals OOM-killed each other on a 4GB/6-core box, while 3-4 ran clean and were still ~2x faster than serial. Override via NIX_PARALLEL_JOBS. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
322 lines
12 KiB
Bash
Executable File
322 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Validation entry point for CI and local/agent review.
|
|
#
|
|
# Default mode (what CI runs on every push/PR): fmt-check, statix, and eval
|
|
# are scoped to files that actually changed against a base ref, plus
|
|
# whichever hosts/packages those changes can affect. This exists because
|
|
# the unscoped sweep below is slow enough to time out CI runners -- see
|
|
# --full-check.
|
|
#
|
|
# --full-check: the historical full sweep (every host, every package,
|
|
# fmt --check ./statix check . over the whole tree). Slow -- minutes, not
|
|
# seconds. CI never passes this; run it locally before a release or after
|
|
# touching modules/common/*, flake.nix, or variables.nix if you want extra
|
|
# confidence beyond what the changed-files scope already covers for those
|
|
# paths (see below).
|
|
#
|
|
# --dry-run: adds `nix build --dry-run --no-link` for whatever scope is
|
|
# active (changed-files scope by default, full scope under --full-check).
|
|
#
|
|
# Per-host/per-package eval and dry-run build calls run concurrently (see
|
|
# scripts/lib/nix-parallel.sh) since they're independent of each other.
|
|
# Concurrency defaults to core count capped by available memory (~1GB/job)
|
|
# rather than plain core count, since each concurrent `nix eval` evaluates a
|
|
# whole NixOS system closure and can OOM a small/memory-constrained CI
|
|
# runner otherwise; override via NIX_PARALLEL_JOBS if a runner has more (or
|
|
# less) room than that estimate assumes.
|
|
set -euo pipefail
|
|
|
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
# shellcheck source=lib/nix-bootstrap.sh
|
|
source "${script_dir}/lib/nix-bootstrap.sh"
|
|
# shellcheck source=lib/nix-eval.sh
|
|
source "${script_dir}/lib/nix-eval.sh"
|
|
# shellcheck source=lib/nix-parallel.sh
|
|
source "${script_dir}/lib/nix-parallel.sh"
|
|
|
|
repo_root="$(cd "${script_dir}/.." && pwd)"
|
|
cd "$repo_root"
|
|
|
|
full_check=false
|
|
dry_run=false
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: scripts/codex-maintenance.sh [--full-check] [--dry-run]
|
|
|
|
--full-check Run the full sweep: fmt-check and statix over the whole
|
|
repo, eval every host and package. Slow. Never run by CI.
|
|
--dry-run Additionally run `nix build --dry-run --no-link` for
|
|
whatever scope is active.
|
|
|
|
With neither flag (the CI default), fmt-check/statix/eval are scoped to
|
|
files changed against a base ref (env MAINT_BASE_SHA, else the PR base,
|
|
else HEAD^), plus the hosts/packages those changes can affect.
|
|
EOF
|
|
}
|
|
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--full-check) full_check=true ;;
|
|
--dry-run) dry_run=true ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
*)
|
|
echo "Unknown argument: $arg" >&2
|
|
usage >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
ensure_nix_profile
|
|
|
|
if ! command -v nix >/dev/null 2>&1; then
|
|
echo "ERROR: nix is not available in PATH. Run bash scripts/codex-setup.sh first." >&2
|
|
exit 127
|
|
fi
|
|
|
|
echo "Checking for obvious committed secrets..."
|
|
if grep -RInE 'github_pat_|ghp_|access-tokens|hashedPassword[[:space:]]*=' \
|
|
--exclude-dir=.git \
|
|
--exclude=flake.lock \
|
|
.; then
|
|
echo
|
|
echo "WARNING: Potential secrets or password hashes found. Review before committing."
|
|
else
|
|
echo "No obvious token patterns found."
|
|
fi
|
|
|
|
mapfile -t all_hosts < <(list_flake_targets .)
|
|
mapfile -t all_packages < <(nix eval --json "${NIX_EVAL_FLAGS[@]}" .#packages.x86_64-linux --apply builtins.attrNames | jq -r '.[]')
|
|
|
|
# host_targets_for_dir <hosts-subdir-name>
|
|
# Prints the nixosConfigurations target names whose hostPath is
|
|
# ./hosts/<dir>/host.nix, derived straight from flake.nix's generatedTargets
|
|
# (one mkTarget { ... } call per line) rather than a hand-maintained table,
|
|
# so it can't drift the way a copied mapping would.
|
|
host_targets_for_dir() {
|
|
local dir="$1"
|
|
grep -oE '^[[:space:]]*[A-Za-z0-9_-]+ = mkTarget \{[^}]*hostPath = \./hosts/'"${dir}"'/host\.nix;[^}]*\};' flake.nix \
|
|
| sed -E 's/^[[:space:]]*([A-Za-z0-9_-]+) = mkTarget.*/\1/' \
|
|
|| true
|
|
}
|
|
|
|
declare -a changed_files=()
|
|
scope_desc="full repo"
|
|
|
|
if ! $full_check; then
|
|
resolve_base_ref() {
|
|
if [[ -n "${MAINT_BASE_SHA:-}" ]] && git cat-file -e "${MAINT_BASE_SHA}^{commit}" 2>/dev/null; then
|
|
echo "$MAINT_BASE_SHA"
|
|
return
|
|
fi
|
|
if git rev-parse --verify -q HEAD^ >/dev/null 2>&1; then
|
|
echo "HEAD^"
|
|
return
|
|
fi
|
|
git hash-object -t tree /dev/null
|
|
}
|
|
|
|
base_ref="$(resolve_base_ref)"
|
|
echo
|
|
echo "Changed-files scope: diffing against ${base_ref}"
|
|
mapfile -t changed_files < <(git diff --name-only --diff-filter=ACMR "$base_ref" -- . | sort -u)
|
|
|
|
if [[ ${#changed_files[@]} -eq 0 ]]; then
|
|
echo "No changed files detected."
|
|
else
|
|
printf ' %s\n' "${changed_files[@]}"
|
|
fi
|
|
scope_desc="changed files only (base: ${base_ref})"
|
|
fi
|
|
|
|
# Whole-tree fmt/lint always run under --full-check; otherwise scoped below.
|
|
declare -a changed_nix_files=()
|
|
for f in "${changed_files[@]:-}"; do
|
|
[[ "$f" == *.nix && -f "$f" ]] && changed_nix_files+=("$f")
|
|
done
|
|
|
|
echo
|
|
echo "Checking Nix formatting with nixpkgs-fmt..."
|
|
if $full_check; then
|
|
nix run "${NIX_EVAL_FLAGS[@]}" github:NixOS/nixpkgs/nixos-25.11#nixpkgs-fmt -- --check .
|
|
elif [[ ${#changed_nix_files[@]} -gt 0 ]]; then
|
|
nix run "${NIX_EVAL_FLAGS[@]}" github:NixOS/nixpkgs/nixos-25.11#nixpkgs-fmt -- --check "${changed_nix_files[@]}"
|
|
else
|
|
echo "No changed .nix files; skipping."
|
|
fi
|
|
|
|
echo
|
|
echo "Running statix lint..."
|
|
if $full_check; then
|
|
nix run "${NIX_EVAL_FLAGS[@]}" github:NixOS/nixpkgs/nixos-25.11#statix -- check .
|
|
elif [[ ${#changed_nix_files[@]} -gt 0 ]]; then
|
|
for f in "${changed_nix_files[@]}"; do
|
|
nix run "${NIX_EVAL_FLAGS[@]}" github:NixOS/nixpkgs/nixos-25.11#statix -- check "$f"
|
|
done
|
|
else
|
|
echo "No changed .nix files; skipping."
|
|
fi
|
|
|
|
# Figure out which hosts/packages this run needs to eval (and, under
|
|
# --dry-run, build). full_check always means "everything"; otherwise a
|
|
# change to flake.nix/flake.lock/variables.nix/modules/common/* (repo-wide
|
|
# inputs) or to any other modules/*.nix outside platforms//build-types
|
|
# (whose blast radius isn't safely inferable from the path alone -- see
|
|
# CLAUDE.md's "Grep modules/build-types/*.nix for each build type's imports
|
|
# list") also falls back to everything, on the same reasoning CLAUDE.md
|
|
# already gives interactive sessions for when to run the full sweep.
|
|
# Anything more targeted -- a host.nix, a platform module, a build-type
|
|
# module -- narrows to just the hosts it can affect.
|
|
declare -A affected_hosts=()
|
|
eval_packages=false
|
|
|
|
if $full_check; then
|
|
for h in "${all_hosts[@]}"; do affected_hosts[$h]=1; done
|
|
eval_packages=true
|
|
else
|
|
full_fallback=false
|
|
for f in "${changed_files[@]:-}"; do
|
|
case "$f" in
|
|
flake.nix|flake.lock|variables.nix|modules/common/*)
|
|
full_fallback=true
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if ! $full_fallback; then
|
|
for f in "${changed_files[@]:-}"; do
|
|
case "$f" in
|
|
hosts/*/*)
|
|
hostdir="${f#hosts/}"
|
|
hostdir="${hostdir%%/*}"
|
|
while IFS= read -r t; do
|
|
[[ -n "$t" ]] && affected_hosts[$t]=1
|
|
done < <(host_targets_for_dir "$hostdir")
|
|
;;
|
|
modules/platforms/*.nix)
|
|
platform="$(basename "$f" .nix)"
|
|
for h in "${all_hosts[@]}"; do
|
|
[[ "$h" == "${platform}-"* ]] && affected_hosts[$h]=1
|
|
done
|
|
;;
|
|
modules/build-types/*.nix)
|
|
buildtype="$(basename "$f" .nix)"
|
|
for h in "${all_hosts[@]}"; do
|
|
[[ "$h" == *"-${buildtype}" ]] && affected_hosts[$h]=1
|
|
done
|
|
;;
|
|
modules/installer/*)
|
|
# iso.nix (imported by both the "installer" nixosConfigurations
|
|
# target and netbootSystem, which backs packages.pxe) pulls in
|
|
# common.nix, so a common.nix change reaches all three.
|
|
affected_hosts[installer]=1
|
|
eval_packages=true
|
|
;;
|
|
modules/pxe-boot/*)
|
|
# stage-installer-artifacts.nix is imported by
|
|
# modules/build-types/pxe-boot.nix only -- same blast radius as a
|
|
# build-types/*.nix change, not a packages one.
|
|
for h in "${all_hosts[@]}"; do
|
|
[[ "$h" == *"-pxe-boot" ]] && affected_hosts[$h]=1
|
|
done
|
|
;;
|
|
modules/*)
|
|
full_fallback=true
|
|
;;
|
|
esac
|
|
done
|
|
fi
|
|
|
|
if $full_fallback; then
|
|
echo
|
|
echo "Changed files affect shared config; falling back to evaluating every host/package."
|
|
for h in "${all_hosts[@]}"; do affected_hosts[$h]=1; done
|
|
eval_packages=true
|
|
fi
|
|
fi
|
|
|
|
mapfile -t hosts < <(for h in "${!affected_hosts[@]}"; do echo "$h"; done | sort)
|
|
|
|
echo
|
|
echo "Checking nix-cache host key for drift..."
|
|
if bash "${script_dir}/secrets/sync-nix-cache-host-key.sh" --check; then
|
|
:
|
|
else
|
|
drift_status=$?
|
|
if [[ "$drift_status" -eq 2 ]]; then
|
|
echo "nix-cache unreachable from here -- skipping host-key drift check."
|
|
else
|
|
echo "WARNING: nix-cache's host key has drifted from variables.nix (see above)." >&2
|
|
echo " Run 'bash scripts/secrets/sync-nix-cache-host-key.sh' to fix." >&2
|
|
fi
|
|
fi
|
|
|
|
echo
|
|
if [[ ${#hosts[@]} -eq 0 ]]; then
|
|
echo "No hosts affected by changed files; skipping host eval."
|
|
else
|
|
echo "Evaluating host toplevel derivations (${scope_desc}, up to ${NIX_PARALLEL_JOBS} at a time)..."
|
|
# lxc-* hosts deploy via a directly pct-restore-able tarball instead of
|
|
# nixos-install (see docs/auto-installer.md); proxmox-* hosts can
|
|
# alternatively be built as a standalone disk image (see
|
|
# docs/proxmox-images.md). Both are otherwise-unvalidated buildable
|
|
# surface, easy to silently break without this.
|
|
declare -a host_eval_jobs=()
|
|
for host in "${hosts[@]}"; do
|
|
host_eval_jobs+=("${host}${NIX_PARALLEL_SEP}.#nixosConfigurations.${host}.config.system.build.toplevel.drvPath")
|
|
case "$host" in
|
|
lxc-*)
|
|
host_eval_jobs+=("${host} (tarball)${NIX_PARALLEL_SEP}.#nixosConfigurations.${host}.config.system.build.tarball.drvPath")
|
|
;;
|
|
proxmox-*)
|
|
host_eval_jobs+=("${host} (diskoImagesScript)${NIX_PARALLEL_SEP}.#nixosConfigurations.${host}.config.system.build.diskoImagesScript.drvPath")
|
|
;;
|
|
esac
|
|
done
|
|
run_nix_parallel host_eval_jobs eval --raw "${NIX_EVAL_FLAGS[@]}"
|
|
fi
|
|
|
|
echo
|
|
if ! $eval_packages; then
|
|
echo "No packages affected by changed files; skipping package eval."
|
|
else
|
|
echo "Evaluating buildable packages (up to ${NIX_PARALLEL_JOBS} at a time)..."
|
|
declare -a package_eval_jobs=()
|
|
for pkg in "${all_packages[@]}"; do
|
|
package_eval_jobs+=("packages.x86_64-linux.${pkg}${NIX_PARALLEL_SEP}.#packages.x86_64-linux.${pkg}")
|
|
done
|
|
run_nix_parallel package_eval_jobs eval --raw "${NIX_EVAL_FLAGS[@]}"
|
|
fi
|
|
|
|
if $dry_run; then
|
|
echo
|
|
echo "Running dry-run builds for the active scope (up to ${NIX_PARALLEL_JOBS} at a time). This will not create result symlinks."
|
|
declare -a host_build_jobs=()
|
|
for host in "${hosts[@]:-}"; do
|
|
host_build_jobs+=("Dry-run build: ${host}${NIX_PARALLEL_SEP}.#nixosConfigurations.${host}.config.system.build.toplevel")
|
|
case "$host" in
|
|
lxc-*)
|
|
host_build_jobs+=("Dry-run build: ${host} (tarball)${NIX_PARALLEL_SEP}.#nixosConfigurations.${host}.config.system.build.tarball")
|
|
;;
|
|
proxmox-*)
|
|
host_build_jobs+=("Dry-run build: ${host} (diskoImagesScript)${NIX_PARALLEL_SEP}.#nixosConfigurations.${host}.config.system.build.diskoImagesScript")
|
|
;;
|
|
esac
|
|
done
|
|
run_nix_parallel host_build_jobs build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}"
|
|
|
|
if $eval_packages; then
|
|
echo
|
|
echo "Running dry-run builds for packages."
|
|
declare -a package_build_jobs=()
|
|
for pkg in "${all_packages[@]}"; do
|
|
package_build_jobs+=("Dry-run build: packages.x86_64-linux.${pkg}${NIX_PARALLEL_SEP}.#packages.x86_64-linux.${pkg}")
|
|
done
|
|
run_nix_parallel package_build_jobs build --dry-run --no-link "${NIX_EVAL_FLAGS[@]}"
|
|
fi
|
|
fi
|
|
|
|
echo
|
|
echo "Maintenance checks complete."
|