CI feedback in seconds, before you push. Most CI tools start the clock at
git push. Rehearse runs your.github/workflows/*.ymlon your laptop, on every save, in sub-second. Same YAML, three execution targets, no lock-in.
Rehearse is an OSS GitHub Actions toolchain with a fast pre-push loop
plus a same-binary hosted offload target — the combination is what
distinguishes us from sibling local runners (nektos/act
is the OG in this space; we share the category but ship more features
and the hosted-offload story). It reads your existing
.github/workflows/*.yml and executes it three ways:
- Locally (
runner run) — host subprocess or container backend, sub-second on the typey demo workflow - Pro VM (
runner run --remote) — single-tenant VM with whole-rootfs persistence, ~5s on the same workflow - GitHub Actions (
git push) — same compiled YAML, no Rehearse runtime needed, ~95s for the same workflow
Same source, same output, no lock-in. Free, Apache 2.0, source on github.com/plsft/rehearse.
Comparison with hosted-runner replacements (Blacksmith, Ubicloud, RunsOn, BuildJet, Namespace): see rehearse.sh/vs for the honest take. Short version: those products optimize execution target #3 (post-push). Rehearse adds targets #1 and #2 (pre-push + offload) — different shape, complementary. The OSS runner is your moat against vendor risk: if any hosted-runner SaaS raises prices or disappears, your CI keeps running on a laptop without them.
npm install -g @rehearse/runner
# inside any repo with a .github/workflows/*.yml
runner run .github/workflows/ci.yml
runner watch .github/workflows/ci.yml # re-run on save
runner install-hook # pre-push git hookThe same 9-cell matrix workflow run on three targets — measured today against plsft/typey, a real third-party Bun CLI repo (not a fixture we control):
| Target | Wall | vs GH |
|---|---|---|
Local (runner run, warm) |
1.5s | 63× faster |
Pro (runner run --remote, warm) |
5s | 19× faster |
| GitHub Actions (3 OS × 3 Bun, 9 cells) | 95s | baseline |
Workflow exercises checkout + setup-bun + install + format + lint + tsc
- test + build per cell. The speedup is GH's spin-up tax — cold container, fresh toolchain install, cold module caches on every push. Local skips all of it; Pro keeps everything warm between runs.
Most folks comparison-shop us against act — the OSS project we share
a category with. v0.3.11 warm, GH-hosted Linux:
| Target | Rehearse | act | Speedup |
|---|---|---|---|
our-ci (typecheck + tests, 2 parallel jobs) |
12.19s | 63.78s | 5.23× |
node-matrix [18.x, 20.x, 22.x] (3 parallel cells) |
1.12s | 10.07s | 8.99× |
service-postgres (postgres:16 + 4 psql) |
10.97s | timeout (>360s) | 32.82× — act fails |
hono-bun (real OSS, honojs/hono bun job) |
7.58s | n/a (act lacks bun) | — |
Per-OS numbers and full methodology in
bench/RESULTS.md. Reproducible with
gh workflow run bench.yml.
runner/ — @rehearse/runner — the CLI (binary: `runner`)
ts-ci/ — @rehearse/ci — author workflows in TypeScript
git-engine/ — @rehearse/git-core — pure-TypeScript git protocol
cli/ — @rehearse/cli — `rh` CLI (compile / convert TS pipelines)
bench/ — runner-vs-act bench harness + results
poc/ — fixture workflows used by the bench harness (vite/hono/etc.)
.rehearse/ — TypeScript source for this repo's own CI
.github/ — generated workflow YAML (do not edit by hand)
run:steps withbash/pwsh/cmd+ full$GITHUB_OUTPUT/$GITHUB_ENV/$GITHUB_PATH/$GITHUB_STEP_SUMMARYcontract- 18 in-process action shims:
checkout,setup-{node,python,go,java,dotnet,bun,pnpm,deno,ruby},rust-toolchain,cache+/save+/restore,upload-artifact,download-artifact,codecov,github-script.setup-dotnetis a real shim that runs Microsoft'sdotnet-install.shand caches the SDK. - JavaScript actions (
runs.using: node12 / node16 / node20, plus forward-compat acceptance of node22 / 24 / 25) — auto-cloned at the requested ref, fullINPUT_*/GITHUB_OUTPUTcontract services:with health checks (Docker network alias wired correctly) — local container backend onlystrategy.matrix(variables × include − exclude) — cells run in parallel via per-cellgit worktreeneeds:with topological scheduling and bounded concurrencyif:on jobs and steps — full context: matrix / env / secrets / vars / needs / steps / job / runner / inputs / github- Local composite actions (
./.github/actions/*) AND remote (org/repo[/sub]@ref— auto-cloned) - Local reusable workflows (
uses: ./.github/workflows/foo.yml) withwith:+secrets: inherit ${{ matrix… }},${{ env… }},${{ secrets… }},${{ vars… }},${{ runner… }},${{ needs.<job>.outputs.<n> }},${{ steps.<id>.outputs.<n> }},${{ github.* }}runner --remoteships the workflow to a Pro VM (auto-detects git origin + SHA + monorepo subdir; ships--env-filesecrets to${{ secrets.* }})
- Remote reusable workflows (
org/repo/.github/workflows/foo.yml@ref) — local form works - OIDC /
id-token: write— use long-lived creds via--env-filefor now concurrency:cancellation — parsed, not enforced- Pro-side
services:— works locally via container backend, not yet on hosted Pro VMs
Roadmap lives on the GitHub issues for the repo.
# Requires Node 22+ and pnpm 9+
pnpm install
pnpm turbo typecheck # passes across all 5 workspace packages
pnpm turbo test # 323 tests passing across all packages
pnpm --filter @rehearse/runner build
node runner/dist/cli.js run .github/workflows/ci.ymlTo reproduce the benchmark vs act:
git clone --depth 1 https://github.com/honojs/hono.git poc/playground/hono
docker pull node:22-bookworm-slim postgres:16-alpine catthehacker/ubuntu:act-latest
pnpm tsx bench/compare.ts --skip-coldAll packages are Apache 2.0, published under the @rehearse npm scope.
| Package | Path | What it does |
|---|---|---|
@rehearse/runner |
runner/ |
The local-first runner CLI |
@rehearse/ci |
ts-ci/ |
Author workflows in TypeScript |
@rehearse/git-core |
git-engine/ |
Pure-TypeScript git protocol |
@rehearse/cli |
cli/ |
rh — compile / convert TS pipelines |
Releases are cut by tagging with v*.*.*. The
release workflow on github.com/plsft/rehearse
handles npm publishing and GitHub Release creation automatically.
# Stable release: bumps version everywhere, commits, tags, pushes.
pnpm release patch # 0.1.0 → 0.1.1
pnpm release minor # 0.1.0 → 0.2.0
pnpm release major # 0.1.0 → 1.0.0
# Prerelease channel: published with `--tag next` (or rc / beta).
pnpm release prerelease # 0.1.0 → 0.1.1-next.0
pnpm release prerelease:rc # 0.1.0 → 0.1.1-rc.0
pnpm release prerelease:beta # 0.1.0 → 0.1.1-beta.0
# Explicit version (rare):
pnpm release 0.2.0-rc.1
# Sanity check what would happen:
pnpm release patch --dryWhat the workflow does on every v*.*.* push:
| Tag form | npm dist-tag |
GitHub Release | npm install @rehearse/runner resolves to |
|---|---|---|---|
v0.2.0 |
latest |
marked --latest |
0.2.0 |
v0.2.0-next.0 |
next |
marked --prerelease |
(unchanged; @next resolves to it) |
v0.2.0-rc.1 |
rc |
marked --prerelease |
(unchanged; @rc resolves to it) |
v0.2.0-beta.3 |
beta |
marked --prerelease |
(unchanged; @beta resolves to it) |
Try a prerelease without affecting latest:
npm install @rehearse/runner@nextGitHub Release notes are auto-generated by
gh release create --generate-notes
based on the PRs and commits since the previous tag — no hand-written
CHANGELOG file to keep up to date.
Apache 2.0 across the board. See LICENSE and individual
package directories for the per-package copies.
See CONTRIBUTING.md. Issues and PRs welcome at
https://github.com/plsft/rehearse.