provisioning/reflection/handlers/lib/runner.nu
2026-05-12 02:40:14 +01:00

145 lines
5 KiB
Text

use std log
# Normalize a single step to canonical shape:
# { id, name, cmd, deps: list<string>, abort: bool }
def normalize-step [step: record]: nothing -> record {
let raw_deps = if ($step | columns | any { |c| $c == "depends_on" }) {
$step.depends_on
} else {
[]
}
let deps = $raw_deps | each { |d|
if (($d | describe) | str starts-with "string") { $d } else { $d.step }
}
let raw_err = if ($step | columns | any { |c| $c == "on_error" }) {
$step.on_error
} else {
"warn"
}
let abort = if (($raw_err | describe) | str starts-with "string") {
$raw_err == "abort"
} else {
let strategy = if (($raw_err | columns) | any { |c| $c == "strategy" }) {
$raw_err.strategy
} else { "Continue" }
$strategy == "Stop"
}
let name = if ($step | columns | any { |c| $c == "name" }) {
$step.name
} else if ($step | columns | any { |c| $c == "action" }) {
$step.action
} else {
$step.id
}
{ id: $step.id, name: $name, cmd: $step.cmd, deps: $deps, abort: $abort }
}
# Replace {key} placeholders in a string with values from params record.
def substitute-params [cmd: string, params: record]: nothing -> string {
$params | transpose key val | reduce --fold $cmd { |kv, s|
$s | str replace --all $"({$kv.key})" ($kv.val | into string)
}
}
# Topological sort: returns steps in dependency order.
def topo-sort [steps: list<record>]: nothing -> list<record> {
let max_passes = ($steps | length) + 1
let result = (seq 0 $max_passes) | reduce --fold { done: [], ordered: [] } { |_, acc|
let ready = $steps | where { |s|
let not_done = not ($acc.done | any { |d| $d == $s.id })
let deps_met = ($s.deps | all { |dep| $acc.done | any { |d| $d == $dep } })
$not_done and $deps_met
}
if ($ready | is-empty) {
$acc
} else {
{
done: ($acc.done | append ($ready | each { |s| $s.id })),
ordered: ($acc.ordered | append $ready)
}
}
}
$result.ordered
}
# Execute one step under bash. Returns { step_id, name, status, output }.
def run-step [step: record, params: record]: nothing -> record {
let cmd = substitute-params $step.cmd $params
let r = do { ^bash -c $cmd } | complete
let status = if $r.exit_code == 0 { "pass" } else if $step.abort { "fail" } else { "warn" }
let output = if ($r.stdout | str trim | is-empty) { $r.stderr } else { $r.stdout }
{ step_id: $step.id, name: $step.name, status: $status, output: ($output | str trim) }
}
# Format a single result for console output.
def format-result [result: record]: nothing -> string {
let icon = match $result.status {
"pass" => "v",
"warn" => "!",
"fail" => "x",
"skip" => "-",
_ => "?",
}
$" [($icon)] [($result.status | str upcase)] ($result.name)"
}
# Main entry point — exported for use in handler scripts.
# mode_path: path to the .ncl reflection mode file
# params: record of {key: value} substitutions for cmd fields
# provisioning_root: absolute path to provisioning/ (used as nickel import path)
export def run-reflection-mode [
mode_path: string,
params: record,
provisioning_root: string,
]: nothing -> list<record> {
let export_r = do { ^nickel export --format json --import-path $provisioning_root $mode_path } | complete
if $export_r.exit_code != 0 {
error make { msg: $"Failed to export '($mode_path)': ($export_r.stderr)" }
}
let mode = $export_r.stdout | from json
if not ($mode | columns | any { |c| $c == "steps" }) {
error make { msg: $"Mode '($mode_path)' has no 'steps' field" }
}
let steps = $mode.steps | each { |s| normalize-step $s }
let ordered = topo-sort $steps
log info $"Reflection mode: ($mode.id) — ($ordered | length) steps"
let results = $ordered | reduce --fold [] { |step, acc|
let blocked = $acc | any { |r|
$r.status == "fail" and ($step.deps | any { |d| $d == $r.step_id })
}
if $blocked {
$acc | append { step_id: $step.id, name: $step.name, status: "skip", output: "dependency failed" }
} else {
let result = run-step $step $params
print (format-result $result)
$acc | append $result
}
}
$results
}
# Print summary and exit 1 if any step failed. Exported for handler scripts.
export def print-summary-and-exit [results: list<record>]: nothing -> nothing {
let fails = $results | where { |r| $r.status == "fail" }
let warns = $results | where { |r| $r.status == "warn" }
let passes = $results | where { |r| $r.status == "pass" }
print ""
print $"=== Summary: ($passes | length) pass / ($warns | length) warn / ($fails | length) fail ==="
if not ($fails | is-empty) {
print "Failed steps:"
$fails | each { |r| print $" x ($r.name)\n ($r.output | lines | first | default '')" }
exit 1
}
}