Repository navigation
chore(deploy): mirror the 2026-10-24 backend-sync costcut into deploy config #28736
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" |