Files
computer-playbook/scripts/tests/deploy/ci/all.sh
T
kevinveenbirkenbachandClaude Opus 4.7 9a578952c3 feat(env): SPOT env-pipeline + INFINITO_* namespace
Replace 8 bash env modules with a single Python generator
(cli.meta.env) that materialises env/static.env + runtime context
into a gitignored .env. Each variable lives in its own handler
under utils/env/handlers/; defaults carry inline one-line `# ...`
comments.

Rename to the INFINITO_* schema:
- workflow inputs: APPS, SERVICES_DISABLED, CMD, DEBUG, WHITELIST,
  BUNDLES, FULL_CYCLE, VARIANT, RUN_FLAGS, LIMIT_HOST, PARALLEL
- inventory: INVENTORY_DIR / INVENTORY_FILE / HOST_VARS_FILE,
  PW_FILE -> INVENTORY_PASSWORD_FILE
- venv: VENV_BASE / VENV_FALLBACK / VENV_NAME
- new SPOT defaults: COMPILE_SILENCE, CONTAINER_NO_CA,
  WAIT_HEALTH_TIMEOUT_S, PACKAGE_CACHE_FRONTEND_INIT_IMAGE,
  promoted IMAGE_TAG from GHA-only to general passthrough

compose.yml hardening:
- inline \${VAR:-default} removed; strict \${VAR:?...} everywhere
- new integration lint blocks regressions

Tooling:
- scripts/install/all.sh owns the stamp-file install logic
  (Makefile target delegates)
- scripts/meta/env/load.sh is the single shell entry point;
  8 bash modules deleted
- new shell lint: no INFINITO_* defaults outside env/static.env

Tests:
- 93 new unit tests for utils/env/{builder,parser,runtime,writer}
- restructure tests/lint into repository/ (file-content rules) vs
  filesystem/{shell,python,env}/ (filesystem-shape rules)
- drift + smoke integration tests against the generator

Docs:
- variables.md is now a pointer; per-variable docs live next to
  the key in env/static.env / env/ci.env
- compose.yml.md drops the Default column

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 20:40:34 +02:00

187 lines
5.5 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# SPOT: Deploy exactly ONE app across all distros (serial).
#
# Additionally:
# - Track duration per distro
# - Enforce a global time budget for the whole run (env: MAX_TOTAL_SECONDS)
# - Skip a distro if remaining time is smaller than the max duration of any previous distro run
#
# Required env:
# INFINITO_APPS="web-app-keycloak"
# INFINITO_TEST_DEPLOY_TYPE="server|workstation|universal"
# INFINITO_DISTROS="arch debian ubuntu fedora centos"
# INFINITO_INVENTORY_DIR="/path/to/inventory"
#
# Optional env:
# PYTHON="python3"
# MAX_TOTAL_SECONDS="5400" # override the script default of 19800 seconds
#
# Script-local defaults preserved from the old Make wrapper:
# MISSING_ONLY=true
# MAX_TOTAL_SECONDS=19800
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../../../.." && pwd)"
cd "${REPO_ROOT}"
if [[ -f "scripts/meta/env/load.sh" ]]; then
# shellcheck source=scripts/meta/env/load.sh
source "scripts/meta/env/load.sh"
else
echo "[ERROR] Missing env file: scripts/meta/env/load.sh" >&2
exit 2
fi
: "${INFINITO_APPS:?INFINITO_APPS is required (e.g. INFINITO_APPS=web-app-keycloak)}"
: "${INFINITO_TEST_DEPLOY_TYPE:?INFINITO_TEST_DEPLOY_TYPE is required (server|workstation|universal)}"
: "${INFINITO_DISTROS:?INFINITO_DISTROS is required (e.g. 'arch debian ubuntu fedora centos')}"
: "${INFINITO_INVENTORY_DIR:?INFINITO_INVENTORY_DIR is required}"
case "${INFINITO_TEST_DEPLOY_TYPE}" in
server | workstation | universal) ;;
*)
echo "[ERROR] Invalid INFINITO_TEST_DEPLOY_TYPE: ${INFINITO_TEST_DEPLOY_TYPE}" >&2
exit 2
;;
esac
: "${PYTHON:=python3}"
: "${MISSING_ONLY:=true}"
if [[ -z "${MAX_TOTAL_SECONDS+x}" ]]; then
MAX_TOTAL_SECONDS=19800
fi
export INFINITO_INVENTORY_DIR MISSING_ONLY MAX_TOTAL_SECONDS
if [[ -n "${MAX_TOTAL_SECONDS}" ]]; then
if ! [[ "${MAX_TOTAL_SECONDS}" =~ ^[0-9]+$ ]]; then
echo "[ERROR] MAX_TOTAL_SECONDS must be an integer (seconds), got: '${MAX_TOTAL_SECONDS}'" >&2
exit 2
fi
fi
read -r -a distro_arr <<<"${INFINITO_DISTROS}"
mapfile -t distro_arr < <(printf '%s\n' "${distro_arr[@]}" | shuf)
echo "=== Distro execution order: ${distro_arr[*]} ==="
global_start="$(date +%s)"
deadline=""
if [[ -n "${MAX_TOTAL_SECONDS}" ]]; then
deadline="$((global_start + MAX_TOTAL_SECONDS))"
echo "=== Global time budget enabled: ${MAX_TOTAL_SECONDS}s (deadline epoch=${deadline}) ==="
else
echo "=== Global time budget disabled (MAX_TOTAL_SECONDS was set empty) ==="
fi
max_seen=0
skipped=0
ran=0
failed=0
durations=() # store "distro=seconds" lines
sync_ci_image_for_run() {
local owner tag repo_name
if [[ -n "${OWNER:-}" || -n "${GITHUB_REPOSITORY_OWNER:-}" || -n "${GITHUB_REPOSITORY:-}" ]]; then
owner="$(OWNER="${OWNER:-}" GITHUB_REPOSITORY_OWNER="${GITHUB_REPOSITORY_OWNER:-}" GITHUB_REPOSITORY="${GITHUB_REPOSITORY:-}" scripts/meta/resolve/repository/owner.sh)"
else
owner=""
fi
tag="${INFINITO_IMAGE_TAG}"
repo_name="${INFINITO_IMAGE_REPOSITORY:-}"
# Keep local/dev workflows untouched; only adjust image when CI owner context exists.
if [[ -z "${owner}" ]]; then
return 0
fi
if [[ -z "${repo_name}" ]]; then
repo_name="$(scripts/meta/resolve/repository/name.sh)"
fi
export INFINITO_IMAGE="ghcr.io/${owner}/${repo_name}/${INFINITO_DISTRO}:${tag}"
echo ">>> CI image synced: ${INFINITO_IMAGE}"
}
echo ">>> Installing CI dependencies"
"${PYTHON}" -m pip install ruamel.yaml PyYAML
for distro in "${distro_arr[@]}"; do
now="$(date +%s)"
remaining=""
if [[ -n "${deadline}" ]]; then
remaining="$((deadline - now))"
if ((remaining <= 0)); then
echo "[WARN] Global budget exhausted (remaining=${remaining}s). Stopping further distro runs."
break
fi
# Skip logic: only if we already have a max_seen from a prior run
if ((max_seen > 0 && remaining < max_seen)); then
echo "[WARN] Skipping distro=${distro}: remaining=${remaining}s < max_seen=${max_seen}s (fast-fail heuristic)"
skipped=$((skipped + 1))
continue
fi
fi
echo "=== Running dedicated distro deploy: distro=${distro} app=${INFINITO_APPS} type=${INFINITO_TEST_DEPLOY_TYPE} ==="
if [[ -n "${remaining}" ]]; then
echo ">>> Time budget: remaining=${remaining}s max_seen=${max_seen}s"
fi
export INFINITO_DISTRO="${distro}"
# Re-source defaults.sh so its always-derived INFINITO_CONTAINER block
# (outside the load-once guard) refreshes from the current INFINITO_DISTRO.
# This is the single spot that owns the derivation; consumers below
# (dedicated.sh, python deploy → entity.sh) just inherit the env.
source "scripts/meta/env/load.sh"
sync_ci_image_for_run
distro_start="$(date +%s)"
set +e
scripts/tests/deploy/ci/dedicated.sh \
--apps "${INFINITO_APPS}"
rc=$?
set -e
distro_end="$(date +%s)"
dur="$((distro_end - distro_start))"
durations+=("${distro}=${dur}s")
ran=$((ran + 1))
if ((dur > max_seen)); then
max_seen="$dur"
fi
echo ">>> Duration: distro=${distro} took ${dur}s (max_seen=${max_seen}s)"
if [[ $rc -ne 0 ]]; then
echo "[ERROR] Deploy failed for distro=${distro} app=${INFINITO_APPS} (rc=${rc})" >&2
failed=$((failed + 1))
exit "$rc"
fi
done
global_end="$(date +%s)"
total="$((global_end - global_start))"
echo
echo "=== Summary ==="
echo "app=${INFINITO_APPS} type=${INFINITO_TEST_DEPLOY_TYPE}"
echo "ran=${ran} skipped=${skipped} failed=${failed}"
echo "total_runtime=${total}s max_seen_duration=${max_seen}s"
if [[ -n "${deadline}" ]]; then
now="$(date +%s)"
remaining="$((deadline - now))"
echo "budget=${MAX_TOTAL_SECONDS}s remaining=${remaining}s"
fi
echo "per-distro:"
for line in "${durations[@]}"; do
echo " - ${line}"
done