diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dac2c24d2c24..306fc1ccf5b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -100,6 +100,7 @@ jobs: run_macos_node: ${{ steps.manifest.outputs.run_macos_node }} macos_node_matrix: ${{ steps.manifest.outputs.macos_node_matrix }} run_macos_swift: ${{ steps.manifest.outputs.run_macos_swift }} + run_ios_build: ${{ steps.manifest.outputs.run_ios_build }} run_android_job: ${{ steps.manifest.outputs.run_android_job }} android_matrix: ${{ steps.manifest.outputs.android_matrix }} steps: @@ -204,6 +205,7 @@ jobs: OPENCLAW_CI_DOCS_CHANGED: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.docs_scope.outputs.docs_changed }} OPENCLAW_CI_RUN_NODE: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_node || 'false' }} OPENCLAW_CI_RUN_MACOS: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_macos || 'false' }} + OPENCLAW_CI_RUN_IOS_BUILD: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_ios_build || 'false' }} OPENCLAW_CI_RUN_ANDROID: ${{ github.event_name == 'workflow_dispatch' && (inputs.release_gate || inputs.include_android) && 'true' || steps.changed_scope.outputs.run_android || 'false' }} OPENCLAW_CI_RUN_WINDOWS: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_windows || 'false' }} OPENCLAW_CI_RUN_NODE_FAST_ONLY: ${{ github.event_name == 'workflow_dispatch' && 'false' || steps.changed_scope.outputs.run_node_fast_only || 'false' }} @@ -267,6 +269,8 @@ jobs: const runPluginContractShards = runNodeFull || runNodeFastPluginContracts; const runMacos = parseBoolean(process.env.OPENCLAW_CI_RUN_MACOS) && !docsOnly && isCanonicalRepository; + const runIosBuild = + parseBoolean(process.env.OPENCLAW_CI_RUN_IOS_BUILD) && !docsOnly && isCanonicalRepository; const runAndroid = parseBoolean(process.env.OPENCLAW_CI_RUN_ANDROID) && !docsOnly && isCanonicalRepository; const runWindows = @@ -361,6 +365,7 @@ jobs: runMacos ? [{ check_name: "macos-node", runtime: "node", task: "test" }] : [], ), run_macos_swift: runMacos, + run_ios_build: runIosBuild, run_android_job: runAndroid, android_matrix: createMatrix( runAndroid @@ -2162,6 +2167,76 @@ jobs: done exit 1 + ios-build: + permissions: + contents: read + name: "ios-build" + needs: [preflight] + if: needs.preflight.outputs.run_ios_build == 'true' + runs-on: ${{ github.event_name == 'workflow_dispatch' && 'macos-26' || (github.repository == 'openclaw/openclaw' && 'blacksmith-12vcpu-macos-26' || 'macos-26') }} + timeout-minutes: 45 + steps: + - name: Checkout + env: + CHECKOUT_REPO: ${{ github.repository }} + CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }} + run: | + set -euo pipefail + git init "$GITHUB_WORKSPACE" + git -C "$GITHUB_WORKSPACE" config gc.auto 0 + git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git" + fetch_timeout_seconds=90 + fetch_checkout_ref() { + git -C "$GITHUB_WORKSPACE" \ + -c protocol.version=2 \ + fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \ + "+${CHECKOUT_SHA}:refs/remotes/origin/checkout" & + local fetch_pid="$!" + local elapsed=0 + while kill -0 "$fetch_pid" 2>/dev/null; do + if [ "$elapsed" -ge "$fetch_timeout_seconds" ]; then + kill -TERM "$fetch_pid" 2>/dev/null || true + sleep 10 + kill -KILL "$fetch_pid" 2>/dev/null || true + wait "$fetch_pid" || true + return 124 + fi + sleep 1 + elapsed=$((elapsed + 1)) + done + wait "$fetch_pid" + } + fetch_checkout_ref + git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout + + - name: Select Xcode 26 + run: | + set -euo pipefail + for xcode_app in /Applications/Xcode_26.5.app /Applications/Xcode-26.5.0.app; do + if [ -d "$xcode_app/Contents/Developer" ]; then + sudo xcode-select -s "$xcode_app/Contents/Developer" + break + fi + done + xcodebuild -version + xcode_version="$(xcodebuild -version | awk 'NR == 1 { print $2 }')" + if [[ "$xcode_version" != 26.* ]]; then + echo "error: expected Xcode 26.x, got $xcode_version" >&2 + exit 1 + fi + swift --version + + - name: Setup Node environment + uses: ./.github/actions/setup-node-env + with: + install-bun: "false" + + - name: Install iOS Swift tooling + run: brew install xcodegen swiftlint swiftformat + + - name: Build iOS app + run: pnpm ios:build + android: permissions: contents: read @@ -2313,6 +2388,7 @@ jobs: - checks-windows - macos-node - macos-swift + - ios-build - android if: ${{ !cancelled() && always() && github.event_name != 'push' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }} runs-on: ubuntu-24.04 diff --git a/docs/ci.md b/docs/ci.md index b1e3af8f9b10..9c1e23654133 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -42,6 +42,7 @@ or an explicit manual dispatch. | `checks-windows` | Windows-specific process/path tests plus shared runtime import specifier regressions | Windows-relevant changes | | `macos-node` | macOS TypeScript test lane using the shared built artifacts | macOS-relevant changes | | `macos-swift` | Swift lint, build, and tests for the macOS app | macOS-relevant changes | +| `ios-build` | Xcode project generation plus the iOS app simulator build | iOS app, shared app kit, or Swabble changes | | `android` | Android unit tests for both flavors plus one debug APK build | Android-relevant changes | | `test-performance-agent` | Daily Codex slow-test optimization after trusted activity | Main CI success or manual dispatch | | `openclaw-performance` | Daily/on-demand Kova runtime performance reports with mock-provider, deep-profile, and GPT 5.5 live lanes | Scheduled and manual dispatch | @@ -52,7 +53,7 @@ or an explicit manual dispatch. 2. `preflight` decides which lanes exist at all. The `docs-scope` and `changed-scope` logic are steps inside this job, not standalone jobs. 3. `security-fast`, `check-*`, `check-additional-*`, `check-docs`, and `skills-python` fail quickly without waiting on the heavier artifact and platform matrix jobs. 4. `build-artifacts` overlaps with the fast Linux lanes so downstream consumers can start as soon as the shared build is ready. -5. Heavier platform and runtime lanes fan out after that: `checks-fast-core`, `checks-fast-contracts-plugins-*`, `checks-fast-contracts-channels-*`, `checks-node-core-*`, `checks-windows`, `macos-node`, `macos-swift`, and `android`. +5. Heavier platform and runtime lanes fan out after that: `checks-fast-core`, `checks-fast-contracts-plugins-*`, `checks-fast-contracts-channels-*`, `checks-node-core-*`, `checks-windows`, `macos-node`, `macos-swift`, `ios-build`, and `android`. GitHub may mark superseded jobs as `cancelled` when a newer push lands on the same PR or `main` ref. Treat that as CI noise unless the newest run for the same ref is also failing. Matrix jobs use `fail-fast: false`, and `build-artifacts` reports embedded channel, core-support-boundary, and gateway-watch failures directly instead of queuing tiny verifier jobs. The automatic CI concurrency key is versioned (`CI-v7-*`) so a GitHub-side zombie in an old queue group cannot indefinitely block newer main runs. Manual full-suite runs use `CI-manual-v1-*` and do not cancel in-progress runs. @@ -80,7 +81,7 @@ When the check fails, update the PR body instead of pushing another code commit. Scope logic lives in `scripts/ci-changed-scope.mjs` and is covered by unit tests in `src/scripts/ci-changed-scope.test.ts`. Manual dispatch skips changed-scope detection and makes the preflight manifest act as if every scoped area changed. -- **CI workflow edits** validate the Node CI graph plus workflow linting, but do not force Windows, Android, or macOS native builds by themselves; those platform lanes stay scoped to platform source changes. +- **CI workflow edits** validate the Node CI graph plus workflow linting, but do not force Windows, iOS, Android, or macOS native builds by themselves; those platform lanes stay scoped to platform source changes. - **Workflow Sanity** runs `actionlint`, `zizmor` over all workflow YAML files, the composite-action interpolation guard, and the conflict-marker guard. The PR-scoped `security-fast` job also runs `zizmor` over changed workflow files so workflow security findings fail early in the main CI graph. - **Docs on `main` pushes** are checked by the standalone `Docs` workflow with the same ClawHub docs mirror used by CI, so mixed code+docs pushes do not also queue the CI `check-docs` shard. Pull requests and manual CI still run `check-docs` from CI when docs changed. - **TUI PTY** runs in the `checks-node-core-runtime-tui-pty` Linux Node shard for TUI changes. The shard runs `test/vitest/vitest.tui-pty.config.ts` with `OPENCLAW_TUI_PTY_INCLUDE_LOCAL=1`, so it covers both the deterministic `TuiBackend` fixture lane and the slower `tui --local` smoke that mocks only the external model endpoint. @@ -120,7 +121,7 @@ Treat GitHub titles, comments, bodies, review text, branch names, and commit mes ## Manual dispatches -Manual CI dispatches run the same job graph as normal CI but force every non-Android scoped lane on: Linux Node shards, bundled-plugin shards, plugin and channel contract shards, Node 22 compatibility, `check-*`, `check-additional-*`, built-artifact smoke checks, docs checks, Python skills, Windows, macOS, and Control UI i18n. Standalone manual CI dispatches run Android only with `include_android=true`; the full release umbrella enables Android by passing `include_android=true`. Plugin prerelease static checks, the release-only `agentic-plugins` shard, the full extension batch sweep, and plugin prerelease Docker lanes are excluded from CI. The Docker prerelease suite runs only when `Full Release Validation` dispatches the separate `Plugin Prerelease` workflow with the release-validation gate enabled. +Manual CI dispatches run the same job graph as normal CI but force every non-Android scoped lane on: Linux Node shards, bundled-plugin shards, plugin and channel contract shards, Node 22 compatibility, `check-*`, `check-additional-*`, built-artifact smoke checks, docs checks, Python skills, Windows, macOS, iOS build, and Control UI i18n. Standalone manual CI dispatches run Android only with `include_android=true`; the full release umbrella enables Android by passing `include_android=true`. Plugin prerelease static checks, the release-only `agentic-plugins` shard, the full extension batch sweep, and plugin prerelease Docker lanes are excluded from CI. The Docker prerelease suite runs only when `Full Release Validation` dispatches the separate `Plugin Prerelease` workflow with the release-validation gate enabled. Manual runs use a unique concurrency group so a release-candidate full suite is not cancelled by another push or PR run on the same ref. The optional `target_ref` input lets a trusted caller run that graph against a branch, tag, or full commit SHA while using the workflow file from the selected dispatch ref. @@ -140,7 +141,7 @@ gh workflow run full-release-validation.yml --ref main -f ref= | `blacksmith-16vcpu-ubuntu-2404` | `build-artifacts`, `check-lint` (CPU-sensitive enough that 8 vCPU cost more than they saved); install-smoke Docker builds (32-vCPU queue time cost more than it saved) | | `blacksmith-8vcpu-windows-2025` | `checks-windows` | | `blacksmith-6vcpu-macos-15` | `macos-node` on `openclaw/openclaw`; forks fall back to `macos-15` | -| `blacksmith-12vcpu-macos-26` | `macos-swift` on `openclaw/openclaw`; forks fall back to `macos-26` | +| `blacksmith-12vcpu-macos-26` | `macos-swift` and `ios-build` on `openclaw/openclaw`; forks fall back to `macos-26` | Canonical-repo CI keeps Blacksmith as the default runner path for normal push and pull-request runs. `workflow_dispatch` and non-canonical repository runs use GitHub-hosted runners, but normal canonical runs do not currently probe Blacksmith queue health or automatically fall back to GitHub-hosted labels when Blacksmith is unavailable. @@ -162,6 +163,7 @@ pnpm test:channels pnpm test:contracts:channels pnpm check:docs # docs format + lint + broken links pnpm build # build dist when CI artifact/smoke checks matter +pnpm ios:build # generate and build the iOS app project pnpm ci:timings # summarize the latest origin/main push CI run pnpm ci:timings:recent # compare recent successful main CI runs node scripts/ci-run-timings.mjs # summarize wall time, queue time, and slowest jobs diff --git a/scripts/ci-changed-scope.d.mts b/scripts/ci-changed-scope.d.mts index 9642404a2e9b..8f42893bc160 100644 --- a/scripts/ci-changed-scope.d.mts +++ b/scripts/ci-changed-scope.d.mts @@ -1,6 +1,7 @@ export type ChangedScope = { runNode: boolean; runMacos: boolean; + runIosBuild: boolean; runAndroid: boolean; runWindows: boolean; runSkillsPython: boolean; diff --git a/scripts/ci-changed-scope.mjs b/scripts/ci-changed-scope.mjs index 8abb5bef786e..23357c38b5a6 100644 --- a/scripts/ci-changed-scope.mjs +++ b/scripts/ci-changed-scope.mjs @@ -4,13 +4,14 @@ import { appendFileSync } from "node:fs"; import { isDirectRunUrl } from "./lib/direct-run.mjs"; import { resolveMergeHeadDiffBase } from "./lib/merge-head-diff-base.mjs"; -/** @typedef {{ runNode: boolean; runMacos: boolean; runAndroid: boolean; runWindows: boolean; runSkillsPython: boolean; runChangedSmoke: boolean; runControlUiI18n: boolean }} ChangedScope */ +/** @typedef {{ runNode: boolean; runMacos: boolean; runIosBuild: boolean; runAndroid: boolean; runWindows: boolean; runSkillsPython: boolean; runChangedSmoke: boolean; runControlUiI18n: boolean }} ChangedScope */ /** @typedef {{ runFastOnly: boolean; runPluginContracts: boolean; runCiRouting: boolean }} NodeFastScope */ /** @typedef {{ runFastInstallSmoke: boolean; runFullInstallSmoke: boolean }} InstallSmokeScope */ const FULL_SCOPE = { runNode: true, runMacos: true, + runIosBuild: true, runAndroid: true, runWindows: true, runSkillsPython: true, @@ -21,6 +22,7 @@ const FULL_SCOPE = { const EMPTY_SCOPE = { runNode: false, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -36,6 +38,8 @@ const MACOS_NATIVE_RE = /^(apps\/macos\/|apps\/macos-mlx-tts\/|apps\/ios\/|apps\/shared\/|apps\/swabble\/|Swabble\/)/; const MACOS_SCRIPT_SCOPE_RE = /^(?:scripts\/(?:codesign-mac-app|create-dmg|notarize-mac-artifact|package-mac-app|package-mac-dist)\.sh|scripts\/lib\/plistbuddy\.sh|test\/scripts\/(?:codesign-mac-app|create-dmg|notarize-mac-artifact|package-mac-app|package-mac-dist)\.test\.ts)$/; +const IOS_BUILD_RE = + /^(apps\/ios\/|apps\/shared\/|apps\/swabble\/|Swabble\/|config\/(?:swiftformat|swiftlint\.yml)$|scripts\/ios-(?:configure-signing|team-id|write-version-xcconfig)\.sh$|scripts\/ios-version\.ts$|scripts\/lib\/(?:ios-version\.ts|npm-publish-plan\.mjs|version-script-args\.ts)$)/; const ANDROID_NATIVE_RE = /^(apps\/android\/|apps\/shared\/)/; const NODE_SCOPE_RE = /^(src\/|test\/|extensions\/|packages\/|scripts\/|ui\/|\.github\/|openclaw\.mjs$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|tsconfig.*\.json$|vitest.*\.ts$|tsdown\.config\.ts$|\.oxlintrc\.json$|\.oxfmtrc\.jsonc$)/; @@ -72,19 +76,12 @@ const NODE_FAST_SCOPE_RE = new RegExp( */ export function detectChangedScope(changedPaths) { if (!Array.isArray(changedPaths) || changedPaths.length === 0) { - return { - runNode: true, - runMacos: true, - runAndroid: true, - runWindows: true, - runSkillsPython: true, - runChangedSmoke: true, - runControlUiI18n: true, - }; + return { ...FULL_SCOPE }; } let runNode = false; let runMacos = false; + let runIosBuild = false; let runAndroid = false; let runWindows = false; let runSkillsPython = false; @@ -120,6 +117,10 @@ export function detectChangedScope(changedPaths) { runMacos = true; } + if (IOS_BUILD_RE.test(path)) { + runIosBuild = true; + } + if (!NATIVE_PROTOCOL_GEN_RE.test(path) && ANDROID_NATIVE_RE.test(path)) { runAndroid = true; } @@ -155,6 +156,7 @@ export function detectChangedScope(changedPaths) { return { runNode, runMacos, + runIosBuild, runAndroid, runWindows, runSkillsPython, @@ -298,6 +300,7 @@ export function writeGitHubOutput( } appendFileSync(outputPath, `run_node=${scope.runNode}\n`, "utf8"); appendFileSync(outputPath, `run_macos=${scope.runMacos}\n`, "utf8"); + appendFileSync(outputPath, `run_ios_build=${scope.runIosBuild}\n`, "utf8"); appendFileSync(outputPath, `run_android=${scope.runAndroid}\n`, "utf8"); appendFileSync(outputPath, `run_windows=${scope.runWindows}\n`, "utf8"); appendFileSync(outputPath, `run_skills_python=${scope.runSkillsPython}\n`, "utf8"); diff --git a/src/scripts/ci-changed-scope.test.ts b/src/scripts/ci-changed-scope.test.ts index 6fd74d46210e..3abbbc10e177 100644 --- a/src/scripts/ci-changed-scope.test.ts +++ b/src/scripts/ci-changed-scope.test.ts @@ -16,6 +16,7 @@ const { detectChangedScope: (paths: string[]) => { runNode: boolean; runMacos: boolean; + runIosBuild: boolean; runAndroid: boolean; runWindows: boolean; runSkillsPython: boolean; @@ -133,6 +134,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope([])).toEqual({ runNode: true, runMacos: true, + runIosBuild: true, runAndroid: true, runWindows: true, runSkillsPython: true, @@ -145,6 +147,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["docs/ci.md", "README.md"])).toEqual({ runNode: false, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -157,6 +160,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["src/config/defaults.ts"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -169,6 +173,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["apps/macos/Sources/Foo.swift"])).toEqual({ runNode: false, runMacos: true, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -180,6 +185,17 @@ describe("detectChangedScope", () => { ).toEqual({ runNode: false, runMacos: true, + runIosBuild: false, + runAndroid: false, + runWindows: false, + runSkillsPython: false, + runChangedSmoke: false, + runControlUiI18n: false, + }); + expect(detectChangedScope(["apps/ios/Sources/RootTabs.swift"])).toEqual({ + runNode: false, + runMacos: true, + runIosBuild: true, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -189,6 +205,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["apps/shared/OpenClawKit/Sources/Foo.swift"])).toEqual({ runNode: false, runMacos: true, + runIosBuild: true, runAndroid: true, runWindows: false, runSkillsPython: false, @@ -198,6 +215,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["apps/swabble/Sources/SwabbleKit/WakeWordGate.swift"])).toEqual({ runNode: false, runMacos: true, + runIosBuild: true, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -207,6 +225,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["Swabble/Sources/SwabbleKit/WakeWordGate.swift"])).toEqual({ runNode: false, runMacos: true, + runIosBuild: true, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -215,12 +234,34 @@ describe("detectChangedScope", () => { }); }); - it("does not force macOS for generated protocol model-only changes", () => { + it("enables the iOS build lane for iOS build helper changes", () => { + for (const helperPath of [ + "scripts/ios-team-id.sh", + "scripts/ios-version.ts", + "scripts/lib/ios-version.ts", + "scripts/lib/npm-publish-plan.mjs", + "scripts/lib/version-script-args.ts", + ]) { + expect(detectChangedScope([helperPath])).toEqual({ + runNode: true, + runMacos: false, + runIosBuild: true, + runAndroid: false, + runWindows: false, + runSkillsPython: false, + runChangedSmoke: false, + runControlUiI18n: false, + }); + } + }); + + it("runs the iOS build but not macOS for generated protocol model-only changes", () => { expect( detectChangedScope(["apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift"]), ).toEqual({ runNode: false, runMacos: false, + runIosBuild: true, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -233,6 +274,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["README.md"])).toEqual({ runNode: false, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -243,6 +285,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope([".crabbox.yaml"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -255,6 +298,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope([".github/labeler.yml"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -267,6 +311,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["skills/skill-creator/scripts/test_quick_validate.py"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: true, @@ -279,6 +324,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["skills/pyproject.toml"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: true, @@ -291,6 +337,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope([".github/workflows/ci.yml"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: true, runSkillsPython: false, @@ -311,6 +358,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope([changedPath])).toEqual({ runNode: true, runMacos: true, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -331,6 +379,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope([changedPath])).toEqual({ runNode: true, runMacos: true, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -344,6 +393,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["extensions/memory-lancedb/index.test.ts"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -353,6 +403,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["src/auto-reply/reply/streaming-directives.ts"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -362,6 +413,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["src/process/exec.ts"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: true, runSkillsPython: false, @@ -371,6 +423,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["src/process/exec.windows.test.ts"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: true, runSkillsPython: false, @@ -380,6 +433,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["src/shared/runtime-import.ts"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: true, runSkillsPython: false, @@ -389,6 +443,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["src/shared/runtime-import.test.ts"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: true, runSkillsPython: false, @@ -398,6 +453,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["scripts/npm-runner.mjs"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: true, runSkillsPython: false, @@ -407,6 +463,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["scripts/lib/format-generated-module.mjs"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: true, runSkillsPython: false, @@ -416,6 +473,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["test/scripts/format-generated-module.test.ts"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: true, runSkillsPython: false, @@ -425,6 +483,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["scripts/install.ps1"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: true, runSkillsPython: false, @@ -437,6 +496,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["scripts/install.sh"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -446,6 +506,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["scripts/install-cli.sh"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -455,6 +516,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope([bundledPluginFile("matrix", "package.json")])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -464,6 +526,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope([".github/workflows/install-smoke.yml"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -473,6 +536,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["scripts/e2e/qr-import-docker.sh"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -482,6 +546,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["scripts/e2e/gateway-network-docker.sh"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -491,6 +556,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["scripts/e2e/Dockerfile"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -500,6 +566,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["scripts/e2e/agents-delete-shared-workspace-docker.sh"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -509,6 +576,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["scripts/e2e/plugin-update-unchanged-docker.sh"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -518,6 +586,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["scripts/postinstall-bundled-plugins.mjs"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -527,6 +596,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["scripts/ci-changed-scope.mjs"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -539,6 +609,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["src/plugins/loader.ts"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -548,6 +619,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["src/plugin-sdk/provider-entry.ts"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -557,6 +629,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["packages/gateway-protocol/src/schema/messages.ts"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -566,6 +639,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["packages/gateway-client/src/client.ts"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -575,6 +649,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["src/channels/plugins/catalog.ts"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -584,6 +659,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope([bundledPluginFile("matrix", "index.ts")])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -639,6 +715,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["src/plugins/loader.test.ts"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -648,6 +725,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope([bundledPluginFile("matrix", "index.test.ts")])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -660,6 +738,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["ui/src/i18n/locales/en.ts"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -670,6 +749,7 @@ describe("detectChangedScope", () => { expect(detectChangedScope(["scripts/control-ui-i18n.ts"])).toEqual({ runNode: true, runMacos: false, + runIosBuild: false, runAndroid: false, runWindows: false, runSkillsPython: false, @@ -800,6 +880,7 @@ describe("detectChangedScope", () => { expect(parseGitHubOutput(fs.readFileSync(outputPath, "utf8"))).toEqual({ run_node: "false", run_macos: "false", + run_ios_build: "false", run_android: "false", run_windows: "false", run_skills_python: "false", diff --git a/test/scripts/ci-workflow-guards.test.ts b/test/scripts/ci-workflow-guards.test.ts index 78c89e690f8b..6c0371d8d443 100644 --- a/test/scripts/ci-workflow-guards.test.ts +++ b/test/scripts/ci-workflow-guards.test.ts @@ -386,7 +386,7 @@ describe("ci workflow guards", () => { it("bounds platform checkout fetches without GNU timeout", () => { const workflow = readCiWorkflow(); - for (const jobName of ["checks-windows", "macos-node", "macos-swift"]) { + for (const jobName of ["checks-windows", "macos-node", "macos-swift", "ios-build"]) { const checkoutStep = workflow.jobs[jobName].steps.find((step) => step.name === "Checkout"); expect(checkoutStep.run, jobName).toContain("fetch_checkout_ref()"); @@ -583,6 +583,7 @@ describe("ci workflow guards", () => { "checks-windows", "macos-node", "macos-swift", + "ios-build", "android", ]); expect(timingJob.if).toContain("always()");