Skip to content

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:

  1. Install the zorb binary (or bun/node + the NPM package).
  2. Provide secrets via environment variables that you forward in with -e.
  3. 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":

sh
# 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 zorb

The 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:

sh
zorb --version

GitHub Actions ​

The most common pairing. Wire zorb into a normal run: step:

yml
# .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_TOKEN

A few patterns worth pointing out:

  • --quiet trims 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_xxx works 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 inherit process.env.

Reusable workflow snippet ​

For repos with many jobs, lift the install into a reusable workflow or composite action:

yml
# .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
yml
# .github/workflows/ci.yml
- uses: ./.github/actions/setup-zorb
- run: zorb run test --quiet

GitLab CI ​

yml
# .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_TAG

GitLab'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 ​

yml
# .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_TOKEN

Context-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
# 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:

sh
docker run --rm -v "$PWD":/workspace ghcr.io/example/zorb-ci run test --quiet

Add 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.

sh
zorb run release -e NPM_TOKEN -e GITHUB_TOKEN

If 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:

sh
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:

yml
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:

FlagWhen to use
--quietThe default for CI. Drops zorb's progress chrome; underlying command output still appears.
--verboseWhen a build mysteriously fails and the headline output isn't enough. One level up from default.
--debugWhen you suspect zorb itself, not your script. Dumps resolution, env layering, masking decisions.
--no-colorForce-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:

CodeMeaning
0Success.
1Task failed (validation, missing input, step exit, action throw).
nOtherwise the step's exit code.
130SIGINT — runner cancelled the job (e.g. user pressed cancel).
143SIGTERM — 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 set pull: always for releases, or pull: if-not-present (the default) for everything else. Floating :latest plus 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 magic CI=true propagation — pass it explicitly via -e CI=true if a step needs it.

A reference shape ​

A workflow that holds up well in CI usually looks like:

yml
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 build

Then your CI is one line:

sh
zorb run ci --quiet

Each 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 ​

MIT licensed