From b495fa89a9526bef54224f99d91cba80e6c43ece Mon Sep 17 00:00:00 2001 From: elkaix Date: Sun, 16 Aug 2026 01:51:42 -0400 Subject: [PATCH] feat(desktop): publish releases to a dedicated update channel electron-updater's public GitHub provider resolves the repository's newest release. This repository publishes CLI releases continuously, so that is almost always a CLI release carrying no latest-mac.yml, and every desktop update check fails. Publish desktop releases to PyModel/pythinker-desktop-releases instead, where the latest release always describes the desktop app. Releases are created with releaseType: release because the publisher otherwise creates a draft, and a draft is invisible to the updater. Publishing across repositories needs an installation token; GITHUB_TOKEN cannot reach another repository. There is deliberately no fallback: a fallback would publish to the wrong repository and silently restore the bug this fixes, so a missing secret fails the release. Both jobs now also assert that app-update.yml is present inside the packaged application. Without it a build cannot self-update, and the app reports itself as non-updatable, so shipping one is a silent regression. --- .github/workflows/desktop-release.yml | 38 ++++++++++++++++++--- apps/desktop/package.json | 3 +- apps/desktop/tests/packaging-config.spec.ts | 15 ++++++++ 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index 2d2120ba..b43164da 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -46,8 +46,8 @@ jobs: working-directory: apps/desktop run: node --import tsx scripts/stage-runtime.ts - # On a desktop-v* tag, --publish always creates or updates the draft-or-release - # for that tag; contents: write makes GITHUB_TOKEN sufficient. + # On a desktop-v* tag, --publish always creates or updates the release + # in pythinker-desktop-releases with the GitHub App token below. # Without Developer ID signing secrets, electron-builder publishes an # ad-hoc/self-signed app. macOS auto-update will not accept unsigned updates, # but this still proves packaging and the feed shape. @@ -73,11 +73,29 @@ jobs: echo 'CSC_IDENTITY_AUTO_DISCOVERY=false' >> "$GITHUB_ENV" echo 'No macOS signing certificate configured; building unsigned.' fi + - name: Mint releases-repo token + id: releases_token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # pinned from v3.2.0 + with: + app-id: ${{ secrets.DESKTOP_RELEASES_APP_ID }} + private-key: ${{ secrets.DESKTOP_RELEASES_APP_PRIVATE_KEY }} + owner: PyModel + repositories: pythinker-desktop-releases - name: Package and publish desktop release working-directory: apps/desktop run: pnpm exec electron-builder --mac dmg zip --publish always env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ steps.releases_token.outputs.token }} + + - name: Verify macOS packaged update configuration + shell: bash + run: | + app_bundle="$(find apps/desktop/dist -maxdepth 2 -type d -name '*.app' -print -quit)" + if [ -z "$app_bundle" ]; then + echo 'macOS application bundle not found' >&2 + exit 1 + fi + test -f "$app_bundle/Contents/Resources/app-update.yml" - name: Upload macOS artifacts for manual runs if: github.event_name == 'workflow_dispatch' @@ -154,11 +172,23 @@ jobs: value="${!input:-}" if [ -n "$value" ]; then printf '%s<<__EOF__\n%s\n__EOF__\n' "$name" "$value" >> "$GITHUB_ENV"; fi done + - name: Mint releases-repo token + id: releases_token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # pinned from v3.2.0 + with: + app-id: ${{ secrets.DESKTOP_RELEASES_APP_ID }} + private-key: ${{ secrets.DESKTOP_RELEASES_APP_PRIVATE_KEY }} + owner: PyModel + repositories: pythinker-desktop-releases - name: Package and publish desktop release working-directory: apps/desktop run: node --import tsx scripts/package-win.ts --publish always env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ steps.releases_token.outputs.token }} + + - name: Verify Windows packaged update configuration + shell: bash + run: test -f apps/desktop/dist/win-unpacked/resources/app-update.yml - name: Upload Windows artifacts for manual runs if: github.event_name == 'workflow_dispatch' diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 5321fef5..4f15d501 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -32,7 +32,8 @@ { "provider": "github", "owner": "PyModel", - "repo": "pythinker-code" + "repo": "pythinker-desktop-releases", + "releaseType": "release" } ], "afterPack": "./scripts/verify-packaged-runtime.ts", diff --git a/apps/desktop/tests/packaging-config.spec.ts b/apps/desktop/tests/packaging-config.spec.ts index 0ec1013d..62c764ce 100644 --- a/apps/desktop/tests/packaging-config.spec.ts +++ b/apps/desktop/tests/packaging-config.spec.ts @@ -28,6 +28,12 @@ interface DesktopPackage { readonly perMachine: boolean readonly shortcutName: string } + readonly publish: readonly { + readonly owner: string + readonly provider: string + readonly releaseType: string + readonly repo: string + }[] readonly productName: string readonly win: { readonly icon: string @@ -58,6 +64,15 @@ describe('desktop packaging configuration', () => { expect(desktopPackage.build.productName).toBe('Pythinker') }) + it('publishes updates to the dedicated desktop release repository', () => { + expect(desktopPackage.build.publish).toContainEqual({ + owner: 'PyModel', + provider: 'github', + releaseType: 'release', + repo: 'pythinker-desktop-releases', + }) + }) + it('keeps desktop download URLs derived from their published release version', () => { const siteSource = readFileSync(resolve(repositoryRoot, 'apps/site/src/App.vue'), 'utf8') const desktopVersionMatch = siteSource.match(/const DESKTOP_VERSION = '([^']+)'/)