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.
This commit is contained in:
2026-05-14 17:20:34 +02:00
parent 096ba7fe16
commit 816a15503e
15 changed files with 1900 additions and 1061 deletions
+112
View File
@@ -354,4 +354,116 @@ def vm_cleanup(
)
# ─────────────────────────────────────────────────────────────────────── new
@vm.command("new")
@click.option(
"--template",
"--template-path",
"template",
required=True,
help="Identifier of the template VM (Workstation: VMX path; ESXi: managed-object ref).",
)
@click.option(
"--snapshot",
"--snapshot-name",
"snapshot",
default="BaseClean",
show_default=True,
help="Snapshot name on the template to clone from.",
)
@click.option(
"--clone-base-dir",
"clone_base_dir",
required=True,
help="Directory under which the new clone folder will be created.",
)
@click.option(
"--job-id",
"job_id",
required=True,
help="Unique job identifier (used to name the clone folder).",
)
@click.option(
"--vmrun-path",
"vmrun_path",
default=None,
help="Override vmrun binary path.",
)
@click.option(
"--guest-os",
type=click.Choice(["windows", "linux"], case_sensitive=False),
default="windows",
show_default=True,
help="Informational; reserved for backend hints in Phase C.",
)
def vm_new(
template: str,
snapshot: str,
clone_base_dir: str,
job_id: str,
vmrun_path: str | None,
guest_os: str,
) -> None:
"""Create a linked clone of the template VM for an ephemeral CI build.
Mirrors ``scripts/New-BuildVM.ps1``. Prints the clone identifier
(Workstation: VMX path) on stdout on success.
Phase C hook: ``--template`` is treated as an opaque string; the backend
is responsible for interpretation. Workstation builds the destination
path under ``--clone-base-dir`` from ``--job-id`` + timestamp.
"""
del guest_os # currently informational only
# Verify the template identifier looks plausible: for Workstation it is
# a VMX path. For other backends in Phase C this validation will move
# into the backend itself.
template_path = Path(template)
if not template_path.is_file():
raise click.ClickException(f"Template VMX not found: {template}")
base = Path(clone_base_dir)
base.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now(tz=UTC).strftime("%Y%m%d_%H%M%S")
clone_name = f"Clone_{job_id}_{timestamp}"
clone_dir = base / clone_name
clone_vmx = clone_dir / f"{clone_name}.vmx"
click.echo("[vm new] creating linked clone...")
click.echo(f" template : {template}")
click.echo(f" snapshot : {snapshot}")
click.echo(f" clone vmx: {clone_vmx}")
try:
backend = _make_backend(vmrun_path)
except BackendNotAvailable as exc:
raise click.ClickException(f"vmrun unavailable: {exc}") from exc
start = time.monotonic()
try:
handle = backend.clone_linked(
template=str(template_path),
snapshot=snapshot,
name=clone_name,
destination=str(clone_vmx),
)
except BackendError as exc:
# Clean up partial clone directory if vmrun left one behind.
if clone_dir.exists():
_try_remove_dir(clone_dir)
raise click.ClickException(f"clone failed: {exc}") from exc
if not clone_vmx.is_file():
raise click.ClickException(
f"backend reported success but clone VMX is missing: {clone_vmx}"
)
elapsed = time.monotonic() - start
click.echo(f"[vm new] clone created in {elapsed:.1f}s")
# Final stdout line: the identifier itself, so PS callers can capture it.
click.echo(handle.identifier)
__all__ = ["cleanup_orphans", "vm"]