文档目录开发 · 18 篇

文档/开发

03

持续集成

NVX 持续集成的作业图、运行器容量、变更分类、宿主资格与维护流程。

译自microsoft/nvx · doc/ci.md。技术细节以英文原文为准。

GitHub Actions 工作流在 Azure 托管的、自托管的 KVM、MSHV 和 WHP 虚拟机上设有两层微虚拟机测试。每个后端都有一个运行器池,运行器按操作系统、后端和 virtual-machine 打标签;其规模见作业图与运行器容量。作业以共享的后端标签为目标,因此任何可用的匹配运行器都能执行它们。这样各后端通道可以并发执行,而不会把工作负载绑定到特定宿主。平台基准测试作业是例外:每个作业还要求 benchmark 标签,每个池中有一个运行器带有该标签,以便每次运行都在同一台 VM 上测量其性能序列(见基准测试)。在开始测量之前,每个平台作业还会检查其运行器是否仍是该序列历史所来自的宿主(见预期基准宿主)。openvmm-vmm-tests 会下载 NVX 客户机产物,并使用 Linux 直接启动内核和 Alpine initramfs,在每个后端上检验 OpenVMM 的 Linux MP 表生命周期、TTRPC 和快照契约。openvmm-unit-tests 在每个操作系统上各运行一次 OpenVMM 单元测试和文档测试,分别在一个 MSHV 运行器和一个 WHP 运行器上。需要 /dev/kvm 或 /dev/mshv 的测试会被忽略,因此 KVM 和 MSHV 运行器用于运行相同的 4,820 个测试,而 MSHV 池拥有最多的运行器。失败的 openvmm-vmm-tests 作业会上传 Petri 的 test_results 目录,其中包括客户机和 VMM 日志、截图以及看门狗检查数据。这些保留七天的产物命名为 openvmm-vmm-tests-<os>-<backend>-<run-id>-<run-attempt>,因此成功的重跑不会替换失败尝试的诊断信息。Linux 从 openvmm/target/vmm_tests/test_results 收集它们;Windows 使用 <runner-temp>/<backend>/test_results。

nvx-microvm-tests-{kvm,mshv,whp} 作业使用 NVX Linux 内核,以及 NVX Linux 内核加上选定的 Alpine 或 Ubuntu initramfs,并通过公开的 OpenVMM CLI 检验 Linux、SMP、virtio、沙箱和快照行为。仅适用于 Alpine 控制台的场景保持显式声明,并且在 Ubuntu initramfs 下会被拒绝。每个作业还会通过其单 vCPU 冒烟测试集启动 Azure Linux initramfs,并执行相同的时间 ABI 检查;调试内核作业会跳过它,就像它们跳过 Ubuntu 测试一样。NVX 层的失败日志按后端上传。

每次启动测试框架(无论是在测试还是基准测试中)都会扫描 OpenVMM 控制台,以获取客户机的时间 ABI输出;测试还会以同样方式扫描它们通过 TCP 读取的每个 virtio 控制台,直到其结束。出现 NVX-TIME-ABI-VIOLATION 事件,或 NVX-TIME-ABI 一致性行失败,都会立即以客户机的代码和详细信息使场景失败。客户机会让控制台保持安静,因为控制台的每个字节都要付出一次端口退出的代价。只有初始时钟步进发生在客户机启动标记之前;其余启动检查在此后完成,失败时仍会以状态 193 关机,可能发生在场景执行到一半时。客户机在每个记录的检查阶段(启动,以及恢复后的捕获和恢复)打印一行 NVX-TIME-ABI,并且仅当 /sbin/nvx-time status 请求时才打印运行时行,请求时最多等待 30 秒以等待待处理的检查。测试运行器在每次冷启动后、在任何其他输入之前发起查询——前提是 shell 位于测试框架读取的控制台上,即 OpenVMM 控制台或通过 TCP 的 virtio 控制台——并等待查询退出,这样控制台对后续输入的回显就不会拆开它的行。如果冷启动时那里没有 shell,则只进行扫描:一次性工作负载,以及托管生命周期(其中 init 启动托管代理而不是 shell)。启动检查失败时仍会以状态 193 关机,从而使运行失败。查询必须以 0 退出,并带有一行通过的 NVX-TIME-ABI 启动行,该行报告 ABI 版本 1、代次 0、合理的 TSC 速率以及后端的 LAPIC 速率;缺少该行意味着客户机镜像或 OpenVMM 未实现该 ABI,客户机 30 秒等待后检查仍待处理也算失败,而仅报告型客户机(NVX-TIME-REPORT)永远不会被接受。如果客户机镜像的 nvx-time 没有 status 子命令,则因该原因失败。运行时行只记录挂钟时间纪律的状态。基准测试从不查询,因此其测量区间保持安静。OpenVMM 的退出状态 193、194 或 195 会分别作为客户机的时间 ABI 一致性、运行时违规或恢复修复关机来报告,并附带之前发生的事件,而不是作为通用退出状态报告。

smp、smp-snapshot 和 restore-processors 场景会在每次启动或恢复后,对每一对在线 CPU 运行客户机 warp 探针 /sbin/nvx-time-probe warp --bound-ns 1000,分两轮进行,两轮之间所有 vCPU 停机 1 秒,这样没有不变 TSC 的宿主会在空闲宿主 CPU 唤醒时校正客户机 TSC(#265)。每一轮中的每一对 CPU 都必须在时间 ABI 的 1 µs 跨 vCPU 偏差界限之内,对向后 TSC 步进和乒乓偏移都是如此;某一对停滞或测量不确定也会失败。smp 按每个请求的处理器数量启动。smp-snapshot 为每个请求的数量捕获一个快照并恢复一次,并将第一个快照再恢复一次,以证明恢复后它仍可复用。restore-processors 捕获一个启动时仅有 1 个在线 CPU、容量为 8 的客户机,并探测每个恢复目标所激活的 CPU。在 1/2/4/8 CPU 的恢复之后,它会不带 --restore-processors 恢复同一快照一次。在 warp 探针之后,每个 smp-snapshot 和 restore-processors 恢复都会运行 /sbin/nvx-time status,该命令会等待恢复的延迟检查;它必须以 0 退出,并带有一行通过的 NVX-TIME-ABI ... phase=restore 行,报告后端的 LAPIC 速率、恢复后的 CPU 数量以及代次 1:每个场景捕获的都是冷启动的客户机,而 OpenVMM 无法捕获已恢复的客户机。该查询还会打印源客户机的启动行和捕获行,这些行保留其检查所覆盖的 CPU 数量,因此只有恢复行必须与恢复后的 CPU 数量匹配。

每次恢复都会启用 OpenVMM 生命周期性能剖析,并且必须恰好报告一条 startup.vp_thread_bind 记录。其 startup.vp_bind_* 记录必须表明:显式的 MSHV 目标恰好绑定 VP 0..N-1,而未指定目标的 MSHV 恢复以及所有 KVM 和 WHP 恢复都会绑定完整容量。

捕获不会等待时钟源:时间 ABI 在每个后端都在 device_initcall 上注册 tsc,因此曾经让时钟源看门狗在恢复前后比较 tsc-early 与 jiffies 的过渡性 tsc-early 窗口(#253)永远不会到达客户机的用户空间。

一旦客户机打印 NVX-RESTORE-PROCESSORS-FAIL 或 NVX-WARP-PROBE-FAIL,恢复就会失败,而不会等待阶段超时;探针失败还会以状态 97 关闭客户机。恢复处理器日志会记录 OpenVMM 的 time ABI rates declared 事件,该事件报告身份 MSR 路径、TSC 同步方法、原生和声明的 TSC 速率,以及相对于快照的速率偏差。

restore-downtime 场景覆盖时间 ABI 的长时间停机情况。它会捕获四个快照,分别在 1 个和 8 个 vCPU 下,各带和不带 rcupdate.rcu_expedited=1,然后等待 30 秒(长于客户机 21 秒的 RCU 停滞超时),再逐一恢复。每个恢复后的客户机都必须运行 warp 探针,在测试框架布置其检查之前通过 nvx-time status 报告一行通过的恢复行,两秒后报告 /sys/kernel/rcu_stall_count 为 0,并显示至少 30 秒的运行时间,这证明单调时间随停机时长前进。这些捕获共享一个停机窗口,因此该场景每个后端增加约一分钟。

time-abi-conformance 场景会启动请求的最大 vCPU 数量,并运行客户机的穷举式 CI 检查 /sbin/nvx-time exhaustive,启动检查将这项检查留给 CI。它在每个在线 CPU 上检查每个叶子 0x40000006..=0x400000ff 和每个基址 0x40000100..=0x4000ff00 是否有其他 hypervisor 签名,检查启动检查读取的每个身份 MSR、HV_X64_MSR_TSC_INVARIANT_CONTROL 的写入规则、对只读身份 MSR 的写入,以及对 IA32_TSC_ADJUST 和 IA32_TSC_DEADLINE 的读取(检查 X1 到 X6)。测试框架要求每个 CPU 上的每项检查都有一行通过的 NVX-TIME-ABI-EXHAUSTIVE、一份包含 status=ok 的摘要、请求的 CPU 数量且无失败,并且退出状态为 0;失败时会列出每项失败的检查及客户机提供的详细信息。客户机命令可容纳在一行控制台内,因此控制台对它的回显会在检查输出之前结束。

cpu-profile-fallback 场景在由内置配置档案服务的运行器上,覆盖 --cpu-profile auto 在没有任何内置配置档案服务的 CPU 上回退到宿主配置档案的行为。它使用 OpenVMM 的 host-cpu-unknown 测试钩子冷启动一个 vCPU,该钩子使 auto 将宿主 CPU 视为未知,并要求恰好有一条 NVX-CPU-PROFILE-FALLBACK: 警告,该警告指明宿主配置档案及其摘要,并链接到 CPU 配置档案申请表,同时还要求冷启动声明该配置档案并通过启动检查。然后它捕获客户机,并在使用 auto 且不带钩子的情况下恢复:恢复必须采用快照的宿主配置档案且不再产生警告,并通过 warp 探针及其恢复检查。

snapshot-core 场景首先向一个由 OpenVMM 在未指定快照目标的情况下启动的客户机发送快照请求。OpenVMM 会释放该请求,因此请求会返回到源客户机,而源客户机必须恰好继续一次。在请求之前,客户机的快照代理保存并覆盖了停滞检测器的设置,并且当请求返回时必须恢复这些设置(nvx-time cancel-capture)。之后,nvx-time status 必须以 0 退出,带有一行代次为 0 的通过启动行且没有恢复行,并且 /sys/module/rcupdate/parameters/rcu_cpu_stall_suppress 必须读出 0。

在运行通过时,test-microvm 会在结束时打印一行 NVX-TIME-ABI-EVIDENCE: 并将其添加到 GitHub 作业摘要中,因为 CI 仅对失败的作业保留客户机日志:内容包括 warp 探针的运行次数及其最差的 max_abs_offset_ns 和 max_backward_ns、每个 nvx-time status 查询所报告的最新检查的计数和 elapsed_us 范围(冷启动后的启动、恢复后的恢复,以及 snapshot-core 释放请求后的捕获)、穷举检查的摘要,以及 restore-downtime 的停滞计数。当客户机报告某项检查的 CPU 时间(cpu_us)时,该行还会给出其范围,以及有多少项检查超过了该后端在其阶段对应的 CPU 时间预算。预算来自客户机测量,并豁免同一 VM 进程中回滚捕获之后的一次捕获;CI 从不会产生这种情况,因为每个 VM 进程最多向 OpenVMM 发送一个快照请求,而捕获失败会使场景失败。scripts/nvx_tools/time_abi.py 中的 CHECK_CPU_BUDGET_US 保存这些预算,每个后端和阶段各一个。elapsed_us 是挂钟时间,包括在工作负载之后的等待,没有预算。CI 对两者都不设门禁:性能门禁是 CI 之外的 A/B 比较。

nvx-microvm-debug-{kvm,mshv,whp} 作业在 CI 调试内核(build/vmlinux-debug,由 kernel/config-microvm-debug 构建)上运行 test-microvm --debug-kernel,该内核包含生产内核省略的软锁定和挂起任务检测器。它会选择同宿主恢复场景 smp、smp-snapshot、restore-processors、restore-downtime 和 snapshot-tiers。这些作业会跳过所有其他测试步骤,因为那些步骤会启动生产内核,而 nvx-microvm-tests-{kvm,mshv,whp} 作业已经覆盖了这些内容:公开的托管执行配置、Ubuntu 和 Azure Linux 客户机测试、Ubuntu 沙箱层和 live-share 冒烟测试、调用方拥有的 live-share 测试,以及 aci_edge_sandboxes 生命周期测试。任何 RCU 停滞、软锁定或挂起任务都会使客户机的时间 ABI 监视器以状态 194 关机,从而使运行失败。这些场景恢复的每个客户机都会在退出前查询 nvx-time status(snapshot-tiers 在其层级断言之后),该命令等待恢复的延迟检查,并且必须报告一行通过的恢复行,因此没有恢复会留下待处理的检查。唯一的例外是 snapshot-tiers 的门禁超时检查,其客户机会被 OpenVMM 故意停在恢复门禁处。测试框架会拒绝 vmlinux-debug.config 缺少这些检测器的内核,因为客户机对这些检测器的启动检查会空洞地通过。为限制成本,PR 只在 KVM 上运行调试内核,dev 推送则在每个后端上运行;每个作业在自己的运行器上约需五分钟,与其他微虚拟机作业并行。这些作业为必需状态检查、开发发布版和性能持久化把关。GitHub 托管的 debug-kernel 作业在共享的 artifacts 作业(使用 guest-images: "false" 的 build-guest-artifacts)旁边构建调试内核,并以自己的键进行缓存,因此内核重建只会延迟调试作业,而调试内核构建失败会使必需状态检查失败并阻止发布。

每个使用 validate-runner 动作的作业都会首先报告 Linux 运行器的内核、CPU、时钟源和 TSC 标志。然后它用 nvx.py doctor --checks H1 H2 H4 --ci-schedule 验证运行器是否满足时间 ABI 要求(见宿主资格),即后端、CPU 指纹与代次,以及在其短调度方案上的 TSC 速率稳定性。微虚拟机和平台作业在下载 OpenVMM 后运行它,并加上 OpenVMM 的 CPU 配置档案检查(H2)和预检(H3),在配置档案检查失败时将指纹保留为产物;其他作业传入 --no-openvmm。这需要几秒钟。它会在日志和作业摘要中报告 CPU 代次、auto 选择的内置 CPU 配置档案以及测得的 TSC 速率,并在运行器不合格时以稳定的代码使作业失败,例如在未知 CPU 代次上返回 E_PROFILE_HOST_UNKNOWN,在这种情况下 auto 会回退到宿主配置档案,而 CI 从不使用宿主配置档案。平台作业还会按 CI 的调度方案运行客户机 warp 探针(H6),这会增加约 25 秒,因为它们的基准测试启动客户机时不会运行微虚拟机场景的探针。doctor 只根据可测量的属性设门禁,在每个后端上都一样:它记录宿主操作系统的不可变 TSC 标志和时钟源作为证据,而客户机 warp 探针测量没有不变 TSC 的宿主所造成的偏差。在时间 ABI 之前,这种 MSHV 运行器 VM(一台 Ice Lake 8370C)上从未恢复过的客户机,在空闲宿主 CPU 唤醒时会遇到跨 vCPU 的 TSC 偏差(#211、#265),这就是探针调度方案包含空闲间隔的原因。在时间 ABI 下,其客户机的 warp 探针测得最多 36 ns,因此 validate-runner 只会记录缺少 nonstop_tsc,而 Linux 运行器设置脚本只会发出警告。OpenVMM vmm-tests 不运行 warp 探针:它们启动 OpenVMM 自己的测试客户机,其判定不依赖偏差界限。运行器标签不编码代次;每次 PR 的 CI 都在一个运行器上捕获和恢复,因此代次从不会混合。

warp 探针取代了 restore-tsc-sync 场景、其仅用于测试的 clearcpuid=tsc_adjust 内核选项、客户机对内核日志中 TSC 偏差和不稳定消息的扫描,以及对这些失败进行分类的全新启动 TSC 对照(#211、#265)。在时间 ABI 下,客户机的 TSC 是 tsc_reliable,因此 Linux 会跳过其 CPU 上线时的偏差检查,并且永远不会记录该防护所寻找的消息;取而代之的是,探针在每一对 CPU 上测量 1 µs 界限。

console-exit 场景在快照恢复后将宿主控制台读取延迟两秒,以检验输出背压。对于每个请求的处理器数量,它要求 64 KiB 载荷和最终标记都按字节精确送达,并保留客户机退出状态 0 和 37。这在不向被测基准测试工作负载添加睡眠的情况下,同时检查设备和宿主中继的排空。测试框架即使进程已经退出,也会等待输出读取器的 EOF 通知,因此延迟的最终输出块不会造成误报失败。

共享客户机产物使用 Docker 在 GitHub 托管的 Ubuntu 运行器上构建。内核、Alpine initramfs、Ubuntu initramfs 和 Ubuntu EROFS 层使用各自独立的缓存键。每个客户机键都包含 nvx-rs/**(其 target 目录除外)、rust-toolchain.toml、共享二进制构建模块、Dockerfile 和 .dockerignore;Rust 构建器镜像本身通过摘要固定。由于 hashFiles 仅对内容做哈希,每个客户机键还以路径感知的 Rust 源码摘要结尾,该摘要由客户机软件包清单记录,并由 nvx.py guest-binary-source-identity 打印。Ubuntu 的键还包括 Canonical 归档固定项、补充软件包锁定文件、通用客户机源码、共享下载和客户机描述符模块以及转换器实现。产物上传会保留 Alpine 文件名,并添加不同的 Ubuntu 文件名。每个后端还会启动 Ubuntu initramfs,并以 UID/GID 65534 在全新的 ext4 暂存副本上运行来自 Ubuntu EROFS 层的 /sbin/nvx-sandbox-smoke,使用 32 MiB 的 --memory-max 和 8 个进程的 --pids-max。该探针会验证沙箱安全配置:固定身份、空的能力集和 no_new_privs、私有 mount、PID 和 UTS 命名空间、私有设备视图和只读 sysfs、容器 cgroup、位于只读镜像层之上的 overlay 根(其 upper 目录位于暂存磁盘上),以及内存控制器会终止达到限制两倍的分配,pids 控制器会在八个进程时停止工作负载。下文的 live-share 运行会重复这些安全配置检查。每个后端还会运行 test-microvm 的公开 managed-exec-config 和 sandbox-lifecycle 场景,这些场景以子进程方式驱动 nvx.py sandbox provision、start、exec、stop 和 deprovision(见运行):请求作用域的工作目录和环境、超时、非零退出、stdout 和 stderr、有界输出和结果报告、跨请求和重启的暖客户机状态、身份拒绝、故障关闭的生命周期转换、崩溃的 OpenVMM 留下的陈旧运行时状态,以及清理后不留下任何 OpenVMM 进程、控制端点、能力或运行时记录,同时层和暂存产物仍然保留。它们的有界证据是后端失败日志的一部分。同一个入口点随后会验证容器内的一个实时 virtio-fs 共享:带有拒绝子目录的可读写 /workspace 共享必须能将客户机写入往返到宿主,而只读的 /opt/hostedtoolcache 共享必须拒绝写入和符号链接。在可读写共享中,客户机还会演练软件包管理器使用的符号链接原语:它创建指向可执行文件的 npm 风格相对 .bin 链接,并通过该链接运行它,重命名、替换和删除链接,以及创建绝对链接、悬空链接、共享外链接和被拒绝路径的链接。随后宿主会检查每个目标都被精确保留(在 Windows 上,通过解码每个 WSL 风格的重解析点,因为 Windows 不会跟随它们),并检查没有任何链接读取或修改了共享之外或被拒绝路径中的宿主数据。然后,托管沙箱会通过 provision、start、exec 和 stop 重复可读写检查,并且必须报告成功结果以及干净卸载的暂存文件系统,这表明 stop 先卸载了共享和 overlay。接着,每个后端会同时将一个可读写 /workspace 共享、一个只读 /opt/hostedtoolcache 共享、一个只读 /config/settings.json 文件和一个可读写 /results/output.txt 文件挂载到同一个沙箱,作为同一个聚合 virtio-fs 设备的子项;每个目录都有自己的拒绝子目录,每个文件旁边都有一个未被任何共享暴露的文件。冒烟脚本对两个共享运行可读写和只读检查,并验证可读写共享中的链接无法写入只读共享。它要求每个文件都是带有其模式的 virtiofs 绑定挂载,并且在其客户机目录中单独存在;只读文件必须以 EROFS 拒绝写入;工作负载可以重写可读写文件,但既不能删除也不能重命名其中任何一个。宿主要求工作区和可读写文件中有客户机的写入,工具缓存、只读文件和被拒绝文件保持不变,共享文件旁边的文件也保持不变;无论是一次性运行,还是托管沙箱(其 format-6 配置必须持久化每个共享及其类型,且其 stop 必须留下干净卸载的暂存文件系统),都是如此。接着,每个后端会挂载一个可读写 /workspace 共享,其访问策略拒绝 nvx-denied、允许 nvx-denied/nvx-allowed,并且只让 nvx-writable 可写;每个宿主目录和文件都对所有身份可写,因此只有 OpenVMM 能拒绝写入。冒烟脚本的 policy 模式要求工作负载的写入仅在 nvx-writable 中成功,在其他所有位置都以 EROFS 失败;将只读文件硬链接到 nvx-writable 必须失败;nvx-denied 只列出 nvx-allowed,其文件保持可读,而 nvx-denied/secret 保持隐藏。宿主要求客户机的写入出现在 nvx-writable 中,且共享没有其他变化;无论是一次性运行,还是托管沙箱(其 format-5 配置必须持久化被拒绝、允许和可写的路径,且其 stop 必须留下干净卸载的暂存文件系统),都是如此。test-microvm 的默认 filesystem-policy 场景以客户机 root 身份覆盖同一策略,而以 root 身份运行时权限检查从不会被模式位阻止:可写路径之外的每次变更(包括通过该标签的第二次挂载进行的变更)都以 EROFS 失败,硬链接以 EXDEV 失败,嵌套在允许路径内的被拒绝路径保持隐藏,无效策略在启动前被拒绝,并且快照恢复会要求相同的策略,并在捕获期间保持一个可写句柄打开。在 Linux 上,一个所有用户可写的共享随后会以 --mount-owner caller 运行。运行器既不授予 OpenVMM CAP_SETUID 也不授予 CAP_SETGID,因此它既不能采用 65534 工作负载身份,也不能丢弃它用来打开 /dev/kvm 或 /dev/mshv 的后端组。因此,冒烟脚本的 eperm 模式要求读取和列出共享以 EPERM 失败且写入失败,而宿主要求共享保持不变;无论是一次性运行,还是配置必须持久化该模式的托管沙箱,都是如此。test-microvm 的 filesystem-owner 场景同样要求客户机 root 和外来客户机身份在运行器上以 EPERM 失败,并要求 root 拥有的导出在启动前被拒绝;在 Windows 上,--mount-owner caller 必须在启动前被拒绝。OpenVMM 单元测试检查每个请求所运行时的宿主身份,包括被映射(squash)到 OpenVMM 之外的导出所有者的客户机 root。在 OpenVMM 能丢弃其补充组的地方,该场景改为要求客户机 root 创建的文件属于共享所有者而非 root,并要求被 squash 的 root 无法对文件执行 chown、无法给它另一个 OpenVMM 的组,也无法创建设备节点。当测试进程还向 OpenVMM 传递 CAP_SETUID 和 CAP_SETGID 时(例如在特权开发者宿主上),该场景要求创建的文件归外来身份所有。Linux/KVM 会运行更广泛的 Ubuntu SMP、托管生命周期、网络快照、无块快照和工作负载身份测试集。

OpenVMM 发布版可执行文件和来源信息由可独立寻址的 build-openvmm-linux-gnu、build-openvmm-linux-musl 和 build-openvmm-windows-msvc 生产者作业构建一次。KVM 工作负载和 MSHV 微虚拟机测试使用 GNU 产物,MSHV 平台工作负载使用 musl 产物,WHP 工作负载使用 Windows MSVC 产物。每个工作负载都可以在其兼容的 OpenVMM 生产者和共享客户机产物作业完成后启动,而无需等待不相关的 OpenVMM 目标。

三个生产者都调用相同的 Python 构建工作流,并通过 build-openvmm --backend 显式传入经过验证的运行器后端。后端承载于 OpenVmmBuildConfig 中;构建工作流将 KVM、MSHV 或 WHP 映射为 GNU、musl 或 MSVC,而不探测运行时设备。因此 CI 为 MSHV 保留其 musl 构建,而无需维护单独的 shell 构建路径。

内核、调试内核和 initramfs 缓存键包括 docker/Dockerfile(英文原文),它固定构建镜像及其工具链,并且每个客户机产物都在其中构建;还包括 .dockerignore(英文原文)(Docker 在 COPY 之前将其应用于构建上下文),以及 build_config.py(英文原文)和 build_constants.py(英文原文),因此工具链、共享构建配置或常量的变更会使缓存的客户机产物及其来源信息失效。Ubuntu 发行版层共享 Ubuntu 输入哈希。

生产者交接使用保留一天的工作流产物,而不是缓存。每个消费者都会下载规范化后的可执行文件及其构建来源信息,然后在 Linux 上恢复可执行权限。

nvx.py 为其 sandbox 命令以及其 run 命令的出站策略和产物步骤运行 nvx-rs,因此微虚拟机测试作业也以同样方式获取 nvx-rs,并且每个发布包都会附带它。每个 NVX CLI 作业都为其操作系统使用 --release --locked --features cli 构建它,将其放在 nvx-rs/target/release/(nvx.py 查找它的位置),并用它运行 CLI 测试,同时用 NVX_RS 为直接运行它的测试命名:nvx-cli-linux 构建静态的 x86_64-unknown-linux-musl 可执行文件,它可在每个自托管 Linux 运行器上运行;nvx-cli-windows 构建带静态 C 运行时的 x86_64-pc-windows-msvc 可执行文件,它不需要 Visual C++ 可再发行组件。每个作业使用 record-nvx-rs-provenance 记录构建的来源信息,并在测试通过后将可执行文件和来源信息作为保留一天的 nvx-rs-linux-musl-executable 和 nvx-rs-linux-musl-provenance 产物上传,或者上传 nvx-rs-windows-msvc-* 对。使用它们的 workflow 以基名 nvx-rs-linux-musl 或 nvx-rs-windows-msvc 命名该对,就像它们命名 OpenVMM 产物一样。该操作系统的每个微虚拟机测试作业都会将可执行文件下载到 nvx-rs/target/release/,并在 Linux 上恢复其可执行权限。平台作业既不运行 nvx.py sandbox 也不运行 nvx.py run,但它们的 package-release(英文原文)步骤会下载两者,将来源信息下载到 build/,以便每个软件包附带其操作系统的 nvx-rs。managed-lifecycle 场景还会在运行器上用 Cargo 构建并运行被忽略的 nvx-rs 测试 tests/managed_lifecycle.rs,就像 test-aci-edge-sandboxes 构建其 crate 的测试一样。

所需产物准备好后,基准测试会与 NVX 测试层并行运行,各自在后端的基准测试运行器上。三者都使用虚拟机性能序列和受限的八 CPU 亲和性策略。开发发布版和性能基线更新仍然要求每条适用的测试和基准测试通道都通过。微虚拟机正确性作业为基准测试结果的每次使用把关,因此基准测试动作本身不运行任何正确性场景(#286)。Required status check 要求变更所调度的每个 nvx-microvm-tests-* 作业成功。Publish development release 和 Persist performance baseline 仅在每个微虚拟机测试作业都成功或被跳过的 dev 推送中运行。PR 的 Performance regression gate 只读取平台作业的结果,不发布任何内容。基准测试依赖的计数 LAPIC 由这些作业的 smp 场景覆盖。smp-lapic 会重复 smp 并断言计数 LAPIC 相关事实,仅供本地显式使用;没有 CI 作业运行它。基准测试列出了使用、把关、发布或持久化结果的作业。工作流使用存储在 OPENVMM_DEPLOY_KEY Actions 密钥中的只读 OpenVMM 部署密钥,在固定提交处获取私有子模块。共享客户机二进制文件和开发发布版软件包通过短生命周期的工作流产物传递,与 OpenVMM 交接和基准测试结果并列。缓存只加速可复现的构建输入和输出;消费者不依赖它们进行交接。PR 会针对最近匹配平台的历史记录把关回归,成功推送到 dev 的运行会在 data/ 下追加其 p50 值。仅元数据的性能作业使用 GitHub 托管的 Ubuntu 运行器。供应说明见运行器引导指南。

持久运行器仅接受推送和同仓库 PR。Fork PR 会运行 GitHub 托管的验证作业,但不会在 Azure 运行器群上执行代码。维护者必须先将外部贡献暂存到受信任的仓库分支,然后才能运行后端矩阵。

作业图与运行器容量

dev 推送或已准备好审查的 PR 会立即启动以下作业:GitHub 托管运行器上的 quality、aci-edge-sandboxes、nvx-rs、nvx-cli-linux 和 nvx-cli-windows,以及对变更进行分类的 openvmm-changes(见变更分类)。当它完成后,artifacts 和 debug-kernel 会在 GitHub 托管运行器上恢复或构建客户机产物,三个 build-openvmm-* 生产者会分别在 KVM、MSHV 和 WHP 运行器上恢复或构建 OpenVMM,openvmm-unit-tests 会在一个 MSHV 运行器和一个 WHP 运行器上启动。openvmm-vmm-tests 等待 artifacts。每个微虚拟机测试和平台作业都要等待 artifacts、其 OpenVMM 生产者以及其操作系统的 NVX CLI 作业,调试内核作业还要等待 debug-kernel。Performance regression gate、Publish development release、Persist performance baseline 和 Required status check 等待它们要检查结果的作业。这些作业之间的必需交接使用同一次运行的工作流产物;缓存只用于跳过重新构建其输入。

NVX CLI 测试不依赖后端,因此每个操作系统只在 nvx-cli-linux 和 nvx-cli-windows 中运行一次,而不是在每个 hypervisor 作业中运行。每个作业都会检出 OpenVMM(其中固定的 CPU 配置档案会被某个测试读取),并使用 Python 3.12,即自托管运行器提供的版本;Windows 作业还会以最小配置档案安装固定的 Rust 发布版,用于 curl shim 测试和 nvx-rs 构建。每个作业首先构建 nvx.py 所运行的 nvx-rs 可执行文件,用它进行测试,然后如上所述将其交给其操作系统的微虚拟机测试和平台作业。Fork PR 缺少 OpenVMM 部署密钥,因此 CLI 作业与 hypervisor 作业一样,仅对推送和同仓库 PR 运行。由于每个操作系统的微虚拟机和平台作业都要等待其 CLI 作业,损坏的 CLI 不会占用 hypervisor 运行器。Required status check、开发发布版和性能持久化要求两个 CLI 作业都通过。

WHP 池有五台运行器,KVM 池有六台,MSHV 池有九台。当一次运行正在进行时,作业需要等待几秒钟才能获得运行器,但 WHP 作业耗时最长:OpenVMM VMM 测试和单元测试在测试之前要在 Windows 上编译 OpenVMM 约 15 分钟和 13 分钟。一次完整运行在其大部分时间内都占用 WHP 池,因此重叠的 PR 会首先在那里排队。每个平台作业都要等待其后端的基准测试运行器,而该运行器还会承接其池中的其他作业,因此重叠运行的平台作业会依次进行测量。增加作业或分片的变更需要运行器才能并行运行;否则只会排队。

变更分类

openvmm-changes 列出 PR 的 base 与 head 之间、或推送的前一个提交与新提交之间发生变更的文件,nvx.py classify-ci-changes 根据这些路径选择测试套件。检测器使用 git diff --name-only --no-renames -z 和分类器的 --null 选项,以便重命名的两侧以及未加引号的文件名(包括内嵌换行符)都参与决策:

  • run-workloads 仅在每个变更文件都是文档(位于 doc/ 下或以 .md 结尾)时为 false。它启用客户机产物、OpenVMM 生产者和平台基准测试。
  • run-tests 在测试输入发生变化时为 true:OpenVMM 固定版本、客户机及其软件包锁定文件、CLI 及其模块、构建和运行测试的工作流和动作、运行器设置脚本、Rust 固定版本,或 aci_edge_sandboxes 和 nvx-rs crate。它启用 NVX 微虚拟机测试。
  • run-openvmm-unit-tests 和 run-openvmm-vmm-tests 启用 OpenVMM 测试套件。推送会将两者都设为 run-tests。PR 仅在某个套件自身的某个输入发生变化时才设置对应项。单元测试依赖 OpenVMM 固定版本、Rust 固定版本、运行器设置脚本、ci.yml、其作业使用的动作、nvx.py 以及 ci.py 导入的模块。VMM 测试还会启动内核和 Alpine initramfs,因此它们会加入这些产物缓存键的每个输入以及 build-guest-artifacts 动作。

因此,如果一个 PR 既不改变 OpenVMM 任一测试套件的输入(例如只改变微虚拟机测试、基准测试或 Ubuntu 或 Azure Linux 客户机),就会跳过两个最占用 WHP 运行器的作业,而合并它的 dev 推送仍会运行这些作业。当比较基准或某个提交不可用、当 diff 或分类器失败、或当分类器重复报告某个套件或完全不报告时,检测器会改为运行完整矩阵,因为缺少输出会跳过其作业。Required status check 将相同的输出传递给 check-required-ci,后者期望每个已调度的作业成功,而其他每个作业被跳过。scripts/test_nvx_tools.py 中的测试会从其作业使用的动作、ci.py 导入的模块以及客户机缓存键推导每个套件的输入,并在某个模式遗漏某项时失败。

宿主资格

python3 scripts/nvx.py doctor --backend <kvm|mshv|whp> 验证宿主是否满足时间 ABI的要求。它按顺序运行 H1 到 H7 检查,每项检查打印一行 NVX-DOCTOR: check=<id> status=<pass|fail> detail="...",并在任何检查失败时以状态 1 退出。--checks 选择子集,--summary 会将包含 CPU 代次、配置档案、速率和偏差指标的 Markdown 表格追加到诸如 $GITHUB_STEP_SUMMARY 的文件。H4 和 H6 在长时间资格验证调度方案上运行,约需三分钟;--ci-schedule 选择 CI 的短调度方案。与时间 ABI 失败代码匹配的失败会在其详细信息开头以方括号形式给出代码,例如 [E_PROFILE_HOST_UNKNOWN]。当检查无法读取其门禁所依据的事实时,它会失败,而绝不会通过:缺失或无法识别的工具输出会使其失败,而某一项检查中的错误只会使该项检查失败。只有证据字段(例如微码和不变 TSC 标志)可以读取为 unknown。

检查 实现
H1 /dev/kvm 或 /dev/mshv 可读且可写,并且 KVM 宿主没有 /dev/mshv;在 Windows 上,WHvGetCapability 报告存在 hypervisor
H2 从 /proc/cpuinfo 或 Windows 注册表读取供应商、家族、型号、步进、微码和操作系统构建号。然后 openvmm --hypervisor <backend> --cpu-fingerprint <path> 写入宿主的 CPU 指纹(默认在探针目录中写入 nvx-cpu-fingerprint-<backend>.json;--cpu-fingerprint 可覆盖它),将其与 auto 从 OpenVMM 目录中选择的固定配置档案进行比对(该目录在每个代次上跨后端共享一个配置档案),并打印一行 NVX-CPU-PROFILE:。该检查从不回退到宿主配置档案,因此 auto 会回退的 CPU 会使 H2 失败。H2 从该行报告代次和配置档案,因此 OpenVMM 固定版本新增的配置档案无需 NVX 变更即可使其宿主合格。H2 要求退出状态为 0、status=pass、后端相同,并且配置档案是所报告代次在目录中的配置档案;它会报告配置档案和 surface 摘要,否则会以 OpenVMM 的代码失败,例如在没有任何配置档案服务的 CPU 上返回 [E_PROFILE_HOST_UNKNOWN],或返回 [E_PROFILE_UNSUPPORTED] 并列出每个不受支持的 CPUID 位。在 MSHV 和 WHP 上,OpenVMM 会在由该配置档案配置的探针分区上检查配置档案之外的条目,就像冷启动所做的那样。使用 --no-openvmm 时,H2 改为使用 NVX 的目录副本映射宿主:skylake-sp(6/85,步进 0 到 4,intel.skylake-sp.v1)、icelake-sp(6/106,intel.icelake-sp.v1)、emeraldrapids(6/207,intel.emeraldrapids.v1)、alderlake(6/151 和 6/154,intel.alderlake.v1)、milan(AMD 25/1,amd.milan.v1)、genoa(AMD 25/17,amd.genoa.v1)或 turin(AMD 26/2,amd.turin.v1)。任何其他 CPU(包括 Cascade Lake 和 Cooper Lake)都会以 E_PROFILE_HOST_UNKNOWN 失败。单元测试使该副本与 OpenVMM 子模块固定修订版的配置档案保持一致(它从 gitlink 的提交读取修订版),因此 NVX CLI 作业在检出 OpenVMM 后验证 CLI。宿主操作系统的不变 TSC 标志(constant_tsc nonstop_tsc,或 Windows 上的 CPUID 位)仅作为证据记录,绝不会使检查失败,因为它们不决定客户机观察到什么:Azure WHP 宿主显示 CPUID 位,但无法向分区提供不变 TSC,其客户机测得数十纳秒的偏差
H3 openvmm --x-time-abi-verify 构建分区并在不运行客户机的情况下运行时间 ABI 预检。其 NVX-TIME-ABI-VERIFY: 行必须报告该后端的 status=ok、合理的声明和原生 TSC 速率、后端的 LAPIC 速率,以及一个目录配置档案形式的 cpu_profile,即任意代次的 <vendor>.<generation>.v<revision>,而不是宿主配置档案的 host——这会拒绝 auto 在无内置配置档案服务的 CPU 上回退到的宿主配置档案;并且当 H2 也运行时,其修订版必须与 H2 命名的配置档案一致。预检失败会报告 OpenVMM 的代码,例如 [E_TSC_SYNC_UNSUPPORTED]
H4 用宿主的单调时钟对 TSC 进行采样,采样之间睡眠以使宿主 CPU 空闲:13 个样本,间隔 10 秒;或使用 --ci-schedule 时 3 个样本,间隔 1 秒。每个样本在两次时钟读取之间读取 TSC,并保留 64 个测量区间中最紧的一个;其不确定性为区间宽度的一半加上时钟分辨率的一半。如果时钟在连续读取中返回相同的值,则其粒度比一次读取更粗,因此探针将最小步进作为其分辨率:Hyper-V 的参考 TSC 页以 100 ns 步进推进 Linux 时钟,尽管 clock_getres 报告 1 ns。间隔稳定性是相对于时间同步从不调整的时钟来判定的,即 Linux 上的 CLOCK_MONOTONIC_RAW 和 Windows 上的 QueryPerformanceCounter:连续样本之间的每个间隔必须在 0.25 ppm 内是确定的,并且各间隔速率必须在 1 ppm 内一致。在 Azure 运行器上,chrony 的频率更新会使 CLOCK_MONOTONIC 的速率在秒与秒之间最多变化几个 ppm,而这与 TSC 无关。整个窗口内的速率是相对于受纪律约束的时钟(Linux 上的 CLOCK_MONOTONIC)测量的,并且当 H3 在同一次调用中运行时,必须在 H3 报告的速率 100 ppm 以内。Linux 宿主的时钟源会作为证据记录
H5 在每一对宿主 CPU 上执行固定线程乒乓往返;max_abs_offset_ns 至多为 1,000,测量是确定的,且没有任何一对停滞
H6 一台具有最大受支持 vCPU 数量(最多 8 个)的微虚拟机通过 nvx-time status 报告一行有效的 NVX-TIME-ABI 启动行,并在每一对 CPU 上运行 nvx-time-probe warp 五次,各次运行之间所有 vCPU 分别停机 0.1、1、5 和 1 秒,以便没有不变 TSC 的宿主在空闲宿主 CPU 唤醒时校正客户机 TSC(#265);然后一台 1 vCPU 的微虚拟机再运行一次。每次运行都保持在 1,000 ns 以内。--ci-schedule 改为运行 CI 的调度方案:两次运行,间隔 1 秒
H7 在 Linux 上,adjtimex 不报告 STA_UNSYNC 和 TIME_ERROR;在 Windows 上,w32tm /query /status 指明已同步的源。当 H7 无法读取其中任一内容时,其详细信息以 cannot verify host UTC synchronization 开头:adjtimex 失败或报告 Linux 未定义的时钟状态,或者 w32tm 失败,或未打印英文的 Source 和 Leap Indicator 行(例如 Windows 显示语言不是英文时)

H2、H4 和 H5 使用一个无依赖的宿主探针 host_time_probe.rs(英文原文),doctor 会针对每个源码版本用 rustc 将其构建到 $RUNNER_TOOL_CACHE/nvx-host-time-probe(在 CI 之外则为 build/host-time-probe)。H2 的配置档案检查、H3 和 H6 需要 OpenVMM 二进制文件,H3 和 H6 还需要客户机产物;--openvmm、--kernel 和 --initrd 可覆盖它们的默认构建路径。--no-openvmm 可在没有 OpenVMM 的情况下验证宿主:此时 H2 仅检查 CPU 身份和代次,H3 和 H6 无法运行。

doctor 只根据可测量的属性设门禁,在每个后端上都一样:带空闲间隔的 1 µs 客户机 warp 探针(H6)、TSC 速率稳定性(H4)以及 CPU 配置档案(H2 和 H3)。宿主操作系统的不变 TSC 标志和时钟源在 doctor 和 CI 中都只是证据。在 CI 中,validate-runner 会在每个使用它的作业中,按短的 --ci-schedule 运行廉价的主机级检查 H1、H2 和 H4。微虚拟机和平台作业在下载 OpenVMM 和客户机产物后运行它,并添加 H2 的 CPU 配置档案检查和 H3,以便 H4 还将测得的速率与后端的原生速率进行比较;其他作业传入 --no-openvmm。客户机 warp 探针需要一次时间 ABI 启动:平台作业在基准测试之前按 CI 的调度方案添加 H6,而微虚拟机启动和恢复场景在每次启动和恢复后运行 CI 的 warp 调度方案并断言其判定。H5 和 H7 仍可用于交互式资格验证。

Rust 工具链

rust-toolchain.toml(英文原文)固定了用于构建 OpenVMM、其测试客户机、aci_edge_sandboxes 和 nvx-rs crate 以及 NVX 编译的宿主工具(例如 doctor 的时间探针和 Windows curl shim)的确切 Rust 发布版。Rustup 会在检出目录中的每次 cargo 和 rustc 运行中应用该文件,包括 openvmm 子模块中的运行;test-openvmm 还会将该发布版导出为 RUSTUP_TOOLCHAIN,因为 flowey 可能在检出目录之外运行 Cargo。该固定项是确切的发布版而非 stable,因此在不同时间供应的运行器会使用相同的编译器构建(#402)。Rust 1.99.0 无法链接 OpenVMM 的 guest_test_uefi,会失败并报 undefined symbol: wcslen(rust-lang/rust#163614)。

自托管运行器将该发布版保存在作业无法修改的受信任 RUSTUP_HOME 中,因此运行器设置脚本会连同其 OpenVMM 目标一起安装它。validate-runner 会禁用 rustup 的自动安装,并在运行器缺少该发布版时、在构建任何内容之前使作业失败,同时指明要重新运行的设置脚本。OpenVMM 二进制缓存键包含该文件的哈希,因此新的发布版会重新构建 OpenVMM,而该文件的变更会运行 OpenVMM 测试矩阵。

要移动固定项,首先确认 guest_test_uefi 能与新发布版链接:

rustup toolchain install RELEASE --profile minimal --target x86_64-unknown-uefi
cd openvmm
cargo +RELEASE build -p guest_test_uefi --target x86_64-unknown-uefi --profile release

然后一起修改 rust-toolchain.toml、OpenVMMBuildConstants.RUST_TOOLCHAIN 以及设置工具清单中的 rust.toolchain,设置脚本和 Specula 运行器设置都会读取这些内容;当它们不一致时,scripts/test_nvx_tools.py 中的测试会失败。在变更合并之前,按照设置指南更新每台自托管运行器。

Rust crate

必需的 aci-edge-sandboxes 作业通过 check-aci-edge-sandboxes(英文原文)动作,在 GitHub 托管的 Ubuntu 和 Windows 运行器上检查 aci_edge_sandboxes crate。该动作运行 rustfmt、将警告视为错误的 Clippy 以及 rustdoc,全部使用 rust-toolchain.toml 固定的 Rust 发布版。它还会运行单元测试、模拟测试和伪 OpenVMM 集成测试,检查声明的最低 Rust 版本,并交叉检查 MXC 编译的 macOS 构建。伪 OpenVMM 测试在没有 hypervisor 的情况下通过控制协议驱动真实的 OpenVMM 后端。它还会运行 nvx.py test-agent-bundling,该命令使用合成捆绑包验证边缘代理捆绑包的暂存,捆绑代理测试必须使用编译时写入的摘要找到该捆绑包,并检查构建会拒绝被篡改的文件、错误的固定项、另一个沙箱 ABI 以及链接:Linux 上的符号链接和 Windows 上的 junction。

必需的 nvx-rs 作业通过 check-nvx-rs(英文原文)动作,在相同的运行器上检查 nvx-rs crate,该 crate 包含后端所依赖的 NVX 机制。它运行 rustfmt、对所有目标和特性以及无默认特性的库将警告视为错误的 Clippy、单元测试、黄金协议测试夹具测试和伪 OpenVMM 实例测试、将警告视为错误的 rustdoc、最低 Rust 版本检查以及 macOS 交叉检查。它还会使用一个夹具软件包验证捆绑产物的暂存。两个动作都会为 --all-features 构建设置 NVX_BUNDLE=skip,以便 bundled 不暂存任何内容,而不是下载固定的发布版;aci_edge_sandboxes 动作还会设置 ACI_EDGE_SANDBOXES_AGENT_BUNDLE=skip,以便 agent-bundled 不下载边缘代理捆绑包。

在 Linux 上,nvx-rs 作业还会针对客户机所运行的 target x86_64-unknown-linux-musl 对客户机二进制文件进行 lint 和测试,并使用 guest 配置档案构建它们。该动作将 musl lint 和测试委托给 nvx.py check-guest-binaries,将全新的、路径重映射的构建、静态 ELF 检查和摘要清单委托给 nvx.py build-guest-binaries,并将预构建二进制文件的集成运行委托给 nvx.py test-guest-binaries。同一个共享例程为每次原生和 Docker 客户机构建提供支持,Alpine、Ubuntu 和 Azure Linux 都附带它的九个可执行文件。

开发发布版作业依赖 aci-edge-sandboxes 和 nvx-rs,并要求它们合并后的 Linux/Windows 结果成功;失败、取消或跳过的 crate 检查无法发布发布版。在上传任何内容之前,它会检查每个平台的软件包都附带 bin/openvmm 和 bin/nvx-rs,或在 Windows 上附带它们的 .exe 形式,download 会安装这些内容。

然后,每个 nvx-microvm-tests-{kvm,mshv,whp} 作业都会在其自托管运行器上运行 nvx.py test-aci-edge-sandboxes;调试内核作业会跳过它,因为它会启动生产内核。该命令使用该 crate 的 OpenVMM 后端驱动 Alpine 客户机的完整 provision、start、exec、stop、start 和 deprovision 周期。它还会检查取消、客户机状态仅持续到 stop、start 会终止之前被中断的 start 所对应的 VM、宿主路径映射、出站规则以及每次执行的环境和工作目录。失败时,它会把 OpenVMM 日志保存在 build/test-results/aci-edge-sandboxes-<backend> 下,并与其他微虚拟机日志一起上传。因此,aci_edge_sandboxes/ 或 nvx-rs/ 下的变更会触发 NVX 微虚拟机测试矩阵。

对抗性活动

独立的 adversarial.yml(英文原文)工作流仅在受信任的手动调度或来自 dev 的计划调度中运行 Copilot 驱动的活动。它不属于 PR CI。该工作流专用的 nvx-adversarial-controller 运行器必须已经具备经过身份验证的 Copilot CLI,以及一个由 NVX_ADVERSARIAL_EXECUTOR 仓库变量指定、管理员拥有的执行器包装器。该工作流不会安装 Copilot 或启动登录。

该包装器会供应一个独立的、一次性的 KVM、MSHV 或 WHP 目标,该目标没有生产或 GitHub 凭据,并且只转发类型化的执行器协议。除了客户机和 OpenVMM 产物之外,目标还需要 Rust 工具链以及带有来源信息的预构建 nvx-rs,供运行它的场景使用,具体见目标上的 nvx-rs。现有的持久微虚拟机和性能运行器不是有效的对抗性目标。目标心跳丢失、策略 oracle 或清理/活动后启动失败会使作业失败,并要求隔离和重装镜像。

常规 Actions 产物仅包含不含客户机文本的公开摘要、编目的用例标识符和重放清单。外部供应器必须将控制器记录和完整目标日志收集到受访问控制的安全存储中。有关架构和操作契约,见 Copilot 驱动的对抗性测试。

Copilot 环境

copilot-setup-steps.yml(英文原文)为 Copilot 云代理会话(包括从 GitHub Web 界面启动的会话)准备 GitHub 托管的运行器。会话的 59 分钟限制包含这些步骤,因此它们只安装固定的工具、恢复缓存并暂存下载内容;它们从不编译 NVX 或 OpenVMM。该工作流无需部署密钥即可初始化公开的 OpenVMM 子模块,授予对运行器 /dev/kvm 的访问权限,将 Python 开发工具安装到基于镜像 Python 3 的虚拟环境中(validate-nvx 也使用该环境),并安装 rust-toolchain.toml、crate 清单和设置工具清单所固定版本的 Rust 和 cargo-nextest。它通过 build-guest-artifacts(英文原文)的 restore-only 输入恢复共享客户机产物,并恢复 build-openvmm 为固定修订版缓存的 KVM OpenVMM 二进制文件,因此代理可以运行微虚拟机测试而无需重新构建其中任何一个。它不构建 nvx-rs:nvx.py 会在命令首次需要它时用 Cargo 构建它,并记录其来源信息。客户机产物恢复使用与保存时相同的动作计算键。二进制文件恢复重复 build-openvmm 所保存缓存条目的键、路径和 gzip 压缩,当它们不一致时,scripts/test_nvx_tools.py 中的测试会失败,因为不匹配否则只会导致静默的缓存未命中。由于代理防火墙会阻止 cdn.kernel.org 和 cdimage.ubuntu.com(即使在容器内也是如此),该工作流还会安装原生客户机构建的前提条件,包括固定 Rust 工具链的 x86_64-unknown-linux-musl 目标;允许 Alpine 的 apk 用于软件包触发器的非特权用户命名空间;并暂存固定的 Linux 和 Ubuntu Base 归档,以便代理可以使用 build-guest --native 重新构建客户机产物。当运行器有单独的 /mnt 磁盘且其可用空间比 / 更多时,它会把工作区挂载到那里。失败的步骤会使 Copilot 跳过之后的所有设置步骤,因此检出后的每个步骤都会在出错时继续。最后一步将 NVX_COPILOT_SETUP 设为 complete 或 incomplete,在 NVX_COPILOT_SETUP_FAILED 中列出失败的步骤 ID,并在设置不完整时使运行失败。Copilot 始终运行 dev 上的版本,即使是基于其他分支的会话也是如此,因此变更只有在合并后才会到达代理会话。当推送更改了该工作流或该工作流步骤从中获取版本、要求、元数据或代码的文件时,推送会将其作为普通工作流运行以进行验证:build-guest-artifacts 动作、所有 nvx-rs/** 文件、固定其 Rust、cargo-nextest 和 shell linter 版本的文件、requirements-dev.txt、SOURCE-MANIFEST.json、.gitmodules、OpenVMM 子模块固定项,以及 NVX CLI 及其模块。

Copilot 代码审查则使用 copilot-code-review.yml(英文原文)。审查不构建代码,因此它只检出仓库,不安装云代理会话所使用的构建和测试依赖。