Skip to content

v1.0.0

Choose a tag to compare

@github-actions github-actions released this 15 Aug 13:14
· 69 commits to main since this release

[1.0.0] - 2026-08-15

Added

  • 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 CIci.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 whilejournal-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.