Skip to content

CI Pipeline

This example creates a CI pipeline triggered by GitHub webhooks with parallel lint and test steps.

Create workspace/.workflows/ci.yaml:

actions:
lint:
type: script
runner: docker
script: |
cd /workspace
npm ci
npm run lint
test:
type: script
runner: docker
script: |
cd /workspace
npm ci
npm test
build:
type: script
runner: docker
script: |
cd /workspace
npm ci
npm run build
echo "OUTPUT: {\"artifact\": \"dist/app.tar.gz\"}"
notify-ci:
type: script
script: |
echo "CI result: {{ input.status }} for job {{ input.job_id }}"
# In production, call Slack/Teams/GitHub API here
input:
status: { type: string }
job_id: { type: string }
tasks:
ci-pipeline:
mode: distributed
input:
body: { type: string }
flow:
lint:
action: lint
test:
action: test
# lint and test run in parallel (no mutual dependencies)
build:
action: build
depends_on: [lint, test]
on_success:
- action: notify-ci
input:
status: "success"
job_id: "{{ hook.job_id }}"
on_error:
- action: notify-ci
input:
status: "failed: {{ hook.error_message }}"
job_id: "{{ hook.job_id }}"
triggers:
github-push:
type: webhook
name: github-ci
task: ci-pipeline
secret: "whsec_your_secret_here"
enabled: true
  1. lint and test run in parallel (no dependency between them)
  2. build waits for both lint and test to complete
  3. An on_success hook notifies when the whole pipeline passes; an on_error hook notifies when any step fails — a hook always fires exactly once on the real terminal outcome, which a flow step reaching past a failure (via its own accept set) can’t guarantee (see Hooks)

The DAG looks like:

lint ──┐
├──> build
test ──┘
  • Parallel steps: lint and test have no mutual dependencies, so they run concurrently
  • Docker runner: runner: docker runs steps inside containers with workspace at /workspace
  • Hooks, not a dependent step: notifying after success or failure is an on_success / on_error hook, not a flow step. continue_on_failure is self-scoped — it only decides whether a step’s own failure fails the job, never whether anything downstream runs (that’s a dependent’s own accept set, see Conditionals). A flow-step notify depending on build would need accept: [completed, failed] (or any) just to run after a failed build — and if build also carried its own continue_on_failure: true so the job doesn’t fail either, you’ve quietly turned a failed pipeline green, the opposite of a CI notification’s job. A hook sidesteps both problems: it always fires exactly once on the real terminal outcome, and firing it never changes what that outcome is. That is why this page puts notification in hooks instead.
  • Webhook trigger: External systems (GitHub) can trigger the pipeline via POST /hooks/github-ci
  • Webhook input: input.body.* (e.g. {{ input.body.ref }}) accesses the parsed JSON body from the webhook request inside flow step templates — hook templates don’t see it, only hook.* / secret.* (see Hooks)
  1. Go to your GitHub repo → Settings → Webhooks → Add webhook
  2. Set the Payload URL to https://stroem.example.com/hooks/github-ci?secret=whsec_your_secret_here
  3. Set Content type to application/json
  4. Select “Just the push event”

Now every push to the repository triggers the CI pipeline.

Terminal window
# Via API (simulating a webhook payload)
curl -X POST http://localhost:8080/api/workspaces/default/tasks/ci-pipeline/execute \
-H "Content-Type: application/json" \
-d '{"input": {"body": {"ref": "refs/heads/main"}}}'
# Via CLI
stroem trigger ci-pipeline --input '{"body": {"ref": "refs/heads/main"}}'

Use for_each to run the same tests across multiple Node.js versions:

actions:
lint:
type: script
runner: docker
script: |
cd /workspace
npm ci
npm run lint
test-version:
type: docker
image: "node:{{ input.node_version }}"
cmd: |
cd /app && npm ci && npm test &&
echo "OUTPUT: {\"node\": \"{{ input.node_version }}\", \"passed\": true}"
input:
node_version: { type: string }
build:
type: script
runner: docker
script: |
cd /workspace
npm ci
npm run build
echo "OUTPUT: {\"artifact\": \"dist/app.tar.gz\"}"
notify-ci:
type: script
script: |
echo "CI result: {{ input.status }}"
input:
status: { type: string }
tasks:
ci-matrix:
mode: distributed
input:
body: { type: string }
flow:
lint:
action: lint
test:
action: test-version
for_each: ["18", "20", "22"]
input:
node_version: "{{ each.item }}"
# lint and test run in parallel; build waits for both
build:
action: build
depends_on: [lint, test]
on_success:
- action: notify-ci
input:
status: "success"
on_error:
- action: notify-ci
input:
status: "failed: {{ hook.error_message }}"

This creates test[0] (Node 18), test[1] (Node 20), test[2] (Node 22) — all running in parallel alongside lint. The test-version action uses type: docker to run each Node.js version’s image directly — note that type: docker does not mount workspace files, so the application source must be baked into the image. The lint and build actions use type: script + runner: docker instead, which mounts the workspace at /workspace. Hooks only see the hook.* / secret.* template context, not individual step outputs — hook.error_message and hook.failed_steps cover the failure case; to notify with a step’s own output, add a step to the flow instead of a hook.