Repository navigation
ci: reuse a pull request's guest images on its dev push - #713
Merged
Merged
Conversation
4 tasks
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Archive integrity checks can be bypassed, and reuse does not reliably enforce its time limit or handle partial cache restores.
3 open issues
What changed in this PR
Reduces repeated guest-image builds on dev pushes by reusing verified artifacts from same-repository pull requests.
Changes:
- Records cache keys and file digests, then validates artifacts before reuse.
- Adds push-time reuse and a pull-request dry run.
- Adds validation tests and documentation.
| File | Description |
|---|---|
| scripts/test_nvx_tools.py | Tests reuse, validation, and workflow wiring. |
| scripts/nvx.py | Adds recording and reuse commands. |
| scripts/nvx_tools/release.py | Enables validation of arbitrary image directories. |
| scripts/nvx_tools/guest_artifact_reuse.py | Implements artifact selection, verification, and installation. |
| doc/usage.md | Documents both commands. |
| doc/ci.md | Explains reuse and fallback behavior. |
| .github/workflows/ci.yml | Wires artifact records and rehearsal job. |
| .github/actions/build-guest-artifacts/action.yml | Attempts reuse before building missing images. |
Review effort: Balanced
Pedro Henrique Penna (ppenna)
force-pushed
the
agents/reuse-pr-guest-images
branch
from
October 21, 2026 15:58
f99d24e to
3f09a51
Compare
Pedro Henrique Penna (ppenna)
force-pushed
the
agents/reuse-pr-guest-images
branch
from
October 21, 2026 16:31
3f09a51 to
7e3c899
Compare
This was referenced Oct 21, 2026
Merged
Pedro Henrique Penna (ppenna)
force-pushed
the
agents/reuse-pr-guest-images
branch
from
October 22, 2026 12:00
7e3c899 to
eac22ea
Compare
Pedro Henrique Penna (ppenna)
force-pushed
the
agents/reuse-pr-guest-images
branch
from
October 22, 2026 12:40
eac22ea to
b6a13d6
Compare
Pedro Henrique Penna (ppenna)
force-pushed
the
agents/reuse-pr-guest-images
branch
from
October 22, 2026 12:59
b6a13d6 to
9058b7d
Compare
The release packaging checks the guest artifacts in build/ against their provenance, their package manifests, and the checkout's sources, but only there: the Azure Linux check found its initramfs through artifact_path, and the Ubuntu manifest check was inline in _guest_release_inputs. A dev push that reuses the guest images of its pull request's run needs the same checks on a downloaded copy before it installs anything into build/, and since CI builds the guest artifact components in jobs of their own, each job checks only its components. Add GUEST_ARTIFACT_COMPONENTS, which maps each component that a package ships, as the components input of the build-guest-artifacts action names it, to its files in build/, and validate_guest_artifacts(directory, components), which requires every file of the given components in the directory and checks each component with the validator that package already uses: the kernel provenance, the Alpine initramfs provenance, the Ubuntu initramfs and distro layer manifests, and the Azure Linux manifest. The Azure Linux check now takes the initramfs that it describes, and the Ubuntu manifest check moves into a function of its own; package and collect-sources pass them the same build/ paths as before, so their checks and messages are unchanged. A test checks that the components hold every guest image file once; that a valid directory passes; that a stale kernel, kernel configuration, Alpine initramfs or package manifest, Ubuntu initramfs, distro layer, or Azure Linux initramfs fails its own component with its validator's message, and no other component; that a component needs only its own files; and that a missing provenance file, no component, or an unknown one fails. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A dev push can't restore the caches that its pull request's runs saved, because GitHub scopes them to the pull request, so it rebuilds the guest images that the pull request already built. To reuse those images instead, the push must be able to tell that the images of a pull request's run are those that its own cache keys would build, and that they arrived intact, but no artifact of the run said which keys built its images. The build-guest-artifacts action now ends by recording its components when its new record-artifact input names an artifact: record-guest-artifacts writes build/guest-artifacts.json, which holds the exact keys of the selected components' cache entries, taken by name from the key step's outputs, and the SHA-256 of each of their files, and the action uploads it under that name for one day, as long as the components' own artifacts last. A record covers only its job's components, so that each guest artifact job, which builds its components beside the others, can reuse them whatever the keys of the others are. artifacts records the kernel and the Alpine initramfs as kernel-alpine-record, and distro-artifacts the Ubuntu and Azure Linux images as ubuntu-azurelinux-record. Those names stay outside the guest-* names of the handoff, since no job of the run downloads a record. The Ubuntu sandbox scratch template is in no record, because every run formats it anew, and a restore-only run, which builds nothing, and the debug kernel job record nothing. Tests check the record's keys and digests for each job's components; the command's refusal of a missing file, a missing key, an unknown component, or invalid keys; that the restore steps of each recorded component use exactly the keys and files that its record names; that the action records and uploads the record last; and that each guest artifact job records its components under a name of its own. doc/usage.md describes the command. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A dev push can't restore the caches that its pull request's runs saved, so it rebuilt every guest image whose inputs the pull request changed. From October 10 to 21, when one job built every guest image, 50 of the 87 dev pushes that ran it built at least one image, which took a median of 5.4 minutes, and up to 11.3, instead of 0.3, and delayed every job that waits for the guest artifacts. In 24 of those 50, the merged pull request's latest run had produced the same images, under the same keys, less than a day before. On a push, the build-guest-artifacts action of artifacts and of distro-artifacts now runs reuse-guest-artifacts after its cache restores and before any build, when a file of its components is missing. It takes the merged pull request's head from the second parent of the push's merge commit, and the latest pull_request run of ci.yml for that head whose head repository is this repository, so never a fork's run. It downloads the job's record from that run, unless the record expired or lacks the SHA-256 digest that GitHub records at upload, and goes on only if the record's cache keys equal the push's own keys for the job's components, so that a change to the other job's components, as by another merge, never matters. It then downloads the components' artifacts under the same conditions, and installs their files only when each artifact matches its digest, every file matches its digest in the record, and the kernel and initramfs provenance, or the Ubuntu and Azure Linux manifests, match the files and the push's checkout. It replaces every file of the components, not only the missing ones, because a cache restore that fails partway can leave a truncated file and still report a miss; a failed installation puts back the files that it replaced. The builds of the components and the Ubuntu determinism check then skip, as on a cache hit, because the run that built the images passed that check. The saves still cache the reused files for later runs, and the jobs still upload them as their own artifacts, so their consumers keep the same-run handoff and no job depends on a cache or on another run. The debug kernel is never reused. Every other outcome builds as before: no pull request run, an expired record or artifact or one without a digest, a run from before the records, other keys, a failed request or download, a file that doesn't check out, or a failed installation. After two minutes, an interval timer stops a stalled download or check the same way, but never the installation, which it could cut short with its files half put back. The step continues on error, never fails the run, and reports why in the job summary and as a notice, or as a warning for a fault. Only pushes set the action's new reuse input, and the two jobs gain actions: read, which listing runs and downloading artifacts needs. Unit tests cover the selection, which excludes forks, deleted forks, other events, workflows, and heads, and expired artifacts; the digest, record, key, and provenance checks for each job's components; the installation, which replaces partial restores and puts every file back when a rename fails or an interruption follows one; the time limit, which spares the installation and leaves nothing behind when it expires; API failures; and the command's outputs. Local HTTP servers check that a download drops the token on its redirect to storage, and that a response cut short falls back like any failed request. Workflow tests check the permissions, that only pushes reuse, where the step runs and what it skips, and that the artifacts that reuse downloads are those that hand over each component's files. doc/ci.md and doc/usage.md describe the reuse. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Only a dev push reuses its pull request's guest artifact components, so no pull request run exercised the reuse against real runs and artifacts before it merged: a mistake in the selection, the download, or the checks would only show as dev pushes that fall back to building. A guest-artifact-reuse job, "Rehearse guest artifact reuse", now runs reuse-guest-artifacts --dry-run for each same-repository pull request once artifacts and distro-artifacts succeed, beside the jobs that wait for them, with one step for each of the two jobs' components, record, and keys. The command takes the pull request's head with --head-sha instead of a merge commit, so it selects the latest run for that head, normally its own, and downloads and checks the record and the artifacts that the job just uploaded exactly as a push would, but installs nothing and reports whether a push would reuse them. It compares each record with the keys that the job's action resolved, which the action now gives as its cache-keys output and each job as its guest-cache-keys output. A rehearsal that its own run can't serve warns, because that is a fault. The job has actions: read, like the two jobs, and runs on a GitHub-hosted runner. The change detector selects it with the guest artifacts, and the required status check, which alone waits for it, requires it to succeed for a same-repository pull request that builds them, so a pull request that breaks the reuse command can't merge. Pushes, full runs, and fork pull requests skip it. Tests cover the rehearsal's selection without a merge commit, its warning, and that it installs nothing, as well as the job's condition, permissions, and steps, the keys' path from each job's action, the required-results policy, and that a change to the reuse, its record, or their commands selects the rehearsal. doc/ci.md and doc/usage.md describe it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna)
force-pushed
the
agents/reuse-pr-guest-images
branch
from
October 22, 2026 13:16
9058b7d to
ffd6b59
Compare
5 tasks
Pedro Henrique Penna (ppenna)
deleted the
agents/reuse-pr-guest-images
branch
October 22, 2026 18:44
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 #702.
Part of #430.
Summary
A
devpush can't restore the caches that its pull request's runs saved, so it rebuilt the guest images that the pull request had already built. Now each guest artifact job of a push installs its components from the merged pull request's latest run ofci.ymlwhen one of their files misses the cache:artifactsthe kernel and the Alpine initramfs, anddistro-artifactsthe Ubuntu and Azure Linux images. It does so only after these checks pass:microsoft/nvx, not a fork;In every other case the job builds as before, and the reuse never fails the run. Consumers still download the push's own same-run artifacts. Each same-repository pull request rehearses the same selection and checks, for both jobs, against the artifacts that its own run uploaded.
This head is rebased onto
devafter #711, #712, #710, #709, #734, #707, and #708; see Adaptations.Why
Measured over the 87
devpushes from October 10 to 21 whose guest artifact job succeeded, when one job built every guest image (raw data and run links: #702 comment):artifactsjob, median / p90 / maxA cold guest artifact job delays everything that waits for it: the OpenVMM VMM tests by the whole build, and the WHP microVM job, beyond its OpenVMM producer and Windows CLI job, by a median of 1.8 min, p90 6.2 min. That happened in 40 of 45 cold first attempts.
The merged PR's latest run had already produced the same images, under the same keys, less than a day earlier, in 24 of the 50 cold pushes. For example, 39592496133 (nvx-rs: build the NVXS unit tests' epoch identities with one function #678), 39529338356 (nvx-rs: run the control session on the shared NVXS session's host role #641), 39506407533 (nvx-rs: add a neutral guest spawn API and launch managed workloads through it #629), and 39481993344 (nvx-rs: add the sandbox store, launch journal, and runtime identity #628).
In the other 26:
nvx-rs/, landed between the PR's last run and its merge. ci: key the guest images and VMM tests on the guest binaries' sources #710 has since narrowed the image keys to the guest binaries' sources, so host-onlynvx-rsmerges no longer change them.These still build.
Changes
Four commits, each with its own tests and docs:
release: validate the guest artifacts of any directory.GUEST_ARTIFACT_COMPONENTSmaps each component that a package ships, named as the action'scomponentsinput names it, to its files.validate_guest_artifacts(directory, components)applies to those components the checks thatpackagealready makes tobuild/: the kernel and Alpine initramfs provenance, and the Ubuntu and Azure Linux manifests against the files and this checkout's sources. The Azure Linux check now takes its initramfs path, and the Ubuntu manifest check moves into a function of its own.packageandcollect-sourcesbehave as before.ci: record the cache keys and digests of the guest artifacts. When its newrecord-artifactinput names an artifact,build-guest-artifactsends by runningnvx.py record-guest-artifacts, which writesbuild/guest-artifacts.json, and uploads it under that name for one day.artifactsuploadskernel-alpine-recordanddistro-artifactsuploadsubuntu-azurelinux-record. Both names stay outside ci: build the guest-artifact components in parallel and download only what each job uses #711'sguest-*handoff names, because no job of the run downloads a record.ci: reuse a pull request's guest images on its dev push. On a push only (reuse: ${{ github.event_name == 'push' }}), after the cache restores and before any build, each of the two jobs runsnvx.py reuse-guest-artifactswhen a file of its components is missing. The command does the following:pull_requestrun ofci.ymlfor that head whosehead_repositoryismicrosoft/nvx. Runs from forks and deleted forks never qualify.guest-kernelandguest-alpine, orguest-ubuntu,guest-ubuntu-distro, andguest-azurelinux. It refuses any record or artifact that has expired or lacks asha256:digest from GitHub, and checks each download against that digest. Every download drops the token on the redirect to storage.validate_guest_artifactspasses for the job's components. A response that a transport error cuts short, such as withhttp.client.IncompleteRead, counts as a failed request.artifactsjob takes about 1.3 minutes. The step continues on error, prints a notice for an expected reason or a warning for a fault, and writes a job-summary line whose heading names the components.artifactsanddistro-artifactsgetactions: read.ci: rehearse guest artifact reuse on pull requests. For same-repository pull requests, the newguest-artifact-reusejob (Rehearse guest artifact reuse) runs afterartifactsanddistro-artifactssucceed.reuse-guest-artifacts --dry-run --head-sha <PR head>step per job, with that job's components, record, and keys, which reach it through the action's newcache-keysoutput and each job'sguest-cache-keysoutput. Each step selects the latest run for the head, normally its own, then downloads and checks that job's record and artifacts exactly as a push would, and installs nothing.run-guest-artifacts, andcheck-required-ciexpects it to succeed for same-repository pull requests that build the guest artifacts. A pull request that breaks the reuse command therefore can't merge.doc/ci.mdgains a "Guest artifact reuse" section after "Guest artifact handoff", and a job-graph paragraph.doc/usage.mddocuments both commands.Adaptations to
devreuseinput, andactions: readfor each, validation limited to that job's components, and reuse from the PR run's per-component artifacts.COMPONENT_ARTIFACTSmaps each component to the artifacts that hand it over, and a test checks it againstci.yml's uploads.test_ci_full_runs_do_not_publish_persist_or_save_cachescounts 16 push conditions: ci: build the guest-artifact components in parallel and download only what each job uses #711's 14, plus the tworeusearguments. The cache saves stay limited to pushes and pull requests.REQUIRED_CI_RESULT_ENVIRONMENTS, and in the required-results policy.queue: maxfor pushes): each push still runs for its own merge commit, so the selection holds, and no queued push is cancelled before it can reuse. A push that waits more than a day finds the PR's artifacts expired and builds.artifacts, whose files the reuse checks before it installs them.Tests
GuestArtifactReuseTests, with a fake GitHub API that serves each job's record and component artifacts built from the release fixture's valid images, cover the following:sha256:digest, which it never downloads, and a missing record or component artifact;Content-Lengthor a chunk;reused, the summary, and the annotations, whose headings name the components, including no token, invalid keys, a missing key, and an unknown component;ReleaseTestscase coversvalidate_guest_artifactsfor each component:guest-*, and the debug kernel job records nothing;releasegetactions: read;reuse;reused != 'true', while the saves and the debug kernel ignore it;Validation
ruff check,ruff format --check,pyrightfor Linux and Windows,check_markdown_links.py,git diff --check, and the affected test classes.test_nvx_toolsandtest_markdown_linksmodules in agit archiveexport. There, only the two tests that need a Git repository fail.eac22eae4, onbd977e35b:test_nvx_toolsandtest_markdown_linksmodules pass: 619 tests. After the rebase onto microvm tests: record each scenario's duration in a JSON Lines report #707, they pass again: 620 tests. The rebase onto adata/commit changed no code, and with the fix for the review's truncated-response finding, 621 tests pass. After the rebase onto ci: build the Linux OpenVMM VMM tests once for the KVM and MSHV jobs #708, on206abd182, 629 tests pass.compileall,test_adversarial,test_hosts, andnvx.py --helpfor every listed command and the two new ones.GuestArtifactReuseTestsandCiConfigurationTestspass, including the four POSIX interval-timer tests that Windows skips, and so does the truncated-response test.ci.ymlhas only the known false positive about ci: queue the dev push runs instead of cancelling the waiting one #734'squeuekey, and the other workflows only the existing unknown-label warning inadversarial.yml.ffd6b59fe, it reported "would reuse" for both jobs in a 16-s job of run 39785367867. Oneac22eae4, it selected its own run, 39774840679, and reported "would reuse" for both jobs in a 23-s job:kernel-alpine-recordwithguest-kernelandguest-alpine, andubuntu-azurelinux-recordwithguest-ubuntu,guest-ubuntu-distro, andguest-azurelinux. Before ci: build the guest-artifact components in parallel and download only what each job uses #711, earlier heads reported "would reuse" for the singleguest-artifactsartifact: 39626393958 and 39635577644.eac22eae4passed: 29 jobs succeeded, including the rehearsal andRequired status check, and the 4 push-only jobs were skipped. The rebase onto microvm tests: record each scenario's duration in a JSON Lines report #707 needed no change. Run 39785367867 on the final head,ffd6b59fe, passed too: 30 jobs succeeded, including the rehearsal andRequired status check, and the 4 push-only jobs were skipped. The runs forb6a13d63band9058b7deawere cancelled when the review fix and the rebase onto ci: build the Linux OpenVMM VMM tests once for the KVM and MSHV jobs #708 superseded them. No run needed a rerun.Expected effect
Required status checkwaits for.Post-merge verification
The reuse runs only on
devpushes:Build kernel and Alpine artifactsorBuild Ubuntu and Azure Linux artifacts, and aBuild guest artifactsstep of seconds, not minutes.devpushes, record on ci: reuse a pull request's guest images on its dev push #702, for each guest artifact job, the cold-build rate, the step and job durations, the time when the guest artifacts are ready, and the run wall time, and compare them with the baseline above.