Skip to content

ci: build the guest-artifact components in parallel and download only what each job uses - #711

Open
Pedro Henrique Penna (ppenna) wants to merge 3 commits into
devfrom
agents/parallel-guest-artifacts
Open

Pedro Henrique Penna (ppenna) wants to merge 3 commits into
devfrom
agents/parallel-guest-artifacts

Conversation

@ppenna

@ppenna Pedro Henrique Penna (ppenna) commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Closes #703.
Part of #430.

Summary

The shared artifacts job 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:

  1. lets build-guest-artifacts build any subset of the guest components (a components input replaces guest-images and debug-kernel);
  2. hands each component over as its own one-day workflow artifact (guest-kernel, guest-alpine, guest-ubuntu, guest-ubuntu-distro, guest-ubuntu-scratch, guest-azurelinux, beside guest-debug-kernel), and makes each job download only the components that it reads;
  3. splits the producer in two: artifacts now builds the kernel and the Alpine initramfs, and a new distro-artifacts job 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 artifacts jobs 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:

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:

Job, images cold Kernel Alpine Ubuntu initramfs Determinism Distro layer Azure Linux Job wall time
artifacts hit 0.9 min 1.0 min
distro-artifacts 1.4¹ 3.0 0.3 0.4 5.3 min

¹ Including the base and guest-binary Docker stages, which artifacts builds too.

Both producers started 0.4 min after the run was created.

  • The three OpenVMM VMM test jobs started at 1.6 min, as soon as artifacts finished, and the KVM debug-kernel job finished at 5.2 min, before distro-artifacts did (5.7 min).
  • The microVM test and platform jobs started when distro-artifacts finished.
  • With the single job, which built the same components one after another, every one of these jobs would have waited for all of them. That single job took a median 5.3 min when it missed.

Experiment, every component cold. My previous head's run hit the cache, because #710's dev push 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 with save-cache: false. The experiment is run 39665813966. The baseline is the single job of #710's dev push 39661815501, which also built every component cold under the same keys an hour earlier:

Job, all components cold Kernel Alpine Ubuntu initramfs Determinism Distro layer Azure Linux Job wall time
Before: one artifacts job (39661815501) 5.6 min 0.7 0.6 2.9 0.3 0.4 10.9 min
After: artifacts (39665813966) 6.1 0.8 7.0 min
After: distro-artifacts (39665813966) 1.3² 3.0 0.3 0.4 5.2 min

² 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, artifacts takes 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 for distro-artifacts about as long as for the single job. The experiment saved no cache, and its branch is deleted.

Changes

  • build-guest-artifacts: a components input (kernel, alpine, ubuntu, azurelinux, debug-kernel; default all but debug-kernel) replaces guest-images and debug-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) builds kernel alpine; the new distro-artifacts (Build Ubuntu and Azure Linux artifacts) builds ubuntu azurelinux beside it, under the same condition (ci: classify microVM tests, benchmarks, and artifact builds separately #712's run-guest-artifacts); debug-kernel builds debug-kernel.
    • Each uploads one artifact per component, under its build filenames, with if-no-files-found: error.
    • The microVM test and platform jobs wait for artifacts and distro-artifacts; the VMM tests and the debug-kernel jobs still wait only for artifacts (and debug-kernel).
    • Required status check checks distro-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:

    Job Downloads
    OpenVMM VMM tests guest-kernel, guest-alpine
    microVM tests every component but guest-debug-kernel
    microVM debug-kernel jobs guest-kernel (runner qualification), guest-alpine, guest-debug-kernel
    platform jobs guest-kernel, guest-alpine, and, when they package a release, guest-ubuntu, guest-ubuntu-distro, guest-azurelinux
  • check-required-ci (ci.py): expects distro-artifacts to succeed whenever run-guest-artifacts is set, and to be skipped otherwise, like artifacts.

  • Docs: doc/ci.md gains a Guest artifact handoff section with the producers, artifacts, files, and consumers, and updates the job graph and the debug-kernel lane; doc/build.md names 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-kernel and guest-alpine and waits for artifacts, and a job that reads an Ubuntu or Azure Linux image also downloads it and waits for distro-artifacts. The structural test below checks every consumer, so a sibling that adds one learns at once what it must download and wait for.

Tests

  • Structural test (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 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, 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, that test-openvmm reads 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.
  • The action: every component step follows exactly its own selection; the default leaves out the debug kernel; with bash, the selection rejects unknown, empty, glob, and comma-joined lists.
  • Updated: required-status policy, the producers' shared condition and dependencies, the microVM and platform jobs' needs, the cache-write drift guard (four save-cache callers), the classification drift guard, and the download counts and order.
  • ci: classify microVM tests, benchmarks, and artifact builds separately #712's change classification: distro-artifacts follows run-guest-artifacts, the drift guard counts it among the producers of the microVM test and platform jobs' inputs, the nvx-rs host-change policy expects it to succeed, and every VMM-test input matches a microVM-test input pattern.

Validation

Expected effect

  • When the guest binaries change, every image misses and the VMM tests run: the VMM tests and the debug-kernel jobs start after about 1.6 min instead of about 6, when the single job would have built every image (run 39690060426, above). When the kernel misses too, every guest consumer starts after about 7.0 min instead of 10.9 (the experiment above).
  • The microVM test and platform jobs wait for distro-artifacts about as long as for the single job, which distro-artifacts pays by building the base and guest-binary Docker stages again (about 0.6 min of GitHub-hosted time).
  • Consumers download only what they read. In run 39624800851, the VMM tests downloaded 15.7 MB in two steps (2–6 s in all) instead of 140 MB (5–7 s), and the debug-kernel job 23 MB. The microVM test and platform jobs, which read every image, now take six and five download steps, which cost about 5–10 s more than the single download did.
  • Once ci: build the OpenVMM VMM tests once per target and hand them to the backend jobs #690 lets the VMM tests build without waiting for guest artifacts, the gain on the VMM tests shrinks; the split then still serves cold-kernel runs and a default-suite job that reads only the kernel and the Alpine initramfs (ci: run the guest smoke and sandbox steps in a job beside the default microVM suite #695).

Post-merge verification

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Cross-platform artifact handoffs and required-gate changes warrant human review with exact-head CI confirmation.

No open issues 🎉

Review effort: Balanced

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>
Copilot AI balanced review requested due to automatic review settings October 21, 2026 22:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Cross-platform artifact handoffs and required-gate changes need final human confirmation against current-head CI results.

No open issues 🎉

Review effort: Balanced

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ci: build the guest-artifact components in parallel and download only what each job uses

2 participants