Commit Graph
48 Commits
Author SHA1 Message Date
Simone a7a22a29be feat(cache): restore and publish the cargo dependency cache around Linux builds
Restore runs after the source is in place and before the build; publish runs
only after a successful build and only on a miss, so the steady state uploads
nothing and a failed build can never become an entry. The whole path is
best-effort: a cache problem degrades to a cold build with a message rather
than turning a good build red.

Gated on Cargo.lock being present, so non-Rust jobs are untouched with no flag
to set. The Windows branch is not touched at all.

The prune script is read through importlib.resources rather than a path
relative to the repo root: the production venv is installed non-editable, so a
parents[] walk lands in site-packages where scripts/ does not exist.

The fake transport gained substring-matched canned results; indexing by call
position broke as soon as the cache added calls mid-flow.

Mutation-tested rather than assumed. Three guards were each disabled in turn --
the publish call, the prune-refusal check, the packing-failure check -- and a
test went red for each. The prune-refusal test initially survived its mutant:
it passed because the fake's fetch left no file behind, so publishing failed
for an unrelated reason. Fixed by making fetch write a real file, so the only
thing preventing publication is the guard under test.
2026-07-22 20:59:23 +02:00
Simone fce7cfd86d feat(cache): content-addressed store for cargo dependency artifacts
One immutable file per key, published by rename(2) inside the store. Two jobs
publishing the same key both produce a valid entry and the last wins; a reader
holding the previous one keeps reading it. No advisory locking, which is the
property a VMware shared folder cannot offer with several jobs in flight --
the objection raised in the furyrs thread.

The key joins its parts with NUL so ("ab","c") and ("a","bc") cannot collide,
and carries a format version so the whole store can be invalidated at once
when the prune rules change and older entries stop being trustworthy.

The feature set is deliberately absent from the key: cargo already encodes it
in each artifact's own hash suffix, so several feature graphs coexist in one
entry rather than fragmenting the store into one entry per variant.
2026-07-22 20:50:22 +02:00
Simone 6bce846953 fix(template): make deploy/prepare deterministic and safe to re-run
Four defects, each of which could waste a full template rebuild:

1. getGuestIPAddress was called without -wait, so vmrun answered "The VMware
   Tools are not running" unless the Tools happened to have registered the IP
   at that exact instant. A guest that was booted, on the network and answering
   SSH could still burn the entire 10-minute Step 7 budget. This race is the
   likely cause of every intermittent "no guest IP" incident so far, including
   ones previously attributed to wedged vmnet DHCP. AGENTS.md #10 already
   prescribed -wait; the code did not follow it.

2. --force did not check whether the target VM was powered on. It deleted the
   live .vmdk.lck as a stale artifact and Step 4 rewrote the base VMDK under
   the running guest, killing it; the damage surfaced ten minutes later as an
   IP timeout because `vmrun list` still showed the OLD instance.

3. Step 6 accepted "VM appears in vmrun list" as proof of power-on, which a
   pre-existing instance satisfies. Check the exit code of `vmrun start`.

4. The cloud-init seed ISO was detached while the VM was still running, and
   VMware rewrote the VMX from its in-memory copy at power-off, restoring
   ide1:0.present = "TRUE". Every clone would boot with the seed attached
   (AGENTS.md #6). Detach it in prepare, after power-off and before the
   snapshot.

Also in this change:

- --then-prepare chains deploy into the matching prepare in one run, reusing
  the VM deploy leaves running. The two commands stay separately invocable.
  The guest toolchain script is resolved and verified BEFORE any VM work, so a
  wrong working directory costs two seconds instead of failing the hand-off
  after a 45-minute deploy; --toolchain-script allows running from anywhere.
- Corrected the final messages: deploy-rust told the user to run prepare-linux,
  and both deploy commands reported a snapshot name (PostInstall-Linux) that no
  command ever creates. The prepare hand-off hint passed a bare filename to
  --template and omitted --snapshot, so copy-pasting it cloned BaseClean, the
  Windows snapshot.
- template backup --exclude, repeatable, for retired templates. An unknown name
  or --exclude without --all-templates is an error rather than a silent no-op.

test_deploy_linux_full_happy_path declared the VM already running before Step 6,
an impossible state that masked the new --force guard; its vmrun mock is now
stateful.
2026-07-22 14:08:56 +02:00
Simone 054bd42ff0 feat(prepare-win): reboot to cmd shell before toolchain, only if needed
Up front, read Winlogon\Shell; if it isn't already cmd.exe, set it and do a
clean reboot before running the toolchain — so the shell is genuinely cmd
(not a killed-explorer half-state) for the whole servicing run. Idempotent:
when the shell is already cmd (re-runs), skip the reboot. Replaces the
earlier kill-explorer approach.
2026-06-09 22:54:43 +02:00
Simone c1be0f276f feat(prepare-win): idle check waits for quiet, reports busiest process
The one-shot idle sample fired ~30s after the final reboot, but Windows
Update / CBS (TiWorker) keeps finalizing for minutes — so a still-busy
template was wrongly rejected. Poll until CPU and RAM are both under
threshold (up to --idle-timeout, default 600s) instead of sampling once,
and report the busiest process to aid diagnosis. Adds --idle-timeout.
2026-06-09 21:52:29 +02:00
Simone 54feb43513 fix(prepare-win): detect reboot by boot-time, not TCP-down
A fast reboot never showed TCP/5986 "down" between polls, so the old
down→up wait could hang on "waiting for VM to go down" until timeout.

Replace it: record the guest LastBootUpTime (FileTime ticks) before the
reboot, then poll until the port is back AND the boot time is newer — a
signal immune to reboot speed. Applies to both the 3010 loop and the final
reboot. Drops _wait_tcp_down in favour of _tcp_open + _guest_boot_ticks.
2026-06-09 21:39:36 +02:00
Simone 5fe68589f5 feat(prepare-win): pre-snapshot idle CPU/RAM check (abort if busy)
After the final reboot and just before the graceful shutdown for the
snapshot, sample the guest's idle CPU and used-RAM percentages (CIM
Win32_PerfFormattedData — locale-independent — averaged over ~5s after a
settle delay). If either exceeds its threshold, abort prepare without
taking the snapshot, so a busy/leaky template isn't frozen into every clone.

New options: --max-idle-cpu (10%), --max-idle-ram (50%), --idle-settle
(30s), --skip-idle-check.
2026-06-09 21:11:21 +02:00
Simone e0b1c1ddcc feat(prepare-win): stream toolchain/WU output to the console live
pypsrp's execute_ps is fully blocking, so Install-CIToolchain / Windows
Update output only appeared at the end behind a "still running (Ns)"
heartbeat. Add WinRmTransport.run_streaming: drive the script via pypsrp's
incremental begin_invoke/poll_invoke, emitting output, Write-Host
(information) and error stream lines as they arrive over the same WinRM
channel.

_run_with_heartbeat now prefers run_streaming (suppressing the CI_EXITCODE
marker line) and falls back to the blocking heartbeat only on an unexpected
pypsrp error — TransportError still propagates so the 3010 reboot handling
is unaffected.
2026-06-09 21:00:18 +02:00
Simone 107bff8c8a fix(prepare-win): wait for reboot to actually happen (down→up)
On exit 3010 the old path issued Restart-Computer, slept 30s, then probed
WinRM. With a pending WU/CBS install the restart is delayed minutes while
the WinRM session stays alive, so the probe saw the still-up pre-reboot
session as "ready" and re-ran the toolchain — which then died mid-run when
the real reboot kicked in (looping "treating as 3010").

Add `_reboot_and_wait`: issue the reboot, then wait for TCP/5986 to go
*down* (`_wait_tcp_down`, up to 300s) before waiting for it to come back
and re-probing WinRM. Use it for both the 3010 loop and the final reboot.
2026-06-09 20:53:19 +02:00
Simone 6a57c1d815 fix(deploy-win): don't re-patch an already-patched ISO
deploy-win patched its input into vm_dir/install-noprompt.iso and only its
own sidecar cache could skip the repack — so feeding an already-patched ISO
(e.g. from `template patch-win-iso`, or via --win-iso) triggered a wasteful
second extract+repack that double-injected the answer files.

deploy-win now detects a patched input (autounattend.xml present at the ISO
root via `iso_patch.iso_has_root_file`) and uses it as-is, skipping Step 4.
Raw ISOs are still patched once and cached as before.
2026-06-09 20:15:30 +02:00
Simone 3680f07ee3 feat(template): check-win-iso (massgrave resolver) + patch-win-iso
Add `ci template check-win-iso`: parse the massgrave windows-server-links
markdown source on GitHub for the newest Windows Server 2025 build, file
name and link, and report whether it is already in <CI_ROOT>/iso/. The
download itself stays manual — the massgrave link is behind a Cloudflare
Turnstile file host, not scriptable.

Add `ci template patch-win-iso --in ISO [--out ISO]`: standalone equivalent
of deploy-win Step 4 — inject autounattend.xml + post-install.ps1 and a
no-prompt boot image into a manually downloaded ISO, producing a
ready-to-deploy `.patched.iso`.

winimage/massgrave.py does the parse (no auto-download). RUNBOOK §5
documents the check → download → patch → deploy workflow. Coverage 93.6%.
2026-06-09 18:53:04 +02:00
Simone c257c931c3 refactor(deploy-win): drop UUP dump, take ISO via --iso-url / --win-iso
On a Linux host the UUP converter only produces the RTM install.wim (build
26100.1), so Windows Update had to install the entire cumulative anyway —
the UUP download/convert machinery added a slow ~5 GB step and several host
tools (aria2/wimtools/cabextract/chntpw) for no benefit.

Replace it: deploy-win now takes the install ISO directly via `--iso-url`
(urllib download + cache by file name under <CI_ROOT>/iso/, optional
--iso-sha256) or a local `--win-iso` (exactly one required). Patch + deploy
flow is unchanged. Supply an already-updated ISO (e.g. a massgrave link) to
skip the WU step entirely.

- Remove winimage/uup.py and its tests.
- Trim setup-host-linux.sh deps to p7zip-full + xorriso + genisoimage.
- Update RUNBOOK §5 and the Windows-host port plan. Coverage 93.6%.
2026-06-09 18:42:22 +02:00
Simone a0497ffa4e feat(deploy-win): cache the patched ISO, skip repack when inputs unchanged
The ISO repack (extract + xorriso, ~minutes) ran on every deploy. Cache it
per VM dir: store an inputs key (base ISO size+mtime + rendered
autounattend.xml + post-install.ps1) in a `.iso.inputs` sidecar next to
install-noprompt.iso. Step 4 skips the repack when the patched ISO and a
matching sidecar already exist, and re-patches automatically when the base
ISO or either answer file changes.
2026-06-08 23:53:18 +02:00
Simone 32d753df3e feat(deploy-win): build the VMX without USB controllers
A headless CI build VM has no USB devices — input is the VMware PS/2
keyboard/mouse + SVGA console. Drop the usb/ehci/usb_xhci controllers from
the generated VMX (one explicit `usb.present = "FALSE"`), trimming device
enumeration at boot. Step 9's usb* line stripping becomes a harmless no-op.
2026-06-08 23:36:29 +02:00
Simone 0df7305841 fix(deploy-win): extract ISO with 7z so no-prompt boot patch works
xorriso -extract reads the ISO9660 name tree, which on a Windows ISO has
no Rock Ridge — names come back upper-cased/truncated, so the
`efi/microsoft/boot/efisys_noprompt.bin` lookup failed ("missing from
ISO") and a repack from that tree would corrupt Windows' long file names.

Extract with 7z instead (reads the UDF/Joliet tree, preserving the real
lower-case paths), excluding 7z's synthetic `[BOOT]` dump. Verified
end-to-end on the cached 26100.32860 ISO: 7z extract → xorriso repack
yields a 5 GB ISO with autounattend.xml + post-install.ps1 at the root and
efisys_noprompt.bin as the UEFI El Torito image (no boot prompt).

Also: cap the Joliet volume id at 16 chars (silence xorriso warning), add
a 7z preflight check, and add p7zip-full to setup-host-linux.sh.
2026-06-08 23:31:18 +02:00
Simone b83a7fa624 feat(template): add deploy-win — UUP download + ISO patch + deploy WS2025
New `ci template deploy-win` builds the Windows Server 2025 template VM on
the Linux host, mirroring the legacy Deploy-WinBuild2025.ps1 ten-step flow
in Python:

- winimage/uup.py: resolve the newest (or pinned) WS2025 build via the
  uupdump API and build a cached, MS-signed ISO via the uup converter.
- winimage/autounattend.py + assets/: render autounattend.xml and the
  guest post-install.ps1 (ported from the PS heredoc; VMware Tools now
  mounted as a 2nd CD, scanned at first logon).
- winimage/iso_patch.py: xorriso repack injecting the answer files at the
  ISO root and swapping the EFI boot image to efisys_noprompt.bin
  (no "press any key" prompt — essential for an unattended headless install).
- commands/template.py: the deploy-win command + _winbuild_vmx_lines
  (the 82-key VMX), _remove_vmx_lines, _wait_install_flag, _vmrun_soft_stop.
  Acquire → patch → VMDK → VMX → install → flag wait → strip media +
  NIC e1000e→vmxnet3 → verify IP → cold snapshot.

Deterministic by build (--build), with --win-iso to bypass UUP. Tests
mirror the prepare-win mocking style; total coverage 93%.
2026-06-08 23:21:02 +02:00
Simone de5348c36c refactor(cli): rename vmgui command to vmware
Rename the GUI launcher command from `vmgui` to `vmware` (`ci vmware ...`).
Updates the click command name, function, log-file prefix, __main__
registration, tests and runbook references. Behaviour unchanged.
2026-06-08 00:32:13 +02:00
Simone 41d646577d fix(vmgui): pre-flight X display, stop failing silently
`vmgui` sent the child's stderr to /dev/null, so when the X display was
unreachable (the common case: sudo -u ci-runner loses the desktop user's
XAUTHORITY and xhost wasn't granted) the GUI died with "cannot open display"
and the command still printed success — "nothing opens".

Now it:
- pre-flights the display with xdpyinfo and aborts with the exact
  `xhost +SI:localuser:ci-runner` hint instead of spawning a doomed process;
- drops an inherited XAUTHORITY that this user can't read (falls back to
  host-based xhost auth);
- logs the GUI's stdio to /tmp/vmgui-*.log and grace-polls 1.5s, surfacing the
  log if the GUI exits immediately;
- errors (non-zero) when no DISPLAY is resolvable.

Runbook §4.7 updated with the "if nothing opens" guidance. 13 tests; green ≥90%.
2026-06-08 00:25:37 +02:00
Simone 41d64fc212 feat(vmgui): GUI launcher command, VMX optional
Add top-level `vmgui` to open the VMware Workstation GUI (renamed from the
earlier `vm open`). --vmx is optional: with it the GUI opens that VM, without
it the GUI starts bare. Power-on/fullscreen flags ignored (with a notice) when
no VMX is given. Needs an X display — sudo -u ci-runner strips $DISPLAY, so
--display / xhost; see runbook §4.7. 9 tests; suite green ≥90%.
2026-06-08 00:16:50 +02:00
Simone cac38f7c0c feat(vm): add vm open to launch the VMware Workstation GUI
Open a VMX in the interactive Workstation GUI (not the headless vmrun path) for
hands-on debugging of a clone. Spawns `vmware` detached (own session, stdio
detached) and returns the child PID.

Flags: --vmx (required), --power-on (-x), --fullscreen (-X), --new-window (-n),
--display (sudo -u ci-runner strips $DISPLAY, so allow forcing it),
--vmware-path. Warns when no X display is set. 7 tests; vm.py 96%.
2026-06-08 00:11:36 +02:00
Simone 575cc81b8f feat(cli): port burn-in/smoke/validate/creds to Python (Phase C1-C6)
Add four native command groups so the Linux host no longer needs pwsh for
manual ops. __main__ registers all four at once, so they ship together.

- bench run     (C2): N concurrent jobs in-process via concurrent.futures,
                      per-round orphan-clone + stale-lock assertions. Replaces
                      Test-CapacityBurnIn.ps1 + Start-BurnInTest*.ps1. No
                      Start-Job/pwsh/$IsWindows.
- bench measure (C3): clone/start/IP/transport/destroy timings appended to
                      benchmark.jsonl (legacy field names kept). Replaces
                      Measure-CIBenchmark.ps1.
- smoke run     (C4): one E2E job; asserts exit 0 + artifact dir + job/success
                      event. ns7zip/nsinnounp presets. Replaces Test-Smoke.ps1
                      + Test-*Build-Linux.ps1.
- validate host (C5): vmrun/snapshot/keyring/permissions/systemd checks.
- validate guest(C6): transport probe with explicit failure cause.
- creds set     (C6): keyring writer matching credentials.py read scheme;
                      password via stdin/prompt only. Replaces
                      Set-CIGuestCredential.ps1.

All groups import backends.load_backend only (Phase D ESXi path stays open).
Suite green, coverage 95.10% (gate 90%); new modules at 100%.
2026-06-07 20:15:10 +02:00
Simone a211833316 feat(build): optional xvfb-run for Linux GUI builds
Add an --xvfb switch to 'build run' and 'job' that wraps the Linux build
command in 'xvfb-run -a sh -c ...', giving GUI toolkits (GTK4/PyGObject)
a headless $DISPLAY. Exported env vars precede xvfb-run so they
propagate into the virtual-display session. No-op (with a notice) for
Windows guests.

The local-ci-build composite action exposes it as the 'xvfb' input
(default false), forwarded as INPUT_XVFB -> --xvfb, mirroring the
use-shared-cache/skip-artifact wiring.

Docs: WORKFLOW-AUTHORING.md parameter table + Linux example note;
LINUX-TEMPLATE-SETUP.md Step 4a (the template ships python3-gi
gir1.2-gtk-4.0 xvfb). Adds build-run unit tests (wrap, plain, Windows
no-op).
2026-06-06 13:38:34 +02:00
Simone 1ff02e97a8 fix(template): clean snapshot chain on deploy --force; seal cloud-init; harden first-boot IP
deploy-linux --force reused the VM directory but left any prior snapshot
chain in place, so vmware-vdiskmanager refused the Step 4 resize with
'disk is part of a snapshot chain'. Add _clean_force_artifacts to remove
snapshot deltas (.vmdk), .vmsn, .vmsd and stale .lck before resize.

prepare-linux Step 7d seals the image: drop the stale cloud-init netplan
(leaving only the MAC-agnostic 99-ci-dhcp-allnics), regenerate networkd
config, and disable cloud-init so it never re-runs on linked clones.

deploy user-data sets package_update/upgrade: false so cloud-init does
not hold the dpkg lock at first boot and delay open-vm-tools past the
IP-detection poll; the step-7 IP timeout is raised 300s -> 600s as a
safety net for slow first boots.

Adds unit tests for _clean_force_artifacts (snapshot chain, lock dir,
clean-dir no-op) and updates prepare-linux stubs for the seal step.
2026-06-06 12:55:34 +02:00
Simone 85003d1ea2 feat: add guest IP retrieval functions and enhance VM preparation logic 2026-05-27 23:34:37 +02:00
Simone c391c9147e Add tests for SSH transport and template deployment functionality
- Introduced a new test to verify behavior when SSH transport connects but is not ready, ensuring it returns False after the deadline.
- Added comprehensive tests for the `deploy-linux` template, covering various scenarios including missing public keys, VM directory existence, SHA256 mismatches, and failures during ISO creation and VM conversion.
- Implemented tests for SSH-related functions, including connection retries and streaming command execution, ensuring proper handling of exit statuses and progress indications.
- Enhanced existing tests to cover edge cases and ensure robustness in the deployment process.
2026-05-27 22:53:24 +02:00
Simone bc67e1ac29 test: bypass real polling loops in job timeout tests and adjust timeout parameter 2026-05-26 21:32:38 +02:00
Simone e2ab05e297 Add coverage gap tests for command functionalities
- Implement tests for disk alert JSON output to include a message field when below threshold.
- Add tests for runner status when Gitea is online but no webhook URL is provided.
- Introduce tests for job report details to ensure no error line appears when there are no failure events.
- Add tests to skip empty JSONL files in job report listing.
2026-05-26 21:18:20 +02:00
Simone d0314351e4 refactor: simplify shutil usage in workstation backend tests 2026-05-26 19:00:38 +02:00
Simone 0e609b22d1 test: add unit tests for VmState and VmHandle functionality in protocol.py 2026-05-26 18:57:58 +02:00
Simone d6194cfb6d test: update IP pool test to use Windows VMX and clarify test purpose 2026-05-26 18:51:01 +02:00
Simone 5867fe1e12 feat(template): cleanup-after-update + ruff fixes
Move disk cleanup out of the Windows Update loop:
- PS scripts: extract Invoke-DiskCleanup function, add -CleanupOnly switch
  for standalone post-update cleanup pass.
- Python command: always pass -SkipCleanup:$true during WU loop; after
  loop exits 0, run a separate -CleanupOnly invocation (skipped if
  --skip-cleanup). Fails fast if cleanup-only returns nonzero.
- Tests: 3 new tests for cleanup-runs-after-loop, skip-cleanup suppresses
  it, and cleanup-failure propagates as ClickException.
- ruff --fix: remove unused shutil import, sort imports, fix UP037/UP035.
2026-05-24 02:41:15 +02:00
Simone f24f7fd07a fix(template): use sys.stdin.readline() for snapshot confirm prompt
input() with a prompt string on some terminals (zsh + Linux Mint) echoes
^M and the prompt doesn't flush before reading, causing the prompt to
appear stuck or not accept input.

Replace with explicit sys.stdout.write()+flush() + sys.stdin.readline()
which guarantees the prompt is flushed before blocking on stdin, and
readline() + .strip() handles both \n and \r\n line endings correctly.

Tests updated to use CliRunner(input=) which populates sys.stdin inside
the runner context (monkeypatching sys.stdin.readline directly is ignored
by CliRunner which replaces sys.stdin entirely).
2026-05-24 02:07:36 +02:00
Simone ce0434e2d5 fix(template): replace click.confirm with input() to avoid ^M on Linux terminals
click.confirm() doesn't strip \r — "y\r" doesn't match "y" and the prompt
loops or fails. Replace with raw input().strip().rstrip('\r').lower() which
handles both \n and \r\n line endings.

Also add --recreate-snapshot flag to skip the interactive prompt entirely
(useful for scripted re-provisioning).

Update snapshot_exists_skip/recreate tests to monkeypatch builtins.input
instead of using CliRunner input=.
2026-05-24 02:02:22 +02:00
Simone 2fa9a080d6 feat(template): add --store-credential to prepare-win + full docstring
- --store-credential: after snapshot, writes build-user credentials into
  the system keyring using the two-entry scheme (service:meta + service)
  that KeyringCredentialStore.get() reads. Works on Windows (Credential
  Manager) and Linux (SecretService / file backend).
- --credential-target: keyring target name, default BuildVMGuest (matches
  config.guest_cred_target).
- Rewrote module docstring: full prose documentation of both `template
  backup` and `template prepare-win` including all 12 provisioning steps.
- 2 new tests covering --store-credential with custom and default targets.
- mypy --strict clean; coverage 90.03%.
2026-05-24 01:30:57 +02:00
Simone 347608d5ac feat(template): add template prepare-win Python command via pypsrp
Replaces the abandoned PSWSMan approach (New-PSSession hangs on Linux)
with a Python-native implementation using the proven pypsrp WinRmTransport.

- New `template prepare-win` command in commands/template.py: starts VM,
  waits for WinRM, uploads and runs Install-CIToolchain-*.ps1, handles
  Windows Update 3010 reboot loop (max 10 iterations), installs CI-StaticIp
  scheduled task, runs post-setup validation, graceful shutdown, snapshot.
- Helpers: _ps_escape, _wait_tcp, _parse_exit_marker, _wait_winrm.
- 40 new tests in test_commands_template.py covering all major branches.
- Fix test_commands_job.py: patch load_config in _patch_common so tests
  don't pick up live /var/lib/ci/config.toml ip_pool and fail on lock.
- Coverage gate: 90.06% (was 88.95% before fix).
2026-05-24 01:22:53 +02:00
Simone 32c723147f feat(ip-pool): pre-assign static IPs to build VMs via VMX guestinfo
Eliminates VMware-Tools IP-detect polling (observed: 39–177 s variance on
Linux host B7 benchmark).  When [ip_pool] is configured the orchestrator:

  1. Allocates a slot IP from a JSON-backed pool (fcntl-locked on Linux)
  2. Injects guestinfo.ip-assignment / netmask / gateway into the cloned VMX
     before vmrun start
  3. Skips _wait_for_ip entirely — uses the pre-assigned IP immediately
  4. Releases the slot in _destroy_clone (all paths: success/failure/interrupt)

New modules / changes:
  src/ci_orchestrator/ip_pool.py      IpSlotPool + _file_lock context manager
  src/ci_orchestrator/config.py       IpPoolConfig dataclass; [ip_pool] TOML
  src/ci_orchestrator/commands/job.py _inject_guestinfo_ip helper; pool wiring
  tests/python/test_ip_pool.py        12 unit tests (acquire/release/persist)
  tests/python/test_config.py         ip_pool config parsing tests
  tests/python/test_commands_job.py   pool path + _inject_guestinfo_ip tests
  config.example.toml                 [ip_pool] section (commented example)

Coverage 90.02%; mypy --strict clean.
2026-05-24 00:26:59 +02:00
Simone b555784de0 feat: B5 — port retention/template-backup to Python, deploy systemd timers
- Add `retention run` command (ports Invoke-RetentionPolicy.ps1): purges
  old artifact/log dirs with aggressive mode when disk space is low.
- Add `template backup` command (ports Backup-CITemplate.ps1): 7z -mx=1
  compressed archives in /var/lib/ci/backups/, prunes to keep-count=3,
  stops/restarts act-runner.service around the copy.
- Update ci-retention-policy.service and ci-backup-template.service to use
  Python; pwsh is no longer required on the Linux host.
- Fix ci-watch-runner-health.service: pass --service-name act-runner
  (Linux service name, not Windows act_runner default).
- Fix _list_orphans in vm.py: wrap is_dir() inside the OSError try block
  so a stat() failure on an entry is silently skipped rather than raised.
- Mark B5 complete in PhaseB-user-checklist.md.
2026-05-21 20:35:07 +02:00
Simone 83a0e46e6b feat(build): live-stream Linux build output; ns7zip via in-guest clone
- SshTransport.run_streaming: incremental channel recv, forwards
  stdout/stderr live (no more one-block-at-end). _linux_build uses it
  with PYTHONUNBUFFERED so the guest python flushes promptly.
- _tail_lines split out for failure messages (no double print).
- Windows live streaming deferred (TODO 8.2) — pypsrp execute_ps
  returns only at completion.
- build-ns7zip.yml: use-git-clone:'true' (in-guest), repo-url ->
  https://gitea.emulab.it/... (guest-reachable; host SSH alias was
  host-only). Requires GiteaPAT in the LocalSystem keyring or a public
  nsis-plugin-ns7zip.
2026-05-17 20:51:48 +02:00
Simone 8d059cd3c4 feat(job): wire use-git-clone + submodules; default in-guest clone
The job command previously discarded --use-git-clone and --submodules
(`del`) and always in-guest cloned with submodules off — making both
action inputs dead no-ops (submodule repos silently broken).

- job.py: --use-git-clone/--host-clone and --submodules/--no-submodules
  are real booleans defaulting to ON. Default = in-guest clone with
  submodules. --host-clone clones on the host into a temp dir
  (_host_clone) and transfers a zip; temp dir cleaned in finally.
  clone_submodules now honoured (was hardcoded False).
- action.yml: use-git-clone / submodules default 'true'; forward the
  explicit positive/negative flag so the Python default is overridden
  deterministically.
- self-test.yml: add smoke-{windows,linux}-hostclone jobs covering the
  host-side transport (default jobs already cover in-guest).
- tests: default-transport, host-clone, and --no-submodules coverage.
2026-05-17 17:00:46 +02:00
Simone 6c9822b929 refactor(artifacts): harmonize Windows to Linux raw-file collection
Windows produced an intermediate C:\CI\output\artifacts.zip that
upload-artifact then re-zipped (zip-in-zip), while Linux deposited raw
files. Make both paths consistent: the build stages raw outputs into a
collect dir; collect zips it only as a transport archive, fetches, and
extracts into the host artifact dir; actions/upload-artifact produces
the single final archive.

- _windows_build: param artifact_zip -> output_dir; stage raw files
  (in-place when source IS the collect dir, else wipe+copy), no
  Compress-Archive; absolute artifact-source resolved.
- _windows_collect: zip guest dir as transport, fetch, extract (mirror
  _linux_collect); guest_path is now the collect directory.
- job.py / build_run CLI: pass output_dir; rename --guest-artifact-zip
  -> --guest-output-dir.
- Tests updated for the raw-file contract.
2026-05-17 16:30:34 +02:00
Simone 57bfc41a5a docs,config: aggiorna AGENTS.md, config.example, HOST-SETUP.md, checklist faseA e fix vari orchestrator (workstation, build, job, wait, winrm, test) 2026-05-17 00:14:04 +02:00
Simone 958e4ae035 test: cover easy missing branches in report/config/credentials/ssh/wait/job 2026-05-14 23:23:55 +02:00
Simone 9dcd793e84 test: add coverage for monitor/vm/winrm to reach 90%% gate
Bring monitor.py 49->99%%, vm.py 79->98%%, winrm.py 76->100%%. Total project coverage 80.10%% -> 90.69%%, satisfying the 90%% gate set in commit 01c8011.
2026-05-14 23:14:08 +02:00
Simone 78d83a2674 test(a5): add pytest regression tests for AGENTS.md errors #9-#12
12 dedicated tests covering: (#9) clone_linked surfaces 'should not be powered on' as typed BackendError; (#10) is_running uses 'vmrun list' only and never falls back to getGuestIPAddress (incl. on vmrun list failure); (#11) two consecutive 'vm new' invocations with identical inputs MUST produce distinct clone names/destinations (timestamp suffix), guarding the orchestrator-side safety net against the machine-id duplicate-IP failure mode; (#12) SshTransport defaults to paramiko AutoAddPolicy with no known_hosts, never shells out to ssh-keygen/ssh.exe/scp.exe, wraps paramiko import/exec/SFTP errors as TransportConnectError. Coverage: 80.10% (gate 80%).
2026-05-14 18:34:04 +02:00
Simone 8382aedd1b feat(a4): port Invoke-CIJob.ps1 to ci_orchestrator job
Phase A4 of plans/implementation-plan-A-B.md. Implements the full job orchestrator (clone -> start -> wait -> probe -> build -> collect -> guaranteed cleanup) as a new commands/job.py click command, registered under python -m ci_orchestrator job. Backend selection goes through backends.load_backend(config) so Phase C can swap in remote drivers without touching the command. The legacy scripts/Invoke-CIJob.ps1 is replaced by a thin PS 5.1 shim that delegates to the Python CLI; tests/python/test_commands_job.py adds 13 cases covering Linux/Windows happy paths, override application, skip-artifact, and cleanup on every failure mode.
2026-05-14 18:34:03 +02:00
Simone 1b80a99b47 feat(a3): port build pipeline (vm new, build run, artifacts collect)
Implements Phase A3 of plans/implementation-plan-A-B.md:

- commands/vm.py: vm new (replaces New-BuildVM.ps1)

- commands/build.py: build run (replaces Invoke-RemoteBuild.ps1) with WinRM/SSH dispatch and stdout streaming for act_runner

- commands/artifacts.py: artifacts collect (replaces Get-BuildArtifacts.ps1) using transport.fetch()

- 3 PS scripts reduced to shims preserving \0

- Pester tests/{New-BuildVM,Wait-VMReady,Remove-BuildVM}.Tests.ps1 removed; equivalent cases covered in pytest

- Phase C hook: build/artifacts accept opaque VmHandle/vmx; no Windows path assumptions

Tests: 91 pytest, ruff clean, mypy --strict clean, coverage 78.27% (>=70 gate).

Master checklist + step A3 attivita + 'definizione di fatto' updated; A3-closeout.md added.
2026-05-14 18:34:03 +02:00
Simone 2e0bfa519c feat(a2): port leaf PS scripts to ci_orchestrator CLI
Implements Phase A2 of plans/implementation-plan-A-B.md:

- commands/wait.py    -> wait-ready (replaces Wait-VMReady.ps1)

- commands/vm.py      -> vm remove + vm cleanup (replaces Remove-BuildVM.ps1, Cleanup-OrphanedBuildVMs.ps1); cleanup accepts injected VmBackend (Phase C ESXi hook preserved)

- commands/monitor.py -> monitor disk + monitor runner (replaces Watch-DiskSpace.ps1, Watch-RunnerHealth.ps1)

- commands/report.py  -> report job (replaces Get-CIJobSummary.ps1)

Each PS script reduced to a 3-line shim that delegates to the Python CLI and preserves \0.

Tests: 69 pytest cases across new test_commands_*.py modules; ruff clean, mypy --strict clean, coverage 74.5% (>=70 gate).
2026-05-14 18:34:03 +02:00
Simone 70504b44e4 Phase A1: bootstrap Python ci_orchestrator package
- pyproject.toml (hatchling) with ruff/mypy/pytest dev deps
- src/ci_orchestrator/: config, credentials, backends (Protocol + WorkstationVmrunBackend), transport (WinRM via pypsrp, SSH via paramiko)
- CLI entry point with PoC 'wait-ready' subcommand (Windows + Linux guests)
- tests/python/: 35 unit tests, 83% coverage, all backends/transport mocked
- gitea/workflows/lint.yml: new 'python' job (ruff + mypy --strict + pytest --cov-fail-under=70)
- config.example.toml + README setup section + .gitignore Python entries

Neutral VmBackend Protocol prepared for Phase C ESXi extension.
See plans/implementation-plan-A-B.md step A1.
2026-05-14 18:34:02 +02:00