Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions actions/winget-submit/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: |
Expand All @@ -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"
129 changes: 127 additions & 2 deletions actions/winget-submit/test_winget_submit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down
45 changes: 38 additions & 7 deletions actions/winget-submit/winget_submit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)
Expand Down
Loading