diff --git a/.github/workflows/reusable-release.yml b/.github/workflows/reusable-release.yml index e839e76b4..59b1b0583 100644 --- a/.github/workflows/reusable-release.yml +++ b/.github/workflows/reusable-release.yml @@ -97,6 +97,18 @@ jobs: release_name: ${{ needs.prepare-release.outputs.release_name }} secrets: inherit + update-winget: + needs: + - prepare-release + - build-and-upload + if: ${{ !inputs.prerelease }} + permissions: + contents: read + uses: ./.github/workflows/update-winget.yml + with: + release_tag: ${{ needs.prepare-release.outputs.tag }} + secrets: inherit + release-ui: needs: prepare-release if: ${{ inputs.release_ui }} diff --git a/.github/workflows/update-winget.yml b/.github/workflows/update-winget.yml index 41b894f67..fd9a656f3 100644 --- a/.github/workflows/update-winget.yml +++ b/.github/workflows/update-winget.yml @@ -1,17 +1,83 @@ name: Update Winget on: - release: - types: - - published + workflow_call: + inputs: + release_tag: + description: "Stable release tag to inspect" + required: true + type: string + release_id: + description: "Optional numeric GitHub release id" + required: false + default: "" + type: string + workflow_dispatch: + inputs: + release_tag: + description: "Stable release tag to inspect" + required: true + type: string + release_id: + description: "Optional numeric GitHub release id" + required: false + default: "" + type: string permissions: contents: read jobs: + resolve-release: + name: Resolve release metadata + runs-on: ubuntu-latest + outputs: + release_id: ${{ steps.release.outputs.release_id }} + release_tag: ${{ steps.release.outputs.release_tag }} + draft: ${{ steps.release.outputs.draft }} + prerelease: ${{ steps.release.outputs.prerelease }} + steps: + - name: Resolve release metadata + id: release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_TAG: ${{ inputs.release_tag }} + RELEASE_ID_INPUT: ${{ inputs.release_id }} + run: | + set -euo pipefail + + if [ -n "$RELEASE_ID_INPUT" ]; then + release_api="repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID_INPUT}" + else + release_api="repos/${GITHUB_REPOSITORY}/releases/tags/${RELEASE_TAG}" + fi + + release_id="$(gh api "$release_api" --jq '.id')" + release_tag="$(gh api "$release_api" --jq '.tag_name')" + draft="$(gh api "$release_api" --jq '.draft')" + prerelease="$(gh api "$release_api" --jq '.prerelease')" + + if [ -z "$release_id" ] || [ "$release_id" = "null" ]; then + echo "Unable to resolve release id for tag '$RELEASE_TAG'" >&2 + exit 1 + fi + + echo "release_id=$release_id" >> "$GITHUB_OUTPUT" + echo "release_tag=$release_tag" >> "$GITHUB_OUTPUT" + echo "draft=$draft" >> "$GITHUB_OUTPUT" + echo "prerelease=$prerelease" >> "$GITHUB_OUTPUT" + + - name: Log resolved release metadata + run: | + echo "Release tag: ${{ steps.release.outputs.release_tag }}" + echo "Release id: ${{ steps.release.outputs.release_id }}" + echo "Draft: ${{ steps.release.outputs.draft }}" + echo "Prerelease: ${{ steps.release.outputs.prerelease }}" + update-winget: name: Submit Winget manifest update - if: ${{ !github.event.release.draft && !github.event.release.prerelease }} + needs: resolve-release + if: ${{ needs.resolve-release.outputs.draft != 'true' && needs.resolve-release.outputs.prerelease != 'true' }} runs-on: ubuntu-latest env: WINGET_PACKAGE_IDENTIFIER: ${{ vars.WINGET_PACKAGE_IDENTIFIER || 'NeuralNomadsAI.CodeNomad' }} @@ -35,8 +101,8 @@ jobs: run: | args=( --repo "${{ github.repository }}" - --release-id "${{ github.event.release.id }}" - --tag "${{ github.event.release.tag_name }}" + --release-id "${{ needs.resolve-release.outputs.release_id }}" + --tag "${{ needs.resolve-release.outputs.release_tag }}" --asset-name-template "$WINGET_WINDOWS_ASSET_NAME_TEMPLATE" --timeout-seconds "$WINGET_ASSET_WAIT_TIMEOUT_SECONDS" --poll-interval-seconds "$WINGET_ASSET_POLL_INTERVAL_SECONDS" @@ -79,7 +145,7 @@ jobs: with: identifier: ${{ env.WINGET_PACKAGE_IDENTIFIER }} version: ${{ steps.release_asset.outputs.version }} - release-tag: ${{ github.event.release.tag_name }} + release-tag: ${{ needs.resolve-release.outputs.release_tag }} 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 index 63931a155..b61639c03 100644 --- a/docs/guides/winget-release-automation.md +++ b/docs/guides/winget-release-automation.md @@ -1,11 +1,13 @@ # Winget release automation -CodeNomad publishes Winget updates from GitHub Releases with `.github/workflows/update-winget.yml`. +CodeNomad publishes Winget updates from the stable GitHub release pipeline. `.github/workflows/reusable-release.yml` now calls `.github/workflows/update-winget.yml` after the release assets finish uploading. ## Trigger -- Runs on `release.published`. -- Exits early for draft or prerelease releases. +- Runs as a reusable workflow from the stable release pipeline, after `build-and-upload` completes. +- Resolves the target release by tag through the GitHub API, then exits early for draft or prerelease releases. +- Can also be rerun manually with `workflow_dispatch` by supplying the stable release tag (and optionally the numeric release id). +- This avoids the old `release.published` trap where a release created by GitHub Actions with the default `GITHUB_TOKEN` does not fan out into a second workflow run. - Waits for the expected stable Windows Tauri asset because the release record can exist before all assets finish uploading. ## Required configuration @@ -28,7 +30,7 @@ CodeNomad publishes Winget updates from GitHub Releases with `.github/workflows/ ## Runtime flow -1. Resolve the release version from `github.event.release.tag_name`. +1. Resolve the target release by tag through the GitHub API, then derive the package version from the resolved release tag. 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`. @@ -38,3 +40,4 @@ CodeNomad publishes Winget updates from GitHub Releases with `.github/workflows/ - 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. +- If a maintainer publishes a release outside the standard release workflow, they should manually run `Update Winget` for that stable tag.