Skip to content

Commit c5890f3

Browse files
committed
Add runtime validation packs
1 parent e1de26c commit c5890f3

10 files changed

Lines changed: 324 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ Collect runtime evidence from a real SSH lab host:
190190
```bash
191191
AUTOMAX_SSH_HOST=web01.example.com \
192192
AUTOMAX_SSH_USER=deploy \
193-
scripts/runtime-evidence.sh
193+
scripts/runtime-evidence.sh and scripts/runtime-validation-packs.sh
194194
```
195195

196196
Task-level and step-level restart points are also accepted:
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<!--
2+
Copyright (C) 2026 Marco Fortina
3+
SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
# Runtime validation packs
7+
8+
Runtime validation packs group release checks, package smoke, per-plugin check
9+
runbooks and SSH evidence collection into named validation profiles.
10+
11+
List available packs:
12+
13+
```bash
14+
scripts/runtime-validation-packs.sh --list
15+
```
16+
17+
Run the package-only pack:
18+
19+
```bash
20+
scripts/runtime-validation-packs.sh --pack package-only
21+
```
22+
23+
A run writes logs, a manifest, a summary and checksums under
24+
`dist/runtime-packs/<pack>-<timestamp>/`.
25+
26+
## Included packs
27+
28+
`package-only` runs release metadata checks and package smoke. It is suitable for
29+
local release rehearsals where no disposable runtime host is available.
30+
31+
`debian-like` and `redhat-like` are disposable VM profiles. They run release
32+
metadata, package smoke, per-plugin check runbooks and SSH evidence collection.
33+
Provide the normal SSH smoke variables before running them:
34+
35+
```bash
36+
AUTOMAX_SSH_HOST=192.0.2.10
37+
AUTOMAX_SSH_USER=deploy
38+
AUTOMAX_SSH_KEY_FILE=~/.ssh/id_ed25519
39+
AUTOMAX_SSH_KNOWN_HOSTS=~/.ssh/known_hosts
40+
```
41+
42+
`privileged-vm` is intended for disposable hosts where destructive or privileged
43+
checks are allowed by the operator. Keep it isolated from long-lived systems.
44+
45+
## Plugin check requirements
46+
47+
The `plugin-checks` step uses `examples/runbooks/scripts/run-all-checks.sh` and
48+
therefore requires a sudo password environment variable:
49+
50+
```bash
51+
export AUTOMAX_SUDO_PASSWORD='...'
52+
```
53+
54+
Use `AUTOMAX_SUDO_PASSWORD_ENV` when the password lives under a different
55+
environment variable name.
56+
57+
## Evidence output
58+
59+
Each pack creates:
60+
61+
```text
62+
summary.txt
63+
manifest.json
64+
SHA256SUMS
65+
logs/<step>.log
66+
```
67+
68+
Steps that require unavailable runtime inputs return `skipped` with `rc=77` in
69+
the manifest. Failed steps stop the pack by default. Use `--keep-going` to collect
70+
all step outcomes before returning a failing summary.

docs/reference/release.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ Keep the following with the release record:
7575
- `scripts/release-check.sh` output;
7676
- runtime evidence from disposable hosts when the release changes runtime
7777
behavior;
78+
- runtime validation pack output from `scripts/runtime-validation-packs.sh`;
7879
- `automax plugins coverage --strict` output for builtin and release-loaded
7980
plugins;
8081
- package smoke output for the built sdist and wheel.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Copyright (C) 2026 Marco Fortina
2+
# SPDX-License-Identifier: AGPL-3.0-or-later
3+
4+
AUTOMAX_RUNTIME_PACK_DESCRIPTION="Debian-like disposable VM validation pack"
5+
AUTOMAX_RUNTIME_PACK_STEPS="release-metadata package-smoke plugin-checks ssh-evidence"
6+
AUTOMAX_RUNTIME_OS_FAMILY="debian-like"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Copyright (C) 2026 Marco Fortina
2+
# SPDX-License-Identifier: AGPL-3.0-or-later
3+
4+
AUTOMAX_RUNTIME_PACK_DESCRIPTION="Release metadata and package smoke validation"
5+
AUTOMAX_RUNTIME_PACK_STEPS="release-metadata package-smoke"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Copyright (C) 2026 Marco Fortina
2+
# SPDX-License-Identifier: AGPL-3.0-or-later
3+
4+
AUTOMAX_RUNTIME_PACK_DESCRIPTION="Privileged disposable VM validation pack"
5+
AUTOMAX_RUNTIME_PACK_STEPS="release-metadata package-smoke plugin-checks ssh-evidence"
6+
AUTOMAX_RUNTIME_PRIVILEGED="1"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Copyright (C) 2026 Marco Fortina
2+
# SPDX-License-Identifier: AGPL-3.0-or-later
3+
4+
AUTOMAX_RUNTIME_PACK_DESCRIPTION="Red Hat-like disposable VM validation pack"
5+
AUTOMAX_RUNTIME_PACK_STEPS="release-metadata package-smoke plugin-checks ssh-evidence"
6+
AUTOMAX_RUNTIME_OS_FAMILY="redhat-like"

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ nav:
6969
- Creating Plugins: guides/creating-plugins.md
7070
- Extended SSH Smoke: guides/ssh-smoke.md
7171
- Runtime Evidence Suite: guides/runtime-evidence.md
72+
- Runtime Validation Packs: guides/runtime-validation-packs.md
7273
- Publishing Documentation: guides/publishing-docs.md
7374
- Reference:
7475
- CLI: reference/cli.md
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
#!/usr/bin/env bash
2+
# Copyright (C) 2026 Marco Fortina
3+
# SPDX-License-Identifier: AGPL-3.0-or-later
4+
5+
set -euo pipefail
6+
7+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
8+
PACK_DIR="${AUTOMAX_RUNTIME_PACK_DIR:-${ROOT}/examples/runtime-packs}"
9+
EVIDENCE_ROOT="${AUTOMAX_RUNTIME_PACK_EVIDENCE_DIR:-${ROOT}/dist/runtime-packs}"
10+
PACK_NAME="${AUTOMAX_RUNTIME_PACK:-package-only}"
11+
KEEP_GOING=0
12+
LIST_ONLY=0
13+
SUDO_PASSWORD_ENV="${AUTOMAX_SUDO_PASSWORD_ENV:-AUTOMAX_SUDO_PASSWORD}"
14+
RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)"
15+
16+
usage() {
17+
cat <<'USAGE'
18+
Usage: scripts/runtime-validation-packs.sh [--pack NAME] [--evidence-dir DIR] [--keep-going] [--list]
19+
20+
Run a named runtime validation pack and collect step logs, a manifest, a summary
21+
and checksums under dist/runtime-packs by default.
22+
23+
Options:
24+
--pack NAME Runtime pack name, defaults to package-only.
25+
--evidence-dir DIR Output directory root, defaults to dist/runtime-packs.
26+
--keep-going Continue after failed steps and return a failing summary.
27+
--list List available packs and exit.
28+
USAGE
29+
}
30+
31+
while [[ $# -gt 0 ]]; do
32+
case "$1" in
33+
--pack)
34+
PACK_NAME="${2:-}"
35+
[[ -n "$PACK_NAME" ]] || { echo "--pack requires a name" >&2; exit 2; }
36+
shift 2
37+
;;
38+
--evidence-dir)
39+
EVIDENCE_ROOT="${2:-}"
40+
[[ -n "$EVIDENCE_ROOT" ]] || { echo "--evidence-dir requires a path" >&2; exit 2; }
41+
shift 2
42+
;;
43+
--keep-going)
44+
KEEP_GOING=1
45+
shift
46+
;;
47+
--list)
48+
LIST_ONLY=1
49+
shift
50+
;;
51+
-h|--help)
52+
usage
53+
exit 0
54+
;;
55+
*)
56+
echo "Unknown option: $1" >&2
57+
usage >&2
58+
exit 2
59+
;;
60+
esac
61+
done
62+
63+
list_packs() {
64+
find "$PACK_DIR" -maxdepth 1 -type f -name '*.env' -printf '%f\n' \
65+
| sed 's/[.]env$//' \
66+
| sort
67+
}
68+
69+
if [[ "$LIST_ONLY" -eq 1 ]]; then
70+
list_packs
71+
exit 0
72+
fi
73+
74+
PACK_FILE="${PACK_DIR}/${PACK_NAME}.env"
75+
if [[ ! -f "$PACK_FILE" ]]; then
76+
echo "Runtime pack not found: $PACK_FILE" >&2
77+
echo "Available packs:" >&2
78+
list_packs >&2
79+
exit 2
80+
fi
81+
82+
# shellcheck source=/dev/null
83+
source "$PACK_FILE"
84+
85+
PACK_STEPS="${AUTOMAX_RUNTIME_PACK_STEPS:-}"
86+
if [[ -z "$PACK_STEPS" ]]; then
87+
echo "Runtime pack $PACK_NAME does not define AUTOMAX_RUNTIME_PACK_STEPS" >&2
88+
exit 2
89+
fi
90+
91+
EVIDENCE_DIR="${EVIDENCE_ROOT}/${PACK_NAME}-${RUN_ID}"
92+
LOG_DIR="${EVIDENCE_DIR}/logs"
93+
mkdir -p "$LOG_DIR"
94+
95+
SUMMARY="${EVIDENCE_DIR}/summary.txt"
96+
MANIFEST="${EVIDENCE_DIR}/manifest.json"
97+
CHECKSUMS="${EVIDENCE_DIR}/SHA256SUMS"
98+
99+
status_names=()
100+
status_states=()
101+
status_codes=()
102+
103+
run_step() {
104+
local step="$1"
105+
local log="${LOG_DIR}/${step}.log"
106+
local rc=0
107+
echo "== $step ==" | tee "$log"
108+
case "$step" in
109+
release-metadata)
110+
(cd "$ROOT" && AUTOMAX_RELEASE_CHECK_SKIP_DOCS=1 AUTOMAX_RELEASE_CHECK_SKIP_PACKAGE=1 scripts/release-check.sh) >>"$log" 2>&1 || rc=$?
111+
;;
112+
package-smoke)
113+
(cd "$ROOT" && scripts/package-smoke.sh) >>"$log" 2>&1 || rc=$?
114+
;;
115+
plugin-checks)
116+
if [[ -z "${!SUDO_PASSWORD_ENV:-}" ]]; then
117+
echo "SKIP: set ${SUDO_PASSWORD_ENV} for plugin-checks" >>"$log"
118+
rc=77
119+
else
120+
(cd "$ROOT" && examples/runbooks/scripts/run-all-checks.sh --keep-going) >>"$log" 2>&1 || rc=$?
121+
fi
122+
;;
123+
ssh-evidence)
124+
(cd "$ROOT" && scripts/runtime-evidence.sh) >>"$log" 2>&1 || rc=$?
125+
;;
126+
*)
127+
echo "Unknown runtime validation step: $step" >>"$log"
128+
rc=2
129+
;;
130+
esac
131+
local state="passed"
132+
if [[ "$rc" -eq 77 ]]; then
133+
state="skipped"
134+
elif [[ "$rc" -ne 0 ]]; then
135+
state="failed"
136+
fi
137+
status_names+=("$step")
138+
status_states+=("$state")
139+
status_codes+=("$rc")
140+
echo "$state rc=$rc log=$log"
141+
if [[ "$state" == "failed" && "$KEEP_GOING" -ne 1 ]]; then
142+
return "$rc"
143+
fi
144+
return 0
145+
}
146+
147+
final_rc=0
148+
for step in $PACK_STEPS; do
149+
if ! run_step "$step"; then
150+
final_rc=$?
151+
break
152+
fi
153+
last_index=$((${#status_states[@]} - 1))
154+
if [[ "${status_states[$last_index]}" == "failed" ]]; then
155+
final_rc="${status_codes[$last_index]}"
156+
fi
157+
done
158+
159+
python - "$PACK_NAME" "$RUN_ID" "$EVIDENCE_DIR" "${status_names[*]}" "${status_states[*]}" "${status_codes[*]}" >"$MANIFEST" <<'PY'
160+
import json
161+
import sys
162+
163+
pack, run_id, evidence_dir, names, states, codes = sys.argv[1:7]
164+
steps = []
165+
for name, state, code in zip(names.split(), states.split(), codes.split()):
166+
steps.append({"name": name, "status": state, "rc": int(code), "log": f"logs/{name}.log"})
167+
print(json.dumps({"pack": pack, "run_id": run_id, "evidence_dir": evidence_dir, "steps": steps}, indent=2, sort_keys=True))
168+
PY
169+
170+
{
171+
echo "Automax runtime validation pack"
172+
echo "pack=${PACK_NAME}"
173+
echo "run_id=${RUN_ID}"
174+
echo "evidence_dir=${EVIDENCE_DIR}"
175+
echo
176+
for i in "${!status_names[@]}"; do
177+
printf '%s: %s rc=%s\n' "${status_names[$i]}" "${status_states[$i]}" "${status_codes[$i]}"
178+
done
179+
} >"$SUMMARY"
180+
181+
(
182+
cd "$EVIDENCE_DIR"
183+
find . -type f ! -name SHA256SUMS -print0 | sort -z | xargs -0 sha256sum >"$CHECKSUMS"
184+
)
185+
186+
cat "$SUMMARY"
187+
echo "manifest=${MANIFEST}"
188+
echo "checksums=${CHECKSUMS}"
189+
exit "$final_rc"

tests/test_documentation_and_regressions.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1807,6 +1807,45 @@ def test_ci_runs_package_smoke_and_checks_all_shell_smokes():
18071807
assert "matrix.python-version == '3.12'" in ci
18081808

18091809

1810+
def test_runtime_validation_pack_script_and_docs_are_in_sync():
1811+
script = Path("scripts/runtime-validation-packs.sh")
1812+
docs = Path("docs/guides/runtime-validation-packs.md")
1813+
mkdocs = Path("mkdocs.yml").read_text(encoding="utf-8")
1814+
readme = Path("README.md").read_text(encoding="utf-8")
1815+
release = Path("docs/reference/release.md").read_text(encoding="utf-8")
1816+
packs = sorted(path.stem for path in Path("examples/runtime-packs").glob("*.env"))
1817+
1818+
assert script.is_file()
1819+
assert os.access(script, os.X_OK)
1820+
assert docs.is_file()
1821+
assert packs == ["debian-like", "package-only", "privileged-vm", "redhat-like"]
1822+
assert "Runtime Validation Packs: guides/runtime-validation-packs.md" in mkdocs
1823+
assert "scripts/runtime-validation-packs.sh" in readme
1824+
assert "scripts/runtime-validation-packs.sh" in release
1825+
text = script.read_text(encoding="utf-8")
1826+
assert "release-metadata" in text
1827+
assert "package-smoke" in text
1828+
assert "plugin-checks" in text
1829+
assert "ssh-evidence" in text
1830+
assert "manifest.json" in text
1831+
assert "SHA256SUMS" in text
1832+
1833+
1834+
def test_runtime_validation_pack_list_command_returns_pack_names():
1835+
result = subprocess.run(
1836+
["bash", "scripts/runtime-validation-packs.sh", "--list"],
1837+
check=True,
1838+
capture_output=True,
1839+
text=True,
1840+
)
1841+
assert result.stdout.splitlines() == [
1842+
"debian-like",
1843+
"package-only",
1844+
"privileged-vm",
1845+
"redhat-like",
1846+
]
1847+
1848+
18101849
def test_runtime_evidence_suite_is_documented_and_optional():
18111850
script = Path("scripts/runtime-evidence.sh")
18121851
guide = Path("docs/guides/runtime-evidence.md").read_text(encoding="utf-8")

0 commit comments

Comments
 (0)