Repository navigation
ci: build the guest-artifact components in parallel and download only what each job uses - #711
Merged
Pedro Henrique Penna (ppenna) merged 3 commits intoOct 22, 2026
Conversation
Closed
4 tasks
Contributor
There was a problem hiding this comment.
🔵 Needs a closer look
Cross-platform artifact handoffs and release gates need full-matrix CI confirmation and final human review.
No open issues 🎉
What changed in this PR
Reduces CI wait time by separating kernel/Alpine builds from distro-image builds and downloading only the guest components each consumer uses.
Changes:
- Adds validated component selection and separate artifact uploads.
- Updates consumer dependencies, downloads, and required-status checks.
- Adds handoff tests and documents the new job structure.
| File | Description |
|---|---|
scripts/test_nvx_tools.py |
Tests component selection, handoffs, and gates. |
scripts/nvx_tools/ci.py |
Requires the distro-artifact job when workloads run. |
doc/ci.md |
Documents producers, consumers, and dependencies. |
doc/build.md |
Documents debug-kernel component selection. |
.github/workflows/run-platform.yml |
Downloads benchmark and release components selectively. |
.github/workflows/run-nvx-microvm-tests.yml |
Selects downloads for normal and debug tests. |
.github/workflows/ci.yml |
Splits producers and updates handoffs and gates. |
.github/actions/build-guest-artifacts/action.yml |
Validates and builds selected components. |
Review effort: Balanced
Pedro Henrique Penna (ppenna)
force-pushed
the
agents/parallel-guest-artifacts
branch
from
October 21, 2026 19:51
3f6ad20 to
012f72d
Compare
The build-guest-artifacts action could build either every guest image or only the CI debug kernel: guest-images chose the kernel and all four images as one unit, and debug-kernel added the debug kernel. A job could therefore not build the kernel and the Alpine initramfs without the Ubuntu and Azure Linux images, whose cold build takes about four of the five minutes that the shared artifacts job takes when it misses the cache (the Ubuntu determinism check alone takes about three), so every job that waits for the guest artifacts waits for all of them. Replace both inputs with a components input that names the components to restore, build, and save: kernel, alpine, ubuntu (the Ubuntu initramfs, its EROFS distro layer, their determinism check, and the sandbox scratch template), azurelinux, and debug-kernel. A first step validates the list, fails on an unknown or empty selection rather than silently selecting nothing, and reports one output per component, which each restore, build, save, and determinism step requires besides its existing restore-only, save-cache, and cache-hit conditions. The cache keys, paths, and build order do not change. The default selects the kernel and every guest image, as guest-images did, so the shared artifacts job and the Copilot setup restore keep their behavior, and the debug-kernel job now selects only debug-kernel. Tests check that every component step follows exactly its own selection, that the default leaves out the debug kernel, and, with bash, that the selection rejects unknown, empty, glob, and comma-joined lists. doc/ci.md and doc/build.md name the new input. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Every job that boots a guest downloaded the whole guest-artifacts artifact, about 140 MB, although the OpenVMM VMM tests boot only the kernel and the Alpine initramfs, the debug-kernel jobs read only those two besides the debug kernel, and the platform jobs never read the sandbox scratch template. One artifact also tied every job to every component, so no job could wait for only the components that it uses. Upload each component from the artifacts job as a one-day artifact of its own, under its build filenames: guest-kernel, guest-alpine, guest-ubuntu, guest-ubuntu-distro, guest-ubuntu-scratch, and guest-azurelinux, beside the existing guest-debug-kernel. Each holds the files of one cache entry, with their provenance and manifests, except guest-ubuntu-scratch, which holds the scratch template that the job formats. The VMM tests download the kernel and the Alpine initramfs, the microVM test jobs every component, and the debug-kernel jobs only the kernel, the Alpine initramfs, and the debug kernel. The platform jobs download every component but the scratch template, and the Ubuntu and Azure Linux images only when they package a release. Each download names its artifact, so a missing component fails the job at once rather than a later step that boots it. A structural test maps every step of each job that downloads guest artifacts to the guest files that it reads: through the nvx.py command or local action that it runs, the guest and kernel that test-microvm selects, and the build paths that its script names, with each step's condition evaluated for every caller and matrix leg. It fails when a job lacks a component that a step reads, downloads one that none reads, waits for a guest artifact job that uploads none of its components, or runs a command or local action that it cannot classify, and it checks that the uploads cover every file that the guest builds produce. doc/ci.md describes the handoff. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The artifacts job restored or built every guest component one after another, and every job that boots a guest waited for all of them. From 2026-10-19, when the guest keys started to cover the Rust guest sources, to 2026-10-21, 159 of 200 artifacts jobs missed the cache and took a median 5.3 minutes instead of 0.3, and 10.3 with a cold kernel. With a warm kernel, which 146 of them had, the Alpine initramfs takes 0.9 of those minutes, while the Ubuntu initramfs, its determinism check, its distro layer, and the Azure Linux initramfs take 4.1. The OpenVMM VMM tests boot only the kernel and the Alpine initramfs, but their Windows job started a median 5.2 minutes later on those cold runs, and it finished last in 74 of the 119 cold runs that ran them. Replaying those runs' cache keys shows that the narrower keys of #699, which has since keyed the guest images on the guest binaries' inputs alone, together with the reuse of a pull request's images on its dev push that #702 proposes, would leave 91 of 172 runs cold instead of 137: most of the rest are pull requests that change the guest binaries, which also run the VMM tests. Replaying the jobs of the 27 runs in which no self-hosted job queued 2 minutes or more, with their measured queue times and durations, moves their p90 from 27.3 to 23.1 minutes and their p99 from 28.2 to 24.0 when, with both, the VMM tests wait only for the kernel and the Alpine initramfs. Splitting the four components into four jobs gains nothing more there. Split the artifacts job in two. artifacts now builds the kernel and the Alpine initramfs, which every job that boots a guest reads, and the new distro-artifacts job builds the Ubuntu initramfs, its distro layer, the sandbox scratch template, and the Azure Linux initramfs beside it, under the same condition. Each uploads the artifacts of its own components, and each still restores its caches and builds, verifies, and saves the components that miss, so the determinism check, the provenance, and the cache keys do not change. The OpenVMM VMM tests and the debug-kernel jobs keep waiting for artifacts alone; the microVM test and platform jobs, which read the Ubuntu and Azure Linux images, wait for both. distro-artifacts builds the base and guest-binary Docker stages again, about 45 seconds of GitHub-hosted time, and its consumers wait for it about as long as they waited for the single job. check-required-ci expects it to succeed whenever the workloads run, so a failed or skipped build fails the required status check, and the release and the performance baseline, which require the platform jobs, stay blocked. Tests check that every guest artifact job builds exactly the components that it uploads and no component builds twice, that the three jobs run beside one another under one condition, the jobs that the microVM and platform jobs wait for, and the required-status policy, and the structural test now checks each job's dependencies on the two producers. doc/ci.md describes the producers, the handoffs, and the measurements. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna)
force-pushed
the
agents/parallel-guest-artifacts
branch
from
October 21, 2026 22:56
012f72d to
46da18e
Compare
Pedro Henrique Penna (ppenna)
deleted the
agents/parallel-guest-artifacts
branch
October 22, 2026 11:08
This was referenced Oct 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #703.
Part of #430.
Summary
The shared
artifactsjob restored or built the kernel, Alpine, Ubuntu, Ubuntu distro layer, and Azure Linux images one after another, and every job that boots a guest downloaded all of them and waited for all of them. This PR:build-guest-artifactsbuild any subset of the guest components (acomponentsinput replacesguest-imagesanddebug-kernel);guest-kernel,guest-alpine,guest-ubuntu,guest-ubuntu-distro,guest-ubuntu-scratch,guest-azurelinux, besideguest-debug-kernel), and makes each job download only the components that it reads;artifactsnow builds the kernel and the Alpine initramfs, and a newdistro-artifactsjob builds the Ubuntu and Azure Linux images beside it, so the OpenVMM VMM tests and the debug-kernel jobs no longer wait for the Ubuntu and Azure Linux builds.The debug-kernel lane, the Ubuntu determinism check, the provenance, and the cache keys, which #710 (#699) narrowed to the guest binaries' inputs, are unchanged.
Why
The measurements are in this comment on #703. In short, over the 200
artifactsjobs from #561 (2026-10-19 10:52 UTC, when the guest keys started to cover the Rust guest sources) to 2026-10-21 13:55 UTC, before #710 merged:guest-artifactsartifact; the VMM tests and the debug-kernel jobs need only the kernel and the Alpine initramfs.Cold-path evidence
This PR's CI, every image cold. #652 changed the guest binaries' sources, so in run 39690060426 of the head
46da18e49, every guest image missed the cache, while the kernel and the debug kernel hit. That is the common cold case:artifactsdistro-artifacts¹ Including the base and guest-binary Docker stages, which
artifactsbuilds too.Both producers started 0.4 min after the run was created.
artifactsfinished, and the KVM debug-kernel job finished at 5.2 min, beforedistro-artifactsdid (5.7 min).distro-artifactsfinished.Experiment, every component cold. My previous head's run hit the cache, because #710's
devpush had just built and saved its new keys. To also measure a cold kernel, I ran ci.yml's two producers once on a temporary branch on GitHub-hosted runners: unchanged, but without their change-detection dependency, with a comment line added to.dockerignore(which every guest key hashes) so that every component missed, and withsave-cache: false. The experiment is run 39665813966. The baseline is the single job of #710'sdevpush 39661815501, which also built every component cold under the same keys an hour earlier:artifactsjob (39661815501)artifacts(39665813966)distro-artifacts(39665813966)² Including the base and guest-binary Docker stages, which the other job builds again (0.6 min more).
Both jobs started 5 s after the run was created and uploaded every artifact (
if-no-files-found: error) in 3–8 s. With a cold kernel, every guest consumer can start after about 7.0 min instead of 10.9. With a warm kernel,artifactstakes about 1.0 min, as in this head's run above. So the VMM tests and the debug-kernel jobs start about 4 min earlier, and the microVM test and platform jobs wait fordistro-artifactsabout as long as for the single job. The experiment saved no cache, and its branch is deleted.Changes
build-guest-artifacts: acomponentsinput (kernel,alpine,ubuntu,azurelinux,debug-kernel; default all butdebug-kernel) replacesguest-imagesanddebug-kernel. A first step validates it and fails on an unknown or empty selection, and every restore, build, determinism, and save step requires its component. The keys, which end with the guest binaries' source identity, are untouched. The Copilot setup keeps restoring every guest image through the default.ci.yml:artifacts(now Build kernel and Alpine artifacts) buildskernel alpine; the newdistro-artifacts(Build Ubuntu and Azure Linux artifacts) buildsubuntu azurelinuxbeside it, under the same condition (ci: classify microVM tests, benchmarks, and artifact builds separately #712'srun-guest-artifacts);debug-kernelbuildsdebug-kernel.if-no-files-found: error.artifactsanddistro-artifacts; the VMM tests and the debug-kernel jobs still wait only forartifacts(anddebug-kernel).Required status checkchecksdistro-artifacts; the release, the performance gate, and performance persistence already require the platform jobs, which need it.Consumers download by name, into
build/, only what they read:guest-kernel,guest-alpineguest-debug-kernelguest-kernel(runner qualification),guest-alpine,guest-debug-kernelguest-kernel,guest-alpine, and, when they package a release,guest-ubuntu,guest-ubuntu-distro,guest-azurelinuxcheck-required-ci(ci.py): expectsdistro-artifactsto succeed wheneverrun-guest-artifactsis set, and to be skipped otherwise, likeartifacts.Docs:
doc/ci.mdgains a Guest artifact handoff section with the producers, artifacts, files, and consumers, and updates the job graph and the debug-kernel lane;doc/build.mdnames the new input.Interaction with the sibling pull requests
The artifact names, one per cache entry plus the scratch template, are the handoff contract: a job that boots the kernel and the Alpine initramfs downloads
guest-kernelandguest-alpineand waits forartifacts, and a job that reads an Ubuntu or Azure Linux image also downloads it and waits fordistro-artifacts. The structural test below checks every consumer, so a sibling that adds one learns at once what it must download and wait for.nvx-rs/**; the split only selects which job restores and builds each key.doc/ci.mdkeeps its GitHub-hosted Linux unit-test leg.distro-artifactsis gated onrun-guest-artifactslikeartifactsanddebug-kernel, and the change-classification test lists it with them. The drift guard counts it among the jobs that build the microVM test and platform jobs' artifacts. The required-results policy has it too (REQUIRED_CI_DISTRO_ARTIFACT_JOB,DISTRO_ARTIFACTS_RESULT). A new assertion checks that every VMM-test input is also a microVM-test input, so whenever the VMM tests select the guest artifacts,distro-artifactshas a consumer. The microVM test and platform jobs checkneeds.distro-artifacts.result == 'success', so a skipped or faileddistro-artifactsfails the required check and blocks the release and performance persistence, which require the platform jobs.c61080e23. ci: queue the dev push runs instead of cancelling the waiting one #734'squeue:concurrency key is untouched, and nvx-rs: add a guest mapping API with explicit export and target roots #652's guest changes are why this head's run built every image cold.steps.components.outputs.<component>instead ofguest-images, run in both producers (each keepsid: guest-artifacts, and needsactions: readand its own key outputs), and its rehearsal job must read both producers' keys. Its key record must not ride in aguest-*component artifact, or the structural test's component table must name it.guest-kernelandguest-alpineinstead ofguest-artifacts, and wait forartifactsonly; its build-only producer needs no guest artifact. Any newnvx.pycommand that a consumer runs goes into the structural test's command table.distro-artifacts; a default-suite job downloads onlyguest-kernelandguest-alpineand waits only forartifacts.Tests
test_ci_jobs_download_exactly_the_guest_artifacts_that_they_read): maps every step of each job that downloads guest artifacts to the guest files that it reads, through thenvx.pycommand or local action that it runs, the guest and kernel thattest-microvmselects, and thebuild/paths that its script names, evaluating each step's condition per caller and matrix leg. It fails when a job lacks a component that a step reads, downloads one that none reads, waits for a guest artifact job that uploads none of its components, or runs a command or local action that it cannot classify. It also checks that the uploads cover every file that the guest builds produce, that each producer builds exactly the components that it uploads and none twice, and, by mocking, thattest-openvmmreads the kernel and the Alpine initramfs. Eight seeded regressions, such as a missing Azure Linux download or an Ubuntu sandbox step in the debug-kernel jobs, each fail it, before and after the rebase.needs, the cache-write drift guard (foursave-cachecallers), the classification drift guard, and the download counts and order.distro-artifactsfollowsrun-guest-artifacts, the drift guard counts it among the producers of the microVM test and platform jobs' inputs, thenvx-rshost-change policy expects it to succeed, and every VMM-test input matches a microVM-test input pattern.Validation
devatc61080e23(ci: classify microVM tests, benchmarks, and artifact builds separately #712, ci: queue the dev push runs instead of cancelling the waiting one #734, setup: install the pinned Rust release without updating rustup #732, Reject unrepresentable sandbox resource limits #735, nvx-rs: add a guest mapping API with explicit export and target roots #652, nvx-rs: separate distro verification from launch planning #653, and nvx-rs: parse the mount mode, owner, and kind names with one function #685) for the head46da18e49:test_nvx_tools,test_markdown_links, andtest_guest_binariesmodules: 609 tests pass, 29 skipped (Linux- or POSIX-only);ruff check,ruff format --check,pyrightfor Linux and Windows,check_markdown_links.py, andgit diff --check: pass;ci.yml, the two reusable workflows, and the Copilot setup: onlyunexpected key "queue" for "concurrency", a false positive of that release on ci: queue the dev push runs instead of cancelling the waiting one #734's key, which GitHub accepts;012f72d4b: the same checks passed on6f0facefd(ci: key the guest images and VMM tests on the guest binaries' sources #710, ci: link the Windows OpenVMM test builds with /Brepro for sccache #714, ci: run the Linux OpenVMM unit tests on a GitHub-hosted runner #717, openvmm: refuse kernel command lines that the guest kernel would truncate #661, and nvx-rs: add the VMM supervisor with a spawn allowlist, stop, and reattach #656; 606 tests), and the CI test classes again onf2b4a11ef(nvx-rs: parse legacy egress rules with the MXC form's helpers #682). CI run 39665274328 passed in full.3f6ad206f: everyvalidate-nvxmodule andtest_adversarial.py,test_hosts.py, and thenvx.py --helpcommands passed, except 18test_setup_azureshell-script failures thatdevshows on this host too (noscripts/setupchange here). CI run 39624800851 passed in full.ci.yml, so its CI runs the full matrix.Expected effect
distro-artifactsabout as long as for the single job, whichdistro-artifactspays by building the base and guest-binary Docker stages again (about 0.6 min of GitHub-hosted time).Post-merge verification
artifactsanddistro-artifactsdurations and the WHP VMM tests' start and wall time.