refactor(artifacts): harmonize Windows to Linux raw-file collection
Lint / pssa (push) Failing after 23s
Lint / python (push) Successful in 50s

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.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Simone
2026-05-17 16:30:34 +02:00
parent 2e1f3ec477
commit 456db0a3e2
6 changed files with 131 additions and 58 deletions
+50 -15
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import json
import tarfile
import zipfile
from pathlib import Path
from typing import Any, ClassVar
@@ -78,8 +79,34 @@ def _patch(monkeypatch: pytest.MonkeyPatch) -> None:
# ── Windows happy path ────────────────────────────────────────────────────
def _zip_payload(tmp_path: Path) -> bytes:
"""Build a real zip whose contents the fake transport will 'fetch'."""
src = tmp_path / "zsrc"
src.mkdir()
(src / "binary.bin").write_bytes(b"\x01\x02\x03")
(src / "log.txt").write_text("hello", encoding="utf-8")
zip_local = tmp_path / "transfer.zip"
with zipfile.ZipFile(zip_local, "w") as zf:
for f in src.iterdir():
zf.write(f, arcname=f.name)
return zip_local.read_bytes()
def _win_transport_with_zip(payload: bytes) -> type:
class _WinTransport(_FakeTransport):
def fetch(self, remote_path: str, local_path: str) -> None:
self.fetched.append((remote_path, str(local_path)))
Path(local_path).write_bytes(payload)
return _WinTransport
def test_collect_windows_happy_path(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
_patch(monkeypatch)
payload = _zip_payload(tmp_path)
monkeypatch.setattr(
artifacts_module, "WinRmTransport", _win_transport_with_zip(payload)
)
out_dir = tmp_path / "artifacts"
result = CliRunner().invoke(
cli,
@@ -89,18 +116,19 @@ def test_collect_windows_happy_path(monkeypatch: pytest.MonkeyPatch, tmp_path: P
"--ip-address",
"10.0.0.7",
"--guest-artifact-path",
"C:\\CI\\output\\artifacts.zip",
"C:\\CI\\output",
"--host-artifact-dir",
str(out_dir),
],
)
assert result.exit_code == 0, result.output
assert (out_dir / "artifacts.zip").is_file()
instance = _FakeTransport.instances[0]
assert any("Test-Path" in c for c in instance.runs)
assert instance.fetched == [
("C:\\CI\\output\\artifacts.zip", str(out_dir / "artifacts.zip"))
]
# Raw files extracted into host_dir (no zip-in-zip).
assert (out_dir / "binary.bin").read_bytes() == b"\x01\x02\x03"
assert (out_dir / "log.txt").read_text(encoding="utf-8") == "hello"
cmds = _FakeTransport.instances[0].runs
assert any("Compress-Archive" in c for c in cmds)
# Temp transport zip on the guest is cleaned up.
assert any("Remove-Item" in c and "ci-artifacts-transfer.zip" in c for c in cmds)
def test_collect_windows_artifact_missing(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
@@ -109,7 +137,9 @@ def test_collect_windows_artifact_missing(monkeypatch: pytest.MonkeyPatch, tmp_p
class _Missing(_FakeTransport):
def __init__(self, *a: Any, **kw: Any) -> None:
super().__init__(*a, **kw)
self.run_overrides["Test-Path"] = _FakeResult(stdout="MISSING\r\n")
self.run_overrides["-and (Get-ChildItem"] = _FakeResult(
stdout="MISSING\r\n"
)
monkeypatch.setattr(artifacts_module, "WinRmTransport", _Missing)
result = CliRunner().invoke(
@@ -120,22 +150,23 @@ def test_collect_windows_artifact_missing(monkeypatch: pytest.MonkeyPatch, tmp_p
"--ip-address",
"10.0.0.7",
"--guest-artifact-path",
"C:\\CI\\output\\artifacts.zip",
"C:\\CI\\output",
"--host-artifact-dir",
str(tmp_path / "out"),
],
)
assert result.exit_code != 0
assert "artifact not found" in result.output
assert "artifact not found or empty" in result.output
def test_collect_windows_include_logs(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
_patch(monkeypatch)
payload = _zip_payload(tmp_path)
class _WithLogs(_FakeTransport):
class _WithLogs(_win_transport_with_zip(payload)): # type: ignore[misc]
def __init__(self, *a: Any, **kw: Any) -> None:
super().__init__(*a, **kw)
self.run_overrides["Get-ChildItem"] = _FakeResult(
self.run_overrides["C:\\CI\\build"] = _FakeResult(
stdout="C:\\CI\\build\\one.log\r\nC:\\CI\\build\\sub\\two.log\r\n"
)
@@ -148,7 +179,7 @@ def test_collect_windows_include_logs(monkeypatch: pytest.MonkeyPatch, tmp_path:
"--ip-address",
"10.0.0.7",
"--guest-artifact-path",
"C:\\CI\\output\\artifacts.zip",
"C:\\CI\\output",
"--host-artifact-dir",
str(tmp_path / "out"),
"--include-logs",
@@ -164,6 +195,10 @@ def test_collect_writes_manifest_when_job_id_set(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
_patch(monkeypatch)
payload = _zip_payload(tmp_path)
monkeypatch.setattr(
artifacts_module, "WinRmTransport", _win_transport_with_zip(payload)
)
out = tmp_path / "out"
result = CliRunner().invoke(
cli,
@@ -173,7 +208,7 @@ def test_collect_writes_manifest_when_job_id_set(
"--ip-address",
"10.0.0.7",
"--guest-artifact-path",
"C:\\CI\\output\\artifacts.zip",
"C:\\CI\\output",
"--host-artifact-dir",
str(out),
"--job-id",
@@ -189,7 +224,7 @@ def test_collect_writes_manifest_when_job_id_set(
assert data["jobId"] == "run-42"
assert data["commit"] == "abc1234"
names = [f["name"] for f in data["files"]]
assert "artifacts.zip" in names
assert "binary.bin" in names and "log.txt" in names
# ── Linux happy path ──────────────────────────────────────────────────────
+3 -2
View File
@@ -360,5 +360,6 @@ def test_build_run_windows_host_source_dir(
assert instance.copies[0][1] == "C:\\CI\\src-transfer.zip"
cmds = [c[0] for c in instance.runs]
assert any("Expand-Archive" in c for c in cmds)
# Packaging step references the configured artifact source dir.
assert any("Compress-Archive" in c and "'dist'" in c for c in cmds)
# Staging copies the raw source into the collect dir (no zip-in-zip).
assert any("Copy-Item" in c and "'dist'" in c for c in cmds)
assert not any("Compress-Archive" in c and "'dist'" in c for c in cmds)
+2 -2
View File
@@ -322,8 +322,8 @@ def test_job_windows_branch_with_overrides(
# CPU/RAM override patched the clone VMX.
clone_vmx = next((tmp_path / "vms").rglob("*.vmx"), None)
# After successful job the clone dir is deleted, so the file is gone.
# Check that the Windows collect helper got the correct artifact path instead.
assert calls["windows_collect"][0]["guest_path"].endswith("artifacts.zip")
# Check that the Windows collect helper got the collect dir instead.
assert calls["windows_collect"][0]["guest_path"] == "C:\\CI\\output"
assert clone_vmx is None # cleaned up
assert backend.calls[-1] == "delete"