greening-ci-local
The local twin of greening-ci. Instead of watching GitHub Actions and
fixing-then-pushing, this runs the workflow in local Docker containers via
Agent-CI and fixes-then-retries in place. The win: catch a CI failure on
your own machine — before a push, before a remote build-matrix dispatch — without
spending remote runner minutes or shipping a half-broken release.
greening-ci (remote) and greening-ci-local (this) are siblings: same
fix-and-loop discipline, different engine. Reach for local when you want to
validate BEFORE the remote run exists; reach for remote when the run is already
dispatched (or the failure only reproduces on real runners — Depot/macOS VMs).
Requirements — the same ones agent-ci needs
- Docker daemon up. Each job runs in a container. On macOS the fleet uses
OrbStack (
open -a OrbStack; confirm with docker info). If it's down,
agent-ci fails fast with a /var/run/docker.sock error — that's the daemon,
not a workflow failure. Start it, confirm docker info, re-run. Can't start a
daemon → fall back to greening-ci (push + watch remote).
--github-token — every fleet ci.yml calls a SocketDev/socket-registry
reusable workflow; agent-ci needs the token to fetch it (bare flag →
gh auth token).
- macOS matrix legs need
tart + sshpass on Apple Silicon; without them
those legs are SKIPPED, the rest still run. Linux/musl legs run in Docker.
- Some legs genuinely can't run locally (Depot OIDC, runner-only system libs like
libatomic.so.1 missing from the base image). Treat an env-gap failure as
"validated up to the local boundary," not a code defect — see Classify below.
How it drives the fix-and-retry loop
run.mts is eyes-only, the local twin of greening-ci's runner: it launches
Agent-CI with --pause-on-failure, watches for the launcher's exit-77 (a step
paused) or a clean exit (green), dumps the paused-step log tail to a tmp file,
classifies the failure as code-vs-env-gap deterministically, and prints a
JSON verdict on its final line. The retry loop, the budget, and the env-gap
classification are all in the script — the fix-authoring stays yours.
- Invoke the runner. A single workflow (the common case for validating one
release/build workflow):
node .claude/skills/fleet/greening-ci-local/run.mts --workflow .github/workflows/<wf>.yml [--no-matrix]. Omit --workflow to run all
PR/push workflows for the branch. --no-matrix validates one representative
leg fast before the full fan-out; --budget-sec N raises the wall-clock cap
for a full local matrix.
- Parse the last stdout line as JSON. Shape:
{
"status": "green" | "paused" | "error",
"runnerName": "build-curl-linux-x64" | null,
"retryCmd": "agent-ci retry --name build-curl-linux-x64" | null,
"classification": "code" | "env-gap" | null,
"envGapReason": "Depot/OIDC is unavailable locally — needs a real runner" | null,
"logTailPath": "/tmp/greening-ci-local.../paused-step.log" | null,
"elapsedSec": 142
}
- Branch on
status:
"green": done. Every leg that can run locally passed. Report and exit.
"paused" + classification: "code": read logTailPath, author the fix
locally in the checkout (this is the genuine AI judgment — see the
classification table in the remote greening-ci SKILL.md for the common
patterns), then re-invoke the runner with --retry <runnerName> (add
--from-step <N> to skip earlier passing steps). This resumes the SAME
paused runner — never a full pipeline restart.
"paused" + classification: "env-gap": the local boundary, not a defect
(Docker base image missing a runner-only lib, Depot/OIDC unavailable, a
macOS leg skipped for no tart). Record envGapReason, abort the runner if
needed, and report "green up to ; needs a real runner."
"error": Agent-CI itself failed before a pausable step (bad args, daemon
down, workflow parse error). Read logTailPath for the real cause; a
/var/run/docker.sock error means the daemon, not a workflow failure.
- Loop the runner until
status: "green" (all non-skipped legs pass) or the
budget expires.
Budgets
- Single non-matrix workflow: ~10 min wall-clock is plenty for the local legs.
- Full local matrix (Linux + musl): longer — Docker image pulls + per-leg builds.
If a leg hasn't progressed in ~15 min it's wedged (image pull stall / disk),
not slow; investigate rather than wait.
Output
Report, per leg: passed / fixed-then-passed / skipped (why) / env-gap (which
step + that it needs a real runner). A run is "locally green" when every leg that
CAN run locally passed. Name any leg that could only be validated remotely so the
caller knows the remote dispatch still has to cover it.
Relationship to remote greening-ci + republishing-stubs
- A clean
greening-ci-local pass on a release/build workflow is the recommended
pre-flight before the real remote dispatch — it catches code/config breaks
in containers first. republishing-stubs Phase 0.5 calls this on stubs.yml.
- It does NOT replace the remote run for release artifacts: the local pass can't
produce the real Depot cross-builds or sign/publish. After local-green, dispatch
remotely and confirm with
greening-ci --mode=release.
1---2name: greening-ci-local3description: Run Agent-CI locally in Docker, fix paused failures, and retry until the workflow is green.4---56# greening-ci-local78The local twin of `greening-ci`. Instead of watching GitHub Actions and9fixing-then-pushing, this runs the workflow in **local Docker containers via10Agent-CI** and fixes-then-**retries** in place. The win: catch a CI failure on11your own machine — before a push, before a remote build-matrix dispatch — without12spending remote runner minutes or shipping a half-broken release.1314`greening-ci` (remote) and `greening-ci-local` (this) are siblings: same15fix-and-loop discipline, different engine. Reach for local when you want to16validate BEFORE the remote run exists; reach for remote when the run is already17dispatched (or the failure only reproduces on real runners — Depot/macOS VMs).1819## Requirements — the same ones agent-ci needs2021- **Docker daemon up.** Each job runs in a container. On macOS the fleet uses22 **OrbStack** (`open -a OrbStack`; confirm with `docker info`). If it's down,23 agent-ci fails fast with a `/var/run/docker.sock` error — that's the daemon,24 not a workflow failure. Start it, confirm `docker info`, re-run. Can't start a25 daemon → fall back to `greening-ci` (push + watch remote).26- **`--github-token`** — every fleet `ci.yml` calls a `SocketDev/socket-registry`27 reusable workflow; agent-ci needs the token to fetch it (bare flag →28 `gh auth token`).29- **macOS matrix legs** need `tart` + `sshpass` on Apple Silicon; without them30 those legs are SKIPPED, the rest still run. Linux/musl legs run in Docker.31- Some legs genuinely can't run locally (Depot OIDC, runner-only system libs like32 `libatomic.so.1` missing from the base image). Treat an env-gap failure as33 "validated up to the local boundary," not a code defect — see Classify below.3435## How it drives the fix-and-retry loop3637`run.mts` is **eyes-only**, the local twin of `greening-ci`'s runner: it launches38Agent-CI with `--pause-on-failure`, watches for the launcher's exit-77 (a step39paused) or a clean exit (green), dumps the paused-step log tail to a tmp file,40**classifies** the failure as code-vs-env-gap deterministically, and prints a41JSON verdict on its final line. The retry loop, the budget, and the env-gap42classification are all in the script — the **fix-authoring** stays yours.43441. **Invoke the runner.** A single workflow (the common case for validating one45 release/build workflow):46 `node .claude/skills/fleet/greening-ci-local/run.mts --workflow47 .github/workflows/<wf>.yml [--no-matrix]`. Omit `--workflow` to run all48 PR/push workflows for the branch. `--no-matrix` validates one representative49 leg fast before the full fan-out; `--budget-sec N` raises the wall-clock cap50 for a full local matrix.512. **Parse the last stdout line as JSON.** Shape:52 ```json53 {54 "status": "green" | "paused" | "error",55 "runnerName": "build-curl-linux-x64" | null,56 "retryCmd": "agent-ci retry --name build-curl-linux-x64" | null,57 "classification": "code" | "env-gap" | null,58 "envGapReason": "Depot/OIDC is unavailable locally — needs a real runner" | null,59 "logTailPath": "/tmp/greening-ci-local.../paused-step.log" | null,60 "elapsedSec": 14261 }62 ```633. **Branch on `status`:**64 - `"green"`: done. Every leg that can run locally passed. Report and exit.65 - `"paused"` + `classification: "code"`: read `logTailPath`, author the fix66 locally in the checkout (this is the genuine AI judgment — see the67 classification table in the remote `greening-ci` SKILL.md for the common68 patterns), then re-invoke the runner with `--retry <runnerName>` (add69 `--from-step <N>` to skip earlier passing steps). This resumes the SAME70 paused runner — never a full pipeline restart.71 - `"paused"` + `classification: "env-gap"`: the local boundary, not a defect72 (Docker base image missing a runner-only lib, Depot/OIDC unavailable, a73 macOS leg skipped for no tart). Record `envGapReason`, abort the runner if74 needed, and report "green up to <step>; <leg> needs a real runner."75 - `"error"`: Agent-CI itself failed before a pausable step (bad args, daemon76 down, workflow parse error). Read `logTailPath` for the real cause; a77 `/var/run/docker.sock` error means the daemon, not a workflow failure.784. **Loop** the runner until `status: "green"` (all non-skipped legs pass) or the79 budget expires.8081## Budgets8283- Single non-matrix workflow: ~10 min wall-clock is plenty for the local legs.84- Full local matrix (Linux + musl): longer — Docker image pulls + per-leg builds.85 If a leg hasn't progressed in ~15 min it's wedged (image pull stall / disk),86 not slow; investigate rather than wait.8788## Output8990Report, per leg: passed / fixed-then-passed / skipped (why) / env-gap (which91step + that it needs a real runner). A run is "locally green" when every leg that92CAN run locally passed. Name any leg that could only be validated remotely so the93caller knows the remote dispatch still has to cover it.9495## Relationship to remote greening-ci + republishing-stubs9697- A clean `greening-ci-local` pass on a release/build workflow is the recommended98 **pre-flight** before the real remote dispatch — it catches code/config breaks99 in containers first. `republishing-stubs` Phase 0.5 calls this on `stubs.yml`.100- It does NOT replace the remote run for release artifacts: the local pass can't101 produce the real Depot cross-builds or sign/publish. After local-green, dispatch102 remotely and confirm with `greening-ci --mode=release`.