diff --git a/.github/workflows/release-cut.yml b/.github/workflows/release-cut.yml index cf1a2a986..ac272e686 100644 --- a/.github/workflows/release-cut.yml +++ b/.github/workflows/release-cut.yml @@ -703,21 +703,6 @@ jobs: fail-fast: false matrix: include: - # Why: run the signed/notarized desktop DMG build on a Blacksmith - # macOS runner (~2x faster than GitHub macos-15: ~8min vs ~15-18min). - # The 6 vCPU tier is chosen over 12 vCPU because this job is - # dominated by notarization (an Apple-server round-trip, not CPU), - # so 12 vCPU was measured only ~30s faster while costing 2x/min. - # OS stays pinned to macos-15 to match the prior toolchain exactly; - # only the host changes. Blacksmith reports a genuine macOS host, so - # `runner.os == 'macOS'` and `matrix.platform == 'mac'` gates below - # still resolve correctly. Other jobs stay GitHub-hosted. - - os: blacksmith-6vcpu-macos-15 - platform: mac - release_command: node config/scripts/ensure-native-runtime.mjs --runtime=electron && 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 # Why: windows-latest moved to the Windows 2025 / VS 2026 image before # node-gyp could detect VS 18, breaking native dependency install. - os: windows-2022 @@ -778,8 +763,8 @@ jobs: 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. + # Cache the Electron binary + electron-builder tool downloads + # (winCodeSign, nsis, squirrel, AppImage). Saves ~30-90s per job. - name: Cache electron-builder downloads uses: actions/cache@v5 with: @@ -809,22 +794,6 @@ jobs: if: runner.os == 'Linux' run: sudo apt-get update && sudo apt-get install -y python3-gi gir1.2-atspi-2.0 at-spi2-core xclip xdotool rpm - - 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 }} - - # Why: `plutil -lint` accepts duplicate plist keys, but `codesign` - # rejects duplicate entitlements after the expensive app build. - - name: Verify macOS entitlements - if: matrix.platform == 'mac' - run: pnpm verify:macos-entitlements - # Why: telemetry's transport gate (`src/main/telemetry/client.ts:IS_OFFICIAL_BUILD`) # requires the build identity to be the literal string `stable` or `rc`, # substituted by electron-vite's `define` block at build time. Derive @@ -875,35 +844,6 @@ jobs: ORCA_DIAGNOSTICS_TOKEN_URL: https://www.onorca.dev/diagnostics/token ORCA_POSTHOG_WRITE_KEY: ${{ secrets.ORCA_POSTHOG_WRITE_KEY }} - # 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@v4 - 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 (Linux) if: matrix.platform == 'linux-x64' || matrix.platform == 'linux-arm64' uses: nick-fields/retry@v4 @@ -1140,14 +1080,13 @@ jobs: exit 1 fi - # Why post-publish for macOS/Linux: electron-builder packs and uploads - # in a single `--publish always` invocation, so there is no cheap - # insertion point between pack and upload without splitting those steps. - # Running verify last still blocks the bad release: the - # binary is uploaded to the draft, but a failed matrix job blocks - # the `publish-release` job (which depends on `build`) from flipping - # the release from draft → published, so users never see it. A human - # then deletes the draft and re-cuts. + # Why post-publish for Linux: electron-builder packs and uploads in a + # single `--publish always` invocation, so there is no cheap insertion + # point between pack and upload without splitting those steps. Running + # verify last still blocks the bad release: the binary is uploaded to the + # draft, but a failed matrix job blocks `publish-release` from flipping + # the release from draft → published, so users never see it. A human then + # deletes the draft and re-cuts. # # Why this guards against: a misconfigured CI run where # `ORCA_POSTHOG_WRITE_KEY` is unset or the tag fails to classify @@ -1158,10 +1097,153 @@ jobs: - name: Verify telemetry constants present in app.asar run: node config/scripts/verify-telemetry-constants.mjs + build-mac: + needs: + - cut + - create-release + if: needs.cut.outputs.should_release == 'true' + # Why: SignPath validates all jobs that can influence its artifact. Keep + # the non-GitHub-hosted Blacksmith macOS build out of the Windows matrix. + runs-on: blacksmith-6vcpu-macos-15 + permissions: + actions: read + contents: write + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + ref: refs/tags/${{ needs.cut.outputs.tag }} + + - name: Setup pnpm + uses: pnpm/action-setup@v6 + with: + run_install: false + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version-file: package.json + cache: pnpm + + # 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@v5 + with: + path: | + ~/Library/Caches/electron + ~/Library/Caches/electron-builder + key: electron-builder-mac-${{ hashFiles('pnpm-lock.yaml') }} + restore-keys: | + electron-builder-mac- + + # 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@v4 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 30 + command: pnpm install --frozen-lockfile + + - name: Verify macOS signing environment + 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 }} + + # Why: `plutil -lint` accepts duplicate plist keys, but `codesign` + # rejects duplicate entitlements after the expensive app build. + - name: Verify macOS entitlements + run: pnpm verify:macos-entitlements + + # Why: telemetry's transport gate (`src/main/telemetry/client.ts:IS_OFFICIAL_BUILD`) + # requires the build identity to be the literal string `stable` or `rc`, + # substituted by electron-vite's `define` block at build time. + - name: Classify release tag for telemetry build identity + id: tag-classify + shell: bash + env: + TAG: ${{ needs.cut.outputs.tag }} + run: | + set -euo pipefail + if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then + identity=rc + elif [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + identity=stable + else + echo "::error::Tag $TAG does not match stable or rc pattern; refusing to build official artifact" + exit 1 + fi + echo "identity=$identity" >>"$GITHUB_OUTPUT" + echo "Classified $TAG as $identity" + + - name: Build app + run: pnpm build:release + env: + # Why: Vite's web build crossed Node's default old-space ceiling on + # the macOS release runner, leaving v1.4.2-rc.8 as an incomplete draft. + NODE_OPTIONS: --max-old-space-size=4096 + ORCA_BUILD_IDENTITY: ${{ steps.tag-classify.outputs.identity }} + ORCA_DIAGNOSTICS_TOKEN_URL: https://www.onorca.dev/diagnostics/token + ORCA_POSTHOG_WRITE_KEY: ${{ secrets.ORCA_POSTHOG_WRITE_KEY }} + + - name: Publish release artifacts (macOS) + uses: nick-fields/retry@v4 + with: + timeout_minutes: 45 + max_attempts: 3 + retry_wait_seconds: 30 + command: node config/scripts/ensure-native-runtime.mjs --runtime=electron && ORCA_MAC_RELEASE=1 pnpm exec electron-builder --config config/electron-builder.config.cjs --mac --publish always + 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: Verify release remains draft after artifact upload + # Why: the macOS build must never be the actor that exposes a partial + # release. If an uploader or GitHub transition flips draft early, fail + # this job and block publish-release. + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ needs.cut.outputs.tag }} + run: | + set -euo pipefail + releases_json="$(gh api "repos/$GITHUB_REPOSITORY/releases?per_page=100")" + # Why: release upload must validate the draft before it is publicly visible. + draft="$(jq -e -r --arg tag "$TAG" ' + map(select(.tag_name == $tag)) + | if length == 1 and (.[0].draft | type) == "boolean" then (.[0].draft | tostring) else empty end + ' <<<"$releases_json")" || { + echo "::error::Release $TAG was not found in the draft-aware releases list, or its draft state was missing." + exit 1 + } + if [[ "$draft" != "true" ]]; then + echo "::error::Release $TAG was published during the mac artifact upload." + exit 1 + fi + + # Why post-publish for macOS: electron-builder packs and uploads in a + # single `--publish always` invocation, so there is no cheap insertion + # point between pack and upload without splitting those steps. + - name: Verify telemetry constants present in app.asar + run: node config/scripts/verify-telemetry-constants.mjs + publish-release: needs: - cut - build + - build-mac - terminal-rendering-golden runs-on: ubuntu-latest permissions: diff --git a/config/scripts/package-electron-runtime-contract.test.mjs b/config/scripts/package-electron-runtime-contract.test.mjs index 1a947f270..f0ed71f06 100644 --- a/config/scripts/package-electron-runtime-contract.test.mjs +++ b/config/scripts/package-electron-runtime-contract.test.mjs @@ -66,16 +66,19 @@ describe('Electron runtime package contract', () => { release_command ]) ) + const macReleaseCommand = parsedWorkflow.jobs['build-mac'].steps.find( + (step) => step.name === 'Publish release artifacts (macOS)' + ).with.command - expect([...releaseCommands.keys()].sort()).toEqual(['linux-arm64', 'linux-x64', 'mac', 'win']) - for (const command of releaseCommands.values()) { + expect([...releaseCommands.keys()].sort()).toEqual(['linux-arm64', 'linux-x64', 'win']) + for (const command of [...releaseCommands.values(), macReleaseCommand]) { expect(command).toContain('node config/scripts/ensure-native-runtime.mjs --runtime=electron') expect(command).toContain('electron-builder') expect(command.indexOf('ensure-native-runtime')).toBeLessThan( command.indexOf('electron-builder') ) } - expect(releaseCommands.get('mac')).toContain(' && ORCA_MAC_RELEASE=1 ') + expect(macReleaseCommand).toContain(' && ORCA_MAC_RELEASE=1 ') expect(releaseCommands.get('linux-x64')).toContain(' && pnpm exec electron-builder ') expect(releaseCommands.get('linux-x64')).toContain('--linux AppImage deb rpm --x64') expect(releaseCommands.get('linux-arm64')).toContain('ORCA_LINUX_ARM64_RELEASE=1') @@ -96,6 +99,20 @@ describe('Electron runtime package contract', () => { expect(windowsReleaseEntry.os).toBe('windows-2022') }) + it('keeps the Blacksmith macOS release build out of the SignPath Windows matrix', () => { + const releaseWorkflow = parse( + readFileSync(join(projectDir, '.github/workflows/release-cut.yml'), 'utf8') + ) + const buildMatrixRunners = releaseWorkflow.jobs.build.strategy.matrix.include.map( + ({ os }) => os + ) + + expect(buildMatrixRunners).not.toContain('blacksmith-6vcpu-macos-15') + expect(releaseWorkflow.jobs['build-mac']['runs-on']).toBe('blacksmith-6vcpu-macos-15') + expect(releaseWorkflow.jobs['publish-release'].needs).toContain('build') + expect(releaseWorkflow.jobs['publish-release'].needs).toContain('build-mac') + }) + it('preflights SignPath module install before Windows signing side effects', () => { const releaseWorkflow = readFileSync( join(projectDir, '.github/workflows/release-cut.yml'),