Repository navigation
Description
Objective. Restore CI throughput on the GitForge pipeline: the merge campaign is bounded by one runner's container-creation timeout and by the fact that every job rebuilds the world, not by FireCraft's own build times.
Why. Seven pull requests are in flight, all waiting on GitForge receipts, and the queue is serialized behind a single runner with capacity 4. A full branch run is 9 jobs of 10–40 minutes each, and the release job alone is ~67 minutes because it builds the DEB, the tarballs, the CLI and the web bundle on every branch push. Measured on 2026-11-11: 63 queued jobs across 7 runs, with clippy-test queued behind a resource_class_conflict for four different runs at once.
Two infra defects sit under that queue. Neither is a FireCraft code defect, and neither is visible in the FireCraft repo — they live in the runner's deployment — which is why they have gone unrecorded until now.
Measured evidence (from the authoritative store, /var/lib/gitforge/gitforge.db, jobs started 2026-11-10 onward)
| Failure class | Count | Shape |
|---|---|---|
failed to pull image … 404 |
9 | The runner tried to pull an image that was not loaded on its host. The local registry (127.0.0.1:5000) does not carry firecraft-ci-rust at all, so a missing local image is always a 404. Resolved for now: firecraft-ci-rust:1 and :2 are both loaded on the runner host as of 2026-11-11. The class returns whenever an image is rebuilt or a new tag is referenced. |
failed to create workspace container: Timeout error |
2 | The runner's systemd drop-in grants a 600 s sandbox acquisition window (GITFORGE_SANDBOX_ACQUIRE_SECS=600), but bollard's per-request client timeout still caps container creation at 120 s (docker_client_timeout() in crates/gitforge-sandbox/src/docker.rs, defaulting to bollard's DEFAULT_TIMEOUT). The two budgets disagree: a container that takes 121 s to create dies even though 480 s of the acquisition window remain. |
failed to create workspace container: 404 No such … |
1 | A workspace container that vanished between create and use. |
job failed (a step's exit code) |
23 | Mostly the release job failing inside xtask dist --native on branches that carried the DEB desktop integration: debconf: delaying package configuration then a panic in xkbcommon-dl-0.4.2/src/x11.rs — the CI image has no X11, so the desktop-integration path panics rather than degrading. Not on main. |
One of the two timeout failures reddened main itself: run b61f198e (2026-11-11T17:04Z) failed at clippy-test with infrastructure_failure, cancelling its six downstream jobs. main currently has no green GitForge run of its own.
The chain's real cost, measured on the same store (2026-11-11/12)
One branch run, docs/deployment-receipt (PR #127, commit fcfe6db1):
| Job | Started → finished | Duration |
|---|---|---|
fmt-layers |
22:23Z → 22:33Z | 10.0 min |
clippy-test |
03:07Z → 03:39Z | 31.0 min |
wasm-web |
03:47Z → 04:14Z | 26.8 min |
corpus-golden |
04:14Z → 04:25Z | 10.8 min |
perf-budgets |
04:25Z → 04:36Z | 10.4 min |
fuzz |
04:33Z → 05:03Z | 29.2 min |
generated-docs |
05:03Z → 05:14Z | 10.9 min |
release |
05:14Z → running | ~67–88 min |
The first job of that chain finished at 22:33Z and the seventh at 05:14Z — about seven hours of wall clock before the last job even started. Every one of those jobs compiled the workspace from scratch, because Cargo's target output went to a disposable directory inside the job container: no job ever started warm. The release job alone carries the merge receipt for a branch push.
Work
- Make the two budgets agree. Set
GITFORGE_DOCKER_TIMEOUT_SECS=600on the runner service, so bollard's client timeout matches the acquisition window the runner already grants. The code comment atcrates/gitforge-sandbox/src/docker.rs:270states the rule — "keep bollard's default unless the operator raises it" — and the operator raised the acquisition window but not the client timeout. - Stop pulling images that cannot be pulled. Either pre-warm every referenced tag on every runner host before the commit lands, or make the runner fail fast with a clear "image not present on this host" message instead of a registry 404 that reads like a network fault.
- Bring the second runner online.
fedora-dockeris registered with capacity 5 and has been offline since 2026-11-05;t5500-docker(capacity 2) has been offline since 2026-10-11.fedorais ready for it: the CI images:1,:2and:chromiumare already loaded there, the NAS is mounted,/nas/Temp/repos/GitForgeis present, and both the git server (42782) and the API (42781) bind0.0.0.0and answer from it. Registering it roughly doubles fleet capacity. - Do not build the release artifact on every branch push. The
releasejob costs ~67 minutes per run and produces artifacts nobody consumes for a branch. The:2pipeline configuration already in flight on PR docs: reconcile FireCraft status, execution docs, and wiki sources #123 gates it withskip_if; landing that PR is the cheap fix and needs no runner changes. - Start every job from a warm Cargo target tree. The runner can bind-mount a host directory at
/cargo-targetand exportCARGO_TARGET_DIR, so compiled artifacts survive the job — but it takes an exclusive advisory lock on every writable host directory a job mounts, for the job's whole lifetime (GitForge GF-11: two jobs writing one target tree clobber each other's binaries, measured on DIL on 2026-11-05). So the cache directory must be one per job, not one per image: a single shared directory would serialize every Cargo job in the fleet into one lane. PR feat(ci): a warm Cargo cache per job, so a run starts warm #129 (decision 0098) declares{cache_root}/cargo/{image}-<job>per job, and the offline validator now enforces that shape and runs in the GitHub backup CI'sfmtjob. The first run after the change is still cold; every later run of every branch reuses the artifacts.
Acceptance
- A
GITFORGE_DOCKER_TIMEOUT_SECSvalue at least as large asGITFORGE_SANDBOX_ACQUIRE_SECSis set on every runner service, and the setting is recorded where the runner is deployed. - A job whose workspace container takes longer than 120 s to create completes instead of failing with
infrastructure_failure. -
fedora-dockerreports a heartbeat and takes jobs; the runner inventory shows more than one online runner. -
mainhas a green GitForge run on its own head SHA. - A branch push does not build the release artifacts.
- A warm run's per-job durations are read from the authoritative store and recorded here, so the throughput claim rests on a measurement rather than on the mechanism.
Boundaries. These are runner-deployment and GitForge-service changes, not FireCraft source changes. Changing the runner's service configuration, re-registering a runner, and touching the runner registration secret are operator actions that need explicit authorization — the registration token is a shared secret and was not read or moved while preparing this issue. Nothing in this issue has been applied.
Tracking: task #227; campaign RP-07.
Next action (owner: CI/release). Item 4 is a merge, not a change: PR #123 carries the skip_if pipeline. Item 5 is PR #129 and needs the same merge queue to drain before it can be rebased onto the :2 pipeline shape. Item 1 is a one-line service setting that removes a failure class the code already documents the remedy for, and it is the cheapest item on the list. Item 3 needs the runner registration secret, so it needs an operator with that secret; item 2 is a code change in GitForge.
Activity
ci: size the pipeline watchdogs from the fleet's measured wall-clock
ci: size the pipeline watchdogs from the fleet's measured wall-clock
ci: size the pipeline watchdogs from the fleet's measured wall-clock
ci: size the pipeline watchdogs from the fleet's measured wall-clock
ci: size the pipeline watchdogs from the fleet's measured wall-clock
ci: size the pipeline watchdogs from the fleet's measured wall-clock
ci: size the pipeline watchdogs from the fleet's measured wall-clock
ci: size the pipeline watchdogs from the fleet's measured wall-clock
aliasfoxkde commentedon Nov 11, 2026
Fourth defect class found: the pipeline watchdogs are shorter than a passing job can take.
The issue above lists two runner-deployment defects and one release-job cost. A third, in the FireCraft repo itself, is now measured and fixed.
A job's
timeout:in.gitforge.ymlis a watchdog, not a gate: it kills the container and recordstimed_outwith no diagnostic about what was slow. The budgets were sized by guess, and the fleet's own record falsifies the guess. Measured on 2026-11-11 with 9 runs in flight, from the authoritative store:Three jobs finished inside 2 minutes of their budget, and the release job's worst successful run finished 1.7 minutes inside 90. Two perf-budgets runs were killed at 20.2 min and 20.3 min while still working, and one fmt-layers run was killed at 20.5 min.
The cause is the fleet shape this issue already describes: one runner, capacity 4, 16 cores, and each container compiles the workspace. A co-tenant wave inflates a job roughly 4x —
fmt-layersmeasures 0.3 min idle and 20.1 min loaded. A budget that a passing job can outrun is indistinguishable from a failure.Fix (FireCraft repo, no runner change): each budget is now ~2.5x the worst successful duration —
fmt-layers45m,clippy-test150m,wasm-web90m,corpus-golden45m,perf-budgets45m,fuzz120m,generated-docs45m,release180m. No gate is weakened: a budget decides when a container is killed, not whether a check passes. The rationale and the measurement are recorded in the pipeline file itself.The change rides on the eight open PRs (
#113,#118,#119,#122,#123,#124,#125,#127) because each of their receipts needs the headroom to finish;maininherits it as those land. Verified in the store: every new run carries the raised budgets (fmt-layers:2700 … release:10800), and each push superseded its predecessor run, so the queue did not double.This does not address items 1–4 above: the docker client timeout, the image-pull 404s, the second runner, and the release job on every branch push are unchanged. The release job's 180-minute budget buys the receipt time; it does not make the job cheaper — item 4 remains the fix for that.
feat(app): the animation timeline and the Optimize panel (#118)
brand: refresh FireCraft flame identity and editor artwork (#119)
fix(app): a group's bounds must not swallow the canvas origin (#124)
fix(xtask): keep the changelog's squashed entries on regeneration (#125)
docs: reconcile FireCraft status, execution docs, and wiki sources (#123
aliasfoxkde commentedon Nov 12, 2026
Fleet measurement, 2026-11-12 (merge-campaign queue). While waiting on receipts for six merge candidates, the runner inventory was read from the authoritative store and the queue's real shape became visible.
fuzz055e2394— feat/history-rewind-palette @d7586723clippy-testaacdd78e— docs/ledger-post-train @4982c644wasm-web0dda5775— feat/mask-stack-ui-v2 @754b0d80fmt-layers54b2799d— feat/history-rewind-palette @dec2e770(a merge candidate)Finding 1 — a superseded run's in-flight job keeps its runner slot. Runs
055e2394and0dda5775were both markedcancelledat 05:28Z, superseded by the newer pushes on the same refs. Their never-dispatched rows were cancelled, but their running jobs were not:supersede_stale_runsupdates onlystatus IN ('pending','queued')and deliberately leavesassigned/runningrows to the runner lifecycle (crates/gitforge-db/src/queries.rs:920, and the doc comment above it says so explicitly). Both orphans then held a swarmone-docker slot for the rest of their run —fuzzfor 29 minutes andwasm-webfor 27 — producing results for a SHA that is already history. Two of four slots were spent on work whose receipt nobody can use.Finding 2 — a terminal run can have a live child. The run row for
0dda5775readcancelledfrom 05:28Z while itswasm-webjob held a lease with heartbeats through 06:48Z. A receipt read frompipeline_runs.statusis therefore not a receipt read from the work: the run says the push is finished while the runner is still compiling it. A CI receipt for a superseded SHA is worse than no receipt — it looks complete.Finding 3 — the CLI has no verb for the operation the API allows.
POST /api/v1/jobs/{id}/cancelauthorizes the repository owner (authorize_repo→claims.user_id == repo.owner_id), so the FireCraft owner can cancel their own jobs; the route only requiresadmin|maintainerfor other repos' jobs (crates/gitforge-api/src/routes/ci.rs:103,:128,:1433).gitforge pipelinehas nocancel, so the operation is reachable only by hand-rolling a bearer request.gitforge pipeline cancel <run-or-job>(and agitforge job cancel <id>) would close the gap.Next action (owner: CI/release). Item 3 above is a GitForge CLI change and needs no runner change and no registration secret. The slot leak in Finding 1 needs either the supersede path to cancel in-flight jobs (the runner would then release the container) or the CLI verb from Finding 3 to make the operator's recourse available. Both are GitForge-repo work, outside FireCraft's boundary; recorded here because the FireCraft merge queue is what measures them.
docs(release): the live web deployment's digest receipt — it is not t…