From 73ce20afb4003ac05ae82c8988dcbb1ea5d59c75 Mon Sep 17 00:00:00 2001 From: Rian Stockbower Date: Thu, 4 Jun 2026 09:58:44 -0400 Subject: [PATCH 1/3] fix(winget): preserve bootstrap schema headers Closes #29 --- actions/winget-submit/test_winget_submit.py | 73 ++++++++++++++++++++- actions/winget-submit/winget_submit.py | 22 ++++++- 2 files changed, 91 insertions(+), 4 deletions(-) diff --git a/actions/winget-submit/test_winget_submit.py b/actions/winget-submit/test_winget_submit.py index a76ee90..f6a3599 100644 --- a/actions/winget-submit/test_winget_submit.py +++ b/actions/winget-submit/test_winget_submit.py @@ -218,11 +218,26 @@ def test_render_bootstrap_manifests_updates_values_without_mutating_source(tmp_p f"{package_id}.installer.yaml", } assert all(pathlib.Path(path).is_relative_to(rendered) for path in output_paths) - rendered_version = yaml.safe_load((rendered / f"{package_id}.yaml").read_text()) - rendered_locale = yaml.safe_load((rendered / f"{package_id}.locale.en-US.yaml").read_text()) + rendered_version_text = (rendered / f"{package_id}.yaml").read_text() + rendered_locale_text = (rendered / f"{package_id}.locale.en-US.yaml").read_text() + rendered_installer_text = (rendered / f"{package_id}.installer.yaml").read_text() + assert ( + rendered_version_text.splitlines()[0] + == "# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.10.0.schema.json" + ) + assert ( + rendered_locale_text.splitlines()[0] + == "# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.10.0.schema.json" + ) + assert ( + rendered_installer_text.splitlines()[0] + == "# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.10.0.schema.json" + ) + rendered_version = yaml.safe_load(rendered_version_text) + rendered_locale = yaml.safe_load(rendered_locale_text) assert rendered_version["PackageVersion"] == "1.2.3" assert rendered_locale["PackageVersion"] == "1.2.3" - rendered_installer = yaml.safe_load((rendered / f"{package_id}.installer.yaml").read_text()) + rendered_installer = yaml.safe_load(rendered_installer_text) assert rendered_installer["PackageVersion"] == "1.2.3" installers = {item["Architecture"]: item for item in rendered_installer["Installers"]} assert installers["x64"]["InstallerUrl"] == assets.x64.url @@ -231,6 +246,58 @@ def test_render_bootstrap_manifests_updates_values_without_mutating_source(tmp_p assert installers["arm64"]["InstallerSha256"] == "b" * 64 +def test_render_bootstrap_manifests_requires_schema_metadata(tmp_path): + package_id = "OpenCLICollective.codereview-cli" + source = tmp_path / "tool" + winget_dir = source / "packaging" / "winget" + winget_dir.mkdir(parents=True) + _write_manifest( + winget_dir / f"{package_id}.yaml", + { + "PackageIdentifier": package_id, + "PackageVersion": "0.0.0", + "DefaultLocale": "en-US", + "ManifestType": "version", + "ManifestVersion": "1.10.0", + }, + ) + _write_manifest( + winget_dir / f"{package_id}.locale.en-US.yaml", + { + "PackageIdentifier": package_id, + "PackageVersion": "0.0.0", + "PackageLocale": "en-US", + "Publisher": "Open CLI Collective", + "PackageName": "Code Review CLI", + "ShortDescription": "Automated pull-request review CLI", + "ManifestType": "defaultLocale", + "ManifestVersion": "1.10.0", + }, + ) + _write_manifest( + winget_dir / f"{package_id}.installer.yaml", + { + "PackageIdentifier": package_id, + "PackageVersion": "0.0.0", + "InstallerType": "zip", + "Installers": [ + {"Architecture": "x64", "InstallerUrl": "old-x64", "InstallerSha256": "old-x64-sha"}, + {"Architecture": "arm64", "InstallerUrl": "old-arm64", "InstallerSha256": "old-arm64-sha"}, + ], + "ManifestType": "installer", + }, + ) + + with pytest.raises(winget_submit.SubmitError, match="ManifestVersion"): + winget_submit.render_bootstrap_manifests( + package_id=package_id, + version="1.2.3", + working_dir=source, + output_dir=tmp_path / "rendered", + assets=_assets(), + ) + + def test_render_bootstrap_manifests_rejects_output_inside_source(tmp_path): source = tmp_path / "tool" source.mkdir() diff --git a/actions/winget-submit/winget_submit.py b/actions/winget-submit/winget_submit.py index ee145ba..61407a2 100644 --- a/actions/winget-submit/winget_submit.py +++ b/actions/winget-submit/winget_submit.py @@ -182,7 +182,7 @@ def render_bootstrap_manifests( if src == installer_manifest: _update_installer_manifest(data, assets) dest = output / src.name - dest.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8") + dest.write_text(_dump_manifest(data, src), encoding="utf-8") rendered.append(dest) return rendered @@ -330,6 +330,26 @@ def _update_installer_manifest(data: dict, assets: WindowsAssets) -> None: raise SubmitError(f"installer manifest missing architectures: {', '.join(missing)}") +def _dump_manifest(data: dict, path: Path) -> str: + manifest_type = _required_manifest_string(data, "ManifestType", path) + manifest_version = _required_manifest_string(data, "ManifestVersion", path) + schema = ( + "https://aka.ms/" + f"winget-manifest.{manifest_type}.{manifest_version}.schema.json" + ) + return ( + f"# yaml-language-server: $schema={schema}\n\n" + f"{yaml.safe_dump(data, sort_keys=False)}" + ) + + +def _required_manifest_string(data: dict, key: str, path: Path) -> str: + value = data.get(key) + if not isinstance(value, str) or not value.strip(): + raise SubmitError(f"{path}: {key} is required for bootstrap rendering") + return value + + def _github_request(url: str, token: str, accept: str) -> Request: headers = { "Accept": accept, From 2c52cd61d7630f8e4edaf95b3e9be371a917188c Mon Sep 17 00:00:00 2001 From: Rian Stockbower Date: Thu, 4 Jun 2026 10:03:20 -0400 Subject: [PATCH 2/3] test(winget): tighten schema render coverage --- actions/winget-submit/test_winget_submit.py | 47 +++++++++++---------- actions/winget-submit/winget_submit.py | 9 ++-- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/actions/winget-submit/test_winget_submit.py b/actions/winget-submit/test_winget_submit.py index f6a3599..4347146 100644 --- a/actions/winget-submit/test_winget_submit.py +++ b/actions/winget-submit/test_winget_submit.py @@ -149,7 +149,7 @@ def test_render_bootstrap_manifests_updates_values_without_mutating_source(tmp_p "PackageVersion": "0.0.0", "DefaultLocale": "en-US", "ManifestType": "version", - "ManifestVersion": "1.10.0", + "ManifestVersion": "1.9.0", }, ) _write_manifest( @@ -162,7 +162,7 @@ def test_render_bootstrap_manifests_updates_values_without_mutating_source(tmp_p "PackageName": "Code Review CLI", "ShortDescription": "Automated pull-request review CLI", "ManifestType": "defaultLocale", - "ManifestVersion": "1.10.0", + "ManifestVersion": "1.9.0", }, ) installer_path = winget_dir / f"{package_id}.installer.yaml" @@ -185,7 +185,7 @@ def test_render_bootstrap_manifests_updates_values_without_mutating_source(tmp_p }, ], "ManifestType": "installer", - "ManifestVersion": "1.10.0", + "ManifestVersion": "1.9.0", }, ) original_installer = installer_path.read_text() @@ -223,15 +223,15 @@ def test_render_bootstrap_manifests_updates_values_without_mutating_source(tmp_p rendered_installer_text = (rendered / f"{package_id}.installer.yaml").read_text() assert ( rendered_version_text.splitlines()[0] - == "# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.10.0.schema.json" + == "# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.9.0.schema.json" ) assert ( rendered_locale_text.splitlines()[0] - == "# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.10.0.schema.json" + == "# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.9.0.schema.json" ) assert ( rendered_installer_text.splitlines()[0] - == "# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.10.0.schema.json" + == "# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.9.0.schema.json" ) rendered_version = yaml.safe_load(rendered_version_text) rendered_locale = yaml.safe_load(rendered_locale_text) @@ -246,7 +246,8 @@ def test_render_bootstrap_manifests_updates_values_without_mutating_source(tmp_p assert installers["arm64"]["InstallerSha256"] == "b" * 64 -def test_render_bootstrap_manifests_requires_schema_metadata(tmp_path): +@pytest.mark.parametrize("missing_key", ["ManifestType", "ManifestVersion"]) +def test_render_bootstrap_manifests_requires_schema_metadata(tmp_path, missing_key): package_id = "OpenCLICollective.codereview-cli" source = tmp_path / "tool" winget_dir = source / "packaging" / "winget" @@ -274,28 +275,30 @@ def test_render_bootstrap_manifests_requires_schema_metadata(tmp_path): "ManifestVersion": "1.10.0", }, ) - _write_manifest( - winget_dir / f"{package_id}.installer.yaml", - { - "PackageIdentifier": package_id, - "PackageVersion": "0.0.0", - "InstallerType": "zip", - "Installers": [ - {"Architecture": "x64", "InstallerUrl": "old-x64", "InstallerSha256": "old-x64-sha"}, - {"Architecture": "arm64", "InstallerUrl": "old-arm64", "InstallerSha256": "old-arm64-sha"}, - ], - "ManifestType": "installer", - }, - ) + installer = { + "PackageIdentifier": package_id, + "PackageVersion": "0.0.0", + "InstallerType": "zip", + "Installers": [ + {"Architecture": "x64", "InstallerUrl": "old-x64", "InstallerSha256": "old-x64-sha"}, + {"Architecture": "arm64", "InstallerUrl": "old-arm64", "InstallerSha256": "old-arm64-sha"}, + ], + "ManifestType": "installer", + "ManifestVersion": "1.10.0", + } + installer.pop(missing_key) + _write_manifest(winget_dir / f"{package_id}.installer.yaml", installer) - with pytest.raises(winget_submit.SubmitError, match="ManifestVersion"): + rendered = tmp_path / "rendered" + with pytest.raises(winget_submit.SubmitError, match=missing_key): winget_submit.render_bootstrap_manifests( package_id=package_id, version="1.2.3", working_dir=source, - output_dir=tmp_path / "rendered", + output_dir=rendered, assets=_assets(), ) + assert not any(rendered.iterdir()) def test_render_bootstrap_manifests_rejects_output_inside_source(tmp_path): diff --git a/actions/winget-submit/winget_submit.py b/actions/winget-submit/winget_submit.py index 61407a2..d164dac 100644 --- a/actions/winget-submit/winget_submit.py +++ b/actions/winget-submit/winget_submit.py @@ -175,16 +175,17 @@ def render_bootstrap_manifests( raise SubmitError(f"winget bootstrap template missing: {', '.join(missing)}") output.mkdir(parents=True, exist_ok=True) - rendered: list[Path] = [] + rendered: list[tuple[Path, str]] = [] for src in [version_manifest, *locale_manifests, installer_manifest]: data = _load_manifest(src, package_id) data["PackageVersion"] = version if src == installer_manifest: _update_installer_manifest(data, assets) dest = output / src.name - dest.write_text(_dump_manifest(data, src), encoding="utf-8") - rendered.append(dest) - return rendered + rendered.append((dest, _dump_manifest(data, src))) + for dest, text in rendered: + dest.write_text(text, encoding="utf-8") + return [dest for dest, _ in rendered] def build_update_command(wingetcreate: Path, package_id: str, version: str, assets: WindowsAssets, token: str) -> list[str]: From cb175d8b1507bba424cb5a8dd5235038868b4a10 Mon Sep 17 00:00:00 2001 From: Rian Stockbower Date: Thu, 4 Jun 2026 10:13:11 -0400 Subject: [PATCH 3/3] fix(winget): make bootstrap rendering atomic --- actions/winget-submit/test_winget_submit.py | 135 +++++++++++++------- actions/winget-submit/winget_submit.py | 12 +- 2 files changed, 100 insertions(+), 47 deletions(-) diff --git a/actions/winget-submit/test_winget_submit.py b/actions/winget-submit/test_winget_submit.py index 4347146..1e3e951 100644 --- a/actions/winget-submit/test_winget_submit.py +++ b/actions/winget-submit/test_winget_submit.py @@ -148,8 +148,8 @@ def test_render_bootstrap_manifests_updates_values_without_mutating_source(tmp_p "PackageIdentifier": package_id, "PackageVersion": "0.0.0", "DefaultLocale": "en-US", - "ManifestType": "version", - "ManifestVersion": "1.9.0", + "ManifestType": " version ", + "ManifestVersion": " 1.9.0 ", }, ) _write_manifest( @@ -161,8 +161,8 @@ def test_render_bootstrap_manifests_updates_values_without_mutating_source(tmp_p "Publisher": "Open CLI Collective", "PackageName": "Code Review CLI", "ShortDescription": "Automated pull-request review CLI", - "ManifestType": "defaultLocale", - "ManifestVersion": "1.9.0", + "ManifestType": " defaultLocale ", + "ManifestVersion": " 1.9.0 ", }, ) installer_path = winget_dir / f"{package_id}.installer.yaml" @@ -184,8 +184,8 @@ def test_render_bootstrap_manifests_updates_values_without_mutating_source(tmp_p "InstallerSha256": "CHECKSUM_ARM64_PLACEHOLDER", }, ], - "ManifestType": "installer", - "ManifestVersion": "1.9.0", + "ManifestType": " installer ", + "ManifestVersion": " 1.9.0 ", }, ) original_installer = installer_path.read_text() @@ -236,9 +236,15 @@ def test_render_bootstrap_manifests_updates_values_without_mutating_source(tmp_p rendered_version = yaml.safe_load(rendered_version_text) rendered_locale = yaml.safe_load(rendered_locale_text) assert rendered_version["PackageVersion"] == "1.2.3" + assert rendered_version["ManifestType"] == "version" + assert rendered_version["ManifestVersion"] == "1.9.0" assert rendered_locale["PackageVersion"] == "1.2.3" + assert rendered_locale["ManifestType"] == "defaultLocale" + assert rendered_locale["ManifestVersion"] == "1.9.0" rendered_installer = yaml.safe_load(rendered_installer_text) assert rendered_installer["PackageVersion"] == "1.2.3" + assert rendered_installer["ManifestType"] == "installer" + assert rendered_installer["ManifestVersion"] == "1.9.0" installers = {item["Architecture"]: item for item in rendered_installer["Installers"]} assert installers["x64"]["InstallerUrl"] == assets.x64.url assert installers["x64"]["InstallerSha256"] == "a" * 64 @@ -246,48 +252,17 @@ def test_render_bootstrap_manifests_updates_values_without_mutating_source(tmp_p assert installers["arm64"]["InstallerSha256"] == "b" * 64 +@pytest.mark.parametrize("manifest_name", ["version", "locale", "installer"]) @pytest.mark.parametrize("missing_key", ["ManifestType", "ManifestVersion"]) -def test_render_bootstrap_manifests_requires_schema_metadata(tmp_path, missing_key): +def test_render_bootstrap_manifests_requires_schema_metadata(tmp_path, manifest_name, missing_key): package_id = "OpenCLICollective.codereview-cli" source = tmp_path / "tool" winget_dir = source / "packaging" / "winget" winget_dir.mkdir(parents=True) - _write_manifest( - winget_dir / f"{package_id}.yaml", - { - "PackageIdentifier": package_id, - "PackageVersion": "0.0.0", - "DefaultLocale": "en-US", - "ManifestType": "version", - "ManifestVersion": "1.10.0", - }, - ) - _write_manifest( - winget_dir / f"{package_id}.locale.en-US.yaml", - { - "PackageIdentifier": package_id, - "PackageVersion": "0.0.0", - "PackageLocale": "en-US", - "Publisher": "Open CLI Collective", - "PackageName": "Code Review CLI", - "ShortDescription": "Automated pull-request review CLI", - "ManifestType": "defaultLocale", - "ManifestVersion": "1.10.0", - }, - ) - installer = { - "PackageIdentifier": package_id, - "PackageVersion": "0.0.0", - "InstallerType": "zip", - "Installers": [ - {"Architecture": "x64", "InstallerUrl": "old-x64", "InstallerSha256": "old-x64-sha"}, - {"Architecture": "arm64", "InstallerUrl": "old-arm64", "InstallerSha256": "old-arm64-sha"}, - ], - "ManifestType": "installer", - "ManifestVersion": "1.10.0", - } - installer.pop(missing_key) - _write_manifest(winget_dir / f"{package_id}.installer.yaml", installer) + manifests = _bootstrap_manifest_templates(winget_dir, package_id) + manifests[manifest_name][1].pop(missing_key) + for path, data in manifests.values(): + _write_manifest(path, data) rendered = tmp_path / "rendered" with pytest.raises(winget_submit.SubmitError, match=missing_key): @@ -298,7 +273,29 @@ def test_render_bootstrap_manifests_requires_schema_metadata(tmp_path, missing_k output_dir=rendered, assets=_assets(), ) - assert not any(rendered.iterdir()) + assert not rendered.exists() + + +def test_render_bootstrap_manifests_rejects_unknown_manifest_type(tmp_path): + package_id = "OpenCLICollective.codereview-cli" + source = tmp_path / "tool" + winget_dir = source / "packaging" / "winget" + winget_dir.mkdir(parents=True) + manifests = _bootstrap_manifest_templates(winget_dir, package_id) + manifests["version"][1]["ManifestType"] = "versions" + for path, data in manifests.values(): + _write_manifest(path, data) + + rendered = tmp_path / "rendered" + with pytest.raises(winget_submit.SubmitError, match="ManifestType"): + winget_submit.render_bootstrap_manifests( + package_id=package_id, + version="1.2.3", + working_dir=source, + output_dir=rendered, + assets=_assets(), + ) + assert not rendered.exists() def test_render_bootstrap_manifests_rejects_output_inside_source(tmp_path): @@ -479,6 +476,56 @@ def _write_manifest(path, data): path.write_text(yaml.safe_dump(data, sort_keys=False)) +def _bootstrap_manifest_templates(winget_dir, package_id): + return { + "version": ( + winget_dir / f"{package_id}.yaml", + { + "PackageIdentifier": package_id, + "PackageVersion": "0.0.0", + "DefaultLocale": "en-US", + "ManifestType": "version", + "ManifestVersion": "1.10.0", + }, + ), + "locale": ( + winget_dir / f"{package_id}.locale.en-US.yaml", + { + "PackageIdentifier": package_id, + "PackageVersion": "0.0.0", + "PackageLocale": "en-US", + "Publisher": "Open CLI Collective", + "PackageName": "Code Review CLI", + "ShortDescription": "Automated pull-request review CLI", + "ManifestType": "defaultLocale", + "ManifestVersion": "1.10.0", + }, + ), + "installer": ( + winget_dir / f"{package_id}.installer.yaml", + { + "PackageIdentifier": package_id, + "PackageVersion": "0.0.0", + "InstallerType": "zip", + "Installers": [ + { + "Architecture": "x64", + "InstallerUrl": "old-x64", + "InstallerSha256": "old-x64-sha", + }, + { + "Architecture": "arm64", + "InstallerUrl": "old-arm64", + "InstallerSha256": "old-arm64-sha", + }, + ], + "ManifestType": "installer", + "ManifestVersion": "1.10.0", + }, + ), + } + + def _assets(): return winget_submit.WindowsAssets( x64=winget_submit.WindowsAsset("x64.zip", "https://example.test/x64.zip", "a"), diff --git a/actions/winget-submit/winget_submit.py b/actions/winget-submit/winget_submit.py index d164dac..ce7aa69 100644 --- a/actions/winget-submit/winget_submit.py +++ b/actions/winget-submit/winget_submit.py @@ -25,6 +25,7 @@ HTTP_TIMEOUT_SECONDS = 30 WINGETCREATE_DOWNLOAD_TIMEOUT_SECONDS = 60 WINGETCREATE_COMMAND_TIMEOUT_SECONDS = 300 +WINGET_MANIFEST_TYPES = {"version", "installer", "defaultLocale", "locale"} class SubmitError(Exception): @@ -174,7 +175,6 @@ def render_bootstrap_manifests( if missing: raise SubmitError(f"winget bootstrap template missing: {', '.join(missing)}") - output.mkdir(parents=True, exist_ok=True) rendered: list[tuple[Path, str]] = [] for src in [version_manifest, *locale_manifests, installer_manifest]: data = _load_manifest(src, package_id) @@ -183,6 +183,7 @@ def render_bootstrap_manifests( _update_installer_manifest(data, assets) dest = output / src.name rendered.append((dest, _dump_manifest(data, src))) + output.mkdir(parents=True, exist_ok=True) for dest, text in rendered: dest.write_text(text, encoding="utf-8") return [dest for dest, _ in rendered] @@ -334,13 +335,18 @@ def _update_installer_manifest(data: dict, assets: WindowsAssets) -> None: def _dump_manifest(data: dict, path: Path) -> str: manifest_type = _required_manifest_string(data, "ManifestType", path) manifest_version = _required_manifest_string(data, "ManifestVersion", path) + if manifest_type not in WINGET_MANIFEST_TYPES: + raise SubmitError(f"{path}: ManifestType must be one of {sorted(WINGET_MANIFEST_TYPES)}") + rendered = dict(data) + rendered["ManifestType"] = manifest_type + rendered["ManifestVersion"] = manifest_version schema = ( "https://aka.ms/" f"winget-manifest.{manifest_type}.{manifest_version}.schema.json" ) return ( f"# yaml-language-server: $schema={schema}\n\n" - f"{yaml.safe_dump(data, sort_keys=False)}" + f"{yaml.safe_dump(rendered, sort_keys=False)}" ) @@ -348,7 +354,7 @@ def _required_manifest_string(data: dict, key: str, path: Path) -> str: value = data.get(key) if not isinstance(value, str) or not value.strip(): raise SubmitError(f"{path}: {key} is required for bootstrap rendering") - return value + return value.strip() def _github_request(url: str, token: str, accept: str) -> Request: