AGPL-3.0-or-later · forever.

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