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%.
97 lines
3.2 KiB
Python
97 lines
3.2 KiB
Python
"""Resolve the latest Windows Server 2025 ISO from the massgrave.dev source.
|
|
|
|
The rendered massgrave page links through a Cloudflare-Turnstile-gated file host
|
|
(``zerofs.link``) — not scriptable — so we only *resolve* (parse the source
|
|
markdown on GitHub for the newest build + file name + link). The operator
|
|
downloads it once in a browser, then feeds it to ``template patch-win-iso`` /
|
|
``deploy-win --win-iso``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import urllib.request
|
|
from dataclasses import dataclass
|
|
|
|
import click
|
|
|
|
RAW_MD_URL = (
|
|
"https://raw.githubusercontent.com/massgravel/massgrave.dev/main/docs/"
|
|
"windows-server-links.md"
|
|
)
|
|
_USER_AGENT = "ci-orchestrator/1.0 (+deploy-win)"
|
|
|
|
_WS2025_HEADING = "## Windows Server 2025"
|
|
_BUILD_RE = re.compile(r"Build\s*-\s*([0-9][0-9.]+)")
|
|
# Table row: | English | x64 | [filename.iso](url)|
|
|
_ROW_RE = re.compile(
|
|
r"\|\s*(?P<lang>[^|]+?)\s*\|\s*(?P<arch>[^|]+?)\s*\|\s*\[(?P<file>[^\]]+\.iso)\]\((?P<url>[^)]+)\)"
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class WinServerIso:
|
|
"""A resolved Windows Server ISO entry from the massgrave source."""
|
|
|
|
build: str
|
|
filename: str
|
|
url: str
|
|
lang: str
|
|
arch: str
|
|
|
|
|
|
def fetch_links_md(*, url: str = RAW_MD_URL, timeout: float = 30.0) -> str:
|
|
"""Download the massgrave windows-server-links markdown source."""
|
|
req = urllib.request.Request(url, headers={"User-Agent": _USER_AGENT})
|
|
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
text: str = resp.read().decode("utf-8")
|
|
return text
|
|
|
|
|
|
def _ws2025_section(md: str) -> str:
|
|
"""Return the markdown between the WS2025 heading and the next ``## `` heading."""
|
|
start = md.find(_WS2025_HEADING)
|
|
if start < 0:
|
|
raise click.ClickException("Windows Server 2025 section not found in massgrave source.")
|
|
rest = md[start + len(_WS2025_HEADING):]
|
|
nxt = rest.find("\n## ")
|
|
return rest if nxt < 0 else rest[:nxt]
|
|
|
|
|
|
def resolve_ws2025(
|
|
md: str, *, lang: str = "English", arch: str = "x64"
|
|
) -> WinServerIso:
|
|
"""Resolve the newest WS2025 ISO entry for ``lang``/``arch``.
|
|
|
|
The newest build is the first ``Build - <n>`` table in the section (the
|
|
"updated" one); the OEM tables below it are older RTM media.
|
|
"""
|
|
section = _ws2025_section(md)
|
|
build = ""
|
|
for line in section.splitlines():
|
|
m = _BUILD_RE.search(line)
|
|
if m:
|
|
build = m.group(1)
|
|
row = _ROW_RE.search(line)
|
|
if (
|
|
row
|
|
and build
|
|
and row.group("lang").strip().lower() == lang.lower()
|
|
and row.group("arch").strip().lower() == arch.lower()
|
|
):
|
|
return WinServerIso(
|
|
build=build,
|
|
filename=row.group("file").strip(),
|
|
url=row.group("url").strip(),
|
|
lang=lang,
|
|
arch=arch,
|
|
)
|
|
raise click.ClickException(
|
|
f"No Windows Server 2025 {lang}/{arch} entry found in massgrave source."
|
|
)
|
|
|
|
|
|
def latest_ws2025(*, lang: str = "English", arch: str = "x64", timeout: float = 30.0) -> WinServerIso:
|
|
"""Fetch the source and resolve the newest WS2025 ISO entry."""
|
|
return resolve_ws2025(fetch_links_md(timeout=timeout), lang=lang, arch=arch)
|