DOCS

CI

Manual-only hosted CI behind production opt-ins; local gates run every change.

About 5 min read · 3 sections

On this page

Hosted CI is manual-only. The workflow has no push, pull-request, or schedule triggers: a branch push, a PR (including into production), a tag, or a schedule never starts a hosted runner. The production workflow is also disabled at the repository level (paused manually on October 14, 2026 per the CI cost policy), so all hosted builds are paused until Edi re-enables it and dispatches it himself. See releasing for the release side of the same pause.

Home CI

.github/workflows/home-ci.yml runs the full gate (npm ci, build, check, test) on an isolated self-hosted runner for each push to main or production and for manual runs. It uses no hosted minutes. Because this repository is public, the runner is guarded in three places:

Guard Where What it stops
Trust check before a runner exists edihasaj/home-runners control Worker (control/src/trust.ts) It registers a one-job runner only for a queued job in a home-ci.yml run that edihasaj started from main or production by push or manual run. A fork pull request can edit runs-on but never gets a runner; its job just waits.
Same check inside the VM job-started hook (broker/hooks/trust_check.py) Repeats the check from the job's own GitHub context and fails the job before any step runs.
Isolation Separate public-ci VM and host key Never shared with private repositories; egress firewall blocks the Mac host, home LAN and Tailscale. Broker and hooks are root-owned.

The workflow has a read-only token, uses no secrets, does not keep checkout credentials, and never prints the environment or host details, because its logs are public. scripts/tests/home-ci-policy.test.mjs enforces that and runs as the job's first step. Pull requests from forks get no home CI.

Validation therefore happens locally on every change:

npm run build
npm run check
npm test

npm test runs the suite under a private temporary folder and a private home folder (HOME is <folder>/home and CODEX_HOME is <folder>/home/.codex, overriding an inherited Codex home, so execution hosts keep nothing in ~/.local/state/gw-exec), so the real ones are left as they were. At most eight test files run concurrently because each can start several CLI processes. It also fails a test that hangs (120 s by default for a test with no timeout of its own), lets a test file exit once its tests have ended, and stops every test process when the run is signalled: the test processes exit on the signal, which stops the Copilot CLIs they started (those run in process groups of their own), and the runner removes the stale sockets its run linked to in /tmp/codex-daemon-<uid>, the one place the pinned SDK fixes outside both folders. Every test process, and every process a test starts with its environment, refuses the real per-user roots (gw-exec, gw-native, gw-ssh, the SDK rendezvous and a linked native Codex home) unless they lie in a temporary folder (src/test-root-guard.ts), so a test file run on its own with node --test fails instead of writing there; run single files through node scripts/run-tests.mjs <file>. As a backstop the runner notes the entries of the real ~/.local/state/gw-native and gw-exec before the run and fails it, naming the files, when a new entry's owner.json points its configuration or native home into a temporary folder; it never removes anything there. The tests that need macOS retain their explicit platform guards.

Hosted jobs (opt-in only)

Every job in .github/workflows/ci.yml carries a job-level guard requiring workflow_dispatch on refs/heads/production with the typed boolean run_build input set to true. The input defaults to false, so a plain dispatch runs nothing. Promotion to production happens before a build is requested; historical tags and old workflow definitions must never be dispatched.

workflow_dispatch on refs/heads/production
├── run_build: true                      → verify (Ubuntu 24.04 ARM)
└── run_build: true + run_native: true   → macos-native (macOS 15 ARM)

Policy guards

Two executable guards read the actual workflow and fail if anyone widens it:

Run both locally before touching CI; neither requires a hosted run.