ci-here
v0.1.0
Published
Run your GitHub Actions `run:` steps in your ACTUAL local shell before you push — and DIFF your local environment against the CI environment the workflow declares, so you see exactly why "works locally, fails in CI" (or vice-versa). Not Docker, not full C
Maintainers
Readme
ci-here
"Works on my machine, fails in CI" — this tells you exactly why, before you push.
You change a GitHub Actions workflow, push, wait, watch it fail in CI
("continuous integration" — the checks GitHub runs on every push), tweak,
push again. ci-here cuts that loop: it runs your workflow's shell steps
(run: steps) right here in your normal terminal in seconds, and then spells
out, in plain terms, every way your machine differs from the environment CI
expects — which is usually the real reason it passed for you but failed there.
npx ci-here .github/workflows/ci.ymlNo install, no setup, no API key, no internet, no Docker.
Why this exists (and why it is not act)
The write-YAML → commit → push → wait → see-it-fail loop is miserable.
act is the well-known tool here and is
genuinely useful — but it runs your workflow inside a Docker image that is
neither your machine nor GitHub's actual runner. When it behaves differently
from real CI, it doesn't tell you why. That gap — "it passes in act or
locally but fails in CI" — is exactly the thing that hurts.
ci-here does one thing and tries to do it best: tell you, fast and honestly, where your environment and CI's environment disagree. It runs the steps in your environment (the one you actually develop in), then explicitly compares that environment against what the workflow says it needs, and points at the exact mismatches that cause "works locally, fails in CI" (or the other way around). All of this in under a second, with no image to download.
Being blunt about what it does not do is part of the point.
What you get
ENV-PARITY DIFF (local → what this workflow expects)
[HIGH] runner-os: CI: ubuntu (linux) local: darwin
[HIGH] tool-version: node CI: 20 local: v25.9.0
[HIGH] env-missing: APP_ENV CI: production (unset locally)
[MEDIUM] env-missing: CI CI: true (GH always sets this)
[MEDIUM] secret DEPLOY_TOKEN — referenced, name only
→ 5 difference(s) (3 high) are why local≠CI for this job.
LOCAL RUN (your shell — NOT Docker, NOT full CI)
✓ install deps (<per-step wall time shown here>)
✗ unit tests (<per-step wall time shown here>)
• deploy [skipped] ↪ "some-org/deploy@v2" not executed locally (known limitation)
WHAT ci-here DID NOT DO (be honest about the gap):
! 2 `uses:` step(s) were NOT executed locally …Install / run
Zero-install via npx:
# the workflow path is required (ci-here never auto-runs a workflow you
# didn't explicitly point at — it executes shell commands)
npx ci-here .github/workflows/ci.yml
# just the env-parity diagnosis, run nothing
npx ci-here .github/workflows/ci.yml --no-run
# one job, machine-readable
npx ci-here .github/workflows/ci.yml --job build --jsonIf you run npx ci-here with no path, it lists the workflows it found and
exits without running anything — you must name the file.
Requires Node ≥ 20. No other runtime dependencies.
The environment comparison — what it actually checks
This is the heart of the tool: a side-by-side of your machine vs. what the workflow needs (it labels this the "env-parity diff" in its output). For the selected job, ci-here compares your local environment to what the workflow states or implies it needs, and reports each difference with a severity (how likely it is to break things) and a concrete reason:
| Difference | What it catches |
|---|---|
| runner-os / runner-arch | runs-on: says ubuntu/macos/windows; you are on something else. Shell builtins, paths, case-sensitivity, native deps differ. |
| env-value | A env: the workflow declares has a different value locally. |
| env-missing | A declared env: (or the implicit CI=true GitHub always sets) is unset locally. |
| secret-missing | A ${{ secrets.NAME }} reference — reported by name only. ci-here never reads, stores, or prints a secret value, and blanks it locally. |
| tool-version | actions/setup-node@v4 (and setup-python/go/java) pin a version via with:; ci-here probes the tool on your PATH and reports the delta. |
Only env var names the workflow declares are ever looked up by value;
nothing else from your environment is read. Tool probes run --version
only.
Honest scope — read this before you trust a green
ci-here is not a CI emulator. It deliberately does a narrow thing well.
Supported workflow subset (parsed and acted on):
name, top-levelenv:jobs.<id>withruns-on, job-levelenv:, a basic single/multi-keystrategy.matrix(noinclude/exclude), and job-levelif:- steps:
name,run:(+shell:), step-levelenv:,continue-on-error, and a basicif:(false/true/always()/success()/failure()/runner.os == '…'; anything richer is run anyway and flagged "could not evaluate", never silently skipped) uses:steps are parsed and detected, not executed (see below)
What it does NOT do — by design, not a TODO:
- ❌ Does not run
uses:marketplace actions. It cannot faithfully run arbitrary third-party actions locally, so it does not pretend to. Eachuses:step is reported as skipped — not executed locally (known limitation). For recognised setup actions (checkout,setup-node,setup-python,setup-go,setup-java) it instead prints a faithful note about the local equivalent and surfaces the version delta in the env-parity diff. A job whose steps are alluses:is reported as "no run: steps executed — NOT a green CI", never a false pass. - ❌ Does not start
services:(databases, etc.) or run inside a declaredcontainer:— it uses your host shell. Both are reported as documented gaps. - ❌ Does not run reusable workflows (
uses:at job level). - ❌ Does not evaluate the full GitHub expression language,
needs:ordering across jobs,concurrency,permissions, or trigger semantics. - ❌ No multi-document YAML, anchors, or tags (not used by real workflows; it fails loudly rather than mis-parsing).
Therefore: a clean ci-here run means your declared run: steps passed
in your environment, and here are the env differences vs CI. It does not
mean the GitHub run will be green. Use it to kill the fast, dumb failures
(bad command, missing env, wrong tool version, OS-ism) locally in seconds,
and to understand parity — not as a CI replacement.
There are no benchmark numbers in this README because none have been
independently measured; "seconds" means it runs your run: commands
directly with no image to pull.
Safety
- No network. ci-here never makes a network request of its own.
- No secrets. Secret references are surfaced by name only and blanked to empty locally; no code path reads or prints a secret value.
- Non-destructive. The only commands ever executed are the
run:strings your workflow itself declares, in your current directory. ci-here adds no cleanup,rm, force-push, or extra commands. - Never hangs. Every step has a per-step timeout (default 120s,
--timeout); the process group is killed on timeout. - Exit codes:
0all executedrun:steps passed (or--no-run/--list);1arun:step failed/timed out;2usage error.
CLI
npx ci-here [workflow.yml] [options]
--job <id> run only this job (default: all jobs in file order)
--no-run print the env-parity diff ONLY; execute nothing
--list list jobs + run/uses step breakdown, then exit
--json machine-readable JSON (env-parity + per-step results)
--timeout <ms> per-step timeout (default 120000)
--shell <name> shell for run: steps (default bash; cmd.exe on Windows)
--no-color disable ANSI color
-h, --help -v, --versionFeedback
ci-here's value is the accuracy of the env-parity diff. If it ever says local == CI but your real CI still diverges (a false "all clear"), or it flags a difference that does not matter (noise), that report is the single most valuable thing you can send.
Open an issue with the ci-here-feedback label (or use the
issue template). Your words
are stored and read exactly as written — see FEEDBACK.md.
License
MIT.
