From 80b72c4600fc30b16d4f0ae524dedd99d0681b62 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sun, 9 Aug 2026 19:01:15 -0700 Subject: [PATCH] Add third token mint to daily macOS build workflow The upload step's 2x45m retry budget can outlive the one-hour token, so a third is minted after it for verify and cleanup operations. Release notes are moved to a file to ensure consistency between draft creation and publish. Daily channel description updated with specific UTC release time. --- .github/workflows/daily-mac-build.yml | 74 ++++++++++++++----- .../settings/ReleaseChannelSection.tsx | 3 +- 2 files changed, 56 insertions(+), 21 deletions(-) diff --git a/.github/workflows/daily-mac-build.yml b/.github/workflows/daily-mac-build.yml index ecb5f21bfcc..5670c634d2d 100644 --- a/.github/workflows/daily-mac-build.yml +++ b/.github/workflows/daily-mac-build.yml @@ -32,14 +32,16 @@ name: Daily macOS Dev Build # HOURLY_RELEASE_APP_ID the App's numeric id (shared with hourly/adhoc) # HOURLY_RELEASE_APP_PRIVATE_KEY the App's .pem private key # -# Installation tokens live one hour, which is why this mints twice. Install and -# build need no token at all, and notarization can hold the publish step for tens -# of minutes; minting again once the build is done starts the clock at the first -# call that actually uses it rather than burning a third of it on `pnpm install`. -# A pathological retry can still outrun the second token, but the release is an -# unpublished draft until the manifest check passes, so the damage is a stranded -# invisible draft — and the build-number query counts drafts, so it holds its -# number and the next run does not reuse it. +# Installation tokens live one hour, which is why this mints three times. Install +# and build need no token at all, and notarization can hold the publish step for +# tens of minutes; minting again once the build is done starts the clock at the +# first call that actually uses it rather than burning a third of it on +# `pnpm install`. The upload step's own retry budget (2x45m) can outlive that +# second token, so a third is minted after it for verify/publish/prune/cleanup — +# without it a stuck notary run would strand a draft that cleanup gets a 401 on. +# The release stays an unpublished draft until the manifest check passes, so the +# worst case is still an invisible draft — and the build-number query counts +# drafts, so it holds its number and the next run does not reuse it. on: schedule: @@ -262,6 +264,19 @@ jobs: # Kept at 12 even though the title shows 7: the freshness check above # parses this back out of the body to decide whether main has moved. short_sha="${SHA:0:12}" + # Why a file rather than an inline string: the publish step re-asserts + # this same body, and the freshness check only works if the two agree + # exactly. One source, written once, read twice. + notes_file="$RUNNER_TEMP/daily-release-notes.md" + cat >"$notes_file" <>"$GITHUB_OUTPUT" + echo "notes_file=$notes_file" >>"$GITHUB_OUTPUT" - name: Publish daily macOS artifacts if: steps.freshness.outputs.should_build == 'true' @@ -313,6 +323,23 @@ jobs: APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + # Why a third mint: the upload step above allows two 45-minute attempts, so + # it can outlive the one-hour token minted before it. Every remaining step + # writes to the daily repo, including the failure path that discards the + # draft — a 401 there is exactly the stranded draft nobody can clean up. + # + # Why always(): a failed or cancelled upload is the case that needs this + # most, since the cleanup step below runs only on that path. + - name: Re-mint daily repo token for verify and cleanup + id: app_token_final + if: always() && steps.freshness.outputs.should_build == 'true' + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ secrets.HOURLY_RELEASE_APP_ID }} + private-key: ${{ secrets.HOURLY_RELEASE_APP_PRIVATE_KEY }} + owner: stablyai + repositories: orca-daily + # Why: the updater resolves a tag, then fetches latest-mac.yml from it. A # release missing that manifest is a tag the picker offers and the download # 404s on, so fail loudly instead of leaving a broken entry. @@ -320,7 +347,7 @@ jobs: if: steps.freshness.outputs.should_build == 'true' shell: bash env: - GH_TOKEN: ${{ steps.app_token_publish.outputs.token }} + GH_TOKEN: ${{ steps.app_token_final.outputs.token }} TAG: ${{ steps.release.outputs.tag }} run: | set -euo pipefail @@ -350,15 +377,20 @@ jobs: if: steps.freshness.outputs.should_build == 'true' shell: bash env: - GH_TOKEN: ${{ steps.app_token_publish.outputs.token }} + GH_TOKEN: ${{ steps.app_token_final.outputs.token }} TAG: ${{ steps.release.outputs.tag }} NAME: ${{ steps.daily.outputs.name }} + NOTES_FILE: ${{ steps.release.outputs.notes_file }} run: | set -euo pipefail # --title again: electron-builder resolves this draft by tag and may # rewrite its title on upload. Re-asserting here means the name the # picker reads is the one composed above, whatever it did in between. - gh release edit "$TAG" --repo "$DAILY_REPO" --draft=false --prerelease --title "$NAME" + # --notes-file for the same reason, and it matters more: the next run's + # freshness check parses the source commit back out of this body, so a + # body the publisher overwrote would rebuild an unchanged main daily. + gh release edit "$TAG" --repo "$DAILY_REPO" --draft=false --prerelease \ + --title "$NAME" --notes-file "$NOTES_FILE" echo "Published $TAG as \"$NAME\"" # Why: a draft left behind by a failed publish is invisible to users but still @@ -376,7 +408,9 @@ jobs: steps.publish_live.outcome != 'success' shell: bash env: - GH_TOKEN: ${{ steps.app_token_publish.outputs.token }} + # Fall back to the publish token: if the re-mint itself is what failed, + # the older token is the only one left and may still have time on it. + GH_TOKEN: ${{ steps.app_token_final.outputs.token || steps.app_token_publish.outputs.token }} TAG: ${{ steps.release.outputs.tag }} run: | set -uo pipefail @@ -392,7 +426,7 @@ jobs: if: steps.publish_live.outcome == 'success' shell: bash env: - GH_TOKEN: ${{ steps.app_token_publish.outputs.token }} + GH_TOKEN: ${{ steps.app_token_final.outputs.token }} # Protect the tag this run just shipped; at the retain cap, a bad sort # can otherwise mark the newest release as stale and delete it. TAG: ${{ steps.release.outputs.tag }} diff --git a/src/renderer/src/components/settings/ReleaseChannelSection.tsx b/src/renderer/src/components/settings/ReleaseChannelSection.tsx index c123c50efa3..bfe2321bd32 100644 --- a/src/renderer/src/components/settings/ReleaseChannelSection.tsx +++ b/src/renderer/src/components/settings/ReleaseChannelSection.tsx @@ -25,7 +25,8 @@ const CHANNEL_DESCRIPTIONS: Record = { stable: 'Shipped releases. What everyone else is running.', rc: 'Release candidates cut ahead of each stable.', hourly: 'macOS only. Unvetted builds from main, built every hour. No tests.', - daily: 'macOS only. Unvetted builds from main, cut once a day in the early morning. No tests.', + daily: + 'macOS only. Unvetted builds from main, cut once a day at 14:15 UTC (early morning Pacific). No tests.', adhoc: 'macOS only. One-off builds cut from a branch to try a feature before it lands.' }