build-ops — the CI runner box
DEPLOYED and serving at https://build-ops.testtemplate.org.
Be careful with older notes about this Worker: README.md, CLAUDE.md and pacer-agent-system-doctrine.md § 7 have all said "not deployed; has never served a request", and this file said it too until 2026-09-17. It is wrong. The Worker answers on its custom domain and enforces its own token gate — recorded as #50 on 2026-09-10 and confirmed again on 2026-09-17. When it first went live, and by what route, is still not established.
What is verified, and what is not. status, backups and terminate were exercised live on 2026-09-17, after #80 corrected how this Worker addresses the box. provision and start have not been run — they spend money, and nothing has needed a runner box since. So this Worker reports correctly; whether it can successfully build a box end to end is untested.
cluster-ops' runner op remains the path ADR 0038's own notes point at, and is the one that has actually provisioned boxes — see cluster-ops.md § The runner op.
ADR 0038 gives this Worker the lease, run log, watchdog and UI for the ephemeral OCI instance that runs Docker builds outside the cluster and hosts the self-hosted runners. It holds no cloud-provider client: it delegates its OCI calls to cluster-ops, exactly as ai-ops does.
Everything is gated, /docs included — it serves no page of its own yet, so an unauthenticated route added "for the UI" before then would be a gate hole with no user.
Routes
GET /version
GET /status?arch=x86|arm
GET /backups?arch=x86|arm
POST /provision {"arch": "...", "mode": "...", "confirm": true}
POST /start {"spendConfirm": true, "arch": "..."}
POST /pause {"arch": "..."}
POST /terminate {"resource": "runner", "arch": "..."}
POST /backup {"arch": "..."}
POST /restore {"resource": "runner", "backupId": "...", "arch": "..."}
GET /runs?limit=N
GET /runs/:id
GET /runs/:id/log?offset=N
arch is on every lifecycle route, and it is not optional detail. The two boxes have separate instances, separate volumes and separate backup lists, so a call that omits it acts on x86 and never touches the ARM box. x86 or arm; anything else is a 400 naming the valid set.
This Worker serves no page. GET / is a 404 — the UI ADR 0038 gives it was never built, and the estate dashboard at https://ai-ops.testtemplate.org/dashboard is this box's screen.
Two names for one box, and why both are right
This Worker calls the box build/runner — its own namespace, where its lease and run log live. cluster-ops knows the same box as runner/oci (or runner/oci-arm), because that is the key its name overrides carry:
runner/oci -> instance pacer-runner-oci volume pacer-runner-oci-data
runner/oci-arm -> instance pacer-runner-oci-arm volume pacer-runner-oci-arm-data
/status reports the translation as ociResource, so a future mismatch is visible in the response rather than only in a volume name nobody recognises.
This was broken until 2026-09-17 (#80). Every volume call went to /oci/resources/build/runner, which derives build-runner-data — a name nothing has ever created — so /status?arch=arm reported the volume absent while pacer-runner-oci-arm-data existed with 50 GB of real state. Had provision run, it would have created a second empty volume while the TLS certificate and runner registration sat on the first.
Two name-backs, and they are different strings. You name the box back as runner; this Worker translates that to oci / oci-arm for cluster-ops, whose gate compares the name-back against its own path segment. Forwarding your word verbatim would fail that gate and read as your typo.
It reads cluster-ops' run prefix, deliberately
/runs* here reads the ops-runs/ prefix that cluster-ops owns, not a prefix of its own. That is correct rather than sloppy: a provision run is literally a cluster-ops ops box, launched by cluster-ops and reporting where every other ops box reports. Owning a second prefix would produce a run that appears in one Worker's list and not the other's.
The standing rule this Worker exists to serve
Anything that builds or pushes a Docker image runs on the self-hosted OCI runner box, and the box is torn down afterwards with its volume intact.
build / push an image -> the self-hosted runner box DEFAULT
GitHub-hosted runners -> an option, chosen deliberately NOT the default
This is a default, not a prohibition. ubuntu-latest remains available and is sometimes right — but choosing it is a decision to state in the PR or the log, not something to fall into because a workflow already said runs-on: ubuntu-latest. A runs-on: line already sitting in a workflow is not a decision anyone made recently.
Provision → build → push → destroy, volume intact. A build that leaves the box running is an unfinished build. The volume is never deleted: it carries the TLS certificate and the runner registration, which make the next provision cheap.
What is still unsettled
Stated here so a session does not discover them mid-build and assume it broke something:
- Nobody knows when this Worker first went live. It was believed undeployed for a week while answering on its custom domain and running a
*/10 * * * *watchdog. Three documents still say it is not deployed (#50, #79). provisionandstartare unexercised. They spend money and nothing has needed a box since the addressing was fixed.cluster-opsdeploys were themselves broken — #58. GitHub Actions does not run for this account, so a change to therunnerop could not reach production through CI.scripts/deploy-worker.shis the answer; seecontract.md§ Deploying.
Two related corrections worth carrying, because both were wrong in the obvious direction before being measured:
- The persistent volume is implemented; the attach raced.
runner-box-provision.shhas always hadDATA_VOL_NAME,DATA_MOUNT, and a destroy that detaches and never deletes. What was wrong is subtler: it attached immediately after launch, and **OCI refuses an attach while the instance is stillPROVISIONING**, so the single attempt lost the race and the box came up with no persistent store — behind a warning whose text was garbled because the error had been sent to/dev/null. It now waits forRUNNINGand retries. - Two names for one disk. The script names the volume
${BOX_NAME}-data(pacer-runner-oci-data);cluster-ops' deriver producedrunner-oci-data.startwould have built an empty duplicate, andterminatewould have terminated the box with the real disk still attached and unmounted. Both arches now carryVOLUME_NAME_OVERRIDESentries.
pacer-runner-oci was terminated 2026-09-11 at the owner's instruction to stop billing. The next build provisions a fresh one — that is the intended lifecycle, not an error state.
Rendered from docs/agents/build-ops.md in pacer-serverless, bundled into this Worker. Raw Markdown: ?format=md