CI Local
Reproduce a GitHub Actions run on the local machine. The workflow YAML is the spec:
read it, don't guess at what "the lint step" or "the test step" means.
Scope
GitHub Actions only (.github/workflows/*.yml). If the repo uses GitLab CI, CircleCI,
or Jenkins instead, say so and stop: no translation layer exists for those yet
(add one if it comes up twice, not speculatively).
Phase 1: Find the merge-gating jobs
- If invoked with
[workflow-file] and/or --job name, skip the glob: read only
that file, and if --job is given, extract only that job. Otherwise Glob for
.github/workflows/*.yml (and .yaml). If none exist, tell the user there's no
workflow to mirror and stop.
- Read each file. A job gates merges if its workflow triggers on
pull_request
or push to a protected branch: ignore jobs that only run on schedule,
workflow_dispatch, or release unless the user asks for those specifically.
- For a multi-file or multi-job repo, use a haiku
Explore subagent to read all
workflow files in parallel and return a structured list of: job name, trigger,
runs-on, steps (with uses/run/env/working-directory), services:,
strategy.matrix, and any ${{ secrets.* }} references. Keep this out of the
main thread: workflow YAML is verbose and you only need the extracted summary.
No subagent tool available? Read the workflow files directly and extract the
same summary inline.
Phase 2: Translate steps to local commands
Walk the steps in order and translate each one. Do not invent a generic
lint && test && build: use exactly what the workflow does.
| Workflow step |
Local translation |
actions/checkout |
no-op: you're already in the working tree |
actions/setup-node with node-version: X |
fnm use X (or nvm use X) before the next steps; if neither is installed, warn and continue on whatever node -v reports |
actions/setup-python with python-version: X |
uv python pin X / uv run --python X ...; same fallback-and-warn if uv isn't available |
run: <cmd> with a working-directory: or env: block |
run <cmd> from that directory with those env vars exported for just that command |
cache steps (actions/cache) |
no-op: local disk cache already exists |
a run: step gated by if: on OS or event |
skip if the condition can't hold locally (e.g. runs-on: windows-latest step on a Mac), and say so |
If no explicit version is pinned in the workflow, check .nvmrc / package.json#engines.node
or .python-version / pyproject.toml#requires-python before falling back to whatever's
on PATH.
Can't be replicated, flag, don't fake:
services: (Postgres, Redis, etc.): note the service and image; only attempt it if
Docker is available and the user wants the extra step, otherwise mark the steps that
depend on it as skipped.
${{ secrets.* }}: check if a local .env supplies the same variable name; if not,
mark the step as skipped with the missing secret name, never substitute a fake value.
strategy.matrix: run the one combination that matches the local machine (current
node/python/OS); list the other matrix entries as not covered.
Phase 3: Execute sequentially
Run the translated commands via Bash, in the same order the workflow declares them,
stopping to report clearly the moment one fails (don't silently keep going past a
failed lint step and call the run "done"). Capture stdout/stderr for the report.
Phase 4: Report the parity table
CI Local Parity — <workflow file>, job "<job name>"
Step Local result Notes
---------------------------------------------------------------
checkout n/a already in working tree
setup-node 20 ok fnm use 20
lint (eslint) PASS
typecheck (tsc) PASS
test (vitest) FAIL 2 tests failing, see output above
build SKIPPED not run, blocked by failing test
postgres service NOT REPLICABLE no service container locally (Docker not requested)
deploy (secrets.AWS_*) NOT REPLICABLE secret not present in .env
Any version-fallback warning from Phase 2 (pinned node/python version unavailable,
falling back to whatever's on PATH) must surface as a caveat/NOT REPLICABLE note in
this parity table: never absorbed into a PASS.
Every row's result comes from a command you actually ran this session: never write
PASS, FAIL, or a test count you didn't observe in the Phase 3 output.
State plainly at the end whether the branch would pass the real CI gate, and what's
still unverified because it couldn't run locally.
Examples
Targeted job, --job lint given: skip the glob, read only the named job's steps
from the file the user pointed at, translate and run just those, report a parity
table scoped to that one job.
Python repo, no pinned version in the workflow: actions/setup-python has no
python-version: key → check pyproject.toml#requires-python, find >=3.11, run
uv run --python 3.11 pytest. Note the fallback source in the parity table
("version from pyproject.toml, not workflow"), don't silently treat it as pinned.
Matrix build, strategy.matrix: node: [18, 20, 22]: run once on whatever fnm/nvm
resolves locally (say 20), mark 18 and 22 as NOT REPLICABLE: matrix entry not run locally in the table instead of guessing they'd also pass.
Notes
- This is read-only with respect to git: no commits, no pushes, no workflow file edits.
Installing dependencies (
npm ci, uv sync, etc.) as part of a step is expected and fine.
- If the same repo asks for this repeatedly, that's a signal to fix the actual CI quota/outage,
not to keep leaning on the local stand-in: mention that once, don't nag.
1---2name: ci-local3description: Run the checks a GitHub Actions workflow would run, locally, when Actions is unavailable or out of quota. Parses .github/workflows/*.yml, extracts the jobs/steps that gate merges (lint, typecheck, test, build), translates them to local commands respecting the workflow's pinned node/python versions and env, executes them sequentially, and reports a parity table of what passed locally vs. what can't be replicated (service containers, secrets, matrix dimensions) and why. Activates on "CI quota", "Actions is down/unavailable", "run CI locally", "verify like CI would", "run the pipeline on my machine", or a pre-push request to check a branch the way CI checks it. Not for generating a new workflow file (use ci-generate), not for debugging why a specific CI run failed on GitHub (use fix-bug or review-code), and not for the release/hotfix gating process itself (use gitflow), this skill only produces the local stand-in when the real thing isn't reachable.4---56# CI Local78Reproduce a GitHub Actions run on the local machine. The workflow YAML is the spec:9read it, don't guess at what "the lint step" or "the test step" means.1011## Scope1213GitHub Actions only (`.github/workflows/*.yml`). If the repo uses GitLab CI, CircleCI,14or Jenkins instead, say so and stop: no translation layer exists for those yet15(add one if it comes up twice, not speculatively).1617## Phase 1: Find the merge-gating jobs18191. If invoked with `[workflow-file]` and/or `--job name`, skip the glob: read only20 that file, and if `--job` is given, extract only that job. Otherwise `Glob` for21 `.github/workflows/*.yml` (and `.yaml`). If none exist, tell the user there's no22 workflow to mirror and stop.232. Read each file. A job **gates merges** if its workflow triggers on `pull_request`24 or `push` to a protected branch: ignore jobs that only run on `schedule`,25 `workflow_dispatch`, or `release` unless the user asks for those specifically.263. For a multi-file or multi-job repo, use a haiku `Explore` subagent to read all27 workflow files in parallel and return a structured list of: job name, trigger,28 runs-on, steps (with `uses`/`run`/`env`/`working-directory`), `services:`,29 `strategy.matrix`, and any `${{ secrets.* }}` references. Keep this out of the30 main thread: workflow YAML is verbose and you only need the extracted summary.31 No subagent tool available? Read the workflow files directly and extract the32 same summary inline.3334## Phase 2: Translate steps to local commands3536Walk the steps in order and translate each one. Do not invent a generic37`lint && test && build`: use exactly what the workflow does.3839| Workflow step | Local translation |40|---|---|41| `actions/checkout` | no-op: you're already in the working tree |42| `actions/setup-node` with `node-version: X` | `fnm use X` (or `nvm use X`) before the next steps; if neither is installed, warn and continue on whatever `node -v` reports |43| `actions/setup-python` with `python-version: X` | `uv python pin X` / `uv run --python X ...`; same fallback-and-warn if `uv` isn't available |44| `run: <cmd>` with a `working-directory:` or `env:` block | run `<cmd>` from that directory with those env vars exported for just that command |45| cache steps (`actions/cache`) | no-op: local disk cache already exists |46| a `run:` step gated by `if:` on OS or event | skip if the condition can't hold locally (e.g. `runs-on: windows-latest` step on a Mac), and say so |4748If no explicit version is pinned in the workflow, check `.nvmrc` / `package.json#engines.node`49or `.python-version` / `pyproject.toml#requires-python` before falling back to whatever's50on `PATH`.5152**Can't be replicated, flag, don't fake:**53- `services:` (Postgres, Redis, etc.): note the service and image; only attempt it if54 Docker is available and the user wants the extra step, otherwise mark the steps that55 depend on it as skipped.56- `${{ secrets.* }}`: check if a local `.env` supplies the same variable name; if not,57 mark the step as skipped with the missing secret name, never substitute a fake value.58- `strategy.matrix`: run the one combination that matches the local machine (current59 node/python/OS); list the other matrix entries as not covered.6061## Phase 3: Execute sequentially6263Run the translated commands via `Bash`, in the same order the workflow declares them,64stopping to report clearly the moment one fails (don't silently keep going past a65failed lint step and call the run "done"). Capture stdout/stderr for the report.6667## Phase 4: Report the parity table6869```70CI Local Parity — <workflow file>, job "<job name>"7172Step Local result Notes73---------------------------------------------------------------74checkout n/a already in working tree75setup-node 20 ok fnm use 2076lint (eslint) PASS77typecheck (tsc) PASS78test (vitest) FAIL 2 tests failing, see output above79build SKIPPED not run, blocked by failing test80postgres service NOT REPLICABLE no service container locally (Docker not requested)81deploy (secrets.AWS_*) NOT REPLICABLE secret not present in .env82```8384Any version-fallback warning from Phase 2 (pinned node/python version unavailable,85falling back to whatever's on `PATH`) must surface as a caveat/NOT REPLICABLE note in86this parity table: never absorbed into a PASS.8788Every row's result comes from a command you actually ran this session: never write89PASS, FAIL, or a test count you didn't observe in the Phase 3 output.9091State plainly at the end whether the branch would pass the real CI gate, and what's92still unverified because it couldn't run locally.9394## Examples9596**Targeted job, `--job lint` given:** skip the glob, read only the named job's steps97from the file the user pointed at, translate and run just those, report a parity98table scoped to that one job.99100**Python repo, no pinned version in the workflow:** `actions/setup-python` has no101`python-version:` key → check `pyproject.toml#requires-python`, find `>=3.11`, run102`uv run --python 3.11 pytest`. Note the fallback source in the parity table103(`"version from pyproject.toml, not workflow"`), don't silently treat it as pinned.104105**Matrix build, `strategy.matrix: node: [18, 20, 22]`:** run once on whatever `fnm`/`nvm`106resolves locally (say 20), mark 18 and 22 as `NOT REPLICABLE: matrix entry not run107locally` in the table instead of guessing they'd also pass.108109## Notes110111- This is read-only with respect to git: no commits, no pushes, no workflow file edits.112 Installing dependencies (`npm ci`, `uv sync`, etc.) as part of a step is expected and fine.113- If the same repo asks for this repeatedly, that's a signal to fix the actual CI quota/outage,114 not to keep leaning on the local stand-in: mention that once, don't nag.