Skip to content

fix: Hide -c/--command on min session attach from the CLI help surface #1197

fix: Hide -c/--command on min session attach from the CLI help surface

fix: Hide -c/--command on min session attach from the CLI help surface #1197

Workflow file for this run

name: CI (Linux KVM)
# minvmd Linux/KVM end-to-end on a GitHub-hosted runner.
#
# It needs no preprovisioned runner — KVM is available on GitHub-hosted
# x86_64 Linux runners, and libkrun is materialized from the upstream package
# cache (a cache fetch keyed by the pinned locked_commit, NOT a from-source
# build) alongside the guest kernel + rootfs.
#
# x86_64 ONLY: GitHub-hosted arm64 runners (ubuntu-24.04-arm) do not expose
# /dev/kvm, so the Linux/KVM tests can only run on x86_64. aarch64 + libkrun boot
# is already covered by the macOS lane (ci-macos.yml, Apple Silicon / HVF).
#
# GATE-READY: ALWAYS-TRIGGERED, path-scoped INSIDE the workflow — the
# `changes` job gates the e2e, and the `if: always()` aggregator
# (ci-linux-kvm-success) reports success even when it was path-skipped, so
# the context can join the required checks after this lane soaks green. A
# required context that a trigger-level path filter skips would stay
# "Expected" forever and wedge the PR; do NOT re-add trigger-level `paths:`.
# Until the ruleset flip the lane remains advisory, as before.
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
# Cancel superseded runs so the e2e job isn't queued on stale commits.
concurrency:
group: ci-linux-kvm-${{ github.ref }}-${{ github.workflow }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
permissions:
contents: read
# dorny/paths-filter reads the PR file list on pull_request events.
pull-requests: read
jobs:
changes:
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
kvm: ${{ steps.filter.outputs.kvm }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- name: Detect relevant changes
id: filter
# v3.0.2, SHA-pinned. Runs ONLY on pull_request, where dorny reads
# the PR file list via the API. On push it would need a local diff
# against event.before, which our shallow, credential-free checkout
# (persist-credentials: false) cannot provide — and pushes to main
# SHOULD run everything anyway: they write the main-only caches and
# provide the soak signal. Dispatch likewise runs everything.
if: github.event_name == 'pull_request'
uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706
with:
filters: |
kvm:
- "crates/minvmd/**"
- "crates/minimald/**"
- "scripts/**"
- "Cargo.toml"
- "Cargo.lock"
# The toolchain pin: a rustc bump must re-run the Rust lanes.
- "rust-toolchain.toml"
- ".minimal/minimal.toml"
- ".github/workflows/ci-linux-kvm.yml"
# Composite actions this lane's setup runs through — without
# these, a composite-only edit would skip this lane and break
# it post-merge.
- ".github/actions/setup-libkrun-linux/**"
- ".github/actions/materialize/**"
build-linux:
# BUILD phase: compile everything the KVM tests need and ship it as one
# artifact, so a flaky e2e rerun replays only the ~15-minute test job
# instead of this full build. No /dev/kvm needed here.
needs: [changes]
if: github.event_name != 'pull_request' || needs.changes.outputs.kvm == 'true'
runs-on: ubuntu-latest
timeout-minutes: 60
env:
# Slim debug info (usable backtraces, smaller caches, faster links);
# this job doesn't run the setup-rust composite that sets this on the
# other Linux lanes.
CARGO_PROFILE_DEV_DEBUG: line-tables-only
steps:
- name: Free Disk Space
# The workspace build + libkrun prefix routinely exceed the ~14 GB
# free on a fresh GitHub-hosted Ubuntu runner. `remove_tool_cache:
# true` reclaims /opt/hostedtoolcache (~14 GB); the dtolnay step below
# reinstalls the Rust toolchain we actually need.
uses: endersonmenezes/free-disk-space@7901478139cff6e9d44df5972fd8ab8fcade4db1 # v3
with:
remove_android: true
remove_dotnet: true
remove_haskell: true
remove_tool_cache: true
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- name: Toolchain
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable
with:
toolchain: stable
- name: Install build dependencies
# protoc + libprotobuf-dev: the `minimal` CLI (materialize) build. The
# well-known protos (google/protobuf/*.proto) live in libprotobuf-dev,
# which protobuf-compiler only pulls via recommends — so name it
# explicitly since we use --no-install-recommends. cpio: packs the
# guest initramfs (scripts/build-initramfs.sh). git: source fetches
# during the CLI build.
run: |
sudo sh -c \
'apt-get update && apt-get install -y --no-install-recommends \
protobuf-compiler libprotobuf-dev cpio git'
- name: Install cross + nextest
uses: taiki-e/install-action@3d7d7cd5ac7f994c1892ae0c06165095b9139094 # v2
with:
tool: cross,nextest
- name: Cache cargo + build artifacts
# Restore-only; the save half (main only) runs at the end of the job.
id: cargo-cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-x86_64-minvmd-kvm-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-x86_64-minvmd-kvm-
- name: Materialize libkrun prefix (upstream package)
# Needed at BUILD time: minvmd links libkrun.so (build.rs link search
# + the minvmd_libkrun cfg for the real, non-stub impl). The prefix is
# shipped in the testbed so the test job runs against the exact
# libraries this job linked. Runs after the cargo cache restore so the
# CLI build the cache fetch drives is incremental.
uses: ./.github/actions/setup-libkrun-linux
with:
arch: x86_64
- name: Build guest initramfs (minimald as /init)
run: ./scripts/build-initramfs.sh "$RUNNER_TEMP/initramfs.cpio" x86_64-unknown-linux-musl
- name: Build minvmd
run: cargo build -p minvmd --bin minvmd --locked
- name: Build the minimal CLI
# SEPARATE invocation from the minvmd build: a combined `cargo build
# -p minvmd -p minimal` unifies minvmd's default `libkrun` feature
# into the CLI's default-features=false opt-out, linking libkrun into
# `minimal` (the regression the macOS lane hit and now guards). The
# CLI drives the unified session e2e in the test job.
run: cargo build -p minimal --bin min --locked
- name: Archive integration harnesses + stage the testbed
# `cargo nextest archive` replaces the hand-rolled testbins.json
# manifest: the archive carries every minvmd test binary with exec
# bits intact, and the test job selects harnesses with filtersets.
# The sidecar files stay explicit — libkrun lives outside target/ (an
# archive cannot carry it), and the harnesses/CLI locate minvmd via
# MINVMD_BIN / PATH (their compile-time CARGO_BIN_EXE path does not
# exist on the test runner).
run: |
set -euo pipefail
mkdir -p testbed
cargo nextest archive -p minvmd --locked --archive-file testbed/nextest-archive.tar.zst
cp target/debug/minvmd testbed/minvmd
cp target/debug/min testbed/min
cp "$RUNNER_TEMP/initramfs.cpio" testbed/initramfs.cpio
# Ship the exact libkrun prefix the binaries were linked against.
tar czf testbed/libkrun-prefix.tgz -C "$LIBKRUN_PREFIX" .
- name: Upload testbed
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: kvm-testbed
path: testbed/
if-no-files-found: error
retention-days: 3
- name: Save cargo + build artifacts cache (main only)
# PR branches restore but never write: multi-GB caches written by
# every branch LRU-evict the main entries all PRs restore from.
if: github.ref == 'refs/heads/main' && steps.cargo-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ steps.cargo-cache.outputs.cache-primary-key }}
test-kvm:
# TEST phase: no Rust toolchain, no cargo build — download the testbed,
# pull the guest images, and run the archived harnesses with the
# standalone cargo-nextest binary. A flake rerun of this job alone costs
# ~15 minutes instead of the old 60-minute build+test monolith.
needs: [changes, build-linux]
if: github.event_name != 'pull_request' || needs.changes.outputs.kvm == 'true'
runs-on: ubuntu-latest # x86_64; KVM-capable
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- name: Install nextest
uses: taiki-e/install-action@3d7d7cd5ac7f994c1892ae0c06165095b9139094 # v2
with:
tool: nextest
- name: Enable /dev/kvm access
# GitHub-hosted x86_64 Linux runners expose /dev/kvm but the runner user
# is not in the `kvm` group; widen the node so libkrun can open it. Fail
# fast with a clear message if this runner has no KVM at all.
run: |
if [ ! -e /dev/kvm ]; then
echo "::error::/dev/kvm not present on this runner; cannot run the Linux/KVM tests." >&2
exit 1
fi
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
| sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
ls -l /dev/kvm
- name: Download testbed
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: kvm-testbed
path: ${{ runner.temp }}/testbed
- name: Unpack testbed + resolve paths
# Artifact transfer strips exec bits on the sidecar binaries; restore
# them (the nextest archive preserves its own). MINVMD_BIN points the
# harnesses at the shipped minvmd (their baked CARGO_BIN_EXE path only
# exists on the build runner); the testbed joins PATH so the CLI's
# autospawn and the lifecycle script resolve minvmd/minimal by name.
# The libkrun prefix is unpacked to the same $HOME/.krun path the
# build linked (rpath) and exported for the dynamic loader; its
# krun_add_disk3 export was verified at build time by the
# setup-libkrun-linux composite — minvmd loading it here is the
# runtime backstop.
run: |
set -euo pipefail
TESTBED="$RUNNER_TEMP/testbed"
chmod +x "$TESTBED/minvmd" "$TESTBED/min"
mkdir -p "$HOME/.krun"
tar xzf "$TESTBED/libkrun-prefix.tgz" -C "$HOME/.krun"
echo "$TESTBED" >> "$GITHUB_PATH"
{
echo "TESTBED=$TESTBED"
echo "MINVMD_BIN=$TESTBED/minvmd"
echo "LIBKRUN_PREFIX=$HOME/.krun"
echo "LD_LIBRARY_PATH=$HOME/.krun${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
echo "MINVMD_E2E=1"
echo "MINVMD_KERNEL_PATH=$RUNNER_TEMP/vmlinuz"
echo "MINVMD_ROOTFS_PATH=$RUNNER_TEMP/rootfs.img"
echo "MINVMD_INITRAMFS=$TESTBED/initramfs.cpio"
} >> "$GITHUB_ENV"
- name: Materialize guest kernel (raw Image)
uses: ./.github/actions/materialize
with:
output: virtio-kernel
dest: ${{ runner.temp }}/vmlinuz
arch: x86_64
channel: unstable
- name: Materialize guest rootfs (ext4 image)
uses: ./.github/actions/materialize
with:
output: minvmd-rootfs
dest: ${{ runner.temp }}/rootfs.img
arch: x86_64
channel: unstable
- name: VM integration harnesses (auto-discovered)
# Convention over enumeration: run every harness the archive carries by
# the `_integration` suffix, so a new crates/minvmd/tests/*_integration.rs
# file is picked up here with zero workflow edit (the auto-discovery
# contract; a normal unit test in minvmd guards the suffix).
# `--run-ignored all` runs
# the `#[ignore]`-gated bodies; `--no-tests=fail` reds the job if the
# filterset ever matches nothing. MINVMD_E2E=1 is set job-wide; profile
# vm is serial with fail-fast, so a failure preserves its own boot.log.
# The `not binary(/_root_integration$/)` guard is defensive: minvmd has no root
# harness today (its host-side tap relay moved into the guest minimald),
# so if one is ever added it must get its own sudo-wrapped step rather
# than run unprivileged here.
env:
MINVMD_BOOT_LOG: ${{ runner.temp }}/boot.log
run: |
cargo-nextest nextest run --archive-file "$TESTBED/nextest-archive.tar.zst" \
--workspace-remap "$GITHUB_WORKSPACE" --profile vm \
--run-ignored all --no-tests=fail \
-E 'binary(/_integration$/) and not binary(/_root_integration$/)'
# NOTE: there is no separate concurrent-bridge harness. The retired
# Stage-1 vsock stub (a socat/cat echo on port 2222) that one exercised was
# superseded by minimald's direct SSH session server; session coverage of
# the bridge is minimald_session_integration (in the harness step above)
# plus the daemon lifecycle step below.
- name: Reap leftover VM processes
# The harness runs above can leak __krun-vmm children on failure
# (Guest::drop kills only its own minvmd), and a leftover wedges the
# vsock bridge for the fresh daemons the next two steps spawn
# (lifecycle, then the CLI session e2e). The harnesses run as the
# runner, so leftovers are runner-owned; sudo is kept as a defensive
# backstop. Ported from the macOS lane; the proper fix is for the
# harness to reap its own process group, tracked separately.
run: |
sudo pkill -x minvmd || true
sudo pkill -f __krun-vmm || true
- name: Daemon lifecycle (run --detach → status → stop)
# Exercises the supervised daemon path that manages the lifecycle
# state, which the integration harnesses above do not cover. The script
# resolves minvmd from PATH (the testbed dir, added at unpack).
env:
MINVMD_BOOT_LOG: ${{ runner.temp }}/boot-daemon.log
run: ./scripts/minvmd-lifecycle.sh
- name: Fetch pinned gvproxy switch binary
# minvmd spawns this as the per-host-VM switch that provides the guest's
# egress (NAT + DNS). Without it minvmd boots the VM switchless
# (gvproxy resolution is best-effort and never errors), so the guest's
# overlay has no other end and every egress — including the in-guest
# `pkgs` clone during session mint — dies with "Could not resolve host".
run: ./scripts/fetch-gvproxy.sh "$RUNNER_TEMP/gvproxy"
- name: Session e2e via the CLI (activate/exec/destroy; autospawns minvmd)
# The SAME unified proof every target lane runs (scripts/session-e2e.sh)
# against Deployment Model 3 (native Linux + VM — the target this
# lane owns; see docs/specs/03-spec-networking): from a clean state
# `minimal --provider local-minvmd activate` must auto-spawn minvmd, boot the microVM,
# and round-trip a session exec over the vsock bridge — the Linux/KVM
# counterpart of the macOS lane's CLI gate. Since #748 activate uploads
# the project into the guest; E2E_PROJECT_DIR=/tmp still names a guest
# path. minimal/minvmd resolve from the testbed on PATH.
env:
MINVMD_BOOT_LOG: ${{ runner.temp }}/cli-e2e-boot.log
MINVMD_GVPROXY_BIN: ${{ runner.temp }}/gvproxy
run: E2E_VM=1 E2E_MINIMAL_ARGS="--provider local-minvmd" E2E_PROJECT_DIR=/tmp ./scripts/session-e2e.sh
- name: Upload guest boot console logs
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: minvmd-linux-boot-log-x86_64
path: |
${{ runner.temp }}/boot.log
${{ runner.temp }}/boot-daemon.log
${{ runner.temp }}/cli-e2e-boot.log
if-no-files-found: ignore
# Single required status check for this lane (branch-protection context:
# ci-linux-kvm-success, joins the ruleset after the soak). Green when
# build + tests succeeded OR were path-skipped; red only on failure/cancellation.
ci-linux-kvm-success:
if: always()
needs: [changes, build-linux, test-kvm]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Verify all gating jobs succeeded or were skipped
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
run: exit 1
- run: echo "Linux-KVM lane green"