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.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 02:41:15 +02:00
parent d5f362b8dc
commit f96b9953c5
6 changed files with 356 additions and 225 deletions
+115 -3
View File
@@ -17,7 +17,6 @@ import ci_orchestrator.commands.template as tmpl_mod
from ci_orchestrator.__main__ import cli
from ci_orchestrator.transport.winrm import WinRmResult
# ── fixtures ──────────────────────────────────────────────────────────────────
@@ -345,7 +344,7 @@ def test_parse_exit_marker_malformed() -> None:
def test_wait_tcp_success(monkeypatch: pytest.MonkeyPatch) -> None:
class _FakeConn:
def __enter__(self) -> "_FakeConn":
def __enter__(self) -> _FakeConn:
return self
def __exit__(self, *_: object) -> None:
@@ -699,7 +698,7 @@ def test_prepare_win_recreate_snapshot_flag(
def test_prepare_win_skip_flags(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""--skip-windows-update/tier2/cleanup flags reach the invoke script."""
"""-SkipCleanup:$true always in loop args; --skip-cleanup suppresses post-loop cleanup call."""
vmx = _setup_prepare_env(tmp_path, with_static_ip=False)
monkeypatch.chdir(tmp_path)
backend = _make_backend([True, False])
@@ -1030,3 +1029,116 @@ def test_prepare_win_store_credential_default_target(
assert result.exit_code == 0, result.output
assert any(s[0] == "BuildVMGuest:meta" for s in stored)
assert any(s[0] == "BuildVMGuest" for s in stored)
def test_prepare_win_cleanup_runs_after_loop(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""CleanupOnly invocation fires after WU loop exits 0 (default, no --skip-cleanup)."""
vmx = _setup_prepare_env(tmp_path, with_static_ip=False)
monkeypatch.chdir(tmp_path)
backend = _make_backend([True, False])
captured_scripts: list[str] = []
def _run(script: str = "", **kwargs: object) -> WinRmResult:
captured_scripts.append(script)
if "ConvertTo-Json" in script:
return WinRmResult(stdout="{}", stderr="", returncode=0)
return WinRmResult(stdout="CI_EXITCODE:0", stderr="", returncode=0)
transport: MagicMock = MagicMock()
transport.run.side_effect = _run
transport.is_ready.return_value = True
_patch_prepare(monkeypatch, backend, transport)
result = CliRunner().invoke(
cli,
[
"template", "prepare-win",
"--vmx", str(vmx),
"--ip", "192.168.1.100",
"--admin-password", "adminpass",
"--build-password", "buildpass",
],
)
assert result.exit_code == 0, result.output
assert any("CleanupOnly:$true" in s for s in captured_scripts), (
"Expected a -CleanupOnly:$true script after the WU loop"
)
assert "Post-update cleanup complete" in result.output
def test_prepare_win_skip_cleanup_no_cleanup_call(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""--skip-cleanup suppresses the post-loop CleanupOnly invocation."""
vmx = _setup_prepare_env(tmp_path, with_static_ip=False)
monkeypatch.chdir(tmp_path)
backend = _make_backend([True, False])
captured_scripts: list[str] = []
def _run(script: str = "", **kwargs: object) -> WinRmResult:
captured_scripts.append(script)
if "ConvertTo-Json" in script:
return WinRmResult(stdout="{}", stderr="", returncode=0)
return WinRmResult(stdout="CI_EXITCODE:0", stderr="", returncode=0)
transport: MagicMock = MagicMock()
transport.run.side_effect = _run
transport.is_ready.return_value = True
_patch_prepare(monkeypatch, backend, transport)
result = CliRunner().invoke(
cli,
[
"template", "prepare-win",
"--vmx", str(vmx),
"--ip", "192.168.1.100",
"--admin-password", "adminpass",
"--build-password", "buildpass",
"--skip-cleanup",
],
)
assert result.exit_code == 0, result.output
assert not any("CleanupOnly:$true" in s for s in captured_scripts), (
"--skip-cleanup must suppress the -CleanupOnly:$true call"
)
def test_prepare_win_cleanup_only_fails(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Cleanup-only step returning nonzero exits the command with an error."""
vmx = _setup_prepare_env(tmp_path, with_static_ip=False)
monkeypatch.chdir(tmp_path)
backend = _make_backend([True])
def _run(script: str = "", **kwargs: object) -> WinRmResult:
if "CleanupOnly" in script:
return WinRmResult(stdout="CI_EXITCODE:1", stderr="cleanup error", returncode=0)
if "ConvertTo-Json" in script:
return WinRmResult(stdout="{}", stderr="", returncode=0)
return WinRmResult(stdout="CI_EXITCODE:0", stderr="", returncode=0)
transport: MagicMock = MagicMock()
transport.run.side_effect = _run
transport.is_ready.return_value = True
_patch_prepare(monkeypatch, backend, transport)
result = CliRunner().invoke(
cli,
[
"template", "prepare-win",
"--vmx", str(vmx),
"--ip", "192.168.1.100",
"--admin-password", "adminpass",
"--build-password", "buildpass",
],
)
assert result.exit_code != 0
assert "Disk cleanup failed" in result.output