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).
This commit is contained in:
2026-05-14 17:01:15 +02:00
parent 10da8f4e81
commit 80f6661ad5
17 changed files with 2074 additions and 881 deletions
+16 -123
View File
@@ -1,130 +1,23 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Stops and permanently deletes an ephemeral build VM.
.DESCRIPTION
Performs a graceful shutdown (soft stop) with a timeout fallback to
a hard stop, then uses vmrun deleteVM to remove all VM files.
Finally, removes the clone directory from disk.
This script is designed to be called from a try/finally block so
it always runs, even on build failure.
.PARAMETER VMPath
Full path to the clone VM's .vmx file.
.PARAMETER VmrunPath
Full path to vmrun.exe.
Default: C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe
.PARAMETER GracefulTimeoutSeconds
Seconds to wait for graceful shutdown before forcing power-off.
Default: 15
.EXAMPLE
.\Remove-BuildVM.ps1 -VMPath "F:\CI\BuildVMs\Clone_run-42_20260508_143022\Clone_run-42_20260508_143022.vmx"
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)]
[string] $VMPath,
[string] $VmrunPath = 'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe',
[ValidateRange(1, 60)]
[int] $GracefulTimeoutSeconds = 15
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Continue' # Don't stop on errors — always try next step
$ErrorActionPreference = 'Stop'
Import-Module (Join-Path $PSScriptRoot '_Common.psm1') -Force
$cloneDir = Split-Path $VMPath -Parent
Write-Host "[Remove-BuildVM] Destroying VM: $VMPath"
# ── Step 1: Check if VM exists at all ────────────────────────────────────────
if (-not (Test-Path $VMPath -PathType Leaf)) {
Write-Warning "[Remove-BuildVM] VMX file not found — nothing to remove: $VMPath"
# Still attempt to remove the directory in case of partial clone
if ((Test-Path $cloneDir) -and $PSCmdlet.ShouldProcess($cloneDir, 'Remove partial clone directory')) {
Remove-Item $cloneDir -Recurse -Force -ErrorAction SilentlyContinue
# Shim: delegates to the Python ci_orchestrator CLI.
# Original PS implementation moved to git history; see plans/A2-closeout.md.
$pyArgs = @()
foreach ($a in $args) {
if ($a -is [string] -and $a.Length -gt 1 -and $a.StartsWith('-') -and -not $a.StartsWith('--')) {
$name = $a.Substring(1)
$kebab = [Regex]::Replace($name, '(?<=[a-z0-9])([A-Z])', '-$1')
$kebab = [Regex]::Replace($kebab, '([A-Z]+)([A-Z][a-z])', '$1-$2')
$pyArgs += '--' + $kebab.ToLower()
}
return
}
if (-not $PSCmdlet.ShouldProcess($VMPath, 'Stop and delete build VM')) { return }
# ── Step 2: Graceful stop ─────────────────────────────────────────────────────
Write-Host "[Remove-BuildVM] Sending soft stop (timeout: ${GracefulTimeoutSeconds}s)..."
$stopSoft = Invoke-VmrunBounded -VmrunPath $VmrunPath `
-Operation stop -Arguments @($VMPath, 'soft') -TimeoutSeconds $GracefulTimeoutSeconds
if ($stopSoft.TimedOut) {
Write-Warning "[Remove-BuildVM] Soft stop timed out after ${GracefulTimeoutSeconds}s."
} elseif ($stopSoft.ExitCode -eq 0) {
Write-Host "[Remove-BuildVM] VM stopped gracefully."
}
# ── Step 3: Force stop if still running ──────────────────────────────────────
if ($stopSoft.TimedOut -or $stopSoft.ExitCode -ne 0) {
Write-Host "[Remove-BuildVM] Graceful stop did not succeed — forcing hard stop..."
$stopHard = Invoke-VmrunBounded -VmrunPath $VmrunPath `
-Operation stop -Arguments @($VMPath, 'hard') -TimeoutSeconds 60
if ($stopHard.TimedOut) {
Write-Warning "[Remove-BuildVM] Hard stop timed out after 60s."
else {
$pyArgs += $a
}
}
# Poll vmrun list until VMX is gone — vmware-vmx.exe may hold file locks briefly
# after stop. Waiting for the entry to disappear ensures deleteVM won't race the
# process exit.
$vmxNorm = $VMPath.Trim().ToLower()
$releaseDeadline = (Get-Date).AddSeconds(20)
while ((Get-Date) -lt $releaseDeadline) {
$listedVMs = & $VmrunPath -T ws list 2>&1 |
Where-Object { $_ -notmatch '^Total running' } |
ForEach-Object { "$_".Trim().ToLower() }
if ($listedVMs -notcontains $vmxNorm) { break }
Start-Sleep -Seconds 2
}
# ── Step 4: Delete VM via vmrun — retry up to 3× with backoff ────────────────
Write-Host "[Remove-BuildVM] Deleting VM..."
$deleteOutput = ''
$deleteExit = -1
foreach ($delay in @(0, 3, 6)) {
if ($delay -gt 0) {
Write-Host "[Remove-BuildVM] Retrying deleteVM in ${delay}s..."
Start-Sleep -Seconds $delay
}
$delResult = Invoke-VmrunBounded -VmrunPath $VmrunPath `
-Operation deleteVM -Arguments @($VMPath) -TimeoutSeconds 90
$deleteOutput = $delResult.Output
$deleteExit = $delResult.ExitCode
if ($delResult.TimedOut) {
Write-Warning "[Remove-BuildVM] deleteVM timed out after 90s."
$deleteExit = -1
}
if ($deleteExit -eq 0) { break }
}
if ($deleteExit -ne 0) {
Write-Warning "[Remove-BuildVM] vmrun deleteVM failed after retries (exit $deleteExit): $deleteOutput"
Write-Warning "[Remove-BuildVM] Will attempt manual directory removal."
}
# ── Step 5: Remove clone directory (catches any leftover files) ───────────────
if (Test-Path $cloneDir) {
Write-Host "[Remove-BuildVM] Removing clone directory: $cloneDir"
Remove-Item $cloneDir -Recurse -Force -ErrorAction SilentlyContinue
if (Test-Path $cloneDir) {
Write-Warning "[Remove-BuildVM] Clone directory could not be fully removed: $cloneDir"
Write-Warning " Manual cleanup required."
} else {
Write-Host "[Remove-BuildVM] Clone directory removed."
}
}
Write-Host "[Remove-BuildVM] VM destruction complete."
$venvPython = $env:CI_VENV_PYTHON
if (-not $venvPython) { $venvPython = 'F:\CI\python\venv\Scripts\python.exe' }
& $venvPython -m ci_orchestrator vm remove @pyArgs
exit $LASTEXITCODE