the path · 03
exec pipeline — shell, a conditional gate, cleanup on every outcome.
run itnika try 03-exec-pipeline
03-exec-pipeline.nika.yaml
# SPDX-License-Identifier: Apache-2.0# yaml-language-server: $schema=https://nika.sh/spec/v1/workflow.schema.json## 03 · exec pipeline — shell, a conditional gate, cleanup on every outcome.## Run the tests; ship only if they pass; release the build lock either way.# The interesting part is what "pass" means here: a red suite is not a broken# workflow, it is a NUMBER the next task reads. The pipeline succeeds at# finding out the tests failed.## Demonstrates ·# - the `exec:` verb · its two doors, `shell:` (a line) and `command:` (an argv array)# - `capture: structured` · `.output` is { stdout, stderr, exit_code }# - `timeout:` · a Go-duration string, quoted · hard kill, retries included# - `run.clock` · naming the clock that deadline rides# - `with:` + `when:` · a DATA edge · the exit code crosses, the CEL gate reads it# - `after:` · a STATE edge · admit a task on the producer's outcome# - `on_finally:` · runs on every outcome of a STARTED task (success · fail ·# timeout · cancel) · a task that never started runs none (03-dag.md:1045)## Runs green anywhere the suite is not passing — no Cargo.toml, or no cargo at# all, is a non-zero exit: the gate closes, the deploy is skipped, the notice# fires. Exactly the story a red suite tells. Run it inside a repo whose tests# DO pass and the gate opens onto `./deploy.sh`, which is yours to supply.## Run · nika run examples/03-exec-pipeline.nika.yamlnika: v1workflow: id: exec-pipelinerun: # `timeout:` below has to be measured against something. Left undeclared it # rides the ambient system clock — the honest default, never a refusal — but # the choice is then invisible in the file. Say it out loud. (`virtual` runs # deadlines on a simulated clock instead, for tests that must not wait.) clock: systempermits: # `true`, not a program list, and the checker will tell you why if you try: # « a shell-string command cannot be verified against a `permits.exec` # program allowlist (a pipeline can launch any program) — use the array # form ». `test` below wants a shell line, so the allowlist is off the table # for this file. A workflow whose steps are all `command:` arrays can — and # should — write the tight form instead: `exec: ["cargo", "rm"]`. exec: truetasks: test: timeout: "5m" exec: # Door 1 · a shell line. Pipes, globs and `&&` work; a program allowlist # cannot bound it. shell: "cargo test --workspace --lib" cwd: "." # the directory you ran `nika` from, not the file's capture: structured # → { stdout, stderr, exit_code } on_finally: # ALWAYS runs · best-effort · an error here is logged, never propagated. # A lock outlives the thing that took it unless something like this # releases it on the failure and timeout paths too. - exec: command: ["rm", "-f", "./.build.lock"] deploy: with: # `capture: structured` is what makes this possible: a non-zero exit # becomes DATA instead of a failure. The task above is ✔ and its # `.status` is `success` — it ran the command and found out. The red # suite lives in `exit_code`, and nowhere else. Gate on the number, and # never on the task status, or you will ship a broken build. exit_code: ${{ tasks.test.output.exit_code }} when: ${{ with.exit_code == 0 }} # red → skipped · green → ship exec: # Door 2 · an argv array. No shell, no word-splitting, and every program # in it is checkable against `permits.exec`. command: ["./deploy.sh"] cwd: "." no_ship_notice: # The third edge kind. `with:` carries DATA and `when:` reads it; `after:` # carries STATE — it admits this task on the producer's outcome and nothing # else. The vocabulary is success · failure · skipped · terminal (terminal = # any settled state, cancelled included). # # `deploy` is skipped whenever the gate above closes, so this is "the ship # did not go out" — a fact you cannot express with a value edge, because a # skipped task has no value to bind. after: deploy: skipped exec: command: ["echo", "deploy skipped · the suite was red"]# Both halves of the story, so a reader can see them disagree · read with# `--output json`. In an empty directory, measured:# {"suite_exit_code": 1, "task_status": "success"}# The number is whatever the command returned; the status is about the task.outputs: suite_exit_code: ${{ tasks.test.output.exit_code }} task_status: ${{ tasks.test.status }}nika-spec@6ac29351a · sha256 bf50092e1f5b1c10…