# PRD prd-new: CI artifact-based coverage and local Firecracker candidate flow - **Status:** Active - **Author:** Claude - **Created:** 2026-07-21 - **Issue:** #446 ## Summary Restructure the CI test pipeline to run each test suite exactly once, upload small `.coverage.*` artifacts, and combine them in a lightweight aggregation job. Move the infra build onto the KVM runner so the ~194 MB rootfs never crosses the network for PRs. On main-branch pushes, publish the byte-identical rootfs that was tested. ## Motivation The prior pipeline had two redundant costs: 1. **Duplicate artifact transfers.** `build-infra` (ubuntu-latest) built and uploaded the ~194 MB rootfs; `integration-firecracker` downloaded it; the `coverage` job downloaded it a second time. Combined download overhead: ~83 seconds per run, plus the ~70-second upload. 2. **Duplicate test execution.** `integration-firecracker` ran the Firecracker integration suite; `coverage` ran the entire unit + integration suite again on the same KVM runner to collect coverage data. Every line of Firecracker code was tested twice per CI run. ## Goals - Each test suite (unit, integration-docker, integration-firecracker) executes exactly once per workflow run. - PRs incur no large artifact transfers — the rootfs stays on the KVM runner. - Main-branch pushes publish a byte-for-byte identical rootfs to the one that passed the integration tests. - Concurrent workflow runs cannot cross-publish candidates (naturally enforced by Gitea Actions' per-run artifact scoping). - Failed or cancelled runs block publication (enforced by the `needs:` chain on `publish-infra`). ## Non-goals - Changing test semantics or the coverage policy (ADR 0004). - Removing the KVM runner guard on `integration-firecracker` and `coverage`. - Changing how `publish_infra.py` builds or uploads the rootfs. ## Design ### Job graph ``` unit ──────────────────────────────────┐ integration-docker ────────────────────┤──► coverage ──► publish-infra (main only) integration-firecracker (KVM) ─────────┘ ``` ### `unit` Unchanged except: `coverage run` writes `--data-file=.coverage.unit`; the file is uploaded as the `coverage-unit` artifact. ### `integration-docker` Adds a `coverage` install step. `coverage run` writes `--data-file=.coverage.docker`; the file is uploaded as `coverage-docker`. ### `integration-firecracker` (KVM runner) Replaces the old `stage-firecracker-inputs` → `build-infra` → download chain: 1. Builds the infra candidate locally with `BOT_BOTTLE_FC_DROPBEAR=/var/cache/bot-bottle-fc/dropbear`. 2. Boots the candidate and runs integration tests with coverage, writing `.coverage.firecracker`. 3. Uploads the small `coverage-firecracker` artifact unconditionally. 4. On main-branch pushes only, uploads the rootfs as `infra-candidate` and the dropbear as `firecracker-inputs` so `publish-infra` can verify and publish the byte-identical artifact. ### `coverage` Moves from a KVM runner to `ubuntu-latest`. No tests are re-executed: 1. Downloads `coverage-unit`, `coverage-docker`, and `coverage-firecracker`. 2. Runs `scripts/coverage.sh aggregate critical`, which calls `coverage combine` then `coverage report`. 3. Runs the diff-coverage gate (`scripts/diff_coverage.py`). Coverage files use `relative_files = True` (`.coveragerc`) so they combine cleanly across runners with different absolute workspace paths. ### `publish-infra` Depends on all four predecessor jobs (unchanged gate). Downloads `infra-candidate` and `firecracker-inputs` that were uploaded by `integration-firecracker` on main — the same byte sequence that passed the integration tests. ### Eliminated jobs - `stage-firecracker-inputs`: existed only to copy the dropbear to ubuntu-latest for `build-infra`. No longer needed. - `build-infra`: the infra candidate is now built on the KVM runner in `integration-firecracker`. ### Script changes `scripts/coverage.sh` gains an `aggregate` mode (`coverage.sh aggregate [critical]`) that combines pre-existing `.coverage.*` files instead of re-running tests. The existing run mode (`coverage.sh [critical]`) is preserved for local dev.