Running zorb in CI
The whole point of declaring tasks in zorb.yml is that the same zorb run build you type locally is what CI runs. This page covers the practical details: how to install zorb on a runner, how to feed secrets in cleanly, how to keep the log readable, and what to leave switched off.
The mental model
A CI job is just another shell that calls zorb run <task>. There's no zorb-specific CI plugin, no marketplace action, no service. The runner does three things:
- Install the
zorbbinary (orbun/node+ the NPM package). - Provide secrets via environment variables that you forward in with
-e. - Invoke
zorb run <task>and propagate its exit code.
If the local invocation works, the CI invocation works — by design.
Installing zorb on a runner
zorb ships as a single binary on NPM. Three install flavours, in increasing order of "what's already on this runner":
# 1. Pre-built binary via NPM (smallest, fastest — recommended for CI)
npm install --global zorb
# 2. Via Bun (if the runner already has Bun)
bun add --global zorb
# 3. Via pnpm
pnpm add --global zorbThe published binary embeds its Bun runtime, so the runner doesn't need Bun installed separately for shell or JavaScript/TypeScript actions to work. Python actions still need python3 available on PATH.
Verify:
zorb --versionGitHub Actions
The most common pairing. Wire zorb into a normal run: step:
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install zorb
run: npm install --global zorb
- name: Test
env:
CI: 'true'
run: zorb run test --quiet
- name: Build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: zorb run build -e GITHUB_TOKENA few patterns worth pointing out:
--quiettrims zorb's progress chrome (step headers, hints) and only prints errors. The captured output from the underlying commands still appears.-e GITHUB_TOKEN(no value) forwards whatever GH Actions has populated into the runner's env. The same flag is what you'd use locally —-e GITHUB_TOKEN=ghp_xxxworks too.env:block on the GHA step sets the variable in the runner's process env. zorb then forwards it explicitly via-e. Don't expect zorb to pick it up implicitly — step subprocesses don't inheritprocess.env.
Reusable workflow snippet
For repos with many jobs, lift the install into a reusable workflow or composite action:
# .github/actions/setup-zorb/action.yml
name: Set up zorb
description: Install zorb on the runner
runs:
using: composite
steps:
- shell: bash
run: npm install --global zorb# .github/workflows/ci.yml
- uses: ./.github/actions/setup-zorb
- run: zorb run test --quietGitLab CI
# .gitlab-ci.yml
default:
image: node:20-alpine
stages: [test, build]
test:
stage: test
script:
- npm install --global zorb
- zorb run test --quiet
build:
stage: build
variables:
NODE_ENV: production
script:
- npm install --global zorb
- zorb run build -e DEPLOY_TOKEN
rules:
- if: $CI_COMMIT_TAGGitLab's variables: block sets process env on the runner, just like GitHub's env:. -e DEPLOY_TOKEN then forwards it into the zorb step. Mask sensitive variables in your project settings so they don't leak into job logs even without zorb's masker.
CircleCI
# .circleci/config.yml
version: 2.1
jobs:
test:
docker:
- image: cimg/node:20.0
steps:
- checkout
- run:
name: Install zorb
command: npm install --global zorb
- run:
name: Test
command: zorb run test --quiet
- run:
name: Build
environment:
NODE_ENV: production
command: zorb run build -e DEPLOY_TOKENContext-defined secrets surface as env vars on the executor, so -e DEPLOY_TOKEN forwards them cleanly.
Generic Docker-based runner
If your CI's runner spec is "a container image and a script," roll your own minimal Docker image:
# Dockerfile.zorb
FROM node:20-alpine
RUN npm install --global zorb && \
apk add --no-cache python3 docker-cli
WORKDIR /workspace
ENTRYPOINT ["zorb"]Build once, push it to your registry, and call it from any CI that supports image-based jobs:
docker run --rm -v "$PWD":/workspace ghcr.io/example/zorb-ci run test --quietAdd docker-cli only if your workflow uses docker: steps; add python3 only if it has Python actions. Strip either out otherwise.
Forwarding secrets
The pattern, in three escalating tiers:
Tier 1 — pass through with -e KEY
The CI exposes the secret as a runner env var; you forward it explicitly.
zorb run release -e NPM_TOKEN -e GITHUB_TOKENIf the variable isn't set in the runner's env, the flag silently skips — useful for optional credentials.
Tier 2 — load from a CI-provided file
Some CI systems write secrets to a file. Point --env-file at it:
zorb run deploy --env-file "$CI_SECRETS_FILE"Values from --env-file populate the inline env layer; per-environment files keep production credentials separate from staging ones.
Tier 3 — a secrets: loader
For production-grade pipelines, load secrets at run time from a real secret store:
secrets:
- uses: '@zorb/secrets/load-1password'
with:
vault: CI
items: [NPM_TOKEN, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY]The loader needs its own bootstrap credential (a service account token), which you still pass via -e — but only one credential reaches zorb instead of N. The loader handles the fan-out, and the resulting values are masked in step output.
See Security model → Recommendations for why loaders beat --env-file for real secrets.
Output, colours, and log volume
A few flags that read better in CI than on a terminal:
| Flag | When to use |
|---|---|
--quiet | The default for CI. Drops zorb's progress chrome; underlying command output still appears. |
--verbose | When a build mysteriously fails and the headline output isn't enough. One level up from default. |
--debug | When you suspect zorb itself, not your script. Dumps resolution, env layering, masking decisions. |
--no-color | Force-disable colour. Honoured even if FORCE_COLOR is set in the runner env. |
NO_COLOR=1 (env) | Same effect, runner-wide. |
Most CI log viewers handle ANSI well, so leaving colour on is usually fine. Force-disable only when you're piping output to a non-ANSI consumer (S3, a JSON-shaped sink, a chat notification).
Exit codes
CI systems read the exit code to decide pass/fail. zorb's mapping:
| Code | Meaning |
|---|---|
0 | Success. |
1 | Task failed (validation, missing input, step exit, action throw). |
| n | Otherwise the step's exit code. |
130 | SIGINT — runner cancelled the job (e.g. user pressed cancel). |
143 | SIGTERM — supervisor killed the job (e.g. timeout, OOM). |
Most CI systems treat any non-zero exit as failure and 130/143 as cancellation rather than failure. If yours doesn't, post-process the exit code yourself.
Things to leave switched off in CI
A short kill-list:
--watch— never. It runs forever; CI jobs need to terminate.- Interactive prompts in shell steps. CI has no stdin; tools that prompt will hang. Pass
--yes,--no-input,BATCH=1, or whatever the tool's non-interactive flag is. docker:steps that don't pull deterministically. Pin the image tag and setpull: alwaysfor releases, orpull: if-not-present(the default) for everything else. Floating:latestplus aggressive runner caching is the recipe for "works on my machine."- Trusting
process.env. zorb's strict-env policy means steps see only what you declare. Don't rely on a magicCI=truepropagation — pass it explicitly via-e CI=trueif a step needs it.
A reference shape
A workflow that holds up well in CI usually looks like:
tasks:
ci:
description: The full CI pipeline — same locally and on the runner
steps:
- uses: ./zorb.lint
- uses: ./zorb.typecheck
- uses: ./zorb.test
- uses: ./zorb.build
lint:
steps:
- run: npm run lint
typecheck:
steps:
- run: npm run typecheck
test:
steps:
- run: npm test
build:
steps:
- run: npm run buildThen your CI is one line:
zorb run ci --quietEach task is independently runnable, which means a contributor can reproduce a CI failure locally with the exact command the runner used. That's the property to optimise for.
See also
- CLI reference — every flag in this guide.
- Loading env from a dotenv file —
--env-filepatterns. - Security model — secret forwarding, masking, and what not to do.
- Troubleshooting → My env var isn't visible — the single most common CI gotcha.