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%.
This commit is contained in:
@@ -271,3 +271,56 @@ def test_deploy_win_flag_timeout(tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
)
|
||||
assert result.exit_code != 0
|
||||
assert "install_complete.flag not seen" in result.output
|
||||
|
||||
|
||||
# ── check-win-iso / patch-win-iso ────────────────────────────────────────────
|
||||
|
||||
def test_check_win_iso_not_cached(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from ci_orchestrator.winimage import massgrave as mg
|
||||
(tmp_path / "iso").mkdir(parents=True)
|
||||
monkeypatch.setattr(tmpl_mod, "load_config", lambda: _make_config(tmp_path))
|
||||
info = mg.WinServerIso(build="26100.32860", filename="ws.iso",
|
||||
url="https://zerofs.link/f/AAA/", lang="English", arch="x64")
|
||||
monkeypatch.setattr(tmpl_mod.win_massgrave, "latest_ws2025", lambda **_k: info)
|
||||
result = CliRunner().invoke(cli, ["template", "check-win-iso"])
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "26100.32860" in result.output and "cached: no" in result.output
|
||||
|
||||
|
||||
def test_check_win_iso_cached(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from ci_orchestrator.winimage import massgrave as mg
|
||||
(tmp_path / "iso").mkdir(parents=True)
|
||||
(tmp_path / "iso" / "ws.iso").write_bytes(b"x")
|
||||
monkeypatch.setattr(tmpl_mod, "load_config", lambda: _make_config(tmp_path))
|
||||
info = mg.WinServerIso(build="26100.32860", filename="ws.iso",
|
||||
url="u", lang="English", arch="x64")
|
||||
monkeypatch.setattr(tmpl_mod.win_massgrave, "latest_ws2025", lambda **_k: info)
|
||||
result = CliRunner().invoke(cli, ["template", "check-win-iso"])
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "cached: yes" in result.output
|
||||
|
||||
|
||||
def test_patch_win_iso_standalone(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
src = tmp_path / "in.iso"
|
||||
src.write_bytes(b"iso")
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
def fake_patch(base: Path, *, out_iso: Path, **_k: Any) -> Path:
|
||||
captured["base"] = base
|
||||
captured["out"] = out_iso
|
||||
out_iso.write_bytes(b"patched")
|
||||
return out_iso
|
||||
|
||||
monkeypatch.setattr(tmpl_mod.win_iso_patch, "patch_iso", fake_patch)
|
||||
result = CliRunner().invoke(cli, ["template", "patch-win-iso", "--in", str(src)])
|
||||
assert result.exit_code == 0, result.output
|
||||
assert captured["base"] == src
|
||||
assert str(captured["out"]).endswith("in.iso.patched.iso")
|
||||
|
||||
|
||||
def test_patch_win_iso_missing_input(tmp_path: Path) -> None:
|
||||
result = CliRunner().invoke(
|
||||
cli, ["template", "patch-win-iso", "--in", str(tmp_path / "nope.iso")]
|
||||
)
|
||||
assert result.exit_code != 0
|
||||
assert "input ISO not found" in result.output
|
||||
|
||||
@@ -11,6 +11,41 @@ import pytest
|
||||
|
||||
from ci_orchestrator.winimage import autounattend as au
|
||||
from ci_orchestrator.winimage import iso_patch as ip
|
||||
from ci_orchestrator.winimage import massgrave as mg
|
||||
|
||||
# ── massgrave resolver ───────────────────────────────────────────────────────
|
||||
|
||||
_SAMPLE_MD = """\
|
||||
## Windows Server 2025
|
||||
Build - 26100.32860 (May 2026)
|
||||
| English | x64 | [en-us_windows_server_2025_updated_may_2026_x64_dvd_abc.iso](https://zerofs.link/f/AAA/)|
|
||||
| Italian | x64 | [it-it_windows_server_2025_updated_may_2026_x64_dvd_abc.iso](https://zerofs.link/f/BBB/)|
|
||||
Build - 26100.1742
|
||||
| English | x64 | [X23-81958_26100.1742_en-us.iso](https://oemsoc.download.prss.microsoft.com/x.iso?t=z)|
|
||||
## Windows Server 2022
|
||||
| English | x64 | [en-us_windows_server_2022.iso](https://zerofs.link/f/CCC/)|
|
||||
"""
|
||||
|
||||
|
||||
def test_resolve_ws2025_picks_newest_english() -> None:
|
||||
i = mg.resolve_ws2025(_SAMPLE_MD, lang="English", arch="x64")
|
||||
assert i.build == "26100.32860"
|
||||
assert i.filename.startswith("en-us_windows_server_2025_updated")
|
||||
assert i.url == "https://zerofs.link/f/AAA/"
|
||||
|
||||
|
||||
def test_resolve_ws2025_italian() -> None:
|
||||
assert mg.resolve_ws2025(_SAMPLE_MD, lang="Italian").url == "https://zerofs.link/f/BBB/"
|
||||
|
||||
|
||||
def test_resolve_ws2025_not_found() -> None:
|
||||
with pytest.raises(click.ClickException, match="No Windows Server 2025"):
|
||||
mg.resolve_ws2025(_SAMPLE_MD, lang="Klingon")
|
||||
|
||||
|
||||
def test_ws2025_section_missing() -> None:
|
||||
with pytest.raises(click.ClickException, match="section not found"):
|
||||
mg._ws2025_section("## Windows Server 2022\nnothing here")
|
||||
|
||||
# ── autounattend ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user