CI scaffolds
klera ci <target> writes a starter pipeline that runs flows, captures the
JSON report, converts it to JUnit XML, and posts results to the test-results
pane your CI provider already paints on every PR. Three targets ship today:
GitHub Actions, GitLab CI, CircleCI.
The output is a plain YAML file under your CI provider’s conventional path. You commit it, you edit it. klera does not own the file after generation.
Generate
GitHub Actions
klera ci github-actions --out .github/workflows/klera.ymlWithout --out, the generated YAML is printed to stdout — useful for piping
into a wrapper script or eyeballing before you commit.
Flags
--out <path>— write the generated YAML to<path>. Without it, prints to stdout.--flows <glob>— flow glob the pipeline iterates over. Defaults toflows/*.flow.yaml. Useflows/**/*.flow.{md,yaml}for nested layouts.
What each pipeline does
Every generated pipeline follows the same five-stage shape. Differences are in YAML syntax, not in the data flow.
Install the workspace
pnpm install --frozen-lockfile against Node 20. The scaffolds assume
pnpm — if you use npm or yarn, swap the install command in the generated
file.
Gate or regenerate the prose cache
Every .flow.md ships a paired .flow.json cache. CI confirms the cache
is in sync with the prose before any flows run. Default mode is
compileMode: 'check' — klera compile --all --check fails the build
if any cache is stale and tells the PR author to regenerate locally.
compileMode: 'auto' flips the behaviour: CI regenerates the cache and
posts a Markdown comment with the IR step-list before/after for review.
See Prose-cache CI below.
Run flows
A shell loop walks the configured flow glob and runs klera run "$flow" --report "reports/$base.json" for each one. Failures are captured but
the loop continues so the test-results pane sees every flow, not just
the first to fail.
Render JUnit XML
klera report reports/<flow>.json --junit reports/<flow>.junit.xml
projects each JSON report into JUnit XML. JSON stays canonical; JUnit is
the derivative the CI provider knows how to render.
Publish
Each provider has its own primitive: GitHub uses
dorny/test-reporter, GitLab
uses artifacts.reports.junit, CircleCI uses store_test_results. All
three surface failed klera steps inline on the PR diff with the engine’s
matcher / dispatch / assertion error message.
Cross-link: see Reports for the JSON schema, the
HTML report, and the --junit projection in detail.
GitHub Actions
The generator emits a workflow that fires on push to main and every
pull_request. Truncated:
# Generated by `klera ci github-actions`. Adjust to taste.
name: klera
on:
push: { branches: [main] }
pull_request: { branches: [main] }
jobs:
flows:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with: { version: 9 }
- uses: actions/setup-node@v4
with: { node-version: 20, cache: pnpm }
- run: pnpm install --frozen-lockfile
- name: klera — gate on prose-cache freshness
run: pnpm exec klera compile --all --check
- name: Run klera flows
env:
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
mkdir -p reports
for flow in flows/*.flow.yaml; do
base=$(basename "$flow" .flow.yaml)
pnpm exec klera run "$flow" --report "reports/$base.json" || EXIT=1
done
exit ${EXIT:-0}
- name: Render JUnit XML
if: always()
run: |
for json in reports/*.json; do
base=$(basename "$json" .json)
pnpm exec klera report "$json" --junit "reports/$base.junit.xml"
done
- name: Publish test results
if: always()
uses: dorny/test-reporter@v1
with:
name: klera flows
path: 'reports/*.junit.xml'
reporter: java-junit
- name: Upload klera reports
if: always()
uses: actions/upload-artifact@v4
with: { name: klera-reports, path: reports/ }Optional secrets the generated workflow forwards if set:
OTEL_EXPORTER_OTLP_ENDPOINT/OTEL_EXPORTER_OTLP_HEADERS— ship the run to your OTel backend. See Observability.ANTHROPIC_API_KEY— enables LLM-narrated triage verdicts on failure.
GitLab CI
GitLab has built-in JUnit ingestion via artifacts.reports.junit, so the
generator’s output is shorter — no third-party plugin needed.
# Generated by `klera ci gitlab-ci`. Adjust to taste.
stages: [test]
klera:
stage: test
image: node:20
before_script:
- corepack enable
- corepack prepare pnpm@9 --activate
- pnpm install --frozen-lockfile
script:
- pnpm exec klera compile --all --check
- mkdir -p reports
- |
EXIT=0
for flow in flows/*.flow.yaml; do
base=$(basename "$flow" .flow.yaml)
pnpm exec klera run "$flow" --report "reports/$base.json" || EXIT=1
done
for json in reports/*.json; do
base=$(basename "$json" .json)
pnpm exec klera report "$json" --junit "reports/$base.junit.xml"
done
exit $EXIT
artifacts:
when: always
paths: [reports/]
reports:
junit: reports/*.junit.xmlFailed klera steps surface in the MR’s “Test summary” pane; the JSON archive
ships in artifacts.paths so reviewers can download it for the HTML report.
CircleCI
CircleCI has store_test_results for the test-insights pane and
store_artifacts for the JSON archive — both first-class primitives,
no orb installs required.
# Generated by `klera ci circleci`. Adjust to taste.
version: 2.1
jobs:
klera:
docker: [{ image: cimg/node:20.14 }]
steps:
- checkout
- run:
name: Install pnpm
command: corepack enable && corepack prepare pnpm@9 --activate
- run: pnpm install --frozen-lockfile
- run:
name: klera — gate on prose-cache freshness
command: pnpm exec klera compile --all --check
- run:
name: Run klera flows
command: |
mkdir -p reports
EXIT=0
for flow in flows/*.flow.yaml; do
base=$(basename "$flow" .flow.yaml)
pnpm exec klera run "$flow" --report "reports/$base.json" || EXIT=1
done
exit $EXIT
- run:
name: Render JUnit XML
when: always
command: |
for json in reports/*.json; do
base=$(basename "$json" .json)
pnpm exec klera report "$json" --junit "reports/$base.junit.xml"
done
- store_test_results: { path: reports }
- store_artifacts: { path: reports }
workflows:
klera:
jobs: [klera]Prose-cache CI
Every prose flow is hashed against (prose body + element graph + planner version) and the hash is recorded in the cache’s _meta.combined field.
Stale caches mean the prose changed but the IR hasn’t been recompiled.
Two CI behaviours, controlled by the generator’s compileMode option:
check (default) runs klera compile --all --check. The build fails
on any stale cache. The PR author regenerates locally with
klera compile --all and re-pushes. This is the cheapest mode — no LLM
call in CI.
auto runs the same check, then on a stale-cache exit code regenerates
the affected caches and writes a Markdown flow-diff to the PR comment
thread. Reviewers see the IR step-list before/after side-by-side. Requires
a planner transport in CI — ANTHROPIC_API_KEY for the API mode, or the
local-CLI transport with claude / codex / gemini available on the
runner. See Planner transports.
The compile diff is table-formatted today (no inline screenshots). Pipe
characters in step text are escaped via escapeForCell so the table
stays well-formed.
klera ci emits the check variant by default because it does not
require any planner transport in CI. Adopters with a planner wired up
who want regenerate-on-PR can hand-edit the generated YAML — the
generator option is a one-line tweak and the comment trail in the
emitted file points to the exact lines to flip.
Editing after generation
The generated YAML is yours. Common edits: swap pnpm install for
npm ci / yarn install / bun install; matrix on platform to run
iOS and Android in parallel (the runner image needs the relevant
toolchain); add a schedule: cron to catch backend-data drift overnight.
The generator does not rewrite an existing file at --out. If the path
already has content, klera errors out — pass a different path or remove
the existing file first.