Skip to content

Coming from GitHub Actions, GitLab CI or Buildkite

Testing every push takes a pipeline in your repo plus spool repo connect. This page shows that pipeline, maps the words you already have onto the ones used here, then shows what a pipeline can do after the deploy. For the full GitHub setup, step by step, see Building a GitHub repo.

The pipeline is YAML in your repo, here .spool/ci.yaml; every .spool/*.yaml builds. It clones the commit it was started for, then tests it:

name: ci
env:
SHA: "${{ has(input.sha) ? input.sha : '' }}"
REPO: "${{ has(input.repo) ? input.repo : 'me/app' }}"
jobs:
test:
steps:
- name: test
run: |
git clone --quiet "https://github.com/${REPO}.git" src
cd src
if [ -n "$SHA" ]; then
git fetch --quiet origin "$SHA"
git -c advice.detachedHead=false checkout --quiet "$SHA"
fi
npm ci
npm test

That clones a public repo; the quickstart has the version for a private one. Try it on your machine, with no server involved:

Terminal window
spool run .spool/ci.yaml

Each job starts in an empty directory of its own, removed when the job ends, so the checkout doesn’t stay behind.

Then connect the repo so every push runs it:

Terminal window
spool repo connect https://github.com/me/app --status-credential github-status

That creates a webhook, a handler that starts the pipeline from each pushed commit, and a handler that puts the result on the commit as a hyperspool/ci status. It prints the URL and secret to paste into the repo’s webhook settings. The steps run on managed environments, so there are no runners to set up. Building a GitHub repo walks through it, including the tokens and pull requests.

Branch and path filters go on that command as --when, a CEL expression checked against the push before any run exists:

Terminal window
spool repo connect https://github.com/me/app --status-credential github-status \
--when 'event.ref == "refs/heads/main" && (!event.paths_known || event.paths.exists(p, p.startsWith("api/")))'
Your word Here Notes
workflow file pipeline YAML in your repo, read at the commit being built
on: push spool repo connect --when filters branches and paths
on: pull_request spool repo connect --pull-requests see pull requests; forks run without secrets
on: schedule spool schedule create --cron six fields, seconds first; the schedule keeps a copy of the file
workflow_dispatch spool submit --repo --repo-path runs the file as committed at --ref
paths: / paths-ignore: --when on event.paths guard with !event.paths_known ||
job job one entry under jobs:
step step an entry under steps:; each is a fresh shell, and a job’s steps share one machine
needs: depends_on: same shape
GitHub-hosted runner managed environment started on demand: containers for most steps, 16-core amd64 VMs with sudo for requires: [build], depending on your plan
self-hosted runner your own agent for arm64, macOS, private networks or special hardware
runs-on: labels requires: a job goes only to agents with those capabilities
runs-on: ${{ matrix.os }} requires: ["${{ matrix.target.cap }}"] read per combination, so one job spans platforms
matrix: matrix: one job per combination, read as ${{ matrix.<name> }}
env: / vars env: / vars: vars: is for values expressions read
secret spool secret set an env var in every job
secret for one job credentials: on the job, from spool credential set fails on an untrusted run unless optional_when_untrusted: true
concurrency: concurrency: group plus queue, cancel-running, cancel-queued or skip
a job’s time limit timeout: up to 3 hours 40 minutes on a managed environment
continue-on-error: continue_on_error:
if: ${{ !cancelled() }} if: always() runs even when a job it depends on failed; a failed job holds back only what depends on it
artifact upload spool artifact put --cache stores a build cache
artifact download spool artifact get exits 4 on a miss, so a cache lookup is plain shell
job outputs outputs: spool output set key=value from a step
reusable workflow workflow: job runs another pipeline as a child run
environment approval approval: job can sit anywhere in the pipeline; rejecting cancels the run

Jobs can wait. An approval: job waits for a person and holds no machine while it does, so a pipeline can stop for a decision and pick up hours later with its state intact:

name: release
jobs:
deploy-staging:
steps:
- name: deploy
run: ./deploy.sh staging
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. Neither has a time limit: a run waiting a month holds no agent while it waits.

Runs, schedules, events and child runs share one timeline. One run can build the commit, ship it to staging, sleep through a soak, wait for a person, and promote. As a script:

Terminal window
spool step build -- make release
spool step deploy-staging -- ./deploy.sh staging
spool sleep 14d
verdict="$(spool wait signal ship)"
spool step deploy-prod -- ./deploy.sh production

If the agent dies or the service restarts during those two weeks, the run resumes from its last checkpoint. The authoring model has the rules that make that work.

  • A status badge endpoint.
  • A marketplace. Steps are shell commands and scripts in your language.
  • A GitHub app. Hyperspool reaches GitHub with tokens you store as credentials, and a webhook you add to each repo (or once to a GitHub org).
  • Managed arm64 or macOS machines. Run your own agents for those.