From d666d91cb0f2a681683763a256842ac374035d39 Mon Sep 17 00:00:00 2001 From: Rian Stockbower Date: Fri, 30 Oct 2026 10:31:33 -0400 Subject: [PATCH] fix: retain current winget release metadata --- actions/winget-submit/test_winget_submit.py | 47 ++++++++++++++++++--- actions/winget-submit/winget_submit.py | 39 +++++++++++++---- 2 files changed, 73 insertions(+), 13 deletions(-) diff --git a/actions/winget-submit/test_winget_submit.py b/actions/winget-submit/test_winget_submit.py index 6bce715..defd529 100644 --- a/actions/winget-submit/test_winget_submit.py +++ b/actions/winget-submit/test_winget_submit.py @@ -383,10 +383,10 @@ def test_render_bootstrap_manifests_rejects_output_inside_source(tmp_path): def test_run_submit_existing_package_copies_upstream_manifests(monkeypatch): assets = _assets() calls = {} - monkeypatch.setattr(winget_submit, "load_release_assets", lambda *a: ("checksums", {})) + monkeypatch.setattr(winget_submit, "load_release_assets", lambda *a: ("checksums", {}, {})) monkeypatch.setattr(winget_submit, "resolve_windows_assets", lambda *a, **kw: assets) monkeypatch.setattr(winget_submit, "package_exists", lambda *a: True) - monkeypatch.setattr(winget_submit, "render_update_manifests", lambda directory, package_id, version, assets, token: [directory / "a.yaml"]) + monkeypatch.setattr(winget_submit, "render_update_manifests", lambda directory, package_id, version, assets, token, release: [directory / "a.yaml"]) monkeypatch.setattr(winget_submit, "validate_manifests", lambda *a: None) monkeypatch.setattr(winget_submit, "submit_manifest_pr", lambda package_id, version, files, token, **kw: calls.setdefault("submit", (package_id, version, files, token, kw))) assert winget_submit.run_submit(_args(bootstrap="true")) == 0 @@ -394,7 +394,7 @@ def test_run_submit_existing_package_copies_upstream_manifests(monkeypatch): def test_run_submit_bootstrap_uses_new_github_path(monkeypatch): - monkeypatch.setattr(winget_submit, "load_release_assets", lambda *a: ("checksums", {})) + monkeypatch.setattr(winget_submit, "load_release_assets", lambda *a: ("checksums", {}, {})) monkeypatch.setattr(winget_submit, "resolve_windows_assets", lambda *a, **kw: _assets()) monkeypatch.setattr(winget_submit, "package_exists", lambda *a: False) monkeypatch.setattr(winget_submit, "render_bootstrap_manifests", lambda **kw: []) @@ -554,7 +554,7 @@ def fetch(url, token): return {"content": base64.b64encode(json.dumps(records[name]).encode()).decode()} return [{"name": "1.0.0", "type": "dir"}] monkeypatch.setattr(winget_submit, "github_request_json", fetch) - output = winget_submit.render_update_manifests(tmp_path, package, "1.2.3", _assets(), "token") + output = winget_submit.render_update_manifests(tmp_path, package, "1.2.3", _assets(), "token", {}) assert len(output) == 3 version = yaml.safe_load((tmp_path / f"{package}.yaml").read_text()) assert version["UnknownNewField"] == {"keep": "me"} @@ -563,6 +563,8 @@ def fetch(url, token): assert installer["Installers"][0]["UnknownField"] == "keep" assert installer["Installers"][0]["InstallerUrl"] == _assets().x64.url assert installer["Installers"][1]["InstallerSha256"] == _assets().arm64.sha256 + locale = yaml.safe_load((tmp_path / f"{package}.locale.en-US.yaml").read_text()) + assert "ReleaseNotesUrl" not in locale and "ReleaseDate" not in installer def test_submit_manifest_pr_refuses_non_fork_before_writing(monkeypatch): @@ -601,7 +603,7 @@ def fetch(url, token): return [{"name": "1.0.0", "type": "dir"}] monkeypatch.setattr(winget_submit, "github_request_json", fetch) with pytest.raises(winget_submit.SubmitError, match="exactly one x64 and one arm64"): - winget_submit.render_update_manifests(tmp_path, package, "1.2.3", _assets(), "token") + winget_submit.render_update_manifests(tmp_path, package, "1.2.3", _assets(), "token", {}) def test_submit_manifest_pr_uses_explicit_upstream_only_for_test(monkeypatch, tmp_path): @@ -625,3 +627,38 @@ def api(url, token, method="GET", payload=None): monkeypatch.setattr(winget_submit, "github_api", api) winget_submit.submit_manifest_pr("OpenCLICollective.example", "1.2.3", [path], "token", new_package=True, upstream_repo="alice/winget-pkgs") assert any(url == "https://api.github.com/repos/alice/winget-pkgs/pulls" and method == "POST" for url, method, payload in calls) + + +@pytest.mark.parametrize("release", [{}, {"html_url": "http://example.test", "published_at": "bad"}]) +def test_missing_release_metadata_fails_closed(release): + with pytest.raises(winget_submit.SubmitError): + winget_submit._release_notes_url(release) + with pytest.raises(winget_submit.SubmitError): + winget_submit._release_date(release) + + +def test_render_update_uses_actual_release_metadata(monkeypatch, tmp_path): + import base64, json + package = "OpenCLICollective.newrelic-cli" + records = { + f"{package}.yaml": {"PackageIdentifier": package, "PackageVersion": "1.0.0", "ManifestType": "version", "ManifestVersion": "1.28.0"}, + f"{package}.locale.en-US.yaml": {"PackageIdentifier": package, "PackageVersion": "1.0.0", "ManifestType": "defaultLocale", "ManifestVersion": "1.28.0", "ReleaseNotes": "stale notes", "ReleaseNotesUrl": "https://github.com/old/release"}, + f"{package}.installer.yaml": {"PackageIdentifier": package, "PackageVersion": "1.0.0", "ManifestType": "installer", "ManifestVersion": "1.28.0", "ReleaseDate": "2020-01-01", "Installers": [{"Architecture": "x64", "ReleaseDate": "2020-01-01"}, {"Architecture": "arm64"}]}, + } + def fetch(url, token): + if url.endswith("/1.0.0"): + return [{"name": name, "url": f"https://test/{name}"} for name in records] + if url.startswith("https://test/"): + return {"content": base64.b64encode(json.dumps(records[url.rsplit("/", 1)[-1]]).encode()).decode()} + return [{"name": "1.0.0", "type": "dir"}] + monkeypatch.setattr(winget_submit, "github_request_json", fetch) + release = {"html_url": "https://github.com/open-cli-collective/newrelic-cli/releases/tag/v1.2.3", "published_at": "2026-10-28T14:18:11Z"} + winget_submit.render_update_manifests(tmp_path, package, "1.2.3", _assets(), "token", release) + locale = yaml.safe_load((tmp_path / f"{package}.locale.en-US.yaml").read_text()) + installer = yaml.safe_load((tmp_path / f"{package}.installer.yaml").read_text()) + assert locale["ReleaseNotesUrl"] == release["html_url"] + assert "ReleaseNotes" not in locale + assert installer["ReleaseDate"] == "2026-10-28" + assert installer["Installers"][0]["ReleaseDate"] == "2026-10-28" + assert "ReleaseDate" not in installer["Installers"][1] + assert winget_submit._release_date({"published_at": "2026-10-28T23:30:00-05:00"}) == "2026-10-29" diff --git a/actions/winget-submit/winget_submit.py b/actions/winget-submit/winget_submit.py index 13a6409..a29c91f 100644 --- a/actions/winget-submit/winget_submit.py +++ b/actions/winget-submit/winget_submit.py @@ -87,7 +87,7 @@ def package_exists(package_id: str, github_token: str, request_json=None) -> boo return True -def load_release_assets(repo: str, final_tag: str, github_token: str) -> tuple[str, dict[str, str]]: +def load_release_assets(repo: str, final_tag: str, github_token: str) -> tuple[str, dict[str, str], dict]: if repo.count("/") != 1: raise SubmitError(f"repo must be owner/name, got {repo!r}") release_url = f"https://api.github.com/repos/{repo}/releases/tags/{quote(final_tag, safe='')}" @@ -106,7 +106,7 @@ def load_release_assets(repo: str, final_tag: str, github_token: str) -> tuple[s for name, asset in by_name.items() if asset.get("browser_download_url") } - return checksums_text, download_urls + return checksums_text, download_urls, release def resolve_windows_assets( @@ -214,7 +214,7 @@ def upgrade_generated_manifests(directory: Path, package_id: str, version: str) return paths -def render_update_manifests(directory: Path, package_id: str, version: str, assets: WindowsAssets, token: str) -> list[Path]: +def render_update_manifests(directory: Path, package_id: str, version: str, assets: WindowsAssets, token: str, release: dict) -> list[Path]: """Copy latest published manifests and update only the release-specific fields.""" package_path = package_id_to_winget_path(package_id) base_url = f"https://api.github.com/repos/microsoft/winget-pkgs/contents/{quote(package_path, safe='/')}" @@ -261,16 +261,19 @@ def render_update_manifests(directory: Path, package_id: str, version: str, asse data["PackageVersion"] = version if kind in {"defaultLocale", "locale"}: data.pop("ReleaseNotes", None) - data.pop("ReleaseNotesUrl", None) + if "ReleaseNotesUrl" in data: + data["ReleaseNotesUrl"] = _release_notes_url(release) if kind == "installer": - data.pop("ReleaseDate", None) + if "ReleaseDate" in data: + data["ReleaseDate"] = _release_date(release) if "InstallerUrl" in data or "InstallerSha256" in data: raise SubmitError("root-level installer URL or hash requires manual update") installers = data.get("Installers") if not isinstance(installers, list) or len(installers) != 2 or sorted(i.get("Architecture", "") for i in installers if isinstance(i, dict)) != ["arm64", "x64"]: raise SubmitError("expected exactly one x64 and one arm64 installer; refusing automatic update") for installer in installers: - installer.pop("ReleaseDate", None) + if "ReleaseDate" in installer: + installer["ReleaseDate"] = _release_date(release) _update_installer_manifest(data, assets) data["ManifestVersion"] = TARGET_MANIFEST_VERSION dest = directory / name @@ -281,6 +284,26 @@ def render_update_manifests(directory: Path, package_id: str, version: str, asse return output +def _release_notes_url(release: dict) -> str: + url = release.get("html_url") + if not isinstance(url, str) or not url.startswith("https://github.com/"): + raise SubmitError("GitHub release has no valid public release URL") + return url + + +def _release_date(release: dict) -> str: + published_at = release.get("published_at") + try: + if not isinstance(published_at, str): + raise ValueError("missing timestamp") + published = datetime.datetime.fromisoformat(published_at.replace("Z", "+00:00")) + if published.tzinfo is None: + raise ValueError("timestamp has no timezone") + return published.astimezone(datetime.timezone.utc).date().isoformat() + except ValueError as exc: + raise SubmitError("GitHub release has no valid publication timestamp") from exc + + def validate_manifests(files: list[Path]) -> None: schemas = {} for path in files: @@ -396,7 +419,7 @@ def run_submit(args) -> int: "packages.winget.bootstrap is not true" ) - checksums_text, release_assets = load_release_assets(args.repo, args.final_tag, args.github_token) + checksums_text, release_assets, release = load_release_assets(args.repo, args.final_tag, args.github_token) assets = resolve_windows_assets( checksums_text, release_assets, @@ -407,7 +430,7 @@ def run_submit(args) -> int: root = Path(tmp) rendered_dir = root / "rendered" if mode == MODE_UPDATE: - files = render_update_manifests(rendered_dir, args.package_id, args.version, assets, args.github_token) + files = render_update_manifests(rendered_dir, args.package_id, args.version, assets, args.github_token, release) else: files = render_bootstrap_manifests(package_id=args.package_id, version=args.version, working_dir=args.working_dir, output_dir=rendered_dir, assets=assets) files = upgrade_generated_manifests(rendered_dir, args.package_id, args.version)