* fix: skip orphan cleanup when running inside a Docker container
When agent-ci runs nested inside a container (e.g. smoke-bun-setup step 8),
it shares the host's Docker socket. killOrphanedContainers() checks
agent-ci.pid labels using process.kill(pid, 0), but host PIDs don't
exist in the container's PID namespace — so every container looks like
an orphan. This caused the nested agent-ci to force-remove its own
parent container (exit 137).
Both pruneOrphanedDockerResources() and killOrphanedContainers() now
bail out when /.dockerenv exists.
Fixes #228
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: ignore inherited AGENT_CI_DTU_HOST inside containers
When agent-ci runs nested inside a container (e.g. smoke-bun-setup),
the outer container's AGENT_CI_DTU_HOST=host.docker.internal is
inherited. The nested agent-ci used this to tell the inner container
where the DTU is, but the nested DTU is inside the outer container,
not on the host. The inner container got "Connection refused" trying
to reach host.docker.internal:PORT.
Now when /.dockerenv exists, AGENT_CI_DTU_HOST is ignored and the
container's own IP (via hostname -I) is used instead, so sibling
containers can reach the DTU.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: move /.dockerenv guard to call site so tests still work
The /.dockerenv check inside killOrphanedContainers() broke the
shutdown integration tests — they run inside a Docker container (the
test runner) and need the function to work. Moved the guard to the
session bootstrap in cli.ts where the nested agent-ci problem occurs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add global concurrency limiter across all workflows
In --all mode, each workflow created its own per-wave limiter, allowing
20+ containers to run simultaneously and exhaust available memory (OOM
kills with exit 137). Now a single global limiter (CPU/2) is created in
the orchestrator and shared by all handleWorkflow calls, including
single-job workflows and cold-cache first jobs.
Closes #225
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: factor Docker VM memory into concurrency limit
CPU-based limit alone (cpuCount/2 = 7) still causes OOM kills. Now
reads MemAvailable from /proc/meminfo inside the Docker VM and caps
at floor(available / 3GB per container). Falls back to docker info
with a 4GB reserve if busybox isn't available.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: guard orphan cleanup in the function, not just the call site
The /.dockerenv guard was only in cli.ts, but killOrphanedContainers
is also called directly by the Docker integration tests (via a spawned
tsx subprocess). Inside a container, the test's subprocess would see
the parent container's PID label, misidentify it as an orphan, and
kill the container running the tests.
Put the guard back in both functions AND keep the cli.ts guard.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: increase per-container memory estimate to 4 GB
3 GB per container wasn't enough — tests.yml (vitest + Docker
integration tests) and smoke-binary.yml (full build) both got killed
at 3 concurrent containers. 4 GB gives 2 concurrent containers on a
12 GB Docker VM, with ~5.4 GB each.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: mock fs.existsSync in shutdown unit tests for /.dockerenv guard
The unit tests mock execSync but not fs. Inside a runner container,
the real fs.existsSync("/.dockerenv") returns true, causing
killOrphanedContainers to bail before reaching the mocked execSync.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: skip Docker integration tests for orphan cleanup inside containers
The integration tests spawn a real killOrphanedContainers() subprocess.
Inside a container, the function bails due to /.dockerenv (PID namespace
mismatch), so the tests can't exercise it. Skip them with insideDocker().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add --jobs flag and document concurrency
Adds -j/--jobs <n> to override the auto-detected concurrency limit.
Adds a Concurrency section to the README explaining how the limit is
calculated and how to override it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: catch any missing tool in runner image hint, add Nix recipe and docs
Closes #229
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Revert "feat: catch any missing tool in runner image hint, add Nix recipe and docs"
This reverts commit ca261639e9.
* feat: generic missing-tool detection with Nix/xz recipe
Replace the hardcoded per-tool pattern list with a single generic
"command not found" regex that catches any missing tool. Add detection
for the Nix installer's "you do not have 'xz' installed" message.
Update hint to point to runner-image.md docs instead of inlining a
Dockerfile snippet. Add Nix recipe to runner-image.md and a runner
image section to the README.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
13 KiB
Agent CI
Run GitHub Actions on your machine. Caching in ~0 ms. Pause on failure. Let your agent fix it and retry — without pushing.
Agent CI is a ground-up rewrite of the GitHub Actions orchestration layer that runs entirely on your own machine. It doesn't wrap or shim the runner: it replaces the cloud API that the official GitHub Actions Runner talks to, so the same runner binary that executes your jobs on GitHub.com executes them locally, bit-for-bit.
Actions like actions/checkout, actions/setup-node, and actions/cache work out of the box — no patches, no forks, no network calls to GitHub.
Why another local runner?
Traditional CI is a fire-and-forget loop: push → wait → fail → read logs → push again. Every retry pays the full cost of a fresh run. Existing "run actions locally" tools either re-implement steps in a compatibility layer or require you to maintain a separate config. Agent CI does neither.
| GitHub Actions | Other local runners | Agent CI | |
|---|---|---|---|
| Runner binary | Official | Custom re-implementation | Official |
| API layer | GitHub.com | Compatibility shim | Full local emulation |
| Cache round-trip | Network (~seconds) | Varies | ~0 ms (bind-mount) |
| On failure | Start over | Start over | Pause → fix → retry the failed step |
| Container state on failure | Destroyed | Destroyed | Kept alive |
| Requires a clean commit | Yes | Yes | No — runs against working tree |
~0 ms caching
Agent CI replaces GitHub's cloud cache with local bind-mounts. node_modules, the pnpm store, Playwright browsers, and the runner tool cache all live on your host filesystem and are mounted directly into the container — no upload, no download, no tar/untar. The first run warms the cache; every subsequent run starts with hot dependencies instantly.
Pause on failure
When a step fails, Agent CI pauses instead of tearing down. The container stays alive with all state intact — environment variables, installed tools, intermediate build artifacts. Your edits on the host are synced into the container, so you (or your AI agent) can fix the issue and retry just the failed step. No checkout, no reinstall, no waiting.
This makes Agent CI ideal for AI-agent-driven development: point an agent at the failure, let it fix and retry in a tight loop — without the cost of a full remote CI cycle each time.
Real GitHub Actions Runner, real compatibility
Agent CI does not re-implement GitHub Actions. It emulates the server-side API surface — the Twirp endpoints, the Azure Block Blob artifact protocol, the cache REST API — and feeds jobs to the unmodified, official runner. If your workflow runs on GitHub, it runs here.
Quick start
Prerequisites
- Docker — a running Docker provider:
- macOS: OrbStack (recommended) or Docker Desktop
- Linux: Native Docker Engine
Run
# Run a specific workflow
npx @redwoodjs/agent-ci run --workflow .github/workflows/ci.yml
# Run all relevant workflows for the current branch
npx @redwoodjs/agent-ci run --all
Agent CI runs against your current working tree — uncommitted changes are included automatically. No need to commit or stash before running.
Retry a failed step
npx @redwoodjs/agent-ci retry --name <runner-name>
CLI reference
agent-ci run
Run GitHub Actions workflow jobs locally.
| Flag | Short | Description |
|---|---|---|
--workflow <path> |
-w |
Path to the workflow file |
--all |
-a |
Discover and run all relevant workflows for the current branch |
--pause-on-failure |
-p |
Pause on step failure for interactive debugging |
--quiet |
-q |
Suppress animated rendering (also enabled by AI_AGENT=1) |
--no-matrix |
Collapse all matrix combinations into a single job (uses first value of each key) | |
--github-token [<token>] |
GitHub token for fetching remote reusable workflows (auto-resolves via gh auth token if no value given). Also available as AGENT_CI_GITHUB_TOKEN env var |
|
--commit-status |
Post a GitHub commit status after the run (requires --github-token) |
agent-ci retry
Retry a paused runner after fixing the failure.
| Flag | Short | Description |
|---|---|---|
--name <name> |
-n |
Name of the paused runner to retry (required) |
--from-step <N> |
Re-run from step N, skipping earlier steps | |
--from-start |
Re-run all steps from the beginning |
Without --from-step or --from-start, retry re-runs only the failed step (the default).
agent-ci abort
Abort a paused runner and tear down its container.
| Flag | Short | Description |
|---|---|---|
--name <name> |
-n |
Name of the paused runner to abort (required) |
Remote Docker
Agent CI connects to Docker via DOCKER_HOST. By default it uses the local socket, but you can point it at any remote daemon:
DOCKER_HOST=ssh://user@remote-server npx @redwoodjs/agent-ci run --workflow .github/workflows/ci.yml
Docker host resolution for job containers
For default behavior, env overrides, and remote-daemon caveats, see the CLI package docs:
Secrets
Workflow secrets (${{ secrets.FOO }}) are resolved in order:
.env.agent-cifile in the repo root (KEY=VALUEsyntax,#comments supported)- Shell environment variables — any env var matching a required secret name acts as a fallback
--github-token— automatically providessecrets.GITHUB_TOKEN
# All three approaches work:
# 1. .env.agent-ci file
echo "CLOUDFLARE_API_TOKEN=xxx" >> .env.agent-ci
# 2. Inline env vars
CLOUDFLARE_API_TOKEN=xxx agent-ci run -w .github/workflows/deploy.yml
# 3. --github-token for GITHUB_TOKEN specifically
agent-ci run -w .github/workflows/ci.yml --github-token
Environment variables
All configuration is available via environment variables. For persistent machine-local overrides, create a .env.agent-ci file in your project root — Agent CI loads it automatically (KEY=VALUE syntax, # comments supported).
General
| Variable | Default | Description |
|---|---|---|
GITHUB_REPO |
auto-detected from git remote |
Override the owner/repo used when emulating the GitHub API. Useful when the remote URL can't be detected automatically. |
AI_AGENT |
unset | Set to 1 to enable quiet mode (suppress animated rendering). Same effect as --quiet. |
DEBUG |
unset | Enable verbose debug logging. See Debugging for supported namespaces. |
AGENT_CI_GITHUB_TOKEN |
unset | GitHub token for fetching remote reusable workflows. Alternative to the --github-token CLI flag. |
Docker
| Variable | Default | Description |
|---|---|---|
DOCKER_HOST |
unix:///var/run/docker.sock |
Docker daemon socket or URL. Set to ssh://user@host to use a remote daemon. |
AGENT_CI_DTU_HOST |
host.docker.internal |
Hostname or IP that runner containers use to reach the DTU mock server on the host. |
AGENT_CI_DOCKER_EXTRA_HOSTS |
host.docker.internal:host-gateway |
Comma-separated host:ip entries passed to Docker ExtraHosts. Fully replaces the default when set. |
AGENT_CI_DOCKER_HOST_GATEWAY |
host-gateway |
Override the default host-gateway token or IP for the automatic host mapping. |
AGENT_CI_DOCKER_DISABLE_DEFAULT_EXTRA_HOSTS |
unset | Set to 1 to disable the default host.docker.internal mapping. |
AGENT_CI_DOCKER_BRIDGE_GATEWAY |
auto-detected | Fallback gateway IP when Agent CI runs inside Docker and cannot detect its container IP. |
Runner image
By default, jobs run inside ghcr.io/actions/actions-runner:latest — the official self-hosted runner image. It includes the runner agent, Node.js, git, curl, jq, and unzip, but not build toolchains, python3, xz, or other tools that GitHub's hosted ubuntu-latest VM ships.
If a workflow fails with a missing tool, create a Dockerfile to add it:
# .github/agent-ci.Dockerfile
FROM ghcr.io/actions/actions-runner:latest
RUN sudo apt-get update \
&& sudo apt-get install -y --no-install-recommends <your-packages> \
&& sudo rm -rf /var/lib/apt/lists/*
Agent CI picks it up automatically — no flags, no config. The image is built once and cached by content hash.
For the full guide — directory form with COPY support, per-job overrides, common recipes (Rust, Node native modules, Go, Ruby, Nix), the AGENT_CI_RUNNER_IMAGE escape hatch, and build caching details — see runner-image.md.
YAML compatibility
See compatibility.md for detailed GitHub Actions workflow syntax support.
Debugging
Set DEBUG to enable verbose logging. It accepts comma-separated glob patterns:
| Value | What it shows |
|---|---|
DEBUG=agent-ci:* |
All debug output |
DEBUG=agent-ci:cli |
CLI-level logs only |
DEBUG=agent-ci:runner |
Runner/container logs only |
DEBUG=agent-ci:dtu |
DTU mock-server logs only |
DEBUG=agent-ci:boot |
Boot/startup timing logs only |
DEBUG=agent-ci:cli,agent-ci:dtu |
Multiple namespaces |
DEBUG=agent-ci:* npx @redwoodjs/agent-ci run --workflow .github/workflows/ci.yml
Output goes to stderr. If DEBUG is unset, debug loggers are no-ops with zero overhead.
Using with AI coding agents
Install the agent skill:
npx skills add redwoodjs/agent-ci --skill agent-ci
This works with Claude Code, Cursor, Codex, and 40+ other agents. Then add to your agent instructions (CLAUDE.md, .cursorrules, AGENTS.md, etc.):
## CI
Install the agent-ci skill (one-time setup):
\`\`\`bash
npx skills add redwoodjs/agent-ci --skill agent-ci
\`\`\`
Before completing any work, run the `agent-ci` skill to validate your changes locally. If it fails, fix the issue and re-run. Do not report work as done until it passes.
