From ba3545bab31f81cd40c9c844bb7f9c9e3b3a1b84 Mon Sep 17 00:00:00 2001 From: Rian Stockbower Date: Tue, 11 Aug 2026 10:12:39 -0400 Subject: [PATCH] feat(winget): configure release asset markers --- actions/winget-submit/action.yml | 12 ++ actions/winget-submit/test_winget_submit.py | 129 +++++++++++++++++++- actions/winget-submit/winget_submit.py | 45 +++++-- 3 files changed, 177 insertions(+), 9 deletions(-) diff --git a/actions/winget-submit/action.yml b/actions/winget-submit/action.yml index ca4bd85..67d5430 100644 --- a/actions/winget-submit/action.yml +++ b/actions/winget-submit/action.yml @@ -21,6 +21,14 @@ inputs: description: "Allow first-time package submission if the winget package does not exist" required: false default: "false" + x64-marker: + description: "Substring used to identify the x64 release asset" + required: false + default: "windows_amd64.zip" + arm64-marker: + description: "Substring used to identify the arm64 release asset" + required: false + default: "windows_arm64.zip" github-token: description: "GitHub token for release asset download and winget-pkgs existence lookup" required: true @@ -54,6 +62,8 @@ runs: REPO: ${{ inputs.repo }} WORKING_DIRECTORY: ${{ inputs.working-directory }} BOOTSTRAP: ${{ inputs.bootstrap }} + X64_MARKER: ${{ inputs.x64-marker }} + ARM64_MARKER: ${{ inputs.arm64-marker }} WINGET_GITHUB_TOKEN: ${{ inputs.github-token }} WINGET_TOKEN: ${{ inputs.winget-token }} run: | @@ -65,5 +75,7 @@ runs: --repo "$REPO" \ --working-dir "$WORKING_DIRECTORY" \ --bootstrap "$BOOTSTRAP" \ + --x64-marker "$X64_MARKER" \ + --arm64-marker "$ARM64_MARKER" \ --github-token "$WINGET_GITHUB_TOKEN" \ --winget-token "$WINGET_TOKEN" diff --git a/actions/winget-submit/test_winget_submit.py b/actions/winget-submit/test_winget_submit.py index 1e3e951..dcf3d0b 100644 --- a/actions/winget-submit/test_winget_submit.py +++ b/actions/winget-submit/test_winget_submit.py @@ -79,6 +79,127 @@ def test_resolve_windows_assets_from_checksums_and_release_assets(): assert assets.arm64.sha256 == "def456" +def test_resolve_windows_assets_uses_configured_markers(): + checksums = """\ +abc123 Retune-0.2.0-windows-x64-setup.exe +def456 Retune-0.2.0-windows-arm64-setup.exe +""" + release_assets = { + "Retune-0.2.0-windows-x64-setup.exe": "https://example.test/x64.exe", + "Retune-0.2.0-windows-arm64-setup.exe": "https://example.test/arm64.exe", + } + + assets = winget_submit.resolve_windows_assets( + checksums, + release_assets, + x64_marker="windows-x64-setup.exe", + arm64_marker="windows-arm64-setup.exe", + ) + + assert assets.x64.name == "Retune-0.2.0-windows-x64-setup.exe" + assert assets.x64.url == "https://example.test/x64.exe" + assert assets.arm64.name == "Retune-0.2.0-windows-arm64-setup.exe" + assert assets.arm64.url == "https://example.test/arm64.exe" + + +def test_resolve_windows_assets_rejects_asset_matching_both_markers(): + with pytest.raises(winget_submit.SubmitError, match="both x64 and arm64"): + winget_submit.resolve_windows_assets( + "abc123 Retune-windows-x64-setup.exe\n", + {"Retune-windows-x64-setup.exe": "https://example.test/x64.exe"}, + x64_marker="Retune", + arm64_marker="windows", + ) + + +def test_resolve_windows_assets_rejects_duplicate_architecture_matches(): + with pytest.raises(winget_submit.SubmitError, match="multiple x64"): + winget_submit.resolve_windows_assets( + "abc123 first_windows_amd64.zip\n" + "def456 second_windows_amd64.zip\n" + "ghi789 only_windows_arm64.zip\n", + { + "first_windows_amd64.zip": "https://example.test/first-x64.zip", + "second_windows_amd64.zip": "https://example.test/second-x64.zip", + "only_windows_arm64.zip": "https://example.test/arm64.zip", + }, + ) + + +@pytest.mark.parametrize( + ("x64_marker", "arm64_marker", "message"), + [ + ("", "windows-arm64-setup.exe", "x64 asset marker"), + ("windows-x64-setup.exe", "", "arm64 asset marker"), + ("same", "same", "distinct"), + ], +) +def test_resolve_windows_assets_rejects_malformed_markers(x64_marker, arm64_marker, message): + with pytest.raises(winget_submit.SubmitError, match=message): + winget_submit.resolve_windows_assets( + "abc123 Retune-0.2.0-windows-x64-setup.exe\n" + "def456 Retune-0.2.0-windows-arm64-setup.exe\n", + { + "Retune-0.2.0-windows-x64-setup.exe": "https://example.test/x64.exe", + "Retune-0.2.0-windows-arm64-setup.exe": "https://example.test/arm64.exe", + }, + x64_marker=x64_marker, + arm64_marker=arm64_marker, + ) + + +def test_action_declares_defaults_and_forwards_asset_markers(): + action = yaml.safe_load(pathlib.Path(__file__).with_name("action.yml").read_text()) + inputs = action["inputs"] + submit_step = action["runs"]["steps"][-1] + + assert inputs["x64-marker"]["required"] is False + assert inputs["x64-marker"]["default"] == winget_submit.DEFAULT_X64_MARKER + assert inputs["arm64-marker"]["required"] is False + assert inputs["arm64-marker"]["default"] == winget_submit.DEFAULT_ARM64_MARKER + assert submit_step["env"]["X64_MARKER"] == "${{ inputs.x64-marker }}" + assert submit_step["env"]["ARM64_MARKER"] == "${{ inputs.arm64-marker }}" + assert '--x64-marker "$X64_MARKER"' in submit_step["run"] + assert '--arm64-marker "$ARM64_MARKER"' in submit_step["run"] + + +def test_cli_parses_configured_asset_markers(monkeypatch): + received = {} + + def capture_args(args): + received.update(vars(args)) + return 0 + + monkeypatch.setattr(winget_submit, "run_submit", capture_args) + + assert ( + winget_submit.main( + [ + "submit", + "--package-id", + "OpenCLICollective.Retune", + "--version", + "0.2.0", + "--final-tag", + "v0.2.0", + "--repo", + "open-cli-collective/Retune", + "--x64-marker", + "windows-x64-setup.exe", + "--arm64-marker", + "windows-arm64-setup.exe", + "--github-token", + "github-token", + "--winget-token", + "winget-token", + ] + ) + == 0 + ) + assert received["x64_marker"] == "windows-x64-setup.exe" + assert received["arm64_marker"] == "windows-arm64-setup.exe" + + def test_load_wingetcreate_asset_uses_github_release_digest(): def request_json(url, token): return { @@ -325,7 +446,7 @@ def load_release_assets(repo, final_tag, github_token): calls["load_release_assets"] = (repo, final_tag, github_token) return "checksums", {"asset": "url"} - def resolve_windows_assets(checksums_text, release_assets): + def resolve_windows_assets(checksums_text, release_assets, **_markers): calls["resolve_windows_assets"] = (checksums_text, release_assets) return assets @@ -378,7 +499,11 @@ def test_run_submit_missing_package_with_bootstrap_submits_rendered_directory(mo "load_release_assets", lambda repo, final_tag, github_token: ("checksums", {"asset": "url"}), ) - monkeypatch.setattr(winget_submit, "resolve_windows_assets", lambda checksums, release_assets: assets) + monkeypatch.setattr( + winget_submit, + "resolve_windows_assets", + lambda checksums, release_assets, **_markers: assets, + ) monkeypatch.setattr(winget_submit, "package_exists", lambda package_id, github_token: False) monkeypatch.setattr( winget_submit, diff --git a/actions/winget-submit/winget_submit.py b/actions/winget-submit/winget_submit.py index ce7aa69..69663be 100644 --- a/actions/winget-submit/winget_submit.py +++ b/actions/winget-submit/winget_submit.py @@ -26,6 +26,8 @@ WINGETCREATE_DOWNLOAD_TIMEOUT_SECONDS = 60 WINGETCREATE_COMMAND_TIMEOUT_SECONDS = 300 WINGET_MANIFEST_TYPES = {"version", "installer", "defaultLocale", "locale"} +DEFAULT_X64_MARKER = "windows_amd64.zip" +DEFAULT_ARM64_MARKER = "windows_arm64.zip" class SubmitError(Exception): @@ -127,19 +129,41 @@ def load_wingetcreate_asset(github_token: str, request_json=None) -> DownloadAss raise SubmitError("wingetcreate.exe asset not found in microsoft/winget-create latest release") -def resolve_windows_assets(checksums_text: str, release_assets: dict[str, str]) -> WindowsAssets: +def resolve_windows_assets( + checksums_text: str, + release_assets: dict[str, str], + x64_marker: str = DEFAULT_X64_MARKER, + arm64_marker: str = DEFAULT_ARM64_MARKER, +) -> WindowsAssets: + if not isinstance(x64_marker, str) or not x64_marker.strip(): + raise SubmitError("x64 asset marker is required") + if not isinstance(arm64_marker, str) or not arm64_marker.strip(): + raise SubmitError("arm64 asset marker is required") + x64_marker = x64_marker.strip() + arm64_marker = arm64_marker.strip() + if x64_marker == arm64_marker: + raise SubmitError("x64 and arm64 asset markers must be distinct") + found: dict[str, WindowsAsset] = {} for raw in checksums_text.splitlines(): parts = raw.strip().split() if len(parts) < 2: continue sha256, name = parts[0], parts[-1].lstrip("*") - if "windows_amd64.zip" in name: - arch = "x64" - elif "windows_arm64.zip" in name: - arch = "arm64" - else: + matches = [] + if x64_marker in name: + matches.append("x64") + if arm64_marker in name: + matches.append("arm64") + if not matches: continue + if len(matches) > 1: + raise SubmitError(f"release asset {name} matches both x64 and arm64 markers") + arch = matches[0] + if arch in found: + raise SubmitError( + f"multiple {arch} release assets matched markers: {found[arch].name}, {name}" + ) url = release_assets.get(name) if not url: raise SubmitError(f"release asset {name} listed in checksums.txt was not found") @@ -259,7 +283,12 @@ def run_submit(args) -> int: ) checksums_text, release_assets = load_release_assets(args.repo, args.final_tag, args.github_token) - assets = resolve_windows_assets(checksums_text, release_assets) + assets = resolve_windows_assets( + checksums_text, + release_assets, + x64_marker=getattr(args, "x64_marker", DEFAULT_X64_MARKER), + arm64_marker=getattr(args, "arm64_marker", DEFAULT_ARM64_MARKER), + ) wingetcreate_asset = load_wingetcreate_asset(args.github_token) wingetcreate = Path.cwd() / "wingetcreate.exe" download_file( @@ -420,6 +449,8 @@ def main(argv: list[str] | None = None) -> int: submit.add_argument("--repo", required=True) submit.add_argument("--working-dir", default=".") submit.add_argument("--bootstrap", default="false") + submit.add_argument("--x64-marker", default=DEFAULT_X64_MARKER) + submit.add_argument("--arm64-marker", default=DEFAULT_ARM64_MARKER) submit.add_argument("--github-token", required=True) submit.add_argument("--winget-token", required=True) args = parser.parse_args(argv)