8000 ci: add the Linux-native lane, retire the mothballed netns lane by norrietaylor · Pull Request #697 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions .github/workflows/ci-linux-native.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
name: CI (Linux native)

# The "Linux with native minimald" target lane: workspace tests plus a real
# daemon e2e (DM2 — minimald natively on the host, no VM), which nothing
# covered before this lane existed.
#
# ALWAYS-TRIGGERED, path-scoped INSIDE the workflow: the `changes` job
# (dorny/paths-filter) gates the expensive jobs, and the `if: always()`
# aggregator reports success even when they were path-skipped. This is the
# lane pattern that will replace the ci-docs-skip.yml inverse-path hack once
# the aggregators become the required checks (#687): a required context that
# a trigger-level path filter skips stays "Expected" forever and wedges the
# PR, while a skipped in-workflow job just reports `skipped`, which the
# aggregator treats as pass. Do NOT add trigger-level `paths:` here.
#
# The mothballed networking proofs (netns UC1/UC6, mesh UC7, and the
# networking-proxy / networking-wg feature tests) are deliberately NOT run
# in this lane — see the note on the tests job.
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
workflow_dispatch:

env:
CARGO_TERM_COLOR: always

# Cancel superseded runs on a branch; never cancel a run on main.
concurrency:
group: ci-linux-native-${{ github.ref }}
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:
native: ${{ steps.filter.outputs.native }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Detect relevant changes
id: filter
# v3.0.2, SHA-pinned. On workflow_dispatch there is no diff to filter;
# the downstream `if` treats dispatch as "run everything".
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36
with:
filters: |
native:
- "crates/**"
- "Cargo.toml"
- "Cargo.lock"
- "scripts/**"
- ".minimal/**"
- ".github/actions/setup-rust/**"
- ".github/actions/core-tests/**"
- ".github/workflows/ci-linux-native.yml"

tests:
# Core workspace suite (nextest + doctests), taking over ci.yml's `test`
# job for this lane; ci.yml keeps its copy until the ruleset requires
# this lane's aggregator (#687), then drops it.
#
# MOTHBALLED, deliberately absent (owner decision, #687): the
# `--features networking-proxy` (mTLS reverse proxy) and `--features
# networking-wg` (WireGuard mesh) test steps that ci.yml's `test` job
# still carries, and the netns/mesh namespace proofs that ci-netns.yml
# ran (crates/minimald/tests/netns.rs, mesh_uc7.rs). The code and tests
# stay in-tree and runnable locally; they are just not CI-covered.
needs: [changes]
if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.native == 'true'
runs-on: ubuntu-latest
timeout-minutes: 40
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Run core workspace tests
uses: ./.github/actions/core-tests

native-daemon-e2e:
# The unified session e2e (scripts/session-e2e.sh — the SAME proof every
# target lane runs) against the NATIVE minimald (DM2): `minimal activate`
# auto-spawns `minimald run --detach`, creates a session for this repo's
# own project, `minimal attach --command` execs in the sandbox asserting
# stdout + exit status, then list/destroy/stop. Before this job, no CI
# ran the daemon natively at all. The native target needs no E2E_* knobs:
# host daemon, repo as the project.
needs: [changes]
if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.native == 'true'
runs-on: ubuntu-latest
timeout-minutes: 40
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: ./.github/actions/setup-rust
- name: Enable unprivileged user namespaces
# The session sandbox (hakoniwa) needs userns; Ubuntu 24.04 restricts
# them via AppArmor by default.
run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
- name: Cache minimal package cache
# Session creation materializes the project's stack (rust + socat)
# into ~/.cache/minimal via cache pulls keyed by the pinned upstream
# commit; carry it across runs so the e2e isn't dominated by pulls.
# The e2e script resets XDG_STATE_HOME/XDG_RUNTIME_DIR but leaves
# XDG_CACHE_HOME alone for exactly this reason.
uses: actions/cache@v6
with:
path: ~/.cache/minimal
key: native-e2e-minimal-cache-${{ hashFiles('.minimal/minimal.toml') }}
restore-keys: native-e2e-minimal-cache-
- name: Build minimal + minimald
run: cargo build -p minimald --bin minimald -p minimal --bin minimal
- name: Session e2e (native minimald)
run: |
export PATH="$PWD/target/debug:$PATH"
./scripts/session-e2e.sh

# Single required status check for this lane (branch-protection context:
# ci-linux-native-success). Green when every gating job succeeded OR was
# path-skipped; red only on failure/cancellation.
ci-linux-native-success:
if: always()
needs: [changes, tests, native-daemon-e2e]
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-native lane green"
101 changes: 26 additions & 75 deletions .github/workflows/ci-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,10 @@ jobs:
# boots the same kernel/rootfs/initramfs through the same HVF path as the
# retired standalone boot test and goes further (russh auth + exec,
# stdout + exit status), so a passing session gates the boot. The
# Auto-spawn step then covers the R4.5 CLI proof: from a clean state
# `minimal ls` must auto-spawn minvmd end to end — the macOS integration
# gate that no unit test can cover.
# unified session e2e (scripts/session-e2e.sh) then covers the R4.5
# CLI proof and more: from a clean state `minimal activate` must
# auto-spawn minvmd and a real session exec must round-trip — the
# macOS integration gate that no unit test can cover.
if: ${{ vars.RUN_MACOS_CI != 'false' }}
needs: [artifacts]
runs-on: [self-hosted, macOS, ARM64]
Expand Down Expand Up @@ -226,97 +227,47 @@ jobs:
MINVMD_INITRAMFS="$RUNNER_TEMP/initramfs/initramfs.cpio" \
MINVMD_BOOT_LOG="$RUNNER_TEMP/boot.log" \
"$testbin" --include-ignored --nocapture --exact minimald_exec_over_bridge
- name: Reap Session-E2E leftovers before auto-spawn
- name: Reap Session-E2E leftovers before the CLI e2e
# The session harness (Guest::drop) kills its minvmd but not the
# rest of the process tree it spawned: the detached `__krun-vmm`
# grandchild AND the host gvproxy switch minvmd owns. Either
# leftover wedges the host→guest bridge over libkrun's vsock
# (#588) — a healthy guest accept loop that never receives the
# bridged connection — so the cold `minimal ls` dies with `ssh
# connect: Disconnected`. The `__krun-vmm`-only reap proved
# bridged connection — so the CLI e2e's cold activate dies with
# `ssh connect: Disconnected`. The `__krun-vmm`-only reap proved
# insufficient: main run 29032763009 (on the commit that added
# it) failed exactly this way, with the new VM's proxy-publish
# WARN pointing at a lingering gvproxy. Reap the whole tree —
# any stray minvmd first (so nothing respawns), then the VMM and
# gvproxy — so Auto-spawn starts from the single-VM state it had
# when it was its own job. The proper fix (harness reaps its own
# gvproxy — so the CLI e2e starts from the single-VM state it
# had when it was its own job. The proper fix (harness reaps its own
# process group) is tracked under #588.
run: |
pkill -x minvmd 2>/dev/null || true
pkill -f '__krun-vmm' 2>/dev/null || true
pkill -f gvproxy 2>/dev/null || true
sleep 1
- name: Auto-spawn E2E (minimal ls cold/warm, R4.5)
# The R4.5 CLI proof artifact, on real hardware. From a clean state
# `minimal ls` must auto-spawn minvmd and return within 8 s;
# `minvmd status` must then report running; a second `minimal ls` must
# complete in < 500 ms (warm reuse). This exercises the real
# minimal -> `minvmd run --detach` -> VM boot -> initramfs minimald
# session path end to end.
# Run the prebuilt binaries directly (never `cargo run`, which would
# relink and unsign minvmd). minimal finds `minvmd` via PATH; all three
# of MINVMD_KERNEL_PATH / MINVMD_ROOTFS_PATH / MINVMD_INITRAMFS propagate
# through the `minvmd run --detach` re-exec. A fresh XDG_STATE_HOME
# guarantees the clean (no-daemon) starting state.
- name: Session e2e via the CLI (activate/exec/destroy; autospawns minvmd)
# The unified session e2e (scripts/session-e2e.sh — the SAME
# proof every target lane runs), here against the VM-backed
# daemon: from a clean state, `minimal activate` must auto-spawn
# minvmd (found by name on PATH; the MINVMD_* env propagates
# through the `minvmd run --detach` re-exec), boot the guest,
# create a session, and `minimal attach --command` must exec in
# it over the vsock bridge. Replaces the old autospawn step,
# which only proved `minimal ls` cold/warm — this proves the
# full user path and subsumes it (the cold activate IS the
# autospawn). Runs the prebuilt binaries directly (never
# `cargo run`, which would relink and unsign minvmd).
# E2E_PROJECT_DIR=/tmp: the guest sees no host project dir yet
# (no project sync), and /tmp exists in the guest image.
run: |
set -uo pipefail # not -e: capture failures so we can dump diagnostics
export PATH="$PWD/target/debug:$PATH"
export MINVMD_KERNEL_PATH="$RUNNER_TEMP/kernel/vmlinuz"
export MINVMD_ROOTFS_PATH="$RUNNER_TEMP/rootfs/rootfs.img"
export MINVMD_INITRAMFS="$RUNNER_TEMP/initramfs/initramfs.cpio"
# Capture the guest console; this env var propagates through the
# minimal -> `minvmd run --detach` -> `minvmd run` -> __krun-vmm chain,
# so the silent detached supervisor's boot is still observable here.
export MINVMD_BOOT_LOG="$RUNNER_TEMP/autospawn-boot.log"
# All state (minvmd.toml, locks, the bridge socket) lives under
# $XDG_STATE_HOME/minimal/providers/local-0; a fresh dir gives a
# genuinely clean (NotProvisioned) state on the persistent runner.
# Under /tmp, not $TMPDIR: macOS's deep $TMPDIR would push the
# socket paths past the 104-byte sun_path limit.
export XDG_STATE_HOME="$(mktemp -d /tmp/mnl.XXXXXX)"

# macOS `date` has no %N; use perl's high-resolution clock for ms.
now_ms() { perl -MTime::HiRes=time -e 'printf "%d", time()*1000'; }

# On any failure, dump what the silent detach hides — minimal's own
# error, the persisted state, and the guest boot console — then stop
# the supervisor and fail. An empty boot log means the VM never
# started (supervisor died before krun_start_enter); kernel output
# without a full boot means a stuck boot; a full boot points at a
# UDS/timing issue.
fail() {
echo "::group::autospawn-e2e diagnostics"
echo "--- minimal ls stderr ---"; cat "$RUNNER_TEMP/ls.err" 2>/dev/null || true
echo "--- minvmd.toml ---"; cat "$XDG_STATE_HOME/minimal/providers/local-0/minvmd.toml" 2>/dev/null || echo "(none)"
echo "--- guest boot console (tail) ---"; tail -80 "$MINVMD_BOOT_LOG" 2>/dev/null || echo "(no boot log — VM never started)"
echo "::endgroup::"
minvmd stop >/dev/null 2>&1 || true
exit 1
}
trap 'minvmd stop >/dev/null 2>&1 || true' EXIT

# Cold: must auto-spawn minvmd and return within 8 s.
t0=$(now_ms)
minimal ls 2>"$RUNNER_TEMP/ls.err" || { echo "::error::cold 'minimal ls' failed to auto-spawn minvmd"; fail; }
t1=$(now_ms); cold=$(( t1 - t0 ))
echo "cold 'minimal ls': ${cold}ms"
[ "$cold" -lt 8000 ] || { echo "::error::cold 'minimal ls' took ${cold}ms (>= 8000ms)"; fail; }

# minvmd must now report running (exit 0). Poll briefly to absorb the
# gap between the host UDS accepting and the Running state persisting.
running=0
for _ in $(seq 1 50); do
if minvmd status >/dev/null 2>&1; then running=1; break; fi
sleep 0.1
done
[ "$running" = 1 ] || { echo "::error::minvmd did not report running after auto-spawn"; fail; }

# Warm: minvmd already running, must return in < 500 ms.
t0=$(now_ms)
minimal ls >/dev/null 2>"$RUNNER_TEMP/ls.err" || { echo "::error::warm 'minimal ls' failed"; fail; }
t1=$(now_ms); warm=$(( t1 - t0 ))
echo "warm 'minimal ls': ${warm}ms"
[ "$warm" -lt 500 ] || { echo "::error::warm 'minimal ls' took ${warm}ms (>= 500ms)"; fail; }
export MINVMD_BOOT_LOG="$RUNNER_TEMP/cli-e2e-boot.log"
E2E_VM=1 E2E_PROJECT_DIR=/tmp ./scripts/session-e2e.sh
- name: Upload guest boot console logs
# Capture hvc0 output for diagnosing a stuck/failed boot (e.g. a missing
# virtio driver in the kernel). Always runs so a red boot is debuggable.
Expand All @@ -326,7 +277,7 @@ jobs:
name: minvmd-boot-logs
path: |
${{ runner.temp }}/boot.log
${{ runner.temp }}/autospawn-boot.log
${{ runner.temp }}/cli-e2e-boot.log
if-no-files-found: ignore

build-macos:
Expand Down
Loading
Loading
0