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
33 changes: 10 additions & 23 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -349,29 +349,16 @@ jobs:
WINGET_ID: ${{ fromJSON(needs.goreleaser.outputs.identity-json).packages.winget.id }}
steps:
- uses: actions/checkout@v4
- name: Resolve windows asset URLs from the release
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: |
gh release download "$env:FINAL_TAG" --pattern "checksums.txt" --dir .
$checksums = Get-Content checksums.txt
$x64Name = ($checksums | Select-String "windows_amd64.zip").Line.Split()[-1]
$arm64Name = ($checksums | Select-String "windows_arm64.zip").Line.Split()[-1]
if (-not $x64Name) { Write-Error "no windows_amd64.zip entry in checksums.txt"; exit 1 }
if (-not $arm64Name) { Write-Error "no windows_arm64.zip entry in checksums.txt"; exit 1 }
$base = "https://github.com/$env:REPO/releases/download/$env:FINAL_TAG"
echo "X64_URL=$base/$x64Name" >> $env:GITHUB_ENV
echo "ARM64_URL=$base/$arm64Name" >> $env:GITHUB_ENV
- name: Submit to winget
shell: pwsh
env:
WINGET_TOKEN: ${{ secrets.winget-token }}
run: |
if (-not $env:WINGET_TOKEN) { Write-Error "winget-token not set"; exit 1 }
Invoke-WebRequest https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe
./wingetcreate.exe update $env:WINGET_ID --version $env:VERSION --urls $env:X64_URL $env:ARM64_URL --submit --token $env:WINGET_TOKEN
- uses: open-cli-collective/.github/actions/winget-submit@v1
Comment thread
monit-reviewer marked this conversation as resolved.
with:
package-id: ${{ fromJSON(needs.goreleaser.outputs.identity-json).packages.winget.id }}
version: ${{ needs.goreleaser.outputs.version }}
final-tag: ${{ needs.goreleaser.outputs.final-tag }}
repo: ${{ github.repository }}
working-directory: ${{ inputs.working-directory }}
bootstrap: ${{ fromJSON(needs.goreleaser.outputs.identity-json).packages.winget.bootstrap }}
github-token: ${{ github.token }}
winget-token: ${{ secrets.winget-token }}
# Surface ANY failure (missing token, asset resolution, wingetcreate) — a
# continue-on-error job would otherwise go green with no trace.
- name: Note winget failure
Expand Down
15 changes: 15 additions & 0 deletions .github/workflows/test-actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,21 @@ jobs:
shell: bash
working-directory: actions/release-preflight

# winget-submit helper logic: package existence decision, asset resolution, and
# first-submission manifest rendering.
winget-submit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install "PyYAML==6.0.2" "pytest>=8,<9"
shell: bash
- run: python -m pytest -q
shell: bash
working-directory: actions/winget-submit

# identity.py unit tests — PASS + every drift rule + missing-manifest, in tmp dirs.
identity-unit:
runs-on: ubuntu-latest
Expand Down
19 changes: 18 additions & 1 deletion actions/identity-check/identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from xml.etree.ElementTree import ParseError

SCHEMA = "open-cli-identity/v1"
WINGET_BOOTSTRAP_TYPE_ERROR = "packages.winget.bootstrap must be a boolean"


class ManifestError(Exception):
Expand Down Expand Up @@ -63,6 +64,7 @@ def normalize(m: dict) -> dict:
"""The stable shape #7/#8 consume. Defaults fill what the workflows need."""
pkgs = m.get("packages", {}) or {}
hb = pkgs.get("homebrew", {}) or {}
winget = pkgs.get("winget", {}) or {}
tag = m.get("tag", {}) or {}
return {
"binary": m["binary"],
Expand All @@ -76,7 +78,7 @@ def normalize(m: dict) -> dict:
"canonical_cask": hb.get("canonical_cask"),
"alias_casks": hb.get("alias_casks", []) or [],
},
"winget": {"id": (pkgs.get("winget", {}) or {}).get("id")},
"winget": {"id": winget.get("id"), "bootstrap": _winget_bootstrap(winget)},
"chocolatey": {"id": (pkgs.get("chocolatey", {}) or {}).get("id")},
"linux": {"package_name": (pkgs.get("linux", {}) or {}).get("package_name")},
"snap": {"state": (pkgs.get("snap", {}) or {}).get("state")},
Expand All @@ -85,6 +87,15 @@ def normalize(m: dict) -> dict:
}
Comment thread
monit-reviewer marked this conversation as resolved.


def _winget_bootstrap(winget: dict) -> bool:
if "bootstrap" not in winget:
return False
value = winget.get("bootstrap")
if not isinstance(value, bool):
raise ManifestError(WINGET_BOOTSTRAP_TYPE_ERROR)
return value


def _validate_keychain_probe(m: dict) -> list[str]:
errors: list[str] = []
probe = m.get("keychain_probe")
Expand Down Expand Up @@ -190,6 +201,12 @@ def validate(manifest_path: str, working_dir: str, repo_root: str = ".") -> list

errors.extend(_validate_keychain_probe(m))

winget_cfg = pkgs.get("winget", {}) or {}
try:
_winget_bootstrap(winget_cfg)
except ManifestError as exc:
errors.append(str(exc))

# --- linux nfpm + homebrew cask (declared-channel; both read .goreleaser) ---
# alias_casks are intentionally NOT checked here: they live only in the
# manifest and are generated by the #8 alias post-step, so there is no
Expand Down
19 changes: 19 additions & 0 deletions actions/identity-check/test_identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,10 +262,29 @@ def test_export_json_shape(tmp_path):
assert norm["tag"]["prefix"] == "v"
assert norm["archives"]["name_template"].startswith("slck_v")
assert norm["packages"]["homebrew"]["alias_casks"] == ["slack-chat-cli"]
assert norm["packages"]["winget"] == {
"id": "OpenCLICollective.slack-chat-cli",
"bootstrap": False,
}
assert norm["packages"]["linux"]["package_name"] == "slck"
assert norm["version_file"] == "version.txt"


def test_export_json_winget_bootstrap_true(tmp_path):
m = copy.deepcopy(BASE_MANIFEST)
m["packages"]["winget"]["bootstrap"] = True
wd = build(tmp_path, manifest=m)
norm = identity.normalize(identity.load_manifest(manifest_path(wd)))
assert norm["packages"]["winget"]["bootstrap"] is True


def test_winget_bootstrap_must_be_boolean(tmp_path):
m = copy.deepcopy(BASE_MANIFEST)
m["packages"]["winget"]["bootstrap"] = "true"
wd = build(tmp_path, manifest=m)
assert any("packages.winget.bootstrap must be a boolean" in e for e in identity.validate(manifest_path(wd), wd, wd))


# --- monorepo: tool-local identity + packaging under tools/<tool>, but the
# goreleaser config lives at the repo root and resolves via --repo-root, not
# --working-dir (distribution.md §8.3). Models atlassian-cli's cfl tool. ---
Expand Down
69 changes: 69 additions & 0 deletions actions/winget-submit/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Winget submit
Comment thread
monit-reviewer marked this conversation as resolved.
description: Submit an existing winget package update or bootstrap a first-time package submission on a Windows runner.
inputs:
package-id:
description: "Winget PackageIdentifier, for example OpenCLICollective.codereview-cli"
required: true
version:
description: "PackageVersion to publish"
required: true
final-tag:
description: "GitHub release tag containing the published assets"
required: true
repo:
description: "GitHub repository containing the release assets, owner/name"
required: true
working-directory:
description: "Tool root containing packaging/winget templates"
required: false
default: "."
bootstrap:
description: "Allow first-time package submission if the winget package does not exist"
required: false
default: "false"
github-token:
description: "GitHub token for release asset download and winget-pkgs existence lookup"
required: true
winget-token:
description: "GitHub token used by wingetcreate to submit to microsoft/winget-pkgs"
required: true
runs:
using: composite
steps:
- name: Assert Windows runner
shell: bash
env:
RUNNER_OS_NAME: ${{ runner.os }}
run: |
set -euo pipefail
if [ "$RUNNER_OS_NAME" != "Windows" ]; then
echo "::error::winget-submit requires a Windows runner because wingetcreate.exe is Windows-only"
exit 1
fi
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- shell: bash
run: pip install "PyYAML==6.0.2"
- shell: bash
env:
ACTION_PATH: ${{ github.action_path }}
PACKAGE_ID: ${{ inputs.package-id }}
VERSION: ${{ inputs.version }}
FINAL_TAG: ${{ inputs.final-tag }}
REPO: ${{ inputs.repo }}
WORKING_DIRECTORY: ${{ inputs.working-directory }}
BOOTSTRAP: ${{ inputs.bootstrap }}
WINGET_GITHUB_TOKEN: ${{ inputs.github-token }}
WINGET_TOKEN: ${{ inputs.winget-token }}
run: |
set -euo pipefail
python "$ACTION_PATH/winget_submit.py" submit \
Comment thread
monit-reviewer marked this conversation as resolved.
--package-id "$PACKAGE_ID" \
--version "$VERSION" \
--final-tag "$FINAL_TAG" \
--repo "$REPO" \
--working-dir "$WORKING_DIRECTORY" \
--bootstrap "$BOOTSTRAP" \
--github-token "$WINGET_GITHUB_TOKEN" \
--winget-token "$WINGET_TOKEN"
Loading
Loading