{ "id": "validate-playbooks", "postconditions": [ "All playbook directories contain playbook.ncl", "All playbook.ncl files conform to PlaybookDef schema", "All step scripts exist relative to their playbook directory", "All playbooks with rollback_strategy = automatic have rollback.nu", "All .nu files pass nu --ide-check 50 with zero diagnostics", "Dry-run tests (where present) executed and results reported" ], "preconditions": [ "{provisioning_root}/catalog/playbooks/ exists", "nickel is available in PATH", "nu is available in PATH", "jq is available in PATH", "{schemas_root}/playbook.ncl is accessible" ], "steps": [ { "action": "list_playbook_dirs", "actor": "Agent", "cmd": "find {provisioning_root}/catalog/playbooks -maxdepth 1 -type d ! -name playbooks | sort", "depends_on": [], "id": "discover_playbooks", "note": "Enumerate all playbook directories. Each must contain playbook.ncl at minimum.", "on_error": { "strategy": "Stop" } }, { "action": "verify_manifest_present", "actor": "Agent", "cmd": "find {provisioning_root}/catalog/playbooks -maxdepth 2 -name 'playbook.ncl' | while read f; do echo \"OK: $f\"; done; find {provisioning_root}/catalog/playbooks -maxdepth 1 -mindepth 1 -type d | while read d; do test -f \"$d/playbook.ncl\" || echo \"MISSING playbook.ncl in $d\"; done", "depends_on": [ { "kind": "OnSuccess", "step": "discover_playbooks" } ], "id": "check_playbook_ncl_exists", "note": "Every playbook directory must contain playbook.ncl. Missing manifests are reported but do not stop subsequent checks on other playbooks.", "on_error": { "strategy": "Continue" } }, { "action": "nickel_export_each_playbook", "actor": "Agent", "cmd": "find {provisioning_root}/catalog/playbooks -maxdepth 2 -name 'playbook.ncl' | while read f; do name=$(basename $(dirname $f)); echo \"--- $name\"; echo \"let pb = import \\\"{schemas_root}/playbook.ncl\\\" in (import \\\"$f\\\") | pb.PlaybookDef\" | nickel export /dev/stdin 2>&1 | tail -1 && echo \"$name: schema OK\" || echo \"$name: SCHEMA FAIL\"; done", "depends_on": [ { "kind": "Always", "step": "check_playbook_ncl_exists" } ], "id": "validate_schema_conformance", "note": "Apply PlaybookDef contract to each playbook.ncl. Schema failure means the playbook runner will refuse to execute this playbook.", "on_error": { "strategy": "Continue" } }, { "action": "verify_step_scripts", "actor": "Agent", "cmd": "find {provisioning_root}/catalog/playbooks -maxdepth 2 -name 'playbook.ncl' | while read f; do dir=$(dirname $f); name=$(basename $dir); nickel export \"$f\" 2>/dev/null | jq -r '.steps[].script' | while read script; do test -f \"$dir/$script\" && echo \"OK: $name/$script\" || echo \"MISSING: $name/$script\"; done; done", "depends_on": [ { "kind": "Always", "step": "validate_schema_conformance" } ], "id": "check_step_scripts_exist", "note": "Every step script declared in playbook.ncl must exist as a file relative to the playbook directory.", "on_error": { "strategy": "Continue" } }, { "action": "verify_rollback_nu_when_automatic", "actor": "Agent", "cmd": "find {provisioning_root}/catalog/playbooks -maxdepth 2 -name 'playbook.ncl' | while read f; do dir=$(dirname $f); name=$(basename $dir); strategy=$(nickel export \"$f\" 2>/dev/null | jq -r '.rollback_strategy // \"none\"'); if [ \"$strategy\" = \"automatic\" ]; then test -f \"$dir/rollback.nu\" && echo \"OK: $name/rollback.nu\" || echo \"MISSING rollback.nu for automatic rollback: $name\"; fi; done", "depends_on": [ { "kind": "Always", "step": "validate_schema_conformance" } ], "id": "check_rollback_script", "note": "When rollback_strategy = 'automatic, rollback.nu MUST exist. validate-playbooks reflection mode enforces this before any execution.", "on_error": { "strategy": "Continue" } }, { "action": "nu_ide_check_all_playbook_scripts", "actor": "Agent", "cmd": "find {provisioning_root}/catalog/playbooks -name '*.nu' | while read f; do result=$(nu --ide-check 50 \"$f\" 2>&1); [ -z \"$result\" ] && echo \"OK: $f\" || echo \"FAIL: $f\\n$result\"; done", "depends_on": [ { "kind": "Always", "step": "check_step_scripts_exist" }, { "kind": "Always", "step": "check_rollback_script" } ], "id": "nushell_ide_check_scripts", "note": "All .nu files in playbook directories must pass nu --ide-check 50 with zero diagnostics. This catches Nushell syntax errors and undefined variable references before runtime.", "on_error": { "strategy": "Continue" } }, { "action": "execute_dry_run_tests", "actor": "Agent", "cmd": "find {provisioning_root}/catalog/playbooks -path '*/tests/dry_run.nu' | while read f; do name=$(basename $(dirname $(dirname $f))); echo \"--- dry_run: $name\"; nu \"$f\" 2>&1 && echo \"$name dry_run: PASS\" || echo \"$name dry_run: FAIL\"; done", "depends_on": [ { "kind": "Always", "step": "nushell_ide_check_scripts" } ], "id": "run_dry_run_tests", "note": "When tests/dry_run.nu is present, execute it as a smoke test. Failures are reported but do not fail the overall validation — the dry_run.nu is advisory, not mandatory.", "on_error": { "strategy": "Continue" } }, { "action": "emit_playbook_validation_report", "actor": "Agent", "cmd": "echo '=== playbook validation complete ===' && echo 'All MISSING and FAIL entries above must be resolved before playbooks can be executed'", "depends_on": [ { "kind": "Always", "step": "run_dry_run_tests" } ], "id": "summary_report", "note": "Final summary. MISSING and FAIL lines are blocking. dry_run FAIL lines are advisory.", "on_error": { "strategy": "Stop" } } ], "strategy": "Override", "trigger": "Validate all playbooks: schema conformance, script existence, and Nushell ide-check" }