fix(release): filter desktop-only tags and consolidate release pipeline (#1386)

- release-cut.yml: require `^v[0-9]` tag shape when picking "latest
  stable" from GitHub releases. Previously only `-rc.*` was excluded, so
  publishing `mobile-v0.0.1` made it the latest stable, `strip_pre()`
  reduced it to `mobile`, `Number("mobile")` → NaN → 0, and a patch-bump
  produced `v0.0.1` (run 25304336767).
- homebrew-bump.yml: drop the blanket `release.published` trigger and
  add tag-shape guards. Any published release in this repo (including
  mobile) would otherwise fire the tap bump. Workflow_call from the
  desktop pipeline is the only intended path now.
- Inline create-release / build / publish-release / e2e / homebrew-bump
  jobs from release.yml into release-cut.yml, and delete release.yml.

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil 2026-05-04 00:21:21 -07:00 committed by GitHub
parent 98b484eb9b
commit c58e06383c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 254 additions and 279 deletions

View File

@ -80,7 +80,7 @@ All releases are cut from the **Cut Release** GitHub Actions workflow. There is
- **ref**: the branch, tag, or SHA to build from. Defaults to `main`.
3. Run it.
The workflow resolves the next version from GitHub Releases, bumps `package.json`, tags, pushes, and kicks off the multi-platform build via `release.yml`.
The workflow resolves the next version from GitHub Releases, bumps `package.json`, tags, pushes, and runs the multi-platform build + publish inline.
**How the next version is chosen:**

View File

@ -2,15 +2,21 @@ name: Homebrew Cask Bump
# Why: keep the stablyai/homebrew-orca tap in sync with GitHub Releases.
# Triggers:
# - release.published with !prerelease → release workflow just finalized a
# stable tag (see publish-release step in release.yml)
# - workflow_call → release.yml finished publishing a stable tag
# - workflow_call → release-cut.yml finished publishing a stable tag
# - workflow_dispatch → manual backfill for a specific tag
#
# The job computes the sha256 of orca-macos-arm64.dmg + orca-macos-x64.dmg
# from the release, templates Casks/orca.rb in this repo with the new
# version and hashes, then PRs the result into stablyai/homebrew-orca. It
# does NOT run for -rc.* tags: the stable cask should only track GA.
#
# Why no `release.published` trigger: other products shipped from this repo
# (e.g. mobile under `mobile-v*`) also publish GitHub Releases. A blanket
# `release.published` trigger would fire this workflow for any of them and
# attempt to download `orca-macos-*.dmg` from a non-desktop release, at
# best 404ing and at worst (if the filter were weaker) rewriting the cask
# to a non-desktop version. The tap is only ever bumped by the desktop
# release pipeline calling this workflow directly.
on:
workflow_call:
@ -19,8 +25,6 @@ on:
description: Release tag to sync (e.g. v1.3.25)
required: true
type: string
release:
types: [published]
workflow_dispatch:
inputs:
tag:
@ -31,12 +35,15 @@ on:
jobs:
bump-cask:
runs-on: ubuntu-latest
# Why: skip prereleases and RC tags. The stable cask tracks GA only; a
# beta channel would be a separate cask in a separate job.
# Why: defense in depth. The desktop release pipeline already refuses
# to invoke this workflow for `-rc.*` tags, but a manual backfill could
# still pass one in — reject it here so the stable cask can never point
# at an RC. Also requires the desktop tag shape `v<digit>...` so a
# mis-typed mobile tag like `mobile-v0.0.1` is rejected up front.
if: >
inputs.tag != '' ||
(github.event.release.prerelease == false &&
!contains(github.event.release.tag_name, '-rc.'))
inputs.tag != '' &&
!contains(inputs.tag, '-rc.') &&
startsWith(inputs.tag, 'v')
permissions:
contents: read
steps:

View File

@ -183,20 +183,28 @@ jobs:
run: |
set -euo pipefail
# Latest stable release tag, picked by *tag shape* (no `-rc.`)
# rather than the GitHub `isPrerelease` flag. electron-builder's
# publish step has flipped that flag back to `false` on RC
# releases in the past (issue surfaced 2026-04-27 when
# v1.3.22-rc.2 became "latest" on GitHub and poisoned the
# compute-next-version math here). Filtering by tag format is
# the authoritative signal — an `-rc.N` tag is never stable,
# regardless of what the release metadata says.
# Latest stable release tag, picked by *tag shape*:
# - must start with `v<digit>` (desktop convention, e.g. v1.3.32)
# - must NOT contain `-rc.` (not a prerelease)
#
# Why not the GitHub `isPrerelease` flag: electron-builder's publish
# step has flipped that flag back to `false` on RC releases before
# (v1.3.22-rc.2 on 2026-04-27 briefly became "latest" on GitHub and
# poisoned the math here). Tag format is authoritative.
#
# Why the `^v[0-9]` prefix: other products shipped from this repo
# use their own prefixes (e.g. `mobile-v0.0.1`). Without the prefix
# gate the latest mobile release would be selected as "latest
# stable", then `strip_pre()` would reduce `mobile-v0.0.1` to
# `mobile`, `Number("mobile")` → NaN → 0, and a patch-bump would
# produce `0.0.1` — exactly the wedge on 2026-05-04 (run
# 25304336767). Any non-desktop tag shape must be excluded here.
latest_stable="$(gh release list \
--repo "$GITHUB_REPOSITORY" \
--exclude-drafts \
--limit 50 \
--json tagName \
--jq '[.[] | .tagName | select(test("-rc\\.") | not)] | .[0] // ""')"
--jq '[.[] | .tagName | select(test("^v[0-9]") and (test("-rc\\.") | not))] | .[0] // ""')"
latest_stable="${latest_stable#v}"
echo "Latest stable: ${latest_stable:-<none>}"
@ -369,10 +377,227 @@ jobs:
git push origin "$TAG"
fi
release:
create-release:
needs: cut
if: needs.cut.outputs.should_release == 'true'
uses: ./.github/workflows/release.yml
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: refs/tags/${{ needs.cut.outputs.tag }}
- name: Create draft release with auto-generated notes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.cut.outputs.tag }}
run: |
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "Release $TAG already exists."
exit 0
fi
is_rc=false
if [[ "$TAG" == *"-rc."* ]]; then
is_rc=true
fi
gh release create "$TAG" \
--draft \
--generate-notes \
--prerelease="$is_rc"
# Why: E2E runs alongside the release for visibility (failures surface as a
# red check on the tag), but is NOT in `publish-release`'s needs list.
# Releases already take a while and the suite is already a required check
# on PRs, so gating here would mostly delay shipping without adding signal.
e2e:
needs: cut
if: needs.cut.outputs.should_release == 'true'
uses: ./.github/workflows/e2e.yml
with:
ref: refs/tags/${{ needs.cut.outputs.tag }}
build:
needs:
- cut
- create-release
if: needs.cut.outputs.should_release == 'true'
strategy:
fail-fast: false
matrix:
include:
- os: macos-15
platform: mac
release_command: ORCA_MAC_RELEASE=1 pnpm exec electron-builder --config config/electron-builder.config.cjs --mac --publish always
eb_cache_path: |
~/Library/Caches/electron
~/Library/Caches/electron-builder
- os: windows-latest
platform: win
release_command: pnpm exec electron-builder --config config/electron-builder.config.cjs --win --publish always
eb_cache_path: |
~\AppData\Local\electron\Cache
~\AppData\Local\electron-builder\Cache
- os: ubuntu-latest
platform: linux
release_command: pnpm exec electron-builder --config config/electron-builder.config.cjs --linux --publish always
eb_cache_path: |
~/.cache/electron
~/.cache/electron-builder
runs-on: ${{ matrix.os }}
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: refs/tags/${{ needs.cut.outputs.tag }}
# pnpm must be on PATH before setup-node so setup-node can locate the store for caching.
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: package.json
cache: pnpm
# Why: release builds hit the same native-module postinstall path as
# PR CI, so keep the pinned node-gyp override here too instead of
# relying on pnpm's bundled copy. Scoped to Linux via runner.os (not
# a specific matrix image) because the failing postinstall has only
# been observed on Linux runners — see run 25081763129. The macOS
# and Windows release jobs exercise the same pnpm install path and
# have not reproduced it, so keep the gate narrow until we know why.
# Using runner.os instead of matrix.os == 'ubuntu-latest' means the
# gate still works if another Linux matrix entry is added later.
- name: Use external node-gyp to avoid pnpm's bundled copy (Linux only)
if: runner.os == 'Linux'
run: |
npm install -g node-gyp@11.5.0
echo "npm_config_node_gyp=$(npm root -g)/node-gyp/bin/node-gyp.js" >> "$GITHUB_ENV"
# Cache the Electron binary + electron-builder tool downloads (notarytool,
# winCodeSign, nsis, squirrel, AppImage). Saves ~30-90s per job, incl. mac.
- name: Cache electron-builder downloads
uses: actions/cache@v4
with:
path: ${{ matrix.eb_cache_path }}
key: electron-builder-${{ matrix.platform }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
electron-builder-${{ matrix.platform }}-
# Why: pnpm install triggers electron's postinstall, which downloads the
# Electron binary from GitHub release assets. GitHub's download CDN
# occasionally returns 504s that fail the whole release. Retry on
# failure so transient network errors don't require a manual re-run.
- name: Install dependencies
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
retry_wait_seconds: 30
command: pnpm install --frozen-lockfile
- name: Verify macOS signing environment
if: matrix.platform == 'mac'
run: node config/scripts/verify-macos-release-env.mjs
env:
CSC_LINK: ${{ secrets.MAC_CERTS }}
CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTS_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
- name: Build app
run: pnpm build:release
# Why: macOS signing secrets (CSC_LINK, CSC_KEY_PASSWORD) must NOT be
# passed to non-macOS builds. electron-builder uses CSC_LINK as the
# code-signing certificate on any platform, so leaking the Apple
# Developer ID cert to the Windows build causes the NSIS installer to
# be signed with an Apple cert whose chain Windows cannot validate,
# breaking the auto-updater with "certificate chain could not be built
# to a trusted root authority" (issue #631).
#
# Why retry: electron-builder downloads NSIS/winCodeSign/squirrel
# binaries and the Electron runtime from GitHub release assets during
# publish. GitHub's download CDN occasionally returns 504s that fail
# the whole release. Retry on failure so transient network errors
# don't require a manual re-run.
- name: Publish release artifacts (macOS)
if: matrix.platform == 'mac'
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
retry_wait_seconds: 30
command: ${{ matrix.release_command }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CSC_LINK: ${{ secrets.MAC_CERTS }}
CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTS_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
- name: Publish release artifacts
if: matrix.platform != 'mac'
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
retry_wait_seconds: 30
command: ${{ matrix.release_command }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish-release:
needs:
- cut
- build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Publish release
# Why: derive `--prerelease` from the tag shape (not from whatever
# electron-builder left the release flagged as). On 2026-04-27,
# electron-builder's publish step flipped `prerelease` back to
# `false` on -rc.N releases, which caused an RC to be marked as
# GitHub's "latest" release and broke release-cut.yml's math.
# Re-asserting here means the final release state is determined
# by the tag — a ground truth electron-builder can't rewrite.
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.cut.outputs.tag }}
run: |
set -euo pipefail
if [[ "$TAG" == *"-rc."* ]]; then
prerelease=true
else
prerelease=false
fi
gh release edit "$TAG" \
--draft=false \
--prerelease="$prerelease" \
--repo "$GITHUB_REPOSITORY"
homebrew-bump:
needs:
- cut
- publish-release
if: ${{ !contains(needs.cut.outputs.tag, '-rc.') }}
uses: ./.github/workflows/homebrew-bump.yml
with:
tag: ${{ needs.cut.outputs.tag }}
secrets: inherit

View File

@ -1,257 +0,0 @@
name: Release
on:
push:
tags:
- 'v*'
workflow_call:
inputs:
tag:
description: Tag to release, for example v1.2.3-rc.0
required: true
type: string
workflow_dispatch:
inputs:
tag:
description: Existing tag to release, for example v1.2.3-rc.0
required: true
type: string
jobs:
resolve-release:
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.resolve.outputs.tag }}
ref: ${{ steps.resolve.outputs.ref }}
steps:
- name: Resolve release tag and ref
id: resolve
run: |
if [[ "${{ github.event_name }}" == "push" ]]; then
release_tag="${{ github.ref_name }}"
release_ref="${{ github.ref }}"
else
release_tag="${{ inputs.tag }}"
release_ref="refs/tags/${{ inputs.tag }}"
fi
echo "tag=$release_tag" >>"$GITHUB_OUTPUT"
echo "ref=$release_ref" >>"$GITHUB_OUTPUT"
create-release:
needs: resolve-release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ needs.resolve-release.outputs.ref }}
- name: Create draft release with auto-generated notes
run: |
if gh release view "${{ needs.resolve-release.outputs.tag }}" --repo "${{ github.repository }}" >/dev/null 2>&1; then
echo "Release ${{ needs.resolve-release.outputs.tag }} already exists."
exit 0
fi
gh release create "${{ needs.resolve-release.outputs.tag }}" \
--draft \
--generate-notes \
--prerelease=${{ contains(needs.resolve-release.outputs.tag, 'rc') }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Why: E2E runs alongside the release for visibility (failures surface as a
# red check on the tag), but it is NOT in `release`'s needs list. Releases
# already take a while and the suite is already a required check on PRs, so
# gating here would mostly just delay shipping without adding much signal.
# Matches the pattern used by noqa's app deploy, which runs E2E with
# continue-on-error so failures are visible but don't block the deploy.
e2e:
needs: resolve-release
uses: ./.github/workflows/e2e.yml
with:
ref: ${{ needs.resolve-release.outputs.ref }}
release:
needs:
- resolve-release
- create-release
strategy:
fail-fast: false
matrix:
include:
- os: macos-15
platform: mac
release_command: ORCA_MAC_RELEASE=1 pnpm exec electron-builder --config config/electron-builder.config.cjs --mac --publish always
eb_cache_path: |
~/Library/Caches/electron
~/Library/Caches/electron-builder
- os: windows-latest
platform: win
release_command: pnpm exec electron-builder --config config/electron-builder.config.cjs --win --publish always
eb_cache_path: |
~\AppData\Local\electron\Cache
~\AppData\Local\electron-builder\Cache
- os: ubuntu-latest
platform: linux
release_command: pnpm exec electron-builder --config config/electron-builder.config.cjs --linux --publish always
eb_cache_path: |
~/.cache/electron
~/.cache/electron-builder
runs-on: ${{ matrix.os }}
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ needs.resolve-release.outputs.ref }}
# pnpm must be on PATH before setup-node so setup-node can locate the store for caching.
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: package.json
cache: pnpm
# Why: release builds hit the same native-module postinstall path as
# PR CI, so keep the pinned node-gyp override here too instead of
# relying on pnpm's bundled copy. Scoped to Linux via runner.os (not
# a specific matrix image) because the failing postinstall has only
# been observed on Linux runners — see run 25081763129. The macOS
# and Windows release jobs exercise the same pnpm install path and
# have not reproduced it, so keep the gate narrow until we know why.
# Using runner.os instead of matrix.os == 'ubuntu-latest' means the
# gate still works if another Linux matrix entry is added later.
- name: Use external node-gyp to avoid pnpm's bundled copy (Linux only)
if: runner.os == 'Linux'
run: |
npm install -g node-gyp@11.5.0
echo "npm_config_node_gyp=$(npm root -g)/node-gyp/bin/node-gyp.js" >> "$GITHUB_ENV"
# Cache the Electron binary + electron-builder tool downloads (notarytool,
# winCodeSign, nsis, squirrel, AppImage). Saves ~30-90s per job, incl. mac.
- name: Cache electron-builder downloads
uses: actions/cache@v4
with:
path: ${{ matrix.eb_cache_path }}
key: electron-builder-${{ matrix.platform }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
electron-builder-${{ matrix.platform }}-
# Why: pnpm install triggers electron's postinstall, which downloads the
# Electron binary from GitHub release assets. GitHub's download CDN
# occasionally returns 504s that fail the whole release. Retry on
# failure so transient network errors don't require a manual re-run.
- name: Install dependencies
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
retry_wait_seconds: 30
command: pnpm install --frozen-lockfile
- name: Verify macOS signing environment
if: matrix.platform == 'mac'
run: node config/scripts/verify-macos-release-env.mjs
env:
CSC_LINK: ${{ secrets.MAC_CERTS }}
CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTS_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
- name: Build app
run: pnpm build:release
# Why: macOS signing secrets (CSC_LINK, CSC_KEY_PASSWORD) must NOT be
# passed to non-macOS builds. electron-builder uses CSC_LINK as the
# code-signing certificate on any platform, so leaking the Apple
# Developer ID cert to the Windows build causes the NSIS installer to
# be signed with an Apple cert whose chain Windows cannot validate,
# breaking the auto-updater with "certificate chain could not be built
# to a trusted root authority" (issue #631).
#
# Why retry: electron-builder downloads NSIS/winCodeSign/squirrel
# binaries and the Electron runtime from GitHub release assets during
# publish. GitHub's download CDN occasionally returns 504s that fail
# the whole release. Retry on failure so transient network errors
# don't require a manual re-run.
- name: Publish release artifacts (macOS)
if: matrix.platform == 'mac'
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
retry_wait_seconds: 30
command: ${{ matrix.release_command }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CSC_LINK: ${{ secrets.MAC_CERTS }}
CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTS_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
- name: Publish release artifacts
if: matrix.platform != 'mac'
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
retry_wait_seconds: 30
command: ${{ matrix.release_command }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish-release:
needs:
- resolve-release
- release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Publish release
# Why: derive `--prerelease` from the tag shape (not from whatever
# electron-builder left the release flagged as). On 2026-04-27,
# electron-builder's publish step flipped `prerelease` back to
# `false` on -rc.N releases, which caused an RC to be marked as
# GitHub's "latest" release and broke release-cut.yml's math.
# Re-asserting here means the final release state is determined
# by the tag — a ground truth electron-builder can't rewrite.
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.resolve-release.outputs.tag }}
run: |
set -euo pipefail
if [[ "$TAG" == *"-rc."* ]]; then
prerelease=true
else
prerelease=false
fi
gh release edit "$TAG" \
--draft=false \
--prerelease="$prerelease" \
--repo "$GITHUB_REPOSITORY"
homebrew-bump:
needs:
- resolve-release
- publish-release
if: ${{ !contains(needs.resolve-release.outputs.tag, '-rc.') }}
uses: ./.github/workflows/homebrew-bump.yml
with:
tag: ${{ needs.resolve-release.outputs.tag }}
secrets: inherit