Skip to content

Building a GitHub repo, end to end

This page takes a GitHub repo from nothing to a green hyperspool/ci check on every push. You install the Hyperspool GitHub App on the repo, connect it with one command and commit a pipeline file. There are no tokens to make and no webhook to paste, and the builds run on Hyperspool’s managed environments, so there are no machines to set up either. Plan on about ten minutes.

You need an invitation to hyperspool.dev, the spool CLI (Getting started installs it), and the right to install apps on the GitHub account that owns the repo. On a GitHub org, that means an owner of the org.

Sign in to the console at hyperspool.dev. If you don’t belong to an org yet, the console offers to create one. Create a token under Settings > Tokens, then:

Terminal window
spool login https://api.hyperspool.dev # prompts for the token
spool context get # the server, org and project in use

context get prints the server and the scope, as org/project. Webhooks, repos and runs all belong to that project.

In the console, open Settings > Connections > Repositories and click Install the GitHub App. GitHub asks which account to install it on and which repositories it may see; pick the repo, or several. Then it asks you to authorize the app, which is how Hyperspool checks that the installation you are linking is yours, and sends you back to the console to link the installation to your org.

The app asks for these permissions:

Permission Used for
Contents: read cloning the commit being built
Commit statuses: read and write posting the result on the commit
Pull requests: read building pull requests
Metadata: read required by GitHub for every app
Organization members: read checking that whoever links an installation on an organization owns it

It sends Hyperspool the pushes and pull requests of those repos, and nothing from any other repo. An installation belongs to one Hyperspool org.

Terminal window
spool repo connect https://github.com/me/app
connected me/app (github.com) as me-app
builds: .spool/*.yaml
pull requests: no
webhook: github
handlers: me-app-ci, me-app-status
through: the GitHub App

With no credentials given, repo connect uses the app for any repo it is installed on. In the console, the repo also shows up under the GitHub App on the Repositories tab with a Connect button that does the same.

That one command created a webhook named github, which the app delivers to, a build handler (me-app-ci) that runs your pipeline for each push, and a status handler (me-app-status) that reports the result on the commit. For a private repo, the app gives each job that clones a token for that one repo, made when the job starts and lasting at least 45 minutes; a public repo clones without one. CI from a repo explains each part.

Commit this as .spool/ci.yaml:

name: ci
jobs:
test:
steps:
- name: test
run: |
spool checkout src
cd src
go test ./...

What each part is for:

  • Each push starts this pipeline as a run of its own, with the pushed commit as its input: input.sha, input.ref and input.repo.
  • spool checkout clones the repo at that commit, with the clone credential the repo was connected with: for a private repo, a token from the app. The job clones the repo itself because it runs wherever an agent picks it up, and each job starts in an empty directory of its own, removed when the job ends.
  • A step that needs the token itself, to fetch a second repo or call the GitHub API, can ask for it under credentials: as github-app:me/app.

The job runs in a managed container that has git and Go. A bigger build can ask for a 16-core VM with requires: [build]; Managed environments lists what each one has.

Check the file before you commit it:

Terminal window
spool workflow check .spool/ci.yaml
ok .spool/ci.yaml (pipeline)

Commit .spool/ci.yaml and push. Within a few seconds the commit on GitHub shows a pending hyperspool/ci check (“Build running”), then “Build passed” or “Build failed”, linking to the run in the console.

Each push makes two runs: the build handler’s, which clones the commit and starts the pipeline, and the pipeline’s own. Follow them from the terminal:

Terminal window
spool ls --tag repo:me/app --limit 5 # this repo's builds
spool logs <run> -f # one run's output, live
spool run follow <run> # the live terminal view

In the console, the runs list filters by the same tag, and a run’s page shows its journal, a waterfall and graph view, and live output per step.

If nothing happens, run spool webhook deliveries github (or open Events in the console, under More). The HANDLERS column says what each handler did with the push: started, skipped_when when the filter declined it (a handler for another repo shows that too), or when_error when the filter failed to evaluate. No delivery at all means the app isn’t installed on the repo, or the repo isn’t one it may see. A failure before the pipeline starts, such as a refused clone, shows on the commit as an error status, “The build could not start”.

The pipeline is read from the pushed commit, so a branch that changes .spool/ci.yaml is built with its own version, and a change takes effect on the next push.

To make the check gate merges, add hyperspool/ci as a required status check in the branch’s protection rules on GitHub. Each .spool/*.yaml file reports under its own name, so .spool/lint.yaml would add a hyperspool/lint check.

Connect again with --pull-requests. Running repo connect again updates what is there:

Terminal window
spool repo connect https://github.com/me/app --pull-requests

Pull requests now build when they are opened or reopened and on each new head, from branches of the repo and from forks alike, and get the same hyperspool/ci status. A new head cancels the build of the one it replaced. The pipeline sees input.event as pull_request, plus the pull request’s number and branches.

Who opened the pull request matters:

  • A pull request from a branch of the repo, by an owner, member or collaborator, runs trusted, the same as a push.
  • Anything else runs untrusted: pull requests from forks, and those from bots such as a dependency updater. An untrusted run gets no project secrets, and a job that declares credentials: fails.
  • On a public repo, forks build normally. On a private repo the clone needs a token, which an untrusted run doesn’t get, so a fork’s pull request gets an error status.
  • To trust a bot’s pull requests, run spool webhook set github --manifest github --trust-actor 'renovate[bot]'.

CI from a repo has the full trust model, the input fields and the status rules.

Every push to every branch builds by default. --when takes a CEL expression over the push and declines the rest before any run starts. This builds pull requests and pushes to main only, so a branch with an open pull request isn’t built twice:

Terminal window
spool repo connect https://github.com/me/app --pull-requests \
--when 'has(event.number) || event.ref == "refs/heads/main"'

A declined push gets a success status described “skipped”, so a required check doesn’t wait for it. A top-level if:, paths: or paths_ignore: in .spool/ci.yaml has no effect on these builds. CI from a repo lists the fields, including the changed paths, and Branch routing covers deciding what runs once a build has started.

Give the app the repo first: Add repos or accounts on the Repositories tab opens the installation’s settings on GitHub, or pick “All repositories” there once. Then run repo connect for it, or click its Connect button. A repo the app sees is never built until it is connected.

If you can’t install apps on the account, connect with two fine-grained tokens and a webhook you paste into the repo instead:

  1. On GitHub, under Settings > Developer settings > Fine-grained tokens, create a token with Contents: read (for cloning) and one with Commit statuses: read and write (for posting the result), both limited to the repo. Store each as a project credential:

    Terminal window
    spool credential set gh-read --kind bearer # prompts for the token
    spool credential set github-status --kind bearer
  2. Connect with them. Passing a credential means the app isn’t used, and so does --no-github-app:

    Terminal window
    spool repo connect https://github.com/me/app \
    --repo-credential gh-read --status-credential github-status
  3. It prints an ingest URL, a signing secret (shown once) and the page to paste them into. Add the webhook there with content type application/json, the secret, and the Pushes and Pull requests events. spool webhook deliveries github shows GitHub’s test delivery arriving.

The pipeline above works unchanged, since spool checkout clones with gh-read. CI from a repo covers the webhook in detail, including an org-wide webhook and having Hyperspool register it for you.

repo connect takes a Gitea or Forgejo URL the same way, with tokens and a pasted webhook, since the GitHub App covers github.com only; CI from a repo covers the differences in tokens, webhooks and trust.

The managed environments are amd64 Linux. To build for arm64 or macOS, run agents of your own on those machines, each advertising a capability you name, such as linux-arm64. A matrix then sends one combination to each:

name: ci
jobs:
build:
matrix:
values:
target:
- { cap: build, triple: x86_64-unknown-linux-gnu }
- { cap: linux-arm64, triple: aarch64-unknown-linux-gnu }
- { cap: darwin-arm64, triple: aarch64-apple-darwin }
fail_fast: false
requires: ["${{ matrix.target.cap }}"]
env:
TARGET: "${{ matrix.target.triple }}"
steps:
- name: build
run: |
spool checkout src
cd src
make release TARGET="$TARGET"

The first combination runs on a managed build VM and the other two wait for your agents, whose toolchains must be on their PATH. Your agents also take untrusted work, such as a fork’s pull request on a public repo: it gets no secrets, but its code runs on your machine. Work from untrusted events covers how to set such a machine up.

  • CI from a repo: how repo builds work, running a pipeline on demand, and handlers of your own.
  • Branch routing: different work for different branches.
  • Credentials and Secrets: what a job can use, and what an untrusted run gets.
  • The tour: runs that wait for a person, sleep through a soak, and promote, all in the run that built the commit.