diff --git a/.github/workflows/desktop.yml b/.github/workflows/desktop.yml index db6da17c..098649dc 100644 --- a/.github/workflows/desktop.yml +++ b/.github/workflows/desktop.yml @@ -73,19 +73,20 @@ jobs: working-directory: frontend/src-tauri run: cargo test - # Remove stale artifacts from the desktop-latest pre-release so that - # only the current build's files are available for download. + # Remove stale artifacts from the desktop-edge rolling pre-release so that + # only the current build's files are available for download. (The stable + # `desktop-latest` channel the installed app polls is never cleaned here.) clean-release: needs: [validate] if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest steps: - - name: Delete old assets from desktop-latest + - name: Delete old assets from desktop-edge if: "!startsWith(github.ref, 'refs/tags/')" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - TAG="desktop-latest" + TAG="desktop-edge" # List all asset IDs on the release and delete them ASSET_IDS=$(gh api "repos/${{ github.repository }}/releases/tags/${TAG}" \ --jq '.assets[].id' 2>/dev/null || true) @@ -170,10 +171,13 @@ jobs: elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then # Auto-tagged rolling build from autotag.yml — use the same # version as the CLI/PyPI release so all surfaces stay in sync. + # Rolling/dev builds go to the `desktop-edge` channel, NOT the + # `desktop-latest` channel the installed app polls — so users on + # stable are never auto-updated onto an unvetted dev build. VERSION="${{ github.ref_name }}" VERSION="${VERSION#v}" - echo "tag=desktop-latest" >> "$GITHUB_OUTPUT" - echo "name=Desktop (Latest Build)" >> "$GITHUB_OUTPUT" + echo "tag=desktop-edge" >> "$GITHUB_OUTPUT" + echo "name=Desktop (Edge Build)" >> "$GITHUB_OUTPUT" echo "prerelease=true" >> "$GITHUB_OUTPUT" else # workflow_dispatch fallback (manual UI dispatch without --ref). @@ -186,8 +190,9 @@ jobs: NEXT_PATCH=$((PATCH + 1)) BUILD=$(git rev-list --count HEAD) VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}.dev${BUILD}" - echo "tag=desktop-latest" >> "$GITHUB_OUTPUT" - echo "name=Desktop (Latest Build)" >> "$GITHUB_OUTPUT" + # Manual dispatches are also dev builds -> the edge channel. + echo "tag=desktop-edge" >> "$GITHUB_OUTPUT" + echo "name=Desktop (Edge Build)" >> "$GITHUB_OUTPUT" echo "prerelease=true" >> "$GITHUB_OUTPUT" fi echo "version=${VERSION}" >> "$GITHUB_OUTPUT" @@ -243,3 +248,43 @@ jobs: prerelease: ${{ steps.release-info.outputs.prerelease }} includeUpdaterJson: true args: ${{ matrix.args }} + + # When a stable `desktop-v*` release is published, repoint the + # `desktop-latest` auto-update channel (the endpoint the installed app + # polls) at it. The stable release's own `latest.json` already references + # this release's signed assets, so we copy it verbatim — installed apps are + # only ever offered vetted stable builds, never `desktop-edge` dev builds. + refresh-stable-channel: + needs: [build-and-release] + if: startsWith(github.ref, 'refs/tags/desktop-v') + runs-on: ubuntu-latest + steps: + - name: Mirror stable latest.json into desktop-latest + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + STABLE_TAG: ${{ github.ref_name }} + REPO: ${{ github.repository }} + run: | + set -euo pipefail + # The stable release's updater manifest may take a moment to become + # downloadable after tauri-action publishes it; retry briefly. + URL="https://github.com/${REPO}/releases/download/${STABLE_TAG}/latest.json" + for attempt in 1 2 3 4 5; do + if curl -fsSL -o latest.json "$URL"; then + echo "Fetched ${STABLE_TAG}/latest.json on attempt ${attempt}" + break + fi + echo "latest.json not ready yet (attempt ${attempt}); sleeping 15s" + sleep 15 + done + test -s latest.json || { echo "::error::Could not fetch ${URL}"; exit 1; } + # Ensure the channel release exists (prerelease so it never usurps + # the stable "Latest" badge), then replace its manifest in place. + if ! gh release view desktop-latest --repo "$REPO" >/dev/null 2>&1; then + gh release create desktop-latest --repo "$REPO" \ + --prerelease \ + --title "Desktop Auto-Update Channel" \ + --notes "Auto-update channel pointer for the desktop app. Mirrors the latest stable \`desktop-v*\` release; the in-app updater polls this \`latest.json\`. Download the app from the latest stable release, not here." + fi + gh release upload desktop-latest latest.json --repo "$REPO" --clobber + echo "desktop-latest now mirrors ${STABLE_TAG}" diff --git a/docs/desktop-auto-update.md b/docs/desktop-auto-update.md index 9193b12f..5b320324 100644 --- a/docs/desktop-auto-update.md +++ b/docs/desktop-auto-update.md @@ -33,25 +33,32 @@ under `plugins.updater`. The `Desktop Build & Release` GitHub Action ([`.github/workflows/desktop.yml`](../.github/workflows/desktop.yml)) -publishes signed binaries plus a `latest.json` manifest to the -`desktop-latest` GitHub release on every push to `main`. The -`tauri-action` step with `includeUpdaterJson: true` generates the -manifest automatically. +builds signed binaries plus a `latest.json` manifest with the +`tauri-action` step (`includeUpdaterJson: true` generates the manifest +automatically). Where it publishes depends on the trigger. -Two release streams exist: +Three release streams exist: -- **`desktop-latest`** (rolling pre-release): updated on every push to - `main`. This is the channel the desktop app currently polls. Users - on this channel get the most recent build the CI produced. +- **`desktop-latest`** (stable auto-update channel): **this is the + channel the installed app polls.** It is *not* built directly — + instead, when a stable `desktop-vX.Y.Z` release is published, the + `refresh-stable-channel` job copies that release's `latest.json` + into `desktop-latest`. So the app is only ever offered vetted stable + builds, and `latest.json` here points at the current `desktop-v*` + assets. - **`desktop-vX.Y.Z`** (tagged stable): created when someone pushes a - `desktop-v*` git tag. Has the same artifacts but is marked as a - proper release rather than a pre-release. + `desktop-v*` git tag. The user-facing stable release with full + installers; also the source of truth the stable channel mirrors. +- **`desktop-edge`** (rolling pre-release): rebuilt on every push to + `main` (via the `autotag` → `desktop.yml` dispatch) and on manual + `workflow_dispatch`. Carries the most recent CI build for testers. + The shipped app does **not** poll this stream, so dev builds never + auto-install onto stable users. -The current updater endpoint points at the rolling `desktop-latest` -stream so that bug fixes (especially security and telemetry-policy -changes) reach users without waiting for a manual stable tag. A future -release may introduce a stable channel that points at -`desktop-v*` tags only. +This split means security and telemetry-policy fixes reach users on +the next **stable** `desktop-v*` tag — cut one to ship an update. +Edge builds are available for anyone who wants to test `main` ahead of +a stable tag, without risking the stable population. ## Signing