Tangled-compatible personal CI server based on Nix
ci docs results.md
5.6 kB · 121 lines
Markdown
at main

Results and routing #

An action's stdout and stderr are its log. What happens next is decided by its result: by default its exit code, or a JSON file it writes to $CI_RESULT. The schema is run.rtw.ci.step.result; the rationale is in DESIGN.md §7–8.

Without a result file #

Plain recipes need nothing: exit 0 is a success, anything else a failure that stops the run.

The result file #

{
  "version": 1,
  "status": "failure",
  "summary": "mx99 /healthz 503 for 4m",
  "next": [
    { "action": "rollback", "prompt": "Roll back?", "then": "stop" },
    { "action": "health-check", "label": "Check again" }
  ],
  "attachments": [{ "label": "health probes", "path": "health.ndjson", "type": "ndjson" }],
  "outputs": { "hosts": ["mx99.eu.sovrn.at"] }
}
Field Meaning
version Required. 1
status Required. success, warning or failure. A warning routes like a success but shows as one on the run page
summary Markdown shown on the run page, at gates and in notifications
next What runs next. Omitted: default routing. "stop": end the run. Otherwise an action name, one option, or a list of options
gated Ask a person before taking next, even if the action has no gate of its own
attachments Files under $CI_ARTIFACTS shown on the run page: { label, path, type }, where path is relative to $CI_ARTIFACTS and type is text, markdown, ndjson or json
outputs An object merged into $CI_STATE/outputs.json, for later actions (and manual child runs) to read

An option in next:

Field Meaning
action Required. A declared action, on the happy path or not
then After that action succeeds: continue (carry on along the happy path) or stop. Default: continue for happy-path steps, stop for anything else
label Button label at the gate
prompt Question for the person
inputs An object given to the action as the JSON file at $CI_INPUTS

A result that is not valid JSON, doesn't match the schema, names an undeclared action or a path outside $CI_ARTIFACTS, or claims success with a non-zero exit, is a failure, and the error is shown on the run.

ci-result #

ci-result is on PATH in every action and writes a valid result:

ci-result ok|warn|fail [flags]
Flag Meaning
--summary TEXT Summary; repeat for more lines
--summary-file FILE Read the summary from a file (- for stdin)
--next ACTION Route to an action; repeat for a choice. The next four flags apply to the most recent --next
--then continue|stop That option's then
--label TEXT, --prompt TEXT That option's label and prompt
--input KEY=VALUE, --inputs-json JSON That option's inputs
--stop End the run after this action (not with --next)
--gated Ask a person before taking --next
--attach [LABEL=]PATH Attach a file; copied into $CI_ARTIFACTS if it is elsewhere. The type comes from the extension (.md, .ndjson, .json, else text)
--output KEY=VALUE, --outputs-json JSON Merge into $CI_STATE/outputs.json
--stdout Print the result instead of writing $CI_RESULT

A fail result is used whatever the exit code, so a recipe can write one and then exit non-zero. An ok or warn result with a non-zero exit is a contract error, so don't write success before the work is done.

Routing #

A run keeps a cursor on the happy path. After each action:

  • No next. On success or warning, a happy-path step moves on to the next step; any other action ends the run. On failure, the run ends.
  • next: "stop". The run ends, as a success unless this result is a failure. This is how an action ends a run early on purpose ("Nothing to deploy").
  • One option. It runs straight away, unless its action declares a gate or the result says gated: true; then the run waits for a person.
  • Several options. Always a gate: the person picks one or dismisses.

Naming a happy-path step in next moves the cursor there: forward skips steps, backward repeats them. After the chosen action succeeds, then: continue resumes the happy path after the cursor and then: stop ends the run.

Rules worth remembering:

  • Failure stops. Without a next, a failed action ends the run.
  • Off-path actions stop after themselves. An action that isn't a step ends the run when it succeeds, unless its option says then: continue or its own result routes on. Nothing drifts back onto the happy path by accident.
  • Gates only get added. A result can ask for a gate (gated, or a choice); it can never skip a gate an action declares.
  • At most 50 transitions per run, so a loop can't spin forever.

How a run ends #

End Status
Ran past the last step success
A result said next: "stop" success, or failure if that result was a failure
A happy-path step with then: stop success if it was the last step, else failure
An off-path action with then: stop ("diverted: stopped after rollback") failure, so a rolled-back run is never green
A failure with no next, a contract error, the transition limit failure
A declared gate dismissed cancelled
A gate raised by a result dismissed that result's status (failure stays failure; success and warning become success)
A gate expired, --until, Ctrl-C cancelled

A manual action run on its own (a child run) ends with that action's own status.