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.
Run tests on push
Section titled “Run tests on push”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 testThat clones a public repo; the quickstart has the version for a private one. Try it on your machine, with no server involved:
spool run .spool/ci.yamlEach 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:
spool repo connect https://github.com/me/app --status-credential github-statusThat 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:
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/")))'Vocabulary
Section titled “Vocabulary”| 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 that wait
Section titled “Jobs that wait”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: releasejobs: 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:
spool step build -- make releasespool step deploy-staging -- ./deploy.sh stagingspool sleep 14dverdict="$(spool wait signal ship)"spool step deploy-prod -- ./deploy.sh productionIf 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.
What isn’t here
Section titled “What isn’t here”- 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.