Skip to content

kkeeland/perseids

v1.0.0MIT

Run your GitHub Actions CI on fresh cloud machines, every job at once, against your exact local commit. One exit code back.

Your CI takes as long as its slowest job, not all of them added up. Perseids reads the workflow you already have, starts one Boat machine per job and matrix entry in your own account, runs them in parallel, and deletes every machine when it's done.

Benchmark: pretix's real CI took 15m26s to 22m11s on Perseids, against 25m18s on GitHub, for the same commit.

Before you start

You need a Boat account and API key. Large machines (the default) need Boat's $20 plan, and xlarge machines its $100 plan.

Install

curl -fsSL https://raw.githubusercontent.com/kkeeland/perseids/main/install.sh | sh

It clones Perseids into ~/.perseids, links the command into ~/.local/bin (no sudo), and tells you the exact command for anything missing. Needs bash, git, jq, curl, ssh and Ruby 2.6+. macOS has all of them except jq (brew install jq). Run it again to update.

Or by hand:

git clone https://github.com/kkeeland/perseids ~/.perseids
mkdir -p ~/.local/bin && ln -s ~/.perseids/bin/perseids ~/.local/bin/perseids

Add your Boat key

Create an API key in Boat (Settings → API keys), then:

perseids key

It asks once, checks the key with Boat, and saves it to ~/.config/perseids/boat.env with mode 600. Or set BOAT_API_KEY. The key only ever goes to Boat's API. It's never printed and never copied to a machine.

Run

cd your-repo
perseids run                 # every job in .github/workflows/ci.yml
perseids run unit e2e        # only these jobs
perseids run --dirty         # include uncommitted changes
perseids exec 'npm test'     # one command on one machine

Split one slow suite across machines

perseids exec --split 8 'npx playwright test --shard={i}/{n}'
perseids exec --split 8 --files 'test/**/*_test.rb' 'ruby -Itest -e "ARGV.each { |f| require \"./#{f}\" }" {files}'

{i} and {n} become each machine's shard number and the count, for tools that shard on their own (jest --shard, pytest --splits {n} --group {i}). With --files, the files in that commit that match the glob are split into groups balanced by file size, the same way every time, and {files} becomes one machine's group, quoted (it's added at the end if the command doesn't have it). The summary shows each shard's time, and the run fails if any shard fails.

Find the commit that broke it

git bisect tests one commit at a time, about five rounds in a row. perseids bisect tests every commit in the range at once, one machine each, and names the first bad commit in the time of one run.

perseids bisect 'npm test'                  # HEAD against where it left main (or the last 30 commits)
perseids bisect 'make test' --good v1.4.0   # your own range; --bad defaults to HEAD

It prints each commit with pass or fail, then the first bad one: its SHA, subject and author. If the range is longer than -n (default 30), it tests an even spread first, then narrows. It tells you when the results don't line up, such as a flaky test that passes after a failure.

What you get

Use it from your agent

Claude Code

/plugin marketplace add kkeeland/perseids
/plugin install perseids@perseids

Then /ci runs the workflow and reports the verdict.

Codex

codex plugin marketplace add kkeeland/perseids
codex plugin add perseids@perseids

Any agent. Add this to AGENTS.md or CLAUDE.md:

Before saying work is done, commit and run `perseids run`. Report the SHA and the exit code.

Run it from GitHub Actions

Add your Boat key as a repository secret named BOAT_API_KEY, then add this workflow. One small runner orchestrates, and every job of ci.yml runs on its own cloud machine. The result shows up as a job summary.

# .github/workflows/perseids.yml
name: perseids
on: [push, pull_request]
jobs:
  perseids:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: kkeeland/perseids@v1
        with:
          boat-api-key: ${{ secrets.BOAT_API_KEY }}
          workflow: ci.yml

Inputs: workflow, jobs, machine (large or xlarge), max and keep-going.

Exit codes

CodeMeaning
0Every job passed.
1A job failed. The summary names the step and prints the end of its log.
2Incomplete. Something couldn't run on a machine, such as a macOS job. Never reported as green.
99A machine failed, not your code. Run it again.

Settings

VariableDefaultEffect
PERSEIDS_TYPElargeMachine size: small, default, large (8 vCPU, 16 GB) or xlarge (16 vCPU).
PERSEIDS_MAX10Machines at once, across every run on your account.
PERSEIDS_TTL7200Seconds before a machine stops itself.
PERSEIDS_TAIL40Log lines printed for each failed job.
PERSEIDS_EVENTpushThe github.event_name that if: conditions see.

Other commands: perseids ls lists your running machines, and perseids reap deletes any left by a run that was killed.

What it runs

services: (with health checks), strategy.matrix (include and exclude), needs:, if: and ${{ }} expressions, env, working-directory, GITHUB_ENV, GITHUB_PATH, GITHUB_OUTPUT, continue-on-error, timeout-minutes, and the setup-node, setup-go, setup-python, setup-java, setup-ruby, pnpm, bun, deno, uv and rust actions.

It doesn't run macOS or Windows jobs, container: jobs, reusable workflows, or other marketplace actions. Those are reported as skipped and the run exits 2. Secrets are never sent to machines.

Custom machines

Need a database server, browsers or a pinned toolchain? Put the setup in .perseids/setup.sh, run perseids image build once, and commit the .perseids/image file it writes.

Development

ruby test/runner_test.rb     # the runner, on your machine
sh test/install_test.sh      # install.sh, in a throwaway HOME
ruby test/bisect_test.rb     # bisect's logic, on your machine
perseids run                 # this repo's own CI, on machines

License

MIT © 2026 Kevin Keeland