145 lines
5 KiB
Text
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
|
|
}
|
|
}
|