orca/.github/workflows/release.yml

248 lines
9.1 KiB
YAML

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"