{{ message }}
Repository navigation
467 lines (433 loc) · 21.8 KB
/
Copy pathdaily-mac-build.yml
File metadata and controls
467 lines (433 loc) · 21.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
name: Daily macOS Dev Build
# Publish daily main builds to orca-daily, separate from hourly and stable/RC feeds. Sign/notarize for update-preserved macOS TCC grants; PR/release pipelines own blocking validation, with E2E detached after publish. Cron 18:15 UTC means 10:15 PST/11:15 PDT, offset from hourly. Use the shared hourly App with write access to orca-daily (setup-hourly-release-token.sh/setup-daily-release-repo.sh). Mint tokens again after build and upload because 2×45min retry can exceed their 1h life. Keep releases draft until manifests verify; drafts retain build numbers.
on:
schedule:
# Once a day, late morning Pacific. Single cron — no DST twin, no clock gate.
- cron: '15 18 * * *'
workflow_dispatch:
inputs:
force:
description: Build even if main has not moved since the last daily
required: false
default: false
type: boolean
permissions:
contents: read
concurrency:
group: daily-mac-build
cancel-in-progress: false
env:
DAILY_REPO: stablyai/orca-daily
# Keep ~30 days so a regression can be bisected without holding a full year.
DAILY_RETAIN_COUNT: 30
jobs:
# Why: the app and the orcad template it ships must come from one commit, so resolve main once.
resolve-main:
if: github.repository == 'stablyai/orca'
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 5
outputs:
head_sha: ${{ steps.resolve.outputs.head_sha }}
steps:
- name: Resolve main
id: resolve
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
head_sha="$(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"
# As release-cut does: every SSH host runs orcad, deployed from the template this build ships.
orcad-template:
needs: resolve-main
permissions:
contents: read
actions: read
uses: ./.github/workflows/node-server-tests.yml
with:
ref: ${{ needs.resolve-main.outputs.head_sha }}
build_template: true
fast_runners: true
build-daily-mac:
needs:
- resolve-main
- orcad-template
if: github.repository == 'stablyai/orca'
outputs:
tag: ${{ steps.release.outputs.tag }}
version: ${{ steps.daily.outputs.version }}
head_sha: ${{ steps.freshness.outputs.head_sha }}
published: ${{ steps.publish_live.outcome == 'success' && 'true' || 'false' }}
runs-on: blacksmith-6vcpu-macos-15
# Allow 150min for install 3×10 + publish 2×45 + ~25 setup/build/verify, so outer timeout cannot strand cleanup mid-retry.
timeout-minutes: 150
env:
NODE_OPTIONS: --max-old-space-size=4096
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.resolve-main.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-daily through env-passed App token (zizmor: artipacked).
persist-credentials: false
- name: Mint daily 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-daily
# Skip unchanged main to avoid duplicate builds/tags and runner cost.
- name: Check whether main moved since the last daily
id: freshness
shell: bash
env:
GH_TOKEN: ${{ steps.app_token.outputs.token }}
FORCED: ${{ github.event_name == 'workflow_dispatch' && inputs.force }}
run: |
set -euo pipefail
head_sha="$(git rev-parse HEAD)"
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 daily’s source commit.
last_body="$(gh release list --repo "$DAILY_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 daily release found; building $head_sha."
exit 0
fi
last_sha="$(gh release view "$last_body" --repo "$DAILY_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 daily built $last_sha); building."
fi
- name: Setup pnpm
if: steps.freshness.outputs.should_build == 'true'
uses: pnpm/setup@v2
with:
install: false
- name: Setup Node.js
if: steps.freshness.outputs.should_build == 'true'
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
if: steps.freshness.outputs.should_build == 'true'
shell: bash
run: echo "ORCA_NODE_RUNTIME_CACHE_DIR=$HOME/Library/Caches/orca-node-runtime" >>"$GITHUB_ENV"
- name: Cache the pinned Node runtime
if: steps.freshness.outputs.should_build == 'true'
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
if: steps.freshness.outputs.should_build == 'true'
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 x64 and arm64 optional natives for both packaged macOS targets.
- name: Install dependencies
if: steps.freshness.outputs.should_build == 'true'
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 using mobile’s React Native/Expo install.
- uses: ./.github/actions/install-mobile-dependencies
if: steps.freshness.outputs.should_build == 'true'
# Why: signing is what makes a daily installable over an existing Orca, so
# a missing cert must fail here rather than after a 20-minute build.
- name: Verify macOS signing environment
if: steps.freshness.outputs.should_build == 'true'
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: Compute daily version
id: daily
if: steps.freshness.outputs.should_build == 'true'
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 "$DAILY_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.
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 are a second floor so unpublishing a
# buggy main release cannot drag this series below a daily already out.
channel_tags="$(gh release list --repo "$DAILY_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_DAILY_RELEASE_NAMES="$names" \
node config/scripts/daily-build-version.mjs \
>"$RUNNER_TEMP/daily-identity.txt"
grep -E '^(version|build_number)=' "$RUNNER_TEMP/daily-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/daily-identity.txt"; then
echo "::error::daily-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/daily-identity.txt" >>"$GITHUB_OUTPUT"
- name: Build app
if: steps.freshness.outputs.should_build == 'true'
run: pnpm build:release
env:
NODE_OPTIONS: --max-old-space-size=4096
# Leave dev identity unset so official-only stable/rc telemetry stays silent.
ORCA_DIAGNOSTICS_TOKEN_URL: https://www.onorca.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
if: steps.freshness.outputs.should_build == 'true'
uses: actions/download-artifact@v8
with:
name: orcad-template
path: out/orcad-template
# Refresh the one-hour token after read-only install/build, before long publish/notary work.
- name: Re-mint daily repo token for publish
id: app_token_publish
if: steps.freshness.outputs.should_build == 'true'
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-daily
- name: Create daily release
id: release
if: steps.freshness.outputs.should_build == 'true'
shell: bash
env:
GH_TOKEN: ${{ steps.app_token_publish.outputs.token }}
TAG: v${{ steps.daily.outputs.version }}
NAME: ${{ steps.daily.outputs.name }}
SHA: ${{ steps.freshness.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}"
# Write one body file for creation and reassertion; freshness parsing requires exact agreement.
notes_file="$RUNNER_TEMP/daily-release-notes.md"
cat >"$notes_file" <<EOF
Automated daily 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.
EOF
# 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 "$DAILY_REPO" \
--title "$NAME" \
--draft \
--notes-file "$notes_file"
echo "tag=$TAG" >>"$GITHUB_OUTPUT"
echo "notes_file=$notes_file" >>"$GITHUB_OUTPUT"
- name: Publish daily macOS artifacts
if: steps.freshness.outputs.should_build == 'true'
uses: nick-fields/retry@v4
with:
# Allow two 45min pack/notarize/upload attempts; a third costs more runner time than a missed daily.
timeout_minutes: 45
max_attempts: 2
retry_wait_seconds: 30
command: node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON config/scripts/pinned-node-downloads.mjs darwin-arm64 darwin-x64 && node config/scripts/ensure-native-runtime.mjs --runtime=electron && if [ -f config/scripts/build-mac-notarization-overlap.mjs ]; then ORCA_MAC_DAILY=1 node config/scripts/build-mac-notarization-overlap.mjs --publish always; else ORCA_MAC_DAILY=1 pnpm exec electron-builder --config config/electron-builder.config.cjs --mac --publish always; fi
env:
# Give the publisher App write access to its configured orca-daily target.
GH_TOKEN: ${{ steps.app_token_publish.outputs.token }}
ORCA_DAILY_BUILD_VERSION: ${{ steps.daily.outputs.version }}
ORCA_BUILD_COMMIT: ${{ steps.daily.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 notary authentication, 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 }}
# Always refresh after upload: 2×45min can outlive the earlier token, including failed/cancelled uploads needing draft cleanup.
- name: Re-mint daily repo token for verify and cleanup
id: app_token_final
if: always() && steps.freshness.outputs.should_build == 'true'
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-daily
# Verify latest-mac.yml before exposing its release to prevent picker downloads returning 404.
- name: Verify update manifest published
if: steps.freshness.outputs.should_build == 'true'
shell: bash
env:
GH_TOKEN: ${{ steps.app_token_final.outputs.token }}
TAG: ${{ steps.release.outputs.tag }}
run: |
set -euo pipefail
assets="$(gh release view "$TAG" --repo "$DAILY_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::Daily draft $TAG is missing $required; the updater could not install it."
exit 1
fi
done
if ! grep -q '\.zip$' <<<"$assets"; then
echo "::error::Daily draft $TAG has no ZIP artifact for the updater to download."
exit 1
fi
# Publish only after manifest validation so the picker cannot offer incomplete assets.
- name: Publish the verified release
id: publish_live
if: steps.freshness.outputs.should_build == 'true'
shell: bash
env:
GH_TOKEN: ${{ steps.app_token_final.outputs.token }}
TAG: ${{ steps.release.outputs.tag }}
NAME: ${{ steps.daily.outputs.name }}
NOTES_FILE: ${{ steps.release.outputs.notes_file }}
run: |
set -euo pipefail
# Builder may rewrite title/body on upload; reassert both so the picker name and next run’s source-SHA freshness check stay correct.
gh release edit "$TAG" --repo "$DAILY_REPO" --draft=false --prerelease \
--title "$NAME" --notes-file "$NOTES_FILE"
echo "Published $TAG as \"$NAME\""
# Remove failed/cancelled stranded drafts, never releases already published by publish_live, including after prune failure. Outer timeout skips cleanup, so retain draft status until verified.
- name: Discard the draft release on failure
if: >-
(failure() || cancelled()) && steps.release.outputs.tag != '' &&
steps.publish_live.outcome != 'success'
shell: bash
env:
# If token refresh fails, let cleanup try the older token while it may still be valid.
GH_TOKEN: ${{ steps.app_token_final.outputs.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 "$DAILY_REPO" --yes ||
echo "::warning::Could not discard draft $TAG; remove it manually."
- name: Prune old daily releases
# Prune only after publication; failed runs leave draft disposal to cleanup.
if: steps.publish_live.outcome == 'success'
shell: bash
env:
GH_TOKEN: ${{ steps.app_token_final.outputs.token }}
# Never prune this run’s just-published tag at the retention cap.
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+=" | .[${DAILY_RETAIN_COUNT}:] | .[].tagName"
stale="$(gh release list --repo "$DAILY_REPO" --limit 200 --json tagName,publishedAt,isDraft \
--jq "$jq_filter")"
if [[ -z "$stale" ]]; then
echo "Nothing to prune; at or under $DAILY_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 "$DAILY_REPO" --yes --cleanup-tag || \
echo "::warning::Could not prune $tag"
done <<<"$stale"
# Dispatch E2E separately so main’s red full suite cannot fail an already-published daily. Use main’s workflow plus exact built SHA because daily tags live in another repo.
post-daily-e2e:
needs: build-daily-mac
if: ${{ needs.build-daily-mac.outputs.published == 'true' && needs.build-daily-mac.outputs.head_sha != '' }}
runs-on: blacksmith-2vcpu-ubuntu-2404
permissions:
actions: write
steps:
- name: Dispatch cut-scoped E2E
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SHA: ${{ needs.build-daily-mac.outputs.head_sha }}
run: |
for attempt in 1 2 3; do
if gh workflow run e2e.yml \
--repo "$GITHUB_REPOSITORY" \
--ref main \
--raw-field "ref=$SHA"; then
echo "Dispatched post-daily E2E for $SHA."
exit 0
fi
[[ "$attempt" -eq 3 ]] || sleep "$((attempt * 5))"
done
echo "::warning::Failed to dispatch post-daily E2E for $SHA after 3 attempts."
# Wait for mac release identity before Windows uploads; local ./ workflow follows the dispatch ref consistently.
build-daily-win:
needs: build-daily-mac
# Start Windows publication only after the mac release is live; its workflow refuses to create a missing release.
if: needs.build-daily-mac.outputs.published == 'true'
permissions:
contents: read
actions: read
uses: ./.github/workflows/dev-channel-win-build.yml
secrets: inherit
with:
channel: daily
tag: ${{ needs.build-daily-mac.outputs.tag }}
ref: ${{ needs.build-daily-mac.outputs.head_sha }}
version: ${{ needs.build-daily-mac.outputs.version }}
orcad_template: true
fast_release: true
You can’t perform that action at this time.
