From fc67a18b39d08476bbc951df68a70692489a29e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pascal=20Andr=C3=A9?= Date: Sun, 31 May 2026 13:09:55 +0200 Subject: [PATCH] chore: TASK-075 automate winget release updates Add a release-published GitHub Actions workflow that waits for the Windows Tauri asset, validates the configured winget-pkgs fork context, and submits the package update through the Komac-backed winget-releaser action. This keeps Winget manifest maintenance aligned with published CodeNomad releases without requiring a persistent local winget-pkgs clone. Include a small helper script to poll the GitHub Release API, resolve the exact Windows ZIP asset, and compute the SHA-256 used for validation and troubleshooting. Document the required PAT and repository variables so maintainers can configure and operate the automation safely. --- .github/workflows/update-winget.yml | 85 ++++++++++ docs/guides/winget-release-automation.md | 40 +++++ scripts/winget/resolve-release-asset.cjs | 201 +++++++++++++++++++++++ 3 files changed, 326 insertions(+) create mode 100644 .github/workflows/update-winget.yml create mode 100644 docs/guides/winget-release-automation.md create mode 100644 scripts/winget/resolve-release-asset.cjs diff --git a/.github/workflows/update-winget.yml b/.github/workflows/update-winget.yml new file mode 100644 index 000000000..41b894f67 --- /dev/null +++ b/.github/workflows/update-winget.yml @@ -0,0 +1,85 @@ +name: Update Winget + +on: + release: + types: + - published + +permissions: + contents: read + +jobs: + update-winget: + name: Submit Winget manifest update + if: ${{ !github.event.release.draft && !github.event.release.prerelease }} + runs-on: ubuntu-latest + env: + WINGET_PACKAGE_IDENTIFIER: ${{ vars.WINGET_PACKAGE_IDENTIFIER || 'NeuralNomadsAI.CodeNomad' }} + WINGET_FORK_OWNER: ${{ vars.WINGET_FORK_OWNER || 'pascalandr' }} + WINGET_WINDOWS_ASSET_NAME_TEMPLATE: ${{ vars.WINGET_WINDOWS_ASSET_NAME_TEMPLATE || 'CodeNomad-Tauri-windows-x64-{version}.zip' }} + WINGET_ASSET_WAIT_TIMEOUT_SECONDS: ${{ vars.WINGET_ASSET_WAIT_TIMEOUT_SECONDS || '900' }} + WINGET_ASSET_POLL_INTERVAL_SECONDS: ${{ vars.WINGET_ASSET_POLL_INTERVAL_SECONDS || '15' }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Wait for Windows Tauri release asset + id: release_asset + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + args=( + --repo "${{ github.repository }}" + --release-id "${{ github.event.release.id }}" + --tag "${{ github.event.release.tag_name }}" + --asset-name-template "$WINGET_WINDOWS_ASSET_NAME_TEMPLATE" + --timeout-seconds "$WINGET_ASSET_WAIT_TIMEOUT_SECONDS" + --poll-interval-seconds "$WINGET_ASSET_POLL_INTERVAL_SECONDS" + --github-output "$GITHUB_OUTPUT" + ) + + node scripts/winget/resolve-release-asset.cjs "${args[@]}" + + - name: Log resolved Winget asset metadata + run: | + echo "Resolved asset: ${{ steps.release_asset.outputs.asset_name }}" + echo "Resolved version: ${{ steps.release_asset.outputs.version }}" + echo "Resolved SHA-256: ${{ steps.release_asset.outputs.asset_sha256 }}" + + - name: Validate fork configuration + env: + GH_TOKEN: ${{ secrets.WINGET_GITHUB_TOKEN }} + EXPECTED_OWNER: ${{ env.WINGET_FORK_OWNER }} + run: | + set -euo pipefail + token_owner="$(gh api user --jq '.login')" + fork_name="$(gh api "repos/$EXPECTED_OWNER/winget-pkgs" --jq '.full_name')" + parent_name="$(gh api "repos/$EXPECTED_OWNER/winget-pkgs" --jq '.parent.full_name')" + is_fork="$(gh api "repos/$EXPECTED_OWNER/winget-pkgs" --jq '.fork')" + + if [ "$token_owner" != "$EXPECTED_OWNER" ]; then + echo "WINGET_GITHUB_TOKEN belongs to '$token_owner' but WINGET_FORK_OWNER is '$EXPECTED_OWNER'" >&2 + exit 1 + fi + + if [ "$is_fork" != "true" ] || [ "$parent_name" != "microsoft/winget-pkgs" ]; then + echo "Configured fork must be $EXPECTED_OWNER/winget-pkgs forked from microsoft/winget-pkgs" >&2 + exit 1 + fi + + echo "Validated fork: $fork_name" + + - name: Submit update to Winget + uses: vedantmgoyal9/winget-releaser@v2 + with: + identifier: ${{ env.WINGET_PACKAGE_IDENTIFIER }} + version: ${{ steps.release_asset.outputs.version }} + release-tag: ${{ github.event.release.tag_name }} + installers-regex: ${{ steps.release_asset.outputs.asset_regex }} + fork-user: ${{ env.WINGET_FORK_OWNER }} + token: ${{ secrets.WINGET_GITHUB_TOKEN }} diff --git a/docs/guides/winget-release-automation.md b/docs/guides/winget-release-automation.md new file mode 100644 index 000000000..63931a155 --- /dev/null +++ b/docs/guides/winget-release-automation.md @@ -0,0 +1,40 @@ +# Winget release automation + +CodeNomad publishes Winget updates from GitHub Releases with `.github/workflows/update-winget.yml`. + +## Trigger + +- Runs on `release.published`. +- Exits early for draft or prerelease releases. +- Waits for the expected stable Windows Tauri asset because the release record can exist before all assets finish uploading. + +## Required configuration + +### Repository secret + +- `WINGET_GITHUB_TOKEN`: Classic GitHub PAT with `public_repo` scope. + - The token owner must own the fork that submits to `microsoft/winget-pkgs`. + - Komac-based submission cannot open the PR with a fine-grained token today. + +### Repository variables + +- `WINGET_PACKAGE_IDENTIFIER` (default fallback in workflow: `NeuralNomadsAI.CodeNomad`) +- `WINGET_FORK_OWNER` (default fallback in workflow: `pascalandr`) +- `WINGET_WINDOWS_ASSET_NAME_TEMPLATE` (default fallback in workflow: `CodeNomad-Tauri-windows-x64-{version}.zip`) +- `WINGET_ASSET_WAIT_TIMEOUT_SECONDS` (optional, default fallback: `900`) +- `WINGET_ASSET_POLL_INTERVAL_SECONDS` (optional, default fallback: `15`) + +`{version}` is replaced from the release tag after trimming a leading `v`. + +## Runtime flow + +1. Resolve the release version from `github.event.release.tag_name`. +2. Poll the release API until exactly one uploaded asset matches the configured Windows Tauri asset template. +3. Download the matched asset once and compute a SHA-256 for logging and verification. +4. Verify the PAT owner matches `WINGET_FORK_OWNER` and that `${WINGET_FORK_OWNER}/winget-pkgs` is a fork of `microsoft/winget-pkgs`. +5. Invoke `vedantmgoyal9/winget-releaser@v2`, which uses Komac under the hood to update the existing `NeuralNomadsAI.CodeNomad` manifest and open the PR. + +## Notes + +- The workflow does not depend on a persistent local `winget-pkgs` clone. +- The current package in `winget-pkgs` uses the release ZIP with a nested NSIS installer, so the workflow targets the stable `CodeNomad-Tauri-windows-x64-{version}.zip` asset. diff --git a/scripts/winget/resolve-release-asset.cjs b/scripts/winget/resolve-release-asset.cjs new file mode 100644 index 000000000..bc644b7fd --- /dev/null +++ b/scripts/winget/resolve-release-asset.cjs @@ -0,0 +1,201 @@ +#!/usr/bin/env node + +const crypto = require("node:crypto") +const fs = require("node:fs") +const { Readable } = require("node:stream") + +function printHelp() { + console.log(`Usage: node scripts/winget/resolve-release-asset.cjs [options] + +Options: + --repo GitHub repository that owns the release + --release-id Numeric GitHub release id + --tag Release tag (used for version derivation) + --asset-name-template