Skip to content

chore(deploy): mirror the 2026-10-24 backend-sync costcut into deploy config #28736

chore(deploy): mirror the 2026-10-24 backend-sync costcut into deploy config

chore(deploy): mirror the 2026-10-24 backend-sync costcut into deploy config #28736

name: Desktop Swift CI
on:
# Nightly calls run these same jobs against one immutable main snapshot.
workflow_call:
inputs:
nightly_main_sha:
description: Main SHA pinned by the unfiltered nightly caller
type: string
required: false
default: ''
push:
branches: main
pull_request:
branches: main
# A later backend/docs commit must not make the last path-selected Desktop
# Swift verdict look current. Run against default-branch HEAD every day at an
# off-round UTC minute; the selector treats this as a full health event.
schedule:
- cron: "17 5 * * *"
# Recovery hatch. `plan-desktop-release.py` requires this workflow's checks on the
# exact source SHA, so any interruption — the workflow being disabled, a run cancelled
# by concurrency, an infrastructure blip — leaves that SHA permanently unreleasable:
# push events do not replay, and neither a force-push nor a PR close/reopen produces a
# run for a commit already on main. Manual dispatch is the only way to re-mint the
# evidence without an unrelated commit. Manual dispatch is path-independent so
# it still compiles a HEAD whose final commit only touched another component.
workflow_dispatch:
permissions:
contents: read
issues: read
concurrency:
# Supersede stale revisions of the same PR, but never let a later main push
# cancel the exact-SHA evidence consumed by desktop release planning.
group: desktop-swift-${{ github.event.pull_request.number || github.sha }}-${{ inputs.nightly_main_sha != '' && format('nightly-{0}', github.run_id) || 'normal' }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
# Abort a stalled git HTTP transfer instead of hanging. Hosted-runner fetches
# from github.com intermittently stop receiving data for 7-10 minutes before
# the connection resets (Backend Hermetic E2E run 39435458398 spent 498 s in
# "Checkout code" and hit its 10-minute cap). With these set, git aborts any
# transfer that stays under 1 KB/s for 60 s, and actions/checkout retries the
# fetch (its built-in 3 attempts); a healthy depth-1 fetch takes ~20 s.
# Applies to every git network call in the workflow, including script steps.
GIT_HTTP_LOW_SPEED_LIMIT: "1000"
GIT_HTTP_LOW_SPEED_TIME: "60"
jobs:
changes:
name: Detect Desktop Swift Changes
runs-on: ubuntu-latest
outputs:
diff_base: ${{ steps.changed.outputs.diff_base }}
full_ci: ${{ inputs.nightly_main_sha != '' && 'true' || (steps.tier.outputs.full_ci) }}
should_run: ${{ inputs.nightly_main_sha != '' && 'true' || (steps.changed.outputs.should_run) }}
should_run_static: ${{ inputs.nightly_main_sha != '' && 'true' || (steps.changed.outputs.should_run_static) }}
should_run_tests: ${{ inputs.nightly_main_sha != '' && 'true' || (steps.changed.outputs.should_run_tests) }}
should_test_compile: ${{ steps.changed.outputs.should_test_compile }}
should_siri_advisory: ${{ steps.changed.outputs.should_siri_advisory }}
should_release_compile: ${{ inputs.nightly_main_sha != '' && 'true' || (steps.changed.outputs.should_release_compile) }}
should_release_test_compile: ${{ steps.changed.outputs.should_release_test_compile }}
should_notification_release_regression: ${{ inputs.nightly_main_sha != '' && 'true' || (steps.changed.outputs.should_notification_release_regression) }}
desktop_swift_changed_files: ${{ steps.changed.outputs.desktop_swift_changed_files }}
swift_test_effective_lane: ${{ inputs.nightly_main_sha != '' && 'full' || steps.changed.outputs.swift_test_effective_lane }}
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
ref: ${{ inputs.nightly_main_sha || github.sha }}
fetch-depth: 0
- name: Inventory all nightly paths
if: inputs.nightly_main_sha != ''
shell: bash
run: git ls-files -- . ':!omi/**' ':!omiGlass/**' > /tmp/nightly-all-files.txt
- name: Resolve current CI tier
id: tier
uses: ./.github/actions/ci-tier
- name: Detect changed paths
id: changed
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
# Resolve the base live against the checkout (matching detect-changes)
# rather than the event-payload base.sha, which can be stale for a
# queued run (FC-stale-event-payload-diff-base, #10785).
DIFF_BASE="origin/${{ github.base_ref }}"
else
DIFF_BASE="${{ github.event.before }}"
fi
if [ -z "$DIFF_BASE" ] || [[ "$DIFF_BASE" =~ ^0+$ ]]; then
DIFF_BASE="$(git rev-parse HEAD^)"
fi
FILES=$(scripts/changed-files "$DIFF_BASE"...HEAD)
echo "$FILES"
echo "diff_base=$DIFF_BASE" >> "$GITHUB_OUTPUT"
IMPACT_FILES=/tmp/desktop-impact-files.txt
printf '%s\n' "$FILES" > "$IMPACT_FILES"
python3 scripts/pre_push_ci_prediction.py \
--changed-files "$IMPACT_FILES" \
--base "$DIFF_BASE" \
--event "${{ github.event_name }}" \
--github-output "$GITHUB_OUTPUT"
# Static macOS work is owned by the deterministic manifest. Querying
# it exclusively prevents portable checks from moving back onto this
# scarce runner while preserving their Linux Repo Checks route.
python3 .github/scripts/run_checks.py \
--lane ci \
--platform macos \
--exclusive-platform \
--base "$DIFF_BASE" \
--skip-pr-body-checks \
--changed-files "${{ inputs.nightly_main_sha != '' && '/tmp/nightly-all-files.txt' || '/tmp/desktop-impact-files.txt' }}" \
--output json > /tmp/macos-manifest-selection.json
SHOULD_RUN_STATIC=$(python3 - <<'PY'
import json
import sys
from pathlib import Path
selection = json.loads(Path("/tmp/macos-manifest-selection.json").read_text())
for check in selection["checks"]:
print(
f"macOS manifest selection: {check['id']} via {', '.join(check['matched_paths'])}: {check['reason']}",
file=sys.stderr,
)
print(str(bool(selection["checks"])).lower())
PY
)
echo "should_run_static=$SHOULD_RUN_STATIC" >> "$GITHUB_OUTPUT"
# The PR test lane defers ratcheted slow suites to the full lane
# (every main push and the scheduled health run) unless this diff
# touches their own inputs. Ship the exact deferral-relevant subset
# to the runner: test sources, the package manifest, and the runner
# scripts whose change must re-baseline the whole suite selection.
DESKTOP_SWIFT_CHANGED=$(printf '%s\n' "$FILES" | grep -Ex \
'desktop/macos/(Desktop/Tests/[^ ]+\.swift|Desktop/Package\.(swift|resolved)|test\.sh|scripts/(run-swift-ci\.sh|swift-test-suites\.sh|swift-test-slow-suites\.json|swift-test-skip-ratchet\.py))' \
|| true)
{
echo "desktop_swift_changed_files<<DESKTOP_SWIFT_CHANGED_EOF"
printf '%s\n' "$DESKTOP_SWIFT_CHANGED"
echo "DESKTOP_SWIFT_CHANGED_EOF"
} >> "$GITHUB_OUTPUT"
# The effective lane must be ONE decision shared by the runner and
# the step budget: a diff that touches the runner, the deferral
# list, the ratchet, the package manifest, or test.sh re-baselines
# the PR lane to the full suite (the runner applies the same rule
# from the forwarded file list). Keying the budget on the event
# type alone made a re-baselined PR run the full suite against the
# tight PR number and false-red at 2013s vs 1800s (run
# 34369508858).
SWIFT_TEST_EFFECTIVE_LANE=pr
if [ "${{ github.event_name }}" != "pull_request" ] || printf '%s\n' "$DESKTOP_SWIFT_CHANGED" | grep -qxE \
'desktop/macos/(Desktop/Package\.(swift|resolved)|test\.sh|scripts/(run-swift-ci\.sh|swift-test-suites\.sh|swift-test-slow-suites\.json|swift-test-skip-ratchet\.py))'; then
SWIFT_TEST_EFFECTIVE_LANE=full
fi
echo "swift_test_effective_lane=$SWIFT_TEST_EFFECTIVE_LANE" >> "$GITHUB_OUTPUT"
# Hosted macOS capacity is deliberately scarce. Run static contracts and the
# optional full suite as steps in one selected job, rather than claiming two
# runners in parallel. The stable Ubuntu aggregate below remains the exact
# release-gate check name.
desktop-swift-verify:
name: Desktop Swift Static & Test Contracts
needs: changes
if: ${{ needs.changes.outputs.full_ci == 'true' && (needs.changes.outputs.should_run_static == 'true' || needs.changes.outputs.should_run_tests == 'true' || needs.changes.outputs.should_test_compile == 'true') }}
runs-on: macos-26
# Bounds manifest checks, launcher coverage, and the debug suite — a
# wedge-guard, not a lane budget: the ceiling must admit one legitimate
# cold-tools run (~15 min from-source bootstrap) ahead of the full lane,
# which still executes every suite on this branch's own runner-changing
# diffs. Two #13219 runs were cancelled by tighter ceilings before
# completing. First pinned-Xcode 26.6 full lane on macos-26 measured
# past 60 minutes (run 34687313733: cancelled mid serial cluster with
# every completed suite green; one wedged batch cost 1650s before its
# bisect recovered), so this ceiling is 90.
timeout-minutes: 90
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
ref: ${{ inputs.nightly_main_sha || github.sha }}
# Static-check diffing needs full history; blob:none keeps that while
# skipping the ~1.25 GB of blob download that made this checkout cost
# a macOS-runner minute per run.
fetch-depth: 0
filter: blob:none
- name: Inventory all nightly paths
if: inputs.nightly_main_sha != ''
shell: bash
run: git ls-files -- . ':!omi/**' ':!omiGlass/**' > /tmp/nightly-all-files.txt
- name: Select and assert pinned Xcode 26.6
run: desktop/macos/scripts/run-swift-ci.sh --select-toolchain
- name: Restore Swift formatter and linter tools
# Restored for the tests lane too, not only static: the launcher
# script tests exercise the pinned swift-format binary, and without
# the restore a tests-only diff pays a ~15 min from-source rebuild.
# The -v3 prefix caches ONLY the built binaries (globbed files,
# ~100 MB): the v2 full-build-tree archives were GB-scale and lost
# every eviction race against the lingering 5.3 GB release .build
# entries, so warm-ups kept paying the from-source rebuild (run
# 34330641589-era measurements: 8-9 min, twice in one day).
if: ${{ always() && !cancelled() && (needs.changes.outputs.should_run_static == 'true' || needs.changes.outputs.should_run_tests == 'true') }}
id: swift-tools-cache
uses: actions/cache/restore@v6
with:
path: |
~/.cache/omi-swift-format/*/.build/release/swift-format
~/.cache/omi-swiftlint/*/swiftlint
key: ${{ runner.os }}-desktop-swift-tools-v3-xcode266-${{ hashFiles('desktop/macos/scripts/swift-format-wrapper.sh', 'desktop/macos/scripts/swiftlint-wrapper.sh') }}
restore-keys: |
${{ runner.os }}-desktop-swift-tools-v3-xcode266-
- name: Warm pinned formatter and linter tools
# One explicit bootstrap instead of whichever launcher test happens
# to touch a wrapper first. A cache miss pays the ~15 min from-source
# build here, visibly, and the save right below captures the built
# binary before any launcher test runs (three #13219 runs each
# rebuilt the tools inside the launcher step and lost them).
if: ${{ always() && !cancelled() && (needs.changes.outputs.should_run_static == 'true' || needs.changes.outputs.should_run_tests == 'true') }}
id: swift_tools_warm
run: |
desktop/macos/scripts/swift-format-wrapper.sh bootstrap
desktop/macos/scripts/swiftlint-wrapper.sh bootstrap
- name: Save Swift formatter and linter tools after warm-up
if: ${{ always() && !cancelled() && (needs.changes.outputs.should_run_static == 'true' || needs.changes.outputs.should_run_tests == 'true') && steps.swift-tools-cache.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v6
with:
path: |
~/.cache/omi-swift-format/*/.build/release/swift-format
~/.cache/omi-swiftlint/*/swiftlint
key: ${{ runner.os }}-desktop-swift-tools-v3-xcode266-${{ hashFiles('desktop/macos/scripts/swift-format-wrapper.sh', 'desktop/macos/scripts/swiftlint-wrapper.sh') }}
- name: Install uv for manifest checks
if: ${{ always() && !cancelled() && needs.changes.outputs.should_run_static == 'true' }}
uses: astral-sh/setup-uv@ecd24dd710f2fb0dca1693a67af11fc4a5c5ec84
with:
# Exact version resolves locally; an unset version means "latest",
# which fetches the astral-sh/versions manifest and has hard-failed
# CI on that network call. Matches backend/Dockerfile UV_VERSION.
version: "0.11.13"
- name: Run macOS-scoped manifest checks
if: ${{ always() && !cancelled() && needs.changes.outputs.should_run_static == 'true' }}
id: macos_manifest_checks
run: |
MANIFEST_ARGS=()
if [ "${{ github.event_name }}" = "pull_request" ]; then
# Repo Checks owns PR-body metadata. This macOS-only lane still
# passes --skip-changelog so it does not depend on labels; the
# checker itself no longer treats that skip as an exemption for
# production desktop paths (SCA-343 / #11778).
MANIFEST_ARGS+=(--skip-changelog)
fi
python3 .github/scripts/run_checks.py \
${{ inputs.nightly_main_sha != '' && '--changed-files /tmp/nightly-all-files.txt --skip-pr-body-checks --keep-going' || '' }} \
--lane ci \
--platform macos \
--exclusive-platform \
--base "${{ needs.changes.outputs.diff_base }}" \
--skip-pr-body-checks \
"${MANIFEST_ARGS[@]}"
# The formatter/linter tools cache save waits until after the launcher
# tests below: on a tests-only diff the manifest lane never runs and the
# from-source bootstrap happens inside the launcher tests, so a save
# placed here would cache nothing and every run would rebuild.
# A Desktop/.build archive is about 5.3 GB. GitHub scopes those archives
# to a PR ref, so they miss for normal new commits and spend over a minute
# uploading without reducing a subsequent run. Retain only SwiftPM's
# dependency cache, which is small and reusable when the manifest matches.
- name: Restore SwiftPM dependency cache
if: ${{ always() && !cancelled() && (needs.changes.outputs.should_run_tests == 'true' || needs.changes.outputs.should_test_compile == 'true') }}
id: swiftpm-cache
uses: actions/cache/restore@v6
with:
path: |
~/Library/Caches/org.swift.swiftpm
key: ${{ runner.os }}-desktop-swift-build-xcode266-${{ hashFiles('desktop/macos/Desktop/Package.swift', 'desktop/macos/Desktop/Package.resolved') }}
restore-keys: |
${{ runner.os }}-desktop-swift-build-xcode266-
- name: Install Swift system dependencies
if: ${{ always() && !cancelled() && (needs.changes.outputs.should_run_tests == 'true' || needs.changes.outputs.should_test_compile == 'true') }}
run: brew install actionlint pkg-config webp
# Generated-client-only PRs keep app AND test compilation, using the same
# debug prebuild as the suite runner (502.55s in run 38131335528). They
# skip suite execution and launcher tests; all other desktop PRs retain
# the existing lane. Main and health events never select this shortcut.
- name: Compile desktop Swift app and tests
if: ${{ always() && !cancelled() && needs.changes.outputs.should_test_compile == 'true' }}
id: desktop_swift_test_compile
working-directory: desktop/macos
run: ./scripts/run-swift-ci.sh --test-compile
# Debug build+test only on the PR critical path. Release compile is a
# separate job (main push, scheduled health, manual dispatch, or a PR
# that touches release-specific inputs) so ordinary PRs do not
# pay for a second configuration — or a second hosted Mac — that tests
# never consume. This step runs BEFORE the launcher tests on purpose:
# its prebuild leaves a warm Desktop/.build, so the launcher suite's
# `swift build --build-tests` fixtures (feature-sentinel negative
# control) compile incrementally instead of paying a second cold
# package build (measured 21 min on run 34301327490-era shapes).
- name: Test desktop Swift app
if: ${{ always() && !cancelled() && needs.changes.outputs.should_run_tests == 'true' }}
id: desktop_swift_tests
working-directory: desktop/macos
env:
# Each isolated worker receives a copy-on-write clone of the prebuilt
# SwiftPM scratch directory, so filtered suites do not contend on the
# shared Desktop/.build lock. Batching collapsed per-invocation
# SwiftPM startup (the reason two workers were the measured ceiling),
# so three workers now match the runner's three cores; measure-block
# suites carry a doubled per-suite budget to absorb the added CPU
# contention. Evidence: PR #12247's runs. Drop back to "2" if the
# performance harness starts timing out.
OMI_SWIFT_TEST_SUITE_WORKERS: "3"
OMI_SWIFT_TEST_SUITE_TIMEOUT_SECONDS: "300"
# Per-invocation overhead (SwiftPM startup + xctest host launch,
# ~36s flat) dominates the suite step; batching amortizes it and
# the bisect fallback bounds a flaky batch's cost to ~3
# invocations, so CI carries 100 suites per invocation (measured
# fast lane at 50: 17 batches/3 workers ≈ 9 min of invocation
# overhead). The uncapped scaled budget (300s + 30s/suite) would
# be 3270s at batch 100 — 54.5 minutes inside this job's
# 60-minute ceiling, leaving a wedged batch no room for its
# bisect fallback — so the batch watchdog is independently capped
# at 25 minutes: a stalled batch fails over to isolation with
# half the job still ahead of it. The local default stays 25 and
# every invocation logs its wall seconds, so a regression is
# visible in the run log.
OMI_SWIFT_TEST_SUITE_BATCH_SIZE: "100"
OMI_SWIFT_TEST_BATCH_CEILING_SECONDS: "1500"
# PRs defer ratcheted slow suites (swift-test-slow-suites.json) to
# the full lane unless this diff touches their own inputs; pushes,
# scheduled health runs, and manual dispatch always run everything.
# The changes job resolves the EFFECTIVE lane with the runner's own
# re-baseline rule so the lane and the budget below always agree.
OMI_SWIFT_TEST_LANE: ${{ needs.changes.outputs.swift_test_effective_lane }}
OMI_SWIFT_TEST_CHANGED_FILES: ${{ needs.changes.outputs.desktop_swift_changed_files }}
OMI_SWIFT_TEST_SELECTION_MODE: shadow
OMI_SWIFT_TEST_SELECTION_RECORD: ${{ runner.temp }}/swift-suite-selection.json
OMI_SWIFT_TEST_SELECTION_BASE: ${{ needs.changes.outputs.diff_base }}
# The PR lane also defers the serial and solo clusters: their
# isolation contract costs one ~30s invocation per member for
# sub-second tests, sequentially, after every worker exits
# (24 invocations / 12.9 min on run 34306382692). Declaring files
# and ratcheted subject watch paths still wake them; the full lane
# always executes them.
OMI_SWIFT_TEST_PR_LANE_DEFER_SERIAL: "1"
# ---- regression guards -------------------------------------------
# Desktop Swift CI once drifted silently to 27-42 min suite steps
# and 60-120 min verdicts because nothing flagged duration. These
# budgets turn drift into a red run instead — the very first
# guarded run caught a 37m07s full lane (run 34363659680) that
# would previously have published green. Lane-aware via the
# changes job's effective-lane output (same re-baseline rule as
# the runner): the PR fast lane measured 15m42s at batch 50
# (~12m at 100; ~24m worst case for auth PRs that wake the serial
# cluster). After the Xcode 26.6 pin, PR #13699 measured 2324s on
# macos-26 (run 34754454417) against the old 1800s PR budget.
# PR #14213 (run 35134593036) then measured 2778s on a cache-hit
# PR lane (one 75-suite batch hit the 1500s ceiling). Run
# 35106014508 (PR #14222) went red ONLY on this guard with all 836
# executed suites green — one order-dependent batch (exit 1 at 78s)
# bisected into a green half (40s), a wedged half that burned its
# full 1500s batch ceiling, and ~1020s of isolated singles, 3923s
# step wall. The PR lane must admit one legitimate bisect cascade,
# so it is 4400s (cold-tools 742s prelude + 4400s + 158s post-step
# still fits the 90m verify-job ceiling). The full lane
# (re-baselined runner diffs, main pushes, scheduled health)
# legitimately spans ~28-40m on a slow runner day and carries 4200s
# against the 70m+ drift class. Raise either only with measured
# evidence in the PR that raises it.
OMI_SWIFT_TEST_STEP_BUDGET_SECONDS: ${{ needs.changes.outputs.swift_test_effective_lane == 'pr' && '4400' || '4200' }}
# Any PR-lane suite whose own test time exceeds this and is not
# ratcheted in swift-test-slow-suites.json fails the run: the fast
# lane cannot silently grow a slow tail. Deferred suites that woke
# (their files or watched subjects changed) are exempt by
# membership in the slow list.
OMI_SWIFT_TEST_SLOW_RATCHET_SECONDS: "60"
run: |
step_started=$SECONDS
./scripts/run-swift-ci.sh --test
elapsed=$((SECONDS - step_started))
echo "swift suite step wall: ${elapsed}s (budget ${OMI_SWIFT_TEST_STEP_BUDGET_SECONDS}s)"
if [ "${OMI_SWIFT_TEST_STEP_BUDGET_SECONDS}" -gt 0 ] && [ "$elapsed" -gt "${OMI_SWIFT_TEST_STEP_BUDGET_SECONDS}" ]; then
echo "FAIL: the Desktop Swift suite step took ${elapsed}s, over its ${OMI_SWIFT_TEST_STEP_BUDGET_SECONDS}s regression budget. Fix the regression (the per-invocation and slowest-suite lines above name the cost centers), or raise the budget in desktop-swift-ci.yml with measured evidence." >&2
exit 1
fi
- name: Upload Swift suite shadow selection
if: ${{ always() && needs.changes.outputs.should_run_tests == 'true' }}
uses: actions/upload-artifact@v7
with:
name: swift-suite-selection-${{ github.sha }}
path: ${{ runner.temp }}/swift-suite-selection.json
if-no-files-found: warn
- name: Desktop launcher script tests
if: ${{ always() && !cancelled() && needs.changes.outputs.should_run_tests == 'true' }}
id: desktop_launcher_tests
working-directory: desktop/macos
env:
# Regression guard, same rationale as the suite step's budget: this
# step measured 1m55s-2m27s once the sentinel build went incremental
# and the reclaim fixture went hermetic, after previously costing
# 17-21 min. 360s catches that class of drift with headroom.
OMI_SWIFT_LAUNCHER_STEP_BUDGET_SECONDS: "360"
run: |
step_started=$SECONDS
# Discovery, not a hardcoded list — a new tests/test-*.sh runs here
# automatically, mirroring swift-test-suites.sh's auto-discovery.
# Three-way parallel (the hosted runner has 3 cores): the serial loop
# cost 9.3 min p50, which was 13% of all macOS-runner demand. Each
# test logs to its own file so parallel output cannot interleave;
# failing logs are replayed in full at the end.
log_dir="$(mktemp -d)"
# The skip hand list stays a `case` pattern: check-launcher-test-skips.py
# parses this exact shape to hold it equal to the # discovery-skip markers.
for t in tests/test-*.sh; do
case "$t" in
tests/test-mounted-navigation-latency.sh|tests/test-named-bundle-resources.sh)
continue
;;
esac
printf '%s\n' "$t"
done \
| xargs -P 3 -I{} bash -c '
t="$1"
log_dir="$2"
log="$log_dir/$(basename "$t").log"
started=$SECONDS
if bash "$t" >"$log" 2>&1; then
echo "== PASS $t ($((SECONDS - started))s)"
# Per-test logs are replayed only on failure, so a passing
# test that paid a from-source bootstrap (~15 min) or another
# hidden slow path was invisible: test-swift-format-wrapper
# passed in 1240s on run 34301327490 with a warm tools cache.
grep -m1 -E "Bootstrapping swift|cache (HIT|stale)" "$log" | sed "s|^| [$t] |" || true
else
echo "== FAIL $t ($((SECONDS - started))s)"
touch "$log.failed"
fi
' _ {} "$log_dir"
failed=0
for marker in "$log_dir"/*.failed; do
[ -e "$marker" ] || continue
failed=1
log="${marker%.failed}"
echo "--- $(basename "$log" .log) output ---"
cat "$log"
done
elapsed=$((SECONDS - step_started))
echo "launcher step wall: ${elapsed}s (budget ${OMI_SWIFT_LAUNCHER_STEP_BUDGET_SECONDS}s)"
if [ "${OMI_SWIFT_LAUNCHER_STEP_BUDGET_SECONDS}" -gt 0 ] && [ "$elapsed" -gt "${OMI_SWIFT_LAUNCHER_STEP_BUDGET_SECONDS}" ]; then
echo "FAIL: the launcher script test step took ${elapsed}s, over its ${OMI_SWIFT_LAUNCHER_STEP_BUDGET_SECONDS}s regression budget. The per-test PASS lines above name the slow test; fix it or raise the budget with measured evidence." >&2
exit 1
fi
exit "$failed"
# Preserve resolved dependency downloads after a failed test command, but
# do not archive Desktop/.build: its PR-scoped 5.3 GB cache misses in
# ordinary new-commit runs and increases billed macOS time on every save.
- name: Save SwiftPM dependency cache after test attempt
if: ${{ always() && !cancelled() && (needs.changes.outputs.should_run_tests == 'true' || needs.changes.outputs.should_test_compile == 'true') && steps.swiftpm-cache.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v6
with:
path: |
~/Library/Caches/org.swift.swiftpm
key: ${{ runner.os }}-desktop-swift-build-xcode266-${{ hashFiles('desktop/macos/Desktop/Package.swift', 'desktop/macos/Desktop/Package.resolved') }}
- name: Require independent macOS phase verdicts
if: ${{ always() && !cancelled() }}
env:
STATIC_REQUIRED: ${{ needs.changes.outputs.should_run_static }}
TESTS_REQUIRED: ${{ needs.changes.outputs.should_run_tests }}
COMPILE_REQUIRED: ${{ needs.changes.outputs.should_test_compile }}
STATIC_OUTCOME: ${{ steps.macos_manifest_checks.outcome }}
LAUNCHER_OUTCOME: ${{ steps.desktop_launcher_tests.outcome }}
TEST_OUTCOME: ${{ steps.desktop_swift_tests.outcome }}
COMPILE_OUTCOME: ${{ steps.desktop_swift_test_compile.outcome }}
run: |
require_success() {
local required="$1"
local name="$2"
local outcome="$3"
if [ "$required" = true ] && [ "$outcome" != success ]; then
echo "FAIL: $name did not succeed (outcome=$outcome)" >&2
return 1
fi
}
require_success "$STATIC_REQUIRED" "macOS manifest checks" "$STATIC_OUTCOME"
require_success "$TESTS_REQUIRED" "desktop launcher script tests" "$LAUNCHER_OUTCOME"
require_success "$TESTS_REQUIRED" "desktop Swift tests" "$TEST_OUTCOME"
require_success "$COMPILE_REQUIRED" "desktop Swift app and test compilation" "$COMPILE_OUTCOME"
desktop-swift:
# Keep this name a literal: desktop release planning looks this check up by
# exact name on the source SHA. GitHub does not evaluate `name:` for a
# SKIPPED job, so an expression here publishes the raw expression text as
# the check name — which made the required check *absent* rather than
# skipped on every commit that did not touch desktop paths, and left the
# planner reporting "missing" instead of the truth.
name: Desktop Swift Build & Tests
needs: [changes, desktop-swift-verify, desktop-swift-release-compile]
if: ${{ always() && needs.changes.outputs.should_run == 'true' }}
runs-on: ubuntu-latest
steps:
- name: Require static contracts and Swift tests
env:
FULL_CI: ${{ needs.changes.outputs.full_ci }}
VERIFY_RESULT: ${{ needs.desktop-swift-verify.result }}
RELEASE_RESULT: ${{ needs.desktop-swift-release-compile.result }}
STATIC_REQUIRED: ${{ needs.changes.outputs.should_run_static }}
TESTS_REQUIRED: ${{ needs.changes.outputs.should_run_tests }}
COMPILE_REQUIRED: ${{ needs.changes.outputs.should_test_compile }}
RELEASE_REQUIRED: ${{ needs.changes.outputs.should_release_compile }}
run: |
if [ "$FULL_CI" != true ]; then
if [ "$STATIC_REQUIRED" = true ] || [ "$TESTS_REQUIRED" = true ] || [ "$COMPILE_REQUIRED" = true ] || [ "$RELEASE_REQUIRED" = true ]; then
notice='fork PR — a maintainer adds label ci:full, then Re-run all jobs'
echo "::notice title=Heavy CI deferred::$notice"
echo "Heavy CI deferred: $notice" >> "$GITHUB_STEP_SUMMARY"
if { [ "$STATIC_REQUIRED" = true ] || [ "$TESTS_REQUIRED" = true ] || [ "$COMPILE_REQUIRED" = true ]; } && [ "$VERIFY_RESULT" != skipped ]; then
echo "Deferred Desktop Swift verification must be skipped, got: $VERIFY_RESULT" >&2
exit 1
fi
if [ "$RELEASE_REQUIRED" = true ] && [ "$RELEASE_RESULT" != skipped ]; then
echo "Deferred Desktop Swift release compile must be skipped, got: $RELEASE_RESULT" >&2
exit 1
fi
exit 0
fi
echo 'Desktop Swift Build & Tests: no heavy checks selected' | tee -a "$GITHUB_STEP_SUMMARY"
exit 0
fi
if [ "$STATIC_REQUIRED" = true ] || [ "$TESTS_REQUIRED" = true ] || [ "$COMPILE_REQUIRED" = true ]; then
test "$VERIFY_RESULT" = success
else
test "$VERIFY_RESULT" = skipped
fi
# Require the release lane only when the selector chose it. Main and
# scheduled health still select it for every relevant SHA.
if [ "$RELEASE_REQUIRED" = true ]; then
test "$RELEASE_RESULT" = success
else
test "$RELEASE_RESULT" = skipped
fi
desktop-swift-release-compile:
# Literal for the same reason as `desktop-swift` above.
name: Desktop Swift Release Compile
needs: changes
# Ordinary PRs no longer claim this second hosted Mac: a release compile
# holds it for ~25 min, and one runner per PR halved concurrent macos-26
# demand against the ~5-runner cap that was producing 60-97 min queues
# (2026-09-08, macos-15 era). The lane still runs for every main push, the
# scheduled health run, manual dispatch, and PRs that change release
# settings, packaging, package manifests, notification regression inputs,
# or this workflow. Ordinary source/test changes defer the release compile
# to main, where the full release test target still builds (#13123/#13467).
if: ${{ needs.changes.outputs.full_ci == 'true' && needs.changes.outputs.should_release_compile == 'true' }}
runs-on: macos-26
# Ceiling must admit a whole-module release compile of a branch that grew
# the Desktop package: attempts 1-3 of the local-embeddings hybrid PR were
# all cancelled at the previous 60m ceiling with the compile finished and
# the regression tests passing <1s before the kill (2026-09-11). The
# baseline main compile remains ~25 min; this only stops re-running and
# re-cancelling ~60 min of work that already succeeded.
timeout-minutes: 90
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
ref: ${{ inputs.nightly_main_sha || github.sha }}
fetch-depth: 1
- name: Inventory all nightly paths
if: inputs.nightly_main_sha != ''
shell: bash
run: git ls-files -- . ':!omi/**' ':!omiGlass/**' > /tmp/nightly-all-files.txt
- name: Select and assert pinned Xcode 26.6
run: desktop/macos/scripts/run-swift-ci.sh --select-toolchain
# Same dependency cache as the verify job: without it every release
# compile re-resolves all SwiftPM dependencies (~1 min of clone traffic).
- name: Restore SwiftPM dependency cache
id: release-swiftpm-cache
uses: actions/cache/restore@v6
with:
path: |
~/Library/Caches/org.swift.swiftpm
key: ${{ runner.os }}-desktop-swift-build-xcode266-${{ hashFiles('desktop/macos/Desktop/Package.swift', 'desktop/macos/Desktop/Package.resolved') }}
restore-keys: |
${{ runner.os }}-desktop-swift-build-xcode266-
# No Desktop/.build build-products cache, by measurement: the release
# compile step took 23-25 min on every recent run whether a 5.3 GB
# archive hit exactly, partially, or not at all (whole-module
# optimization recompiles on any source change, and runners are
# ephemeral). Worse, each main-push save of that 5.3 GB archive evicted
# the small swift-format/swiftlint tool caches from the repository's
# 10 GB cache budget, which made the verify job's launcher tests rebuild
# swift-format from source (~15 min) on cache misses.
- name: Install Swift system dependencies
run: brew install pkg-config webp
- name: Release compile
working-directory: desktop/macos
env:
BUILD_RELEASE_TESTS: ${{ needs.changes.outputs.should_release_test_compile == 'true' || needs.changes.outputs.should_notification_release_regression == 'true' }}
run: |
if [ "$BUILD_RELEASE_TESTS" = true ]; then
./scripts/run-swift-ci.sh --release-test-compile
else
./scripts/run-swift-ci.sh --release-compile
fi
- name: Prove stable compiler emits no Siri metadata
working-directory: desktop/macos
run: |
xcrun swift package --package-path Desktop describe --type json | python3 -c '
import json, sys
targets = {target["name"]: target for target in json.load(sys.stdin)["targets"]}
assert "SiriIntegration/SiriIntents.swift" not in targets["Omi Computer"]["sources"]
assert "SiriIntentServiceTests.swift" not in targets["Omi ComputerTests"]["sources"]
print("Xcode 26.6 SwiftPM source graph excludes Siri intents and tests")'
bundle="$RUNNER_TEMP/Omi-Stable-Compiler.app"
mkdir -p "$bundle/Contents/Resources"
./scripts/embed-app-intents-metadata.sh Desktop "$bundle" Release arm64 --expect-absent
# Runs here, not in the verify job: the regression needs release-mode
# artifacts, and building them twice on two hosted Macs cost ~31 min per
# notification-boundary change. The release planner requires this job's
# check ("Desktop Swift Release Compile") by exact name on the source
# SHA, and the Build & Tests aggregate hard-fails on this job's result,
# so release tagging and selected PRs gate on it.
- name: Test UserNotifications callback regression in release mode
if: ${{ needs.changes.outputs.should_notification_release_regression == 'true' }}
working-directory: desktop/macos
run: ./scripts/run-swift-ci.sh --release-notification-regression
- name: Save SwiftPM dependency cache
if: ${{ github.event_name == 'push' && steps.release-swiftpm-cache.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v6
with:
path: |
~/Library/Caches/org.swift.swiftpm
key: ${{ runner.os }}-desktop-swift-build-xcode266-${{ hashFiles('desktop/macos/Desktop/Package.swift', 'desktop/macos/Desktop/Package.resolved') }}
post-merge-failure-issue:
name: Post-merge Desktop Swift Release Compile failure issue
needs: [changes, desktop-swift-release-compile]
if: ${{ always() && inputs.nightly_main_sha == '' && (github.event_name == 'push' || github.event_name == 'schedule') }}
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- name: Open or update release compile failure issue
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
CHANGES_RESULT: ${{ needs.changes.result }}
RELEASE_EXPECTED: ${{ needs.changes.outputs.should_release_compile }}
RELEASE_RESULT: ${{ needs.desktop-swift-release-compile.result }}
run: |
set -euo pipefail
if [[ "$CHANGES_RESULT" == success && "$RELEASE_EXPECTED" != true ]]; then
exit 0
fi
if [[ "$CHANGES_RESULT" == success && "$RELEASE_RESULT" == success ]]; then
exit 0
fi
title="CI post-merge failure: Desktop Swift Release Compile"
issue_number="$(gh issue list --repo "$REPO" --state open --search "\"$title\" in:title" --limit 100 --json number,title --jq ".[] | select(.title == \"$title\") | .number" | head -n 1)"
printf 'Desktop Swift Release Compile was %s (change detection: %s) on a main or nightly run.\n\nEvidence: %s\n' "$RELEASE_RESULT" "$CHANGES_RESULT" "$RUN_URL" > /tmp/desktop-swift-release-issue.md
if [[ -n "$issue_number" ]]; then
gh issue edit "$issue_number" --repo "$REPO" --body-file /tmp/desktop-swift-release-issue.md
else
gh issue create --repo "$REPO" --title "$title" --body-file /tmp/desktop-swift-release-issue.md
fi
exit 1
# David's runner ruling: this previews the Siri release compiler without
# making every desktop PR wait for the capacity-limited xcode-27 pool.
# PRs now preview only Siri/App Intents or selection-contract inputs. Other
# events keep the existing full/static selection, including exact-SHA main
# evidence and path-independent scheduled/manual health coverage.
desktop-siri-xcode27-advisory:
name: Desktop Siri Xcode 27 Advisory
needs: changes
if: ${{ needs.changes.outputs.full_ci == 'true' && ((github.event_name == 'pull_request' && needs.changes.outputs.should_siri_advisory == 'true') || (github.event_name != 'pull_request' && (needs.changes.outputs.should_run_static == 'true' || needs.changes.outputs.should_run_tests == 'true'))) }}
runs-on: xcode-27
continue-on-error: true
timeout-minutes: 60
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
ref: ${{ inputs.nightly_main_sha || github.sha }}
fetch-depth: 1
- name: Inventory all nightly paths
if: inputs.nightly_main_sha != ''
shell: bash
run: git ls-files -- . ':!omi/**' ':!omiGlass/**' > /tmp/nightly-all-files.txt
- name: Select Xcode 27 release toolchain
run: |
echo "DEVELOPER_DIR=/Applications/Xcode_27.0.app/Contents/Developer" >> "$GITHUB_ENV"
DEVELOPER_DIR=/Applications/Xcode_27.0.app/Contents/Developer xcodebuild -version | grep -Fx 'Xcode 27.0'
- name: Install Swift system dependencies
run: brew install pkg-config webp
- name: Build Siri and test targets with metadata constants
working-directory: desktop/macos/Desktop
run: xcrun swift build -c debug --triple arm64-apple-macosx --jobs 1 --build-tests -Xswiftc -emit-const-values
- name: Run Siri intent and eligibility tests
working-directory: desktop/macos/Desktop
run: xcrun swift test -c debug --triple arm64-apple-macosx --skip-build --filter SiriIntentServiceTests
- name: Check Metadata.appintents extraction
working-directory: desktop/macos
run: |
bundle="$RUNNER_TEMP/Omi-Siri-Advisory.app"
mkdir -p "$bundle/Contents/Resources"
./scripts/embed-app-intents-metadata.sh Desktop "$bundle" Debug arm64
test -s "$bundle/Contents/Resources/Metadata.appintents/extract.actionsdata"