You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
GitHub Action for the Passport CI gate (action.yml) — reusable
composite action wrapping doctor → migrate-plan → passport emit → passport verify as a single CI step; installs a checksum-verified
release binary, no build step. Dogfooded against a real disk image by .github/workflows/passport-gate-demo.yml on every change. See docs/devops/01-passport-ci-gate.md.
Native OpenAI tool-calling for the AI copilot (src/ai/rig_tools.rs)
— rig-core AgentBuilder/multi_turn with real JSON-schema tool
definitions, replacing regex/JSON-scraped completion text for OpenAI.
xAI/Anthropic/Ollama still use the original text-instructed loop.
Cross-run AI memory (src/ai/memory.rs) — a repeated doctor --ai/ migrate-plan --ai run against the same VM folds a summary of prior
findings into the query, capped at the last 20 runs. GUESTKIT_AI_MEMORY_DIR/GUESTKIT_AI_MEMORY=0 to relocate/disable.
MCP server for the AI copilot (src/ai/mcp.rs, --features mcp) — guestkit mcp-serve <disk> [--target <target>] exposes the same 6
read-only evidence tools over stdio to Claude Desktop / other MCP
hosts, independent of guestkit's own agent loop.
guestkit fleet wave-plan (src/fleet/wave.rs) — orders a fleet's
disk images into dependency-aware migration waves (DB-role priority +
NFS storage-dependency edges, Kahn's-algorithm topological sort,
cycles reported rather than dropped or arbitrarily ordered).
guestkit fleet watch (src/fleet/baseline.rs) — scheduled drift
monitoring: diffs each VM's current evidence against a stored golden
baseline (first run establishes it), --fail-on-drift for pipeline
gating. Includes a Kubernetes CronJob template
(deploy/helm/zyvor/templates/fleet-drift-watch-cronjob.yaml) as the
reference scheduled-invocation path.
Helm chart CI — ci.yml's new helm-chart job runs helm lint and helm template against deploy/helm/zyvor, which previously had zero
CI coverage, across default values, every optional PVC-backed feature
enabled at once, hostPath-backed persistence, and all three real
deployment overlays (values-ci.yaml, values-k3s.yaml, values-prod.yaml).
Manual-dispatch workflow for NBD-dependent tests
(self-hosted-nbd-tests.yml) — runs the full test suite with none of ci.yml's NBD skips, for self-hosted runners with working loop/NBD
support. workflow_dispatch only, deliberately never wired to pull_request/push.
Fixed
k3s E2E's zyvor-api pod crash-looped on every run — the Helm chart's
default zyvorApi.agentMtls.enabled: true requires AGENT_BOOTSTRAP_TOKEN
(zyvor-api refuses to start otherwise: "AGENT_MTLS_BIND_ADDR is set but
AGENT_BOOTSTRAP_TOKEN is unset"), but the Deployment template only ever
wired that env var — and the Secret holding it — inside the zyvorApi.auth.enabled block. values-ci.yaml (mTLS on, full auth off)
hit exactly that gap; values-prod.yaml masked it by having both auth and
a token on together, and values-k3s.yaml worked around it by disabling
mTLS outright (its own comment already described the bug). Decoupled the zyvor-api-auth Secret and AGENT_BOOTSTRAP_TOKEN env var from auth.enabled — gated only on the token itself being set, matching what zyvor-api's own config validation actually requires — and set a
CI-only placeholder token in values-ci.yaml so the E2E job now exercises
the mTLS path instead of crash-looping. ci.yml's Helm Chart job now also helm templates all three real overlays (values-ci.yaml, values-k3s.yaml, values-prod.yaml) so a rendering break here is caught
without needing a live k3s cluster.
k3s E2E multi-round debugging — the job was failing on every run;
fixing it required peeling through several layers, each masking the
next:
poll_job (deploy/scripts/e2e-smoke.sh) only recognized "completed" as terminal, so a "failed" job status looked
identical to "still pending" for the full 5-minute poll budget, and
the worker's own error message (live_status.error) was never
printed. Now treats failed/cancelled/timeout as terminal and
prints the error immediately.
install-k3s-ubuntu.sh never got the loop/NBD device setup ci.yml needed earlier this session (guestkit-worker's pod
bind-mounts the host's /dev, so the same root:disk-0660-node /
EACCES-looks-like-timeout issue applies here too). Added it —
confirmed live afterward: inspect/doctor both complete in one
poll with a real bootability score. This specific k3s stack path
does not hit the deeper NBD-attach limitation ci.yml's plain cargo test job still has to skip around.
curl -sf swallows the response body on any non-2xx status, so the
next failure (provision) looked like an empty response
(Expecting value: line 1 column 1) instead of a real API error.
Added a curl_or_die helper (splits HTTP status from body via
curl's -w, prints both on failure).
That revealed a real HTTP 500: "No operating system found in disk image" from provision (POST /vms/{id}/provision, which mounts
the disk synchronously in zyvor-api's own process), on the same
image doctor had just inspected successfully. Suspected (and
partially fixed) an unawaited async migration-plan job racing
provision's own mount via NbdDevice::find_available_device
(src/disk/nbd.rs) — that function does check device availability
and connect as two separate, unlocked steps with no cross-process
coordination, a real bug now flagged with a code comment — but
serializing migration-plan before provision did not fix it,
disproving the race as this failure's cause.
provision_vm's .map_err(|e| ApiError::internal(e.to_string()))
(crates/zyvor-api/src/routes/vms.rs) only shows anyhow's outermost .context() layer via plain Display. Changed to format!("{e:#}")
(alternate Display, full chain) for provision_vm's three guestkit/export::kubevirt-derived map_err calls — correct and
worth keeping, but the next run's error was still byte-identical to
before, because there was no chain to reveal: mount_all_ro
(src/cli/commands/mod.rs) returns Option<String>, not Result<String>, and .context("No operating system found in disk image") on a None (anyhow's Context impl for Option) produces
an error with no wrapped source at all — the context message
genuinely is the entire error. Left the other ~90 .map_err(|e| ApiError::internal(e.to_string())) sites in the crate
alone; most wrap simple error types (serde_json, std::io) where .to_string() isn't lossy.
The real swallowed information was one layer further down: mount_all_ro calls g.inspect_os().unwrap_or_default() — inspect_os() failing for any reason (guestfs launch issue, mount
error, permission problem) collapses to the identical empty-roots None as "genuinely no OS found," discarding whatever inspect_os's
real error was before it could reach any context message. Changed to
log the real error (log::warn!) before discarding it. Didn't widen mount_all_ro's Option<String> return type to Result — it's used
across 9 files where callers only ever branch on Some/None, and
that ripple is out of scope for this investigation. Widened the
default EnvFilter (was "nothing enabled" without RUST_LOG set,
now falls back to warn globally) so mount_all_ro's new log::warn! — and any other log::*! from guestkit's dependency
graph — actually reaches the pod's logs. First attempt at this also
added an explicit tracing_log::LogTracer::init() call, reasoning
that zyvor-api only sets up a tracing subscriber and guestkit
logs through the plain log facade — wrong, and a real
regression: tracing-subscriber's "tracing-log" feature (on by
default) already bridges log into the subscriber as part of .init(), so the explicit call double-registered the global log
logger and panicked at startup with SetLoggerError, crash-looping zyvor-api again. Confirmed via kubectl logs from the next E2E
run (once the "dump pod logs on failure" step below existed to
capture it) and reproduced in an isolated 10-line binary before
re-pushing — removed the explicit LogTracer::init() call and the
now-unneeded direct tracing-log dependency; the isolated repro
confirmed log::warn! still reaches the subscriber correctly
without it.
Also: nothing in k3s-e2e.yml ever captured pod logs on failure —
every fix in this list up to this point was diagnosed purely from
HTTP response bodies, each requiring a full ~20-40min re-run just to
test. Added a failure-only step dumping kubectl logs (all
containers, prefixed by pod) for every deployed component, plus get pods -o wide and describe pods.
That finally showed it: no panic, no error, no mount_all_ro
warning — inspect_os() genuinely returned an empty root list. validate_root_partition/validate_initrd_boot_partition
(src/guestfs/inspect.rs) treat mount/extraction failures as
"not a valid root" by design, not as errors — a real mount
failure and "genuinely no OS" are indistinguishable at that layer
on purpose (LVM volumes on read-only NBD devices can legitimately
fail to mount for benign reasons). validate_initrd_boot_partition
is the cirros-cloud-image path — root filesystem lives inside the
initrd, not on a directly-mountable partition — and shells out to zcat <initrd> | cpio -t to look inside it. crates/zyvor-api/Dockerfile never installed cpio (or gzip),
unlike crates/guestkit-worker/Dockerfile's otherwise-identical
package list, which does. That shell command silently failing
inside zyvor-api's container is why provision (run_migrate_plan
called synchronously in zyvor-api's own process) couldn't find an
OS on exactly the image doctor/inspect (via guestkit-worker,
which has cpio) found one on every time. Added cpio/gzip to zyvor-api's Dockerfile, matching guestkit-worker's package list.
Confirmed live afterward: provision succeeds, generating a real
PVC-referencing manifest — this was the fix. The remaining unguarded curl -sf | python3 call sites past provision (/config, /kubevirt/vms, /kubevirt/namespaces, /vmtools/coverage, /vmtools/policy, /storage/roots, the E2E_KUBEVIRT cluster-inspect
calls) got the same curl_or_die treatment for consistency.
One more layer, different in kind from everything above: /vmtools/coverage now fails with a real, visible error — kube list virtualmachines: ... 404 page not found. values-ci.yaml
sets kubevirt.enabled: true (zyvor-api/worker assume the KubeVirt
CRDs exist), but neither install-k3s-ubuntu.sh nor deploy-remote-k3s.sh ever installed the KubeVirt operator that
registers them — this repo already has that install (pinned v1.4.0, operator + CR manifests, tolerant wait) in deploy/scripts/kind-kubevirt-quickstart.sh, just never ported to
the script this CI workflow actually uses. Ported it. GitHub-hosted
runners have no /dev/kvm (no nested virtualization), so virt-handler won't reach fully healthy and starting a real VirtualMachineInstance still won't work here — but the CRDs
register and the kubevirt.io/v1 API group routes real (empty)
responses instead of 404 as soon as the operator applies them, which
is all this job's default (non-E2E_KUBEVIRT) path needs.
Main CI (ci.yml) had been broken for a while — journal-native
(a default feature) needs libsystemd-dev, missing from every job
except release.yml's; Code Coverage's --all-features also needs libhivex-dev; the musl release build tried to link glibc's libsystemd into a musl target instead of building --no-default-features. Loop/NBD device nodes were root:disk 0660 —
unreadable by the unprivileged test process, which read that as "not
ready" instead of a permissions error; the nbd kernel module was
never loaded in ci.yml at all. guestkit.spec / guestkit-full.spec
were two releases stale, breaking rpmbuild. The k3s E2E workflow was
missing musl-tools / gcc-mingw-w64-x86-64 for the vmtools
cross-builds, and its MinIO upload used mc's default local alias
credentials instead of this deployment's actual root user/password.
Guestfs::launch() didn't transition to Error state when failing
on the "no drives added" precondition — only later failures did.
Windows cross-compile regression in agent/rdp.rs — an automated
lint pass removed the json import as unused (true when checked on a
non-Windows host) but it's real, used code behind #[cfg(target_os = "windows")].
6 pre-existing doc-test compile failures (mem_optimize.rs, cli/parallel.rs, fstab_rewriter.rs) — ambiguous generic return
types and stale API usage in doc examples, never caught because CI
never previously got far enough to reach the doc-test phase.