Coming from GitHub Actions, GitLab CI or Buildkite
Tests on every push is spool repo add plus a pipeline in your repo, and
nothing on that path involves the canvas. This page maps the words you
already have onto the ones used here, then shows the thing you can’t do
in a CI system: a pipeline that keeps running after the deploy.
Run tests on push
Section titled “Run tests on push”Two pieces: a pipeline in your repo, and spool repo add to connect the
repo so pushes run it. The pipeline is YAML at any path you like.
.spool/ci.yaml:
jobs: test: steps: - name: install run: npm ci - name: test run: npm testRun it against a ref without waiting for a push:
spool submit --repo-path .spool/ci.yaml --repo me/app --repo-credential gh-read --ref mainRun it on your own machine, against your working tree, with no server at all:
spool dev # in another terminalspool run .spool/ci.yamlTo fire it on every push, connect the repo. One command, no canvas:
spool repo add https://github.com/me/app \ --credential gh-read --status-credential github-statusThat creates the webhook for the forge, a handler that clones each
pushed commit on an agent and runs .spool/ci.yaml out of that
checkout, and a handler that puts the result on the commit. It prints
the ingest URL and a signing secret to paste into the repo’s webhook
settings. CI from a repo
has the whole flow, including what to do when you want the dispatch to
make a judgement of its own.
Branch and path filters go on that command, as --when. One CEL
expression over the event covers both, and it is evaluated server-side
when the delivery lands, before any run exists, so a filtered push costs
nothing:
spool repo add https://github.com/me/app --credential gh-read \ --when 'event.ref == "refs/heads/main" && event.paths.exists(p, p.startsWith("api/"))'A pipeline can carry the same predicate itself, as a top-level if:,
but only where a handler delivers the event to the pipeline. A
repo add build is spawned rather than delivered, and a spawned run is
not gated on a trigger predicate, so an if: in .spool/ci.yaml does
nothing there. Filter on --when.
Vocabulary
Section titled “Vocabulary”| Your word | Here | Worth knowing |
|---|---|---|
| workflow file | pipeline | YAML in your repo, fetched at the commit that triggered it |
| job | job | one entry under jobs: |
| step | step | entries under steps:; the whole job runs as one activity on one agent |
needs: |
depends_on: |
same shape |
| runner | agent | you run them; each advertises capabilities |
| runner label | requires: |
a job claims only agents carrying those capabilities |
runs-on: ${{ matrix.os }} |
requires: ["${{ matrix.target.cap }}"] |
one job across platforms; the capability is read per combination |
on: push |
top-level if: |
one expression, evaluated before a run is created |
paths: / paths-ignore: |
paths: / paths_ignore: |
same names; they fail open when the forge reports no file list |
| concurrency group | concurrency: |
group plus queue, cancel-running, cancel-queued or skip |
| artifact upload | spool artifact put |
same verb stores a build cache with --key |
| artifact download | spool artifact get |
exits 1 on a miss, so a cache lookup is plain shell |
| secret | credential | resolved to auth headers at dispatch, never stored in the definition |
| environment approval | approval: job |
pauses at any point in the graph, not only before a deploy |
| reusable workflow | workflow: job |
runs another pipeline as a durable child |
| job output | outputs: |
spool output set key=value from a step |
| matrix | matrix: |
one job per combination, read as ${{ matrix.<name> }} |
What is different
Section titled “What is different”Jobs can wait. An approval: job parks on a signal and holds no agent
while it does, so a pipeline can stop for a person and pick up hours later
with its state intact:
promote: depends_on: [deploy-staging] approval: prompt: "Promote to production?" fields: - { name: env, type: select, options: [staging, production] }A wait: job does the same for a timer or a named signal. There is no cap on
how long either parks; a run waiting a month occupies a database row.
The agents are yours. An agent claims work matching its capabilities, so build hardware, GPU boxes and network-restricted machines share one queue inside your infrastructure. Nothing is metered per minute.
One history. Runs, schedules, bus events and child runs are the same product with one timeline, so the thing that happens after the deploy doesn’t move to a different tool with its own logs.
The part that isn’t CI
Section titled “The part that isn’t CI”The reason to move a pipeline here is usually the deploy that watches itself. One run builds the commit, ships it to staging, sleeps through a soak subscribed to alert events, waits for a human, promotes, and reports on how the release landed a week later. It is one script and one run:
spool step build -- make releasespool step deploy-staging -- ./deploy.sh stagingspool sleep 2wverdict="$(spool wait-signal ship)"spool step deploy-prod -- ./deploy.sh productionKill the agent, restart the server, come back in a fortnight: the run resumes
from its last checkpoint. See the authoring model for
the rules that make that work, and examples/release-watch for the whole
thing including the report at the end.
What we don’t have yet
Section titled “What we don’t have yet”- A status badge endpoint.
- A marketplace. Steps are shell commands and scripts in your language; there is no third-party action to pin to a SHA.