Drop the floor step; put `name:` before the command
The floor read the test-count banner and failed the run if it dropped below what a green run reported. It was guarding a real hole -- sigil test silently skips files in test/ that do not match test-.sgl or -test.sgl, so a rename can shrink the suite while CI stays green -- but that hole belongs to sigil test, and fixing it there fixes it once for every repo instead of pasting ten lines of shell into each one. Filed separately; a --strict flag is the shape.
Removing it also removes its only consumer, so sigil test runs plainly again and the output capture and the artifact step go with it.
name: now precedes the command. Keyword arguments are order-free in the job language already, so this is the same step form and compiles to an identical script -- it just does not bury the step's name underneath the code it names.
.kiln/ci.sgv | 176 +++++++++++++++++++++++++++++++++-----------------------------------------------------------------------------------------------------------------------------------------------
1 file changed, 33 insertions(+), 143 deletions(-).kiln/ci.sgvmodified
;;; ci.sgv -- sigil-css CI, defined NATIVELY in kiln's job language.;;; ci.sgv -- sigil-css CI, defined natively in kiln's job language.;;;;;; No `.kiln/ci.sh`. The steps below ARE the pipeline; there is no shell;;; script wrapping them. This is the pilot shape intended to be copied;;; across the extracted repos, so the reasoning is recorded here rather;;; than only in the task note. Every claim below was measured on kiln;;; ad9b9c5 and sigil-css ebd81a4, 2026-09-01, and re-checked by an;;; adversarial reviewer that found four of them wrong the first time.;;; No `.kiln/ci.sh`. The three steps below ARE the pipeline.;;;;;; `on:` IS MANDATORY AND ITS ABSENCE IS SILENT. A workflow with no `on:`;;; matches no event, produces no error and no run, and reads exactly like;;; a controller fault rather than like this file. It does not even lint:;;; a pipeline with no `on:` reports "OK -- no problems found".;;; matches no event, produces no error and no run, and still lints OK --;;; so a missing `on:` reads exactly like a controller fault rather than;;; like this file.;;;;;; NO `branch:` FILTER ON `push`, deliberately. A filter would narrow;;; coverage to one ref; this library has none to spare. `(pull-request)`;;; is NOT declared: kiln's matcher accepts the trigger, but no run of that;;; kind has ever been observed on this controller, and declaring coverage;;; nobody has seen fire is how a green comes to mean less than it looks.;;; Unlike sigil-http and sigil-json, this repo carries no `.woodpecker.yml`;;; at all -- it has never had CI of any kind.;;; NO `branch:` FILTER ON `push`, deliberately: a filter would narrow;;; coverage to one ref. `(pull-request)` is not declared either -- kiln's;;; matcher accepts it, but no run of that kind has been observed on this;;; controller, and declaring coverage nobody has seen fire is how a green;;; comes to mean less than it looks.;;;;;; SINGLE NODE, FOUR NAMED STEPS -- not four jobs. Every node gets its own;;; worker and its own workspace, with the tree delivered again per node;;; (`runner.sgl` takes `backend-workspace` and calls `provide-source!`;;; inside the per-node path). Splitting the stages across nodes would mean;;; shipping the workspace `build/` between them -- 1.8 MB for this repo;;; after `sigil build` in a delivered tree -- which is the artifact fan-in;;; path kiln's own `.kiln/ci.sgv` records as unproven on the persistent;;; backend. And it would not be enough: `sigil deps install` leaves a LINK;;; FARM in the workspace (`.sigil/deps`, 2 entries here, all 2 symlinks);;; pointing at content under `$HOME/.sigil/deps`, so a restored copy of it;;; dangles unless $HOME is shared between the nodes -- an unstated;;; dependency on worker statefulness, true on today's single persistent;;; host and false anywhere else. sigil-http's `.woodpecker.yml` records;;; the same constraint in its own header. The honest cost of one node is that;;; the operator gets ONE status instead of three; that is a symptom of the;;; missing ledger below, not of the node count.;;; ONE NODE, NOT THREE JOBS. Every node gets its own worker and its own;;; delivered tree, and `sigil deps install` leaves a link farm in the;;; workspace pointing at content under $HOME -- so splitting the stages;;; across nodes means either shipping ~1.8 MB of `build/` between them or;;; depending on $HOME being shared between leases. Reasoning and;;; measurements: topics/kiln-native-pipeline-single-node-template.;;;;;; WHAT A STEP NAME BUYS TODAY: readability, and nothing structural.;;; `nodeexec.sgl` wraps a named shell step in `##[kiln:section-start:<name>]`,;;; while the log viewer's parser (`web/views.sgl` `section-marker`, used by;;; both the fold view and the live SSE renderer) requires the prefix;;; `##[kiln:section-start ` -- space and timestamp, the LEGACY `.run`;;; grammar. The two do not meet, so a named step renders as a plain log;;; line. Two more things about the markers, for whoever reconciles the;;; grammar: a FAILING step emits no `section-end` at all (the program dies;;; under `set -e`), and `artifact` steps emit no markers whatsoever.;;; The names are still worth having -- they are the contract any future;;; comparison rests on, and a rename is a breaking change even when it;;; reads better -- but do not tell anyone they will see folds.;;; STEP NAMES DO NOT FOLD IN THE UI TODAY, and that is a kiln bug, not a;;; mistake here: `nodeexec.sgl` emits `##[kiln:section-start:<name>]` and;;; the log viewer's parser wants the legacy `##[kiln:section-start <ms>;;; <name>]`. Keep the names -- they are the contract any future;;; comparison rests on -- but expect plain log lines until that is fixed.;;; There is also no check ledger here: that whole contract lives in each;;; repo's `.kiln/ci.sh`, and this pipeline has none.;;; See topics/kiln-native-pipeline-emits-no-checks.;;;;;; THERE IS NO CHECK LEDGER HERE, AND THAT IS STRUCTURAL. The;;; `$KILN_ARTIFACTS/checks.ledger` contract that `kiln-diff-checks` and the;;; conversion plan's floors are built on is written entirely by a repo's;;; own `.kiln/ci.sh` -- `grep -rn checks.ledger src/` in kiln returns;;; nothing. A natively-defined pipeline emits no ledger by construction.;;; The `floor` step below is therefore NOT that contract and must not be;;; read as it: it asserts only that the suite ran and reported at least the;;; counts a green local run reported.;;;;;; TIMEOUT 1800, AND WHAT THE CLAMP ACTUALLY DOES. `runner.sgl`;;; `node-worker-fm` computes `(min declared ceiling)` with NO diagnostic,;;; where ceiling is `safety.max_job_timeout`. 1800 equals that setting's;;; DEFAULT (`config.sgl:345`) -- it is a ceiling default, not a per-job;;; default -- so 1800 survives the clamp on a controller that has not;;; lowered the key, and is SILENTLY REDUCED on one that has. Nobody;;; copying this file should assume otherwise without reading the key.;;; The effective value is emitted once, in the `Persistent worker ready;;; ... timeout=N` line, and only on the persistent-worker path; on any;;; other backend it is printed nowhere. Note also that omitting `worker:`;;; entirely is NOT the same as declaring a default: `node-worker-fm`;;; returns #f and the node carries no frontmatter at all.;;; `timeout: 1800` is CLAMPED by `safety.max_job_timeout` with no;;; diagnostic -- a plain (min declared ceiling). 1800 is that key's;;; default, so it survives on a controller that has not lowered it and is;;; silently reduced on one that has. The effective value is printed once,;;; in `Persistent worker ready ... timeout=N`, and only on the;;; persistent-worker path.(pipeline ci on: #[ (push) (manual) ] (job ci worker: (worker timeout: 1800) steps: #[ (shell "sigil deps install" name: deps) (shell "sigil build" name: build) ;; `sigil test` is captured rather than piped: a pipeline's exit ;; status is the LAST command's, so `sigil test | tee` reports tee's ;; success and loses the failure. The `|| { rc=$?; ...; exit \"$rc\"; }` ;; form keeps the real status while still printing the log. (shell "sigil test --no-color > kiln-test.out 2>&1 || { rc=$?; cat kiln-test.out; exit \"$rc\"; }cat kiln-test.out" name: test) ;; THE FLOOR. `sigil test` already exits 1 when it finds NO test ;; files, so this exists for the PARTIAL case: a file that stops ;; matching the `test-*.sgl` / `*-test.sgl` discovery pattern is ;; dropped SILENTLY and the run stays green on a smaller suite. ;; Both numbers come from a green local run of ebd81a4: ;; `Files: 1 of 1 run`, `Tests: 33 passed`. ;; ;; `tail -1`, NOT `head -1`, AND THIS IS NOT A STYLE CHOICE. The ;; step captures 2>&1, so anything a test prints lands in this file ;; ahead of the summary. A test that emits a banner-shaped line ;; ("Files: 9 of 9 run") makes `head -1` read the test's output ;; instead of the summary. Measured on sigil-http: with `head -1` the ;; floor stayed GREEN on a run that had fallen from 5 files / 167 ;; tests to 2 / 2. The real summary is always printed last, after the ;; per-file loop, so `tail -1` reads it and the same input goes ;; correctly red. ;; ;; The counts are read with a numeric guard because a numeric ;; comparison in POSIX sh fails OPEN on an empty or non-numeric ;; value. Exit 2 marks that case, and the wording is the point: ;; KILN SEES 1 AND 2 IDENTICALLY (both are a failed node), so the ;; distinction lives in the log for a human, not in the run status. ;; ;; The file floor is only load-bearing where the count is above 1. ;; `Files: 0 of N run` is unreachable -- the line prints only when ;; discovery found something, and every discovered file increments ;; the completed counter on every outcome path -- so in a repo with ;; ONE test file `files >= 1` is a tautology and the test count is ;; the only real assertion. It is kept anyway so all 58 copies of ;; this file have the same shape; say so where it applies. ;; ;; THIS REPO IS THAT CASE: it has ONE test file, so `files >= 1` ;; here CANNOT FAIL and is decorative. The test-count floor is the ;; only assertion in this step that can go red. The total-loss case ;; is still covered, by `sigil test` itself, which exits 1 when it ;; finds no test files at all. ;; ;; One case this floor mis-labels, worth knowing before a repo with ;; NATIVE tests copies it: `sigil test` exits 1 on no tests only when ;; BOTH the native and the .sgl sets are empty. A repo carrying a ;; `build/*/bin/test-*` binary whose entire .sgl suite stops matching ;; discovery gets exit 0, no `Files:` line, and a SETUP-FAILED here -- ;; a total loss of the .sgl suite reported as a tooling problem. ;; ;; NOTE FOR THE NEXT REPO: this step is the only part of the pipeline ;; the job language cannot express. It is shell because there is ;; nowhere else to put it, and that is how `ci.sh` starts. (shell "files=$(sed -n 's/^[[:space:]]*Files:[[:space:]]*\\([0-9][0-9]*\\) of .*/\\1/p' kiln-test.out | tail -1)tests=$(sed -n 's/^[[:space:]]*Tests:[[:space:]]*\\([0-9][0-9]*\\) passed.*/\\1/p' kiln-test.out | tail -1)for v in \"$files\" \"$tests\"; do case \"$v\" in '' | *[!0-9]*) echo \"SETUP-FAILED: floor could not read a count from kiln-test.out\"; exit 2 ;; esacdoneecho \"floor: files=$files (min 1), tests=$tests (min 33)\"[ \"$files\" -ge 1 ] || { echo \"FLOOR BREACHED: $files test files ran, expected at least 1\"; exit 1; }[ \"$tests\" -ge 33 ] || { echo \"FLOOR BREACHED: $tests tests ran, expected at least 33\"; exit 1; }echo 'floor: ok'" name: floor) ;; Staged last on purpose, and note what that costs: steps run under ;; `set -e` and there is no post/always construct, so this does NOT ;; run when an earlier step fails -- measured, $KILN_ARTIFACTS comes ;; back empty on a red run. Exactly the run whose output you would ;; most want kept. (artifact "kiln-test.out" as: "test-output.txt") (shell name: deps "sigil deps install") (shell name: build "sigil build") (shell name: test "sigil test") ]))