perf(release): overlap Windows builds and gate signing on Mac success (#31032)

This commit is contained in:
Neil
2026-10-20 00:21:25 -07:00
committed by GitHub
parent 509f19867c
commit cf6bbd30da
11 changed files with 895 additions and 85 deletions
+84 -65
View File
@@ -128,7 +128,7 @@ jobs:
echo "Resolved $REQUESTED_REF -> $sha"
echo "sha=$sha" >>"$GITHUB_OUTPUT"
# Why: a branch cut before the orcad template landed has none to build or ship.
# Resolve shared build identity while detecting whether this ref ships a template.
orcad-template-support:
needs: resolve-ref
runs-on: ubuntu-latest
@@ -137,13 +137,17 @@ jobs:
contents: read
outputs:
ships: ${{ steps.detect.outputs.ships }}
version: ${{ steps.adhoc.outputs.version }}
commit: ${{ steps.adhoc.outputs.commit }}
name: ${{ steps.adhoc.outputs.name }}
steps:
- name: Checkout the template packager only
- name: Checkout template detection and version scripts
uses: actions/checkout@v6
with:
ref: ${{ needs.resolve-ref.outputs.sha }}
sparse-checkout: |
/config/scripts/packaged-orcad-template.cjs
/config/scripts/
/package.json
/config/tsconfig.relay.json
sparse-checkout-cone-mode: false
persist-credentials: false
@@ -166,6 +170,47 @@ jobs:
echo "::notice::This ref predates the orcad template; the build ships without it."
fi
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: package.json
package-manager-cache: false
- name: Compute adhoc version
id: adhoc
shell: bash
env:
REF: ${{ inputs.ref || github.ref_name }}
LABEL: ${{ inputs.label }}
MAIN_REPO_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
# Why this check: the version scripts are read from the branch being built,
# not from main, so a branch cut before the adhoc channel landed has no
# copy of them. Say that plainly instead of failing with a module-not-found.
for script in adhoc-build-version dev-channel-base-version; do
if [[ ! -f "config/scripts/$script.mjs" ]]; then
echo "::error::$REF has no config/scripts/$script.mjs; rebase it onto a main that has the adhoc channel."
exit 1
fi
done
# Why git tags, not GitHub releases: unpublishing a buggy cut deletes the
# GitHub release and leaves the tag, which still owns that number.
# Releases-only let adhoc sit on a number already taken, so the updater
# would not install it. Empty on failure — the script then falls back
# to package.json.
published="$(GH_TOKEN="$MAIN_REPO_TOKEN" gh api \
"repos/$GITHUB_REPOSITORY/git/matching-refs/tags/v" \
--jq '.[].ref | sub("^refs/tags/"; "")' || true)"
ORCA_PUBLISHED_VERSIONS="$published" ORCA_ADHOC_LABEL="${LABEL:-$REF}" \
node config/scripts/adhoc-build-version.mjs \
>"$RUNNER_TEMP/adhoc-identity.txt"
if ! grep -q '^name=' "$RUNNER_TEMP/adhoc-identity.txt"; then
echo "::error::adhoc-build-version.mjs emitted no release name; $REF's copy of the script is out of sync with this workflow."
exit 1
fi
cat "$RUNNER_TEMP/adhoc-identity.txt" >>"$GITHUB_OUTPUT"
# Design D2, as release-cut does: every desktop build ships the orcad template (server JS plus
# every target's addons), merged from the node-server lanes that qualify each slot at this ref.
# Without it an adhoc build cannot deploy managed orcad to an SSH host. No secrets, and the mac
@@ -199,8 +244,8 @@ jobs:
environment: adhoc-mac-build
outputs:
tag: ${{ steps.release.outputs.tag }}
version: ${{ steps.adhoc.outputs.version }}
head_sha: ${{ steps.adhoc.outputs.head_sha }}
version: ${{ needs.orcad-template-support.outputs.version }}
head_sha: ${{ needs.resolve-ref.outputs.sha }}
published: ${{ steps.publish_live.outcome == 'success' && 'true' || 'false' }}
ships_orcad_template: ${{ needs.orcad-template-support.outputs.ships }}
runs-on: blacksmith-6vcpu-macos-15
@@ -343,42 +388,6 @@ jobs:
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
- name: Compute adhoc version
id: adhoc
shell: bash
env:
REF: ${{ inputs.ref || github.ref_name }}
LABEL: ${{ inputs.label }}
MAIN_REPO_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
# Why this check: the version scripts are read from the branch being built,
# not from main, so a branch cut before the adhoc channel landed has no
# copy of them. Say that plainly instead of failing with a module-not-found.
for script in adhoc-build-version dev-channel-base-version; do
if [[ ! -f "config/scripts/$script.mjs" ]]; then
echo "::error::$REF has no config/scripts/$script.mjs; rebase it onto a main that has the adhoc channel."
exit 1
fi
done
echo "head_sha=$(git rev-parse HEAD)" >>"$GITHUB_OUTPUT"
# Why git tags, not GitHub releases: unpublishing a buggy cut deletes the
# GitHub release and leaves the tag, which still owns that number.
# Releases-only let adhoc sit on a number already taken, so the updater
# would not install it. Empty on failure — the script then falls back
# to package.json.
published="$(GH_TOKEN="$MAIN_REPO_TOKEN" gh api \
"repos/$GITHUB_REPOSITORY/git/matching-refs/tags/v" \
--jq '.[].ref | sub("^refs/tags/"; "")' || true)"
ORCA_PUBLISHED_VERSIONS="$published" ORCA_ADHOC_LABEL="${LABEL:-$REF}" \
node config/scripts/adhoc-build-version.mjs \
>"$RUNNER_TEMP/adhoc-identity.txt"
if ! grep -q '^name=' "$RUNNER_TEMP/adhoc-identity.txt"; then
echo "::error::adhoc-build-version.mjs emitted no release name; $REF's copy of the script is out of sync with this workflow."
exit 1
fi
cat "$RUNNER_TEMP/adhoc-identity.txt" >>"$GITHUB_OUTPUT"
- name: Build app
run: pnpm build:release
env:
@@ -414,9 +423,9 @@ jobs:
shell: bash
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
TAG: v${{ steps.adhoc.outputs.version }}
NAME: ${{ steps.adhoc.outputs.name }}
SHA: ${{ steps.adhoc.outputs.head_sha }}
TAG: v${{ needs.orcad-template-support.outputs.version }}
NAME: ${{ needs.orcad-template-support.outputs.name }}
SHA: ${{ needs.resolve-ref.outputs.sha }}
REF: ${{ inputs.ref || github.ref_name }}
# Via env, not inline `${{ }}`: both land inside a shell string, and an
# expression expanded there is substituted before bash parses the line
@@ -468,8 +477,8 @@ jobs:
# Why: electron-builder's github publisher targets the repo named in the
# config; the token must therefore carry write access to orca-adhoc.
GH_TOKEN: ${{ steps.app_token.outputs.token }}
ORCA_ADHOC_BUILD_VERSION: ${{ steps.adhoc.outputs.version }}
ORCA_BUILD_COMMIT: ${{ steps.adhoc.outputs.commit }}
ORCA_ADHOC_BUILD_VERSION: ${{ needs.orcad-template-support.outputs.version }}
ORCA_BUILD_COMMIT: ${{ needs.orcad-template-support.outputs.commit }}
# beforePack and afterPack fail the package when the template is absent.
ORCA_REQUIRE_ORCAD_TEMPLATE: ${{ needs.orcad-template-support.outputs.ships == 'true' && '1' || '' }}
CSC_LINK: ${{ secrets.MAC_CERTS }}
@@ -516,7 +525,7 @@ jobs:
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
TAG: ${{ steps.release.outputs.tag }}
NAME: ${{ steps.adhoc.outputs.name }}
NAME: ${{ needs.orcad-template-support.outputs.name }}
run: |
set -euo pipefail
# --title again: electron-builder resolves this draft by tag and may
@@ -597,27 +606,37 @@ jobs:
echo "::warning::Could not prune $tag"
done <<<"$stale"
# Why this runs after the mac leg rather than beside it: the tag and version
# are computed inside that job, so until it has run nothing else can name the
# release to upload into. The cost is small enough not to matter — the Windows
# leg measures ~7.5 min (install 2m45, build 35s, NSIS package 3m) against a
# ~9.5 min mac run, which keeps a adhoc run far inside its interval.
#
# Why `./` rather than a pinned `@main`: `uses:` resolves against the ref this
# file itself came from, which for an ordinary dispatch is main. Someone who
# deliberately points the Actions "Use workflow from" picker at a branch
# already gets that branch's copy of this entire file, so this follows the same
# rule instead of inventing a second one.
# Build unsigned Windows artifacts alongside Mac; publication waits below.
build-adhoc-win:
needs: build-adhoc-mac
# Only once the mac release is actually live: there is no release to upload
# into otherwise, and the Windows workflow refuses to create one.
if: needs.build-adhoc-mac.outputs.published == 'true'
needs: [resolve-ref, orcad-template-support, orcad-template]
if: >-
!cancelled() && github.repository == 'stablyai/orca' &&
needs.orcad-template-support.result == 'success' &&
(needs.orcad-template.result == 'success' ||
(needs.orcad-template.result == 'skipped' &&
needs.orcad-template-support.outputs.ships == 'false'))
uses: ./.github/workflows/dev-channel-win-build.yml
secrets: inherit
with:
channel: adhoc
tag: v${{ needs.orcad-template-support.outputs.version }}
ref: ${{ needs.resolve-ref.outputs.sha }}
version: ${{ needs.orcad-template-support.outputs.version }}
orcad_template: ${{ needs.orcad-template-support.outputs.ships == 'true' }}
stage_only: true
# Upload only after Mac succeeds; an unsigned build may overlap its notarization.
publish-adhoc-win:
needs: [build-adhoc-mac, build-adhoc-win]
if: >-
!cancelled() && needs.build-adhoc-mac.result == 'success' &&
needs.build-adhoc-win.result == 'success' &&
needs.build-adhoc-mac.outputs.published == 'true'
permissions:
contents: read
actions: read
uses: ./.github/workflows/dev-channel-win-publish.yml
secrets: inherit
with:
channel: adhoc
tag: ${{ needs.build-adhoc-mac.outputs.tag }}
ref: ${{ needs.build-adhoc-mac.outputs.head_sha }}
version: ${{ needs.build-adhoc-mac.outputs.version }}
orcad_template: ${{ needs.build-adhoc-mac.outputs.ships_orcad_template == 'true' }}
+52 -9
View File
@@ -5,11 +5,8 @@ name: Dev Channel Windows Build
# Windows artifact can be rebuilt for an existing tag without paying for the mac
# leg's packaging and notarization again.
#
# Each mac workflow calls this as a `needs:`-gated job once its release is live,
# passing the tag it created. Both legs land in that one release, so a tag
# carries every platform it managed to build. Measured cost of the Windows leg is
# ~7.5 min (install 2m45, build 35s, NSIS package 3m) against a mac run of ~9.5
# min, so running it after the mac job keeps an hourly well inside its cron.
# Adhoc can stage Windows alongside macOS, then publish only after macOS succeeds.
# Hourly, daily, and manual dispatches keep packaging and publishing together.
#
# Why unsigned: Windows release installers are signed by SignPath *after*
# packaging, and release-cut budgets 1h + 4h for those approval waits. That does
@@ -27,9 +24,8 @@ name: Dev Channel Windows Build
# installer run. Every way out — to another dev build, or back to Stable — works
# through the in-app updater. The picker offers a download for exactly that jump.
#
# Called as a job by each mac workflow once its release is live, and separately
# dispatchable by hand to rebuild a Windows artifact for an existing tag without
# re-running the mac leg's twenty minutes of packaging and notarization:
# Mac workflows call this directly or stage artifacts for a later publish job.
# It is also dispatchable to rebuild Windows for an existing tag:
#
# gh workflow run dev-channel-win-build.yml --ref main \
# -f channel=adhoc -f tag=v1.4.178-adhoc.20260819010203 \
@@ -47,7 +43,7 @@ on:
required: true
type: string
tag:
description: Existing release tag in the channel repo
description: Release tag for staged or published artifacts
required: true
type: string
ref:
@@ -58,6 +54,11 @@ on:
description: Version to package, without the leading v
required: true
type: string
stage_only:
description: Package unsigned artifacts for a later gated publish job
required: false
type: boolean
default: false
orcad_template:
description: Ship the orcad-template artifact the calling run built
required: false
@@ -276,6 +277,7 @@ jobs:
# Why the token is minted here and not at the top: installation tokens live
# one hour and nothing before this point writes anything.
- name: Mint dev channel repo token
if: ${{ !inputs.stage_only }}
id: app_token
uses: actions/create-github-app-token@v2
with:
@@ -289,6 +291,7 @@ jobs:
# while this was building, that would mint a fresh, untitled, Windows-only
# release. Check first and fail instead.
- name: Confirm the target release still exists
if: ${{ !inputs.stage_only }}
shell: bash
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
@@ -300,6 +303,7 @@ jobs:
fi
- name: Publish Windows artifacts
if: ${{ !inputs.stage_only }}
uses: nick-fields/retry@v4
with:
# 30 is the pack + upload budget; there is no notary queue on this leg.
@@ -323,6 +327,7 @@ jobs:
# release carrying the installer but not the manifest is one the picker
# offers and the update 404s on, so assert both.
- name: Verify Windows update manifest published
if: ${{ !inputs.stage_only }}
shell: bash
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
@@ -338,3 +343,41 @@ jobs:
fi
done
echo "Windows artifacts verified on $TAG."
- name: Package Windows artifacts without publishing
if: inputs.stage_only
uses: nick-fields/retry@v4
with:
timeout_minutes: 30
max_attempts: 2
retry_wait_seconds: 30
command: node config/scripts/ensure-native-runtime.mjs --runtime=electron; if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }; pnpm exec electron-builder --config config/electron-builder.config.cjs --win --publish never
env:
ORCA_BUILD_COMMIT: ${{ inputs.ref }}
ORCA_REQUIRE_ORCAD_TEMPLATE: ${{ inputs.orcad_template && '1' || '' }}
- name: Validate staged Windows artifacts
if: inputs.stage_only
shell: bash
run: |
set -euo pipefail
for required in orca-windows-setup.exe orca-windows-setup.exe.blockmap latest.yml; do
if [[ ! -s "dist/$required" ]]; then
echo "::error::Missing or empty staged Windows artifact: $required"
exit 1
fi
done
- name: Stage Windows artifacts for gated publishing
if: inputs.stage_only
uses: actions/upload-artifact@v7
with:
name: dev-channel-windows-${{ inputs.tag }}
path: |
dist/orca-windows-setup.exe
dist/orca-windows-setup.exe.blockmap
dist/latest.yml
if-no-files-found: error
retention-days: 3
compression-level: 0
overwrite: true
@@ -0,0 +1,107 @@
name: Dev Channel Windows Publish
on:
workflow_call:
inputs:
channel:
description: Dev channel whose live release receives the staged artifacts
required: true
type: string
tag:
description: Release tag used when staging the Windows artifacts
required: true
type: string
permissions:
contents: read
actions: read
concurrency:
group: dev-channel-win-publish-${{ inputs.tag }}
cancel-in-progress: false
jobs:
publish-win:
if: github.repository == 'stablyai/orca'
environment: adhoc-mac-build
runs-on: ubuntu-latest
timeout-minutes: 15
env:
CHANNEL: ${{ inputs.channel }}
TAG: ${{ inputs.tag }}
steps:
- name: Vet the requested inputs
shell: bash
run: |
set -euo pipefail
case "$CHANNEL" in
hourly|daily|adhoc) ;;
*) echo "::error::Unknown dev channel '$CHANNEL'."; exit 1 ;;
esac
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-${CHANNEL}\.[0-9]+$ ]]; then
echo "::error::Tag '$TAG' is not a $CHANNEL release tag."
exit 1
fi
# No run-id: artifacts must belong to this run, never another dispatch.
- name: Download the staged Windows artifacts
uses: actions/download-artifact@v8
with:
name: dev-channel-windows-${{ inputs.tag }}
path: staged-windows
- name: Validate staged Windows artifacts
shell: bash
run: |
set -euo pipefail
for required in orca-windows-setup.exe orca-windows-setup.exe.blockmap latest.yml; do
if [[ ! -s "staged-windows/$required" ]]; then
echo "::error::Missing or empty staged Windows artifact: $required"
exit 1
fi
done
- name: Mint dev channel repo token
id: app_token
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-${{ inputs.channel }}
- name: Confirm the target release is live
shell: bash
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: |
set -euo pipefail
draft="$(gh release view "$TAG" --repo "stablyai/orca-$CHANNEL" --json isDraft --jq '.isDraft')"
if [[ "$draft" != false ]]; then
echo "::error::Release $TAG is not live; refusing to publish Windows artifacts."
exit 1
fi
- name: Publish Windows artifacts
uses: nick-fields/retry@v4
with:
timeout_minutes: 5
max_attempts: 2
retry_wait_seconds: 10
command: gh release upload "$TAG" staged-windows/orca-windows-setup.exe staged-windows/orca-windows-setup.exe.blockmap staged-windows/latest.yml --clobber --repo "stablyai/orca-$CHANNEL"
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
- name: Verify Windows update manifest published
shell: bash
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
run: |
set -euo pipefail
assets="$(gh release view "$TAG" --repo "stablyai/orca-$CHANNEL" --json assets --jq '.assets[].name')"
for required in latest.yml orca-windows-setup.exe orca-windows-setup.exe.blockmap; do
if ! grep -qx "$required" <<<"$assets"; then
echo "::error::$TAG is missing $required; Windows could not install this build."
exit 1
fi
done
+22 -3
View File
@@ -1227,6 +1227,7 @@ jobs:
- orcad-template
- release-preflight
- release-javascript
- build-mac
# Why not the implicit success(): a tag without the orcad template skips that job on purpose.
if: >-
!cancelled() &&
@@ -1235,6 +1236,7 @@ jobs:
needs.create-release.result == 'success' &&
needs.release-preflight.result == 'success' &&
needs.release-javascript.result == 'success' &&
needs.build-mac.result == 'success' &&
(needs.orcad-template.result == 'success' ||
(needs.orcad-template.result == 'skipped' && needs.cut.outputs.ships_orcad_template == 'false'))
env:
@@ -1309,7 +1311,7 @@ jobs:
run: |
set -euo pipefail
git fetch --no-tags --depth=1 origin "$WORKFLOW_SHA"
git checkout "$WORKFLOW_SHA" -- config/scripts/assert-github-release-is-draft.mjs
git checkout "$WORKFLOW_SHA" -- config/scripts/assert-github-release-is-draft.mjs config/scripts/run-release-mac-build-workflow.mjs
- name: Restore composite actions from the workflow ref
shell: bash
@@ -1689,9 +1691,26 @@ jobs:
path: signing-stage/**
if-no-files-found: error
# Unsigned packaging overlaps macOS; only successful macOS builds may consume signing quota.
- name: Require macOS success before Windows signing
id: mac-signing-gate
if: matrix.platform == 'win' && github.run_attempt == 1
shell: bash
run: node config/scripts/run-release-mac-build-workflow.mjs wait
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_MAC_BUILD_REF: ${{ github.ref_name }}
RELEASE_MAC_BUILD_RELEASE_RUN_ID: ${{ github.run_id }}
RELEASE_MAC_BUILD_RUN_ID: ${{ needs.build-mac.outputs.run_id }}
RELEASE_MAC_BUILD_WORKFLOW_SHA: ${{ needs.build-mac.outputs.workflow_sha }}
RELEASE_MAC_BUILD_RUN_ATTEMPT: ${{ needs.build-mac.outputs.run_attempt }}
RELEASE_MAC_BUILD_WAIT_DEADLINE_MS: ${{ needs.build-mac.outputs.wait_deadline_ms }}
RELEASE_MAC_BUILD_TAG: ${{ needs.cut.outputs.tag }}
RELEASE_MAC_BUILD_WORKFLOW: release-mac-build.yml
- name: Submit inner binaries signing request
id: submit-inner-signing
if: matrix.platform == 'win' && github.run_attempt == 1 && steps.upload-unsigned-inner.outcome == 'success'
if: matrix.platform == 'win' && github.run_attempt == 1 && steps.mac-signing-gate.outcome == 'success' && steps.upload-unsigned-inner.outcome == 'success'
uses: signpath/github-action-submit-signing-request@v2
with:
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
@@ -1927,7 +1946,7 @@ jobs:
# so the release job waits while the signing request is approved in UI.
- name: Submit Windows installer signing request
id: submit-signing-request
if: matrix.platform == 'win' && github.run_attempt == 1
if: matrix.platform == 'win' && github.run_attempt == 1 && steps.mac-signing-gate.outcome == 'success' && steps.upload-unsigned-windows-installer.outcome == 'success'
uses: signpath/github-action-submit-signing-request@v2
with:
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}