mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 09:27:29 +00:00
* chore(endpoints): call orca.dev for feedback, plugin lists and update notes The site moved to orca.dev. Feedback, connection diagnostics, plugin install stats, the plugin kill list, update nudges and the changelog feed now use orca.dev, as do the diagnostics token URL baked into builds and the release download mirror (downloads.orca.dev). The old hosts serve the same paths, so released builds keep working. Skill share links on share.orca.dev are accepted, and the mobile scanner recognizes the orca.dev Android download code. The old hosts stay accepted. * chore(ci): leave the release JavaScript benchmark workflow unchanged Editing it triggers the benchmark, whose parity check currently fails on arm64 because max-quality brotli output for the mobile page differs by architecture. The benchmark only needs some diagnostics URL, and the old host still serves it.
418 lines
20 KiB
YAML
418 lines
20 KiB
YAML
name: Hourly macOS Dev Build
|
||
|
||
# Publish signed/notarized main builds to orca-hourly; notarization preserves TCC grants across updates. PR/release gates own validation. Separate repo avoids hourly tags crowding stable/RC Atom entries. Provision shared HOURLY_RELEASE_APP_ID/private key with setup-hourly-release-token.sh and App write access there. Refresh one-hour token after read-only build; a retry may still outlive it, leaving only an invisible draft whose build number remains reserved until manifests validate.
|
||
|
||
on:
|
||
schedule:
|
||
# Top of every hour. Skipped automatically when main has not moved.
|
||
- cron: '0 * * * *'
|
||
workflow_dispatch:
|
||
inputs:
|
||
force:
|
||
description: Build even if main has not moved since the last hourly
|
||
required: false
|
||
default: false
|
||
type: boolean
|
||
|
||
permissions:
|
||
contents: read
|
||
|
||
concurrency:
|
||
group: hourly-mac-build
|
||
cancel-in-progress: false
|
||
|
||
env:
|
||
HOURLY_REPO: stablyai/orca-hourly
|
||
# Keep ~3 days of history so a regression can be bisected across a weekend.
|
||
HOURLY_RETAIN_COUNT: 72
|
||
|
||
jobs:
|
||
# Avoid occupying the limited Mac pool when main has not moved.
|
||
preflight:
|
||
if: github.repository == 'stablyai/orca'
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 5
|
||
outputs:
|
||
should_build: ${{ steps.freshness.outputs.should_build }}
|
||
head_sha: ${{ steps.freshness.outputs.head_sha }}
|
||
steps:
|
||
- name: Mint hourly 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-hourly
|
||
permission-contents: read
|
||
|
||
- name: Check whether main moved since the last hourly
|
||
id: freshness
|
||
shell: bash
|
||
env:
|
||
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||
MAIN_REPO_TOKEN: ${{ github.token }}
|
||
FORCED: ${{ github.event_name == 'workflow_dispatch' && inputs.force }}
|
||
run: |
|
||
set -euo pipefail
|
||
head_sha="$(GH_TOKEN="$MAIN_REPO_TOKEN" gh api "repos/$GITHUB_REPOSITORY/commits/main" --jq .sha)"
|
||
[[ "$head_sha" =~ ^[0-9a-f]{40}$ ]] || { echo "::error::Could not resolve main"; exit 1; }
|
||
echo "head_sha=$head_sha" >>"$GITHUB_OUTPUT"
|
||
if [[ "$FORCED" == "true" ]]; then
|
||
echo "should_build=true" >>"$GITHUB_OUTPUT"
|
||
echo "Forced dispatch; building $head_sha."
|
||
exit 0
|
||
fi
|
||
# Drafts never shipped; exclude them when comparing the previous hourly’s source commit.
|
||
last_body="$(gh release list --repo "$HOURLY_REPO" --limit 20 --json tagName,isDraft \
|
||
--jq 'map(select(.isDraft | not)) | .[0].tagName // empty' 2>/dev/null || true)"
|
||
if [[ -z "$last_body" ]]; then
|
||
echo "should_build=true" >>"$GITHUB_OUTPUT"
|
||
echo "No prior hourly release found; building $head_sha."
|
||
exit 0
|
||
fi
|
||
last_sha="$(gh release view "$last_body" --repo "$HOURLY_REPO" --json body \
|
||
--jq '.body | capture("commit `(?<sha>[0-9a-f]{7,40})`") | .sha' 2>/dev/null || true)"
|
||
if [[ -n "$last_sha" && "$head_sha" == "$last_sha"* ]]; then
|
||
echo "should_build=false" >>"$GITHUB_OUTPUT"
|
||
echo "main is unchanged since $last_body ($last_sha); skipping."
|
||
else
|
||
echo "should_build=true" >>"$GITHUB_OUTPUT"
|
||
echo "main moved to $head_sha (last hourly built $last_sha); building."
|
||
fi
|
||
|
||
# As release-cut does: every SSH host runs orcad, deployed from the template this build ships.
|
||
orcad-template:
|
||
needs: preflight
|
||
if: needs.preflight.outputs.should_build == 'true'
|
||
permissions:
|
||
contents: read
|
||
actions: read
|
||
uses: ./.github/workflows/node-server-tests.yml
|
||
with:
|
||
ref: ${{ needs.preflight.outputs.head_sha }}
|
||
build_template: true
|
||
fast_runners: false
|
||
|
||
build-hourly-mac:
|
||
needs:
|
||
- preflight
|
||
- orcad-template
|
||
if: needs.preflight.outputs.should_build == 'true'
|
||
outputs:
|
||
tag: ${{ steps.release.outputs.tag }}
|
||
version: ${{ steps.hourly.outputs.version }}
|
||
head_sha: ${{ needs.preflight.outputs.head_sha }}
|
||
published: ${{ steps.publish_live.outcome == 'success' && 'true' || 'false' }}
|
||
# Hosted macos-15 cannot boot the packaged x64 terminal host that afterPack verifies.
|
||
runs-on: blacksmith-6vcpu-macos-15
|
||
# Allow 150min for install 3×10 + publish 2×45 + ~25 setup/build/verify; outer timeout otherwise interrupts cleanup.
|
||
timeout-minutes: 150
|
||
env:
|
||
NODE_OPTIONS: --max-old-space-size=4096
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@v6
|
||
with:
|
||
ref: ${{ needs.preflight.outputs.head_sha }}
|
||
# Version helpers only read HEAD; published versions come from git tags.
|
||
fetch-depth: 1
|
||
# Read this repo without persisted credentials; write orca-hourly via env App token (zizmor: artipacked).
|
||
persist-credentials: false
|
||
|
||
- name: Mint hourly 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-hourly
|
||
|
||
- name: Setup pnpm
|
||
uses: ./.github/actions/setup-pnpm
|
||
|
||
- name: Setup Node.js
|
||
uses: actions/setup-node@v6
|
||
with:
|
||
node-version-file: package.json
|
||
check-latest: true
|
||
cache: pnpm
|
||
cache-dependency-path: |
|
||
pnpm-lock.yaml
|
||
mobile/pnpm-lock.yaml
|
||
|
||
# Cache bundled host Node by exact pin without restore keys.
|
||
- name: Locate the pinned Node runtime cache
|
||
shell: bash
|
||
run: echo "ORCA_NODE_RUNTIME_CACHE_DIR=$HOME/Library/Caches/orca-node-runtime" >>"$GITHUB_ENV"
|
||
|
||
- name: Cache the pinned Node runtime
|
||
uses: actions/cache@v5
|
||
with:
|
||
path: ~/Library/Caches/orca-node-runtime
|
||
key: orca-node-runtime-mac-${{ hashFiles('src/shared/node-runtime-pin.ts') }}
|
||
|
||
- 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-
|
||
|
||
# Install both x64/arm64 optional natives for both macOS packages.
|
||
- name: Install dependencies
|
||
uses: nick-fields/retry@v4
|
||
with:
|
||
timeout_minutes: 10
|
||
max_attempts: 3
|
||
retry_wait_seconds: 30
|
||
command: pnpm install --frozen-lockfile --cpu=current,x64,arm64
|
||
|
||
# Build required out/mobile-web from mobile’s React Native/Expo install.
|
||
- uses: ./.github/actions/install-mobile-dependencies
|
||
|
||
# Require cert before expensive build; signing enables installation over existing Orca.
|
||
- name: Verify macOS signing environment
|
||
run: node config/scripts/desktop-distribution/macos/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: Compute hourly version
|
||
id: hourly
|
||
shell: bash
|
||
env:
|
||
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||
MAIN_REPO_TOKEN: ${{ github.token }}
|
||
run: |
|
||
set -euo pipefail
|
||
# The script continues numbering for its resolved base version; stranded drafts still reserve numbers,
|
||
# although freshness checks exclude them because they never shipped.
|
||
names="$(gh release list --repo "$HOURLY_REPO" --limit 200 --json name \
|
||
--jq '.[].name // empty')"
|
||
# Tags survive release deletion, preventing updater version regression. Use a separate token because
|
||
# GH_TOKEN’s App grant is scoped to the channel repo; lookup failure falls back to package.json.
|
||
# 2026-09-14: deleting v1.4.202 made release-list fallback ship 1.4.202-hourly below installed 1.4.203-hourly.
|
||
main_tags="$(GH_TOKEN="$MAIN_REPO_TOKEN" gh api \
|
||
"repos/$GITHUB_REPOSITORY/git/matching-refs/tags/v" \
|
||
--jq '.[].ref | sub("^refs/tags/"; "")' || true)"
|
||
# Already-shipped channel tags also floor versions: never follow installed 1.4.203-hourly with 1.4.202-hourly.
|
||
channel_tags="$(gh release list --repo "$HOURLY_REPO" --limit 200 --json tagName \
|
||
--jq '.[].tagName' || true)"
|
||
published="$main_tags"$'\n'"$channel_tags"
|
||
echo "Published version sources: $(grep -c . <<<"$main_tags" || true) main tags, $(grep -c . <<<"$channel_tags" || true) channel tags"
|
||
ORCA_PUBLISHED_VERSIONS="$published" ORCA_HOURLY_RELEASE_NAMES="$names" \
|
||
node "$(if [ -f config/scripts/release/versioning/hourly-build-version.mjs ]; then printf %s config/scripts/release/versioning/hourly-build-version.mjs; else printf %s config/scripts/hourly-build-version.mjs; fi)" \
|
||
>"$RUNNER_TEMP/hourly-identity.txt"
|
||
grep -E '^(version|build_number)=' "$RUNNER_TEMP/hourly-identity.txt"
|
||
# Dispatch YAML can be newer than the pinned source’s version script; reject missing output before publishing an untitled release.
|
||
if ! grep -q '^name=' "$RUNNER_TEMP/hourly-identity.txt"; then
|
||
echo "::error::hourly-build-version.mjs emitted no release name; this workflow and main's copy of the script are out of sync."
|
||
exit 1
|
||
fi
|
||
cat "$RUNNER_TEMP/hourly-identity.txt" >>"$GITHUB_OUTPUT"
|
||
|
||
- name: Build app
|
||
run: pnpm build:release
|
||
env:
|
||
NODE_OPTIONS: --max-old-space-size=4096
|
||
# Unset dev identity so official-only stable/rc telemetry stays silent.
|
||
ORCA_DIAGNOSTICS_TOKEN_URL: https://orca.dev/diagnostics/token
|
||
|
||
# After the app build so nothing that cleans out/ can drop it; electron-builder ships it.
|
||
- name: Download the orcad deployment template
|
||
uses: actions/download-artifact@v8
|
||
with:
|
||
name: orcad-template
|
||
path: out/orcad-template
|
||
|
||
# Refresh write token after build to give long notary/upload work a full hour.
|
||
- name: Re-mint hourly repo token for publish
|
||
id: app_token_publish
|
||
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-hourly
|
||
|
||
- name: Create hourly release
|
||
id: release
|
||
shell: bash
|
||
env:
|
||
GH_TOKEN: ${{ steps.app_token_publish.outputs.token }}
|
||
TAG: v${{ steps.hourly.outputs.version }}
|
||
NAME: ${{ steps.hourly.outputs.name }}
|
||
SHA: ${{ needs.preflight.outputs.head_sha }}
|
||
run: |
|
||
set -euo pipefail
|
||
# Freshness parses the 12-character source SHA from the body even though titles show seven.
|
||
short_sha="${SHA:0:12}"
|
||
# Precreate a known tag as draft so builder cannot infer it from package.json.
|
||
# Keep incomplete assets invisible until manifest verification, even if timeout kills the job without cleanup.
|
||
gh release create "$TAG" \
|
||
--repo "$HOURLY_REPO" \
|
||
--title "$NAME" \
|
||
--draft \
|
||
--notes "Automated hourly macOS and Windows dev build from commit \`$short_sha\`.
|
||
|
||
Built from [\`stablyai/orca@$short_sha\`](https://github.com/stablyai/orca/commit/$SHA).
|
||
|
||
**Unvetted.** No tests ran. The macOS build is signed and notarized like a
|
||
release, so it installs through Orca's in-app updater and opens from a manual
|
||
download without a Gatekeeper prompt — but nothing here has been reviewed.
|
||
|
||
**Windows builds are unsigned.** They install and update normally once you
|
||
are on one, but a signed Stable or RC build cannot install one through the
|
||
in-app updater — download \`orca-windows-setup.exe\` below and run it once
|
||
(SmartScreen will warn about an unknown publisher). Every later switch,
|
||
including back to Stable, works in-app from there."
|
||
echo "tag=$TAG" >>"$GITHUB_OUTPUT"
|
||
|
||
- name: Publish hourly macOS artifacts
|
||
uses: nick-fields/retry@v4
|
||
with:
|
||
# Allow two 45min pack/notarize/upload attempts; next hourly picks the same commit rather than paying for a third.
|
||
timeout_minutes: 45
|
||
max_attempts: 2
|
||
retry_wait_seconds: 30
|
||
command: node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON "$(if [ -f config/scripts/orcad-distribution/prebuilds/pinned-node-downloads.mjs ]; then printf %s config/scripts/orcad-distribution/prebuilds/pinned-node-downloads.mjs; else printf %s config/scripts/pinned-node-downloads.mjs; fi)" darwin-arm64 darwin-x64 && node "$(if [ -f config/scripts/native-build/node-addons/ensure-native-runtime.mjs ]; then printf %s config/scripts/native-build/node-addons/ensure-native-runtime.mjs; else printf %s config/scripts/ensure-native-runtime.mjs; fi)" --runtime=electron && if { [ -f config/scripts/desktop-distribution/macos/build-mac-notarization-overlap.mjs ] || [ -f config/scripts/build-mac-notarization-overlap.mjs ]; }; then ORCA_MAC_HOURLY=1 node "$(if [ -f config/scripts/desktop-distribution/macos/build-mac-notarization-overlap.mjs ]; then printf %s config/scripts/desktop-distribution/macos/build-mac-notarization-overlap.mjs; else printf %s config/scripts/build-mac-notarization-overlap.mjs; fi)" --publish always; else ORCA_MAC_HOURLY=1 pnpm exec electron-builder --config config/electron-builder.config.cjs --mac --publish always; fi
|
||
env:
|
||
# Give publisher token write access to configured orca-hourly repo.
|
||
GH_TOKEN: ${{ steps.app_token_publish.outputs.token }}
|
||
ORCA_HOURLY_BUILD_VERSION: ${{ steps.hourly.outputs.version }}
|
||
ORCA_BUILD_COMMIT: ${{ steps.hourly.outputs.commit }}
|
||
# beforePack and afterPack fail the package when the template is absent.
|
||
ORCA_REQUIRE_ORCAD_TEMPLATE: '1'
|
||
CSC_LINK: ${{ secrets.MAC_CERTS }}
|
||
CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTS_PASSWORD }}
|
||
# Require Apple id/app-specific password/team for notarization, separate from signing cert.
|
||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||
|
||
# Builder cannot write this field, so older macOS updaters would install a build that won't launch.
|
||
- name: Gate the update manifest on the minimum macOS
|
||
shell: bash
|
||
env:
|
||
GH_TOKEN: ${{ steps.app_token_publish.outputs.token }}
|
||
TAG: ${{ steps.release.outputs.tag }}
|
||
run: |
|
||
set -euo pipefail
|
||
node config/scripts/desktop-distribution/macos/gate-mac-update-manifests.mjs dist
|
||
gh release upload "$TAG" dist/*-mac.yml --repo "$HOURLY_REPO" --clobber
|
||
|
||
# Verify latest-mac.yml before exposing release; missing manifests cause picker download 404s.
|
||
- name: Verify update manifest published
|
||
shell: bash
|
||
env:
|
||
GH_TOKEN: ${{ steps.app_token_publish.outputs.token }}
|
||
TAG: ${{ steps.release.outputs.tag }}
|
||
run: |
|
||
set -euo pipefail
|
||
assets="$(gh release view "$TAG" --repo "$HOURLY_REPO" --json assets --jq '.assets[].name')"
|
||
echo "Published assets:"
|
||
echo "$assets"
|
||
# Drafts are already invisible; let failure cleanup delete them so set -e cannot mask this explicit failure.
|
||
for required in latest-mac.yml; do
|
||
if ! grep -qx "$required" <<<"$assets"; then
|
||
echo "::error::Hourly draft $TAG is missing $required; the updater could not install it."
|
||
exit 1
|
||
fi
|
||
done
|
||
if ! grep -q '\.zip$' <<<"$assets"; then
|
||
echo "::error::Hourly draft $TAG has no ZIP artifact for the updater to download."
|
||
exit 1
|
||
fi
|
||
|
||
# Publish only after manifests verify so picker never offers incomplete assets.
|
||
- name: Publish the verified release
|
||
id: publish_live
|
||
shell: bash
|
||
env:
|
||
GH_TOKEN: ${{ steps.app_token_publish.outputs.token }}
|
||
TAG: ${{ steps.release.outputs.tag }}
|
||
NAME: ${{ steps.hourly.outputs.name }}
|
||
run: |
|
||
set -euo pipefail
|
||
# Reassert the picker title after electron-builder may rewrite the tag’s draft on upload.
|
||
gh release edit "$TAG" --repo "$HOURLY_REPO" --draft=false --prerelease --title "$NAME"
|
||
echo "Published $TAG as \"$NAME\""
|
||
|
||
# Delete failed/cancelled drafts but never publish_live releases, including after prune failure; outer timeouts run no cleanup.
|
||
- name: Discard the draft release on failure
|
||
if: >-
|
||
(failure() || cancelled()) && steps.release.outputs.tag != '' &&
|
||
steps.publish_live.outcome != 'success'
|
||
shell: bash
|
||
env:
|
||
GH_TOKEN: ${{ steps.app_token_publish.outputs.token }}
|
||
TAG: ${{ steps.release.outputs.tag }}
|
||
run: |
|
||
set -uo pipefail
|
||
# No --cleanup-tag: an unpublished draft never created a git tag.
|
||
echo "Run failed before publish; discarding draft $TAG"
|
||
gh release delete "$TAG" --repo "$HOURLY_REPO" --yes ||
|
||
echo "::warning::Could not discard draft $TAG; remove it manually."
|
||
|
||
- name: Prune old hourly releases
|
||
# Prune only after live publish; failed runs leave draft disposal to cleanup.
|
||
if: steps.publish_live.outcome == 'success'
|
||
shell: bash
|
||
env:
|
||
GH_TOKEN: ${{ steps.app_token_publish.outputs.token }}
|
||
# Exclude the just-published tag from retention pruning.
|
||
TAG: ${{ steps.release.outputs.tag }}
|
||
run: |
|
||
set -euo pipefail
|
||
# Delete pruned tags too; exclude drafts owned by failure cleanup. Rank by publishedAt/tagName:
|
||
# createdAt ties (bulk import/recreation) can evict the just-published release. Partition $TAG to
|
||
# the front before slicing so it owns exactly one retained seat without under-pruning or keeping retain+1.
|
||
jq_filter='map(select(.isDraft | not)) | sort_by(.publishedAt // "", .tagName) | reverse'
|
||
if [[ -n "${TAG:-}" ]]; then
|
||
jq_filter+=" | (map(select(.tagName == \"${TAG//\"/\\\"}\")) + map(select(.tagName != \"${TAG//\"/\\\"}\")))"
|
||
fi
|
||
jq_filter+=" | .[${HOURLY_RETAIN_COUNT}:] | .[].tagName"
|
||
stale="$(gh release list --repo "$HOURLY_REPO" --limit 200 --json tagName,publishedAt,isDraft \
|
||
--jq "$jq_filter")"
|
||
if [[ -z "$stale" ]]; then
|
||
echo "Nothing to prune; at or under $HOURLY_RETAIN_COUNT retained builds."
|
||
exit 0
|
||
fi
|
||
while read -r tag; do
|
||
[[ -n "$tag" ]] || continue
|
||
# Belt-and-suspenders: partition above should already exclude $TAG.
|
||
if [[ -n "${TAG:-}" && "$tag" == "$TAG" ]]; then
|
||
echo "::warning::Prune list still included just-published $tag after protect; skipping delete."
|
||
continue
|
||
fi
|
||
echo "Pruning $tag"
|
||
gh release delete "$tag" --repo "$HOURLY_REPO" --yes --cleanup-tag || \
|
||
echo "::warning::Could not prune $tag"
|
||
done <<<"$stale"
|
||
|
||
# Wait for mac tag/version before Windows upload; local ./ workflow consistently follows the dispatch ref.
|
||
build-hourly-win:
|
||
needs: build-hourly-mac
|
||
# Publish Windows only into the existing live mac release.
|
||
if: needs.build-hourly-mac.outputs.published == 'true'
|
||
permissions:
|
||
contents: read
|
||
actions: read
|
||
uses: ./.github/workflows/dev-channel-win-build.yml
|
||
secrets: inherit
|
||
with:
|
||
channel: hourly
|
||
tag: ${{ needs.build-hourly-mac.outputs.tag }}
|
||
ref: ${{ needs.build-hourly-mac.outputs.head_sha }}
|
||
version: ${{ needs.build-hourly-mac.outputs.version }}
|
||
orcad_template: true
|
||
fast_release: false
|