Skip to content

Commit cc981f8

Browse files
committed
ci: build iOS app for iOS changes
1 parent c9ddf2e commit cc981f8

6 files changed

Lines changed: 180 additions & 16 deletions

File tree

.github/workflows/ci.yml

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ jobs:
100100
run_macos_node: ${{ steps.manifest.outputs.run_macos_node }}
101101
macos_node_matrix: ${{ steps.manifest.outputs.macos_node_matrix }}
102102
run_macos_swift: ${{ steps.manifest.outputs.run_macos_swift }}
103+
run_ios_build: ${{ steps.manifest.outputs.run_ios_build }}
103104
run_android_job: ${{ steps.manifest.outputs.run_android_job }}
104105
android_matrix: ${{ steps.manifest.outputs.android_matrix }}
105106
steps:
@@ -204,6 +205,7 @@ jobs:
204205
OPENCLAW_CI_DOCS_CHANGED: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.docs_scope.outputs.docs_changed }}
205206
OPENCLAW_CI_RUN_NODE: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_node || 'false' }}
206207
OPENCLAW_CI_RUN_MACOS: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_macos || 'false' }}
208+
OPENCLAW_CI_RUN_IOS_BUILD: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_ios_build || 'false' }}
207209
OPENCLAW_CI_RUN_ANDROID: ${{ github.event_name == 'workflow_dispatch' && (inputs.release_gate || inputs.include_android) && 'true' || steps.changed_scope.outputs.run_android || 'false' }}
208210
OPENCLAW_CI_RUN_WINDOWS: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_windows || 'false' }}
209211
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:
267269
const runPluginContractShards = runNodeFull || runNodeFastPluginContracts;
268270
const runMacos =
269271
parseBoolean(process.env.OPENCLAW_CI_RUN_MACOS) && !docsOnly && isCanonicalRepository;
272+
const runIosBuild =
273+
parseBoolean(process.env.OPENCLAW_CI_RUN_IOS_BUILD) && !docsOnly && isCanonicalRepository;
270274
const runAndroid =
271275
parseBoolean(process.env.OPENCLAW_CI_RUN_ANDROID) && !docsOnly && isCanonicalRepository;
272276
const runWindows =
@@ -361,6 +365,7 @@ jobs:
361365
runMacos ? [{ check_name: "macos-node", runtime: "node", task: "test" }] : [],
362366
),
363367
run_macos_swift: runMacos,
368+
run_ios_build: runIosBuild,
364369
run_android_job: runAndroid,
365370
android_matrix: createMatrix(
366371
runAndroid
@@ -2162,6 +2167,76 @@ jobs:
21622167
done
21632168
exit 1
21642169
2170+
ios-build:
2171+
permissions:
2172+
contents: read
2173+
name: "ios-build"
2174+
needs: [preflight]
2175+
if: needs.preflight.outputs.run_ios_build == 'true'
2176+
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'macos-26' || (github.repository == 'openclaw/openclaw' && 'blacksmith-12vcpu-macos-26' || 'macos-26') }}
2177+
timeout-minutes: 45
2178+
steps:
2179+
- name: Checkout
2180+
env:
2181+
CHECKOUT_REPO: ${{ github.repository }}
2182+
CHECKOUT_SHA: ${{ needs.preflight.outputs.checkout_revision }}
2183+
run: |
2184+
set -euo pipefail
2185+
git init "$GITHUB_WORKSPACE"
2186+
git -C "$GITHUB_WORKSPACE" config gc.auto 0
2187+
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
2188+
fetch_timeout_seconds=90
2189+
fetch_checkout_ref() {
2190+
git -C "$GITHUB_WORKSPACE" \
2191+
-c protocol.version=2 \
2192+
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
2193+
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout" &
2194+
local fetch_pid="$!"
2195+
local elapsed=0
2196+
while kill -0 "$fetch_pid" 2>/dev/null; do
2197+
if [ "$elapsed" -ge "$fetch_timeout_seconds" ]; then
2198+
kill -TERM "$fetch_pid" 2>/dev/null || true
2199+
sleep 10
2200+
kill -KILL "$fetch_pid" 2>/dev/null || true
2201+
wait "$fetch_pid" || true
2202+
return 124
2203+
fi
2204+
sleep 1
2205+
elapsed=$((elapsed + 1))
2206+
done
2207+
wait "$fetch_pid"
2208+
}
2209+
fetch_checkout_ref
2210+
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
2211+
2212+
- name: Select Xcode 26
2213+
run: |
2214+
set -euo pipefail
2215+
for xcode_app in /Applications/Xcode_26.5.app /Applications/Xcode-26.5.0.app; do
2216+
if [ -d "$xcode_app/Contents/Developer" ]; then
2217+
sudo xcode-select -s "$xcode_app/Contents/Developer"
2218+
break
2219+
fi
2220+
done
2221+
xcodebuild -version
2222+
xcode_version="$(xcodebuild -version | awk 'NR == 1 { print $2 }')"
2223+
if [[ "$xcode_version" != 26.* ]]; then
2224+
echo "error: expected Xcode 26.x, got $xcode_version" >&2
2225+
exit 1
2226+
fi
2227+
swift --version
2228+
2229+
- name: Setup Node environment
2230+
uses: ./.github/actions/setup-node-env
2231+
with:
2232+
install-bun: "false"
2233+
2234+
- name: Install iOS Swift tooling
2235+
run: brew install xcodegen swiftlint swiftformat
2236+
2237+
- name: Build iOS app
2238+
run: pnpm ios:build
2239+
21652240
android:
21662241
permissions:
21672242
contents: read
@@ -2313,6 +2388,7 @@ jobs:
23132388
- checks-windows
23142389
- macos-node
23152390
- macos-swift
2391+
- ios-build
23162392
- android
23172393
if: ${{ !cancelled() && always() && github.event_name != 'push' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }}
23182394
runs-on: ubuntu-24.04

docs/ci.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ or an explicit manual dispatch.
4242
| `checks-windows` | Windows-specific process/path tests plus shared runtime import specifier regressions | Windows-relevant changes |
4343
| `macos-node` | macOS TypeScript test lane using the shared built artifacts | macOS-relevant changes |
4444
| `macos-swift` | Swift lint, build, and tests for the macOS app | macOS-relevant changes |
45+
| `ios-build` | Xcode project generation plus the iOS app simulator build | iOS app, shared app kit, or Swabble changes |
4546
| `android` | Android unit tests for both flavors plus one debug APK build | Android-relevant changes |
4647
| `test-performance-agent` | Daily Codex slow-test optimization after trusted activity | Main CI success or manual dispatch |
4748
| `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.
5253
2. `preflight` decides which lanes exist at all. The `docs-scope` and `changed-scope` logic are steps inside this job, not standalone jobs.
5354
3. `security-fast`, `check-*`, `check-additional-*`, `check-docs`, and `skills-python` fail quickly without waiting on the heavier artifact and platform matrix jobs.
5455
4. `build-artifacts` overlaps with the fast Linux lanes so downstream consumers can start as soon as the shared build is ready.
55-
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`.
56+
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`.
5657

5758
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.
5859

@@ -80,7 +81,7 @@ When the check fails, update the PR body instead of pushing another code commit.
8081

8182
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.
8283

83-
- **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.
84+
- **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.
8485
- **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.
8586
- **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.
8687
- **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
120121

121122
## Manual dispatches
122123

123-
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.
124+
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.
124125

125126
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.
126127

@@ -140,7 +141,7 @@ gh workflow run full-release-validation.yml --ref main -f ref=<branch-or-sha>
140141
| `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) |
141142
| `blacksmith-8vcpu-windows-2025` | `checks-windows` |
142143
| `blacksmith-6vcpu-macos-15` | `macos-node` on `openclaw/openclaw`; forks fall back to `macos-15` |
143-
| `blacksmith-12vcpu-macos-26` | `macos-swift` on `openclaw/openclaw`; forks fall back to `macos-26` |
144+
| `blacksmith-12vcpu-macos-26` | `macos-swift` and `ios-build` on `openclaw/openclaw`; forks fall back to `macos-26` |
144145

145146
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.
146147

@@ -162,6 +163,7 @@ pnpm test:channels
162163
pnpm test:contracts:channels
163164
pnpm check:docs # docs format + lint + broken links
164165
pnpm build # build dist when CI artifact/smoke checks matter
166+
pnpm ios:build # generate and build the iOS app project
165167
pnpm ci:timings # summarize the latest origin/main push CI run
166168
pnpm ci:timings:recent # compare recent successful main CI runs
167169
node scripts/ci-run-timings.mjs <run-id> # summarize wall time, queue time, and slowest jobs

scripts/ci-changed-scope.d.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export type ChangedScope = {
22
runNode: boolean;
33
runMacos: boolean;
4+
runIosBuild: boolean;
45
runAndroid: boolean;
56
runWindows: boolean;
67
runSkillsPython: boolean;

scripts/ci-changed-scope.mjs

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ import { appendFileSync } from "node:fs";
44
import { isDirectRunUrl } from "./lib/direct-run.mjs";
55
import { resolveMergeHeadDiffBase } from "./lib/merge-head-diff-base.mjs";
66

7-
/** @typedef {{ runNode: boolean; runMacos: boolean; runAndroid: boolean; runWindows: boolean; runSkillsPython: boolean; runChangedSmoke: boolean; runControlUiI18n: boolean }} ChangedScope */
7+
/** @typedef {{ runNode: boolean; runMacos: boolean; runIosBuild: boolean; runAndroid: boolean; runWindows: boolean; runSkillsPython: boolean; runChangedSmoke: boolean; runControlUiI18n: boolean }} ChangedScope */
88
/** @typedef {{ runFastOnly: boolean; runPluginContracts: boolean; runCiRouting: boolean }} NodeFastScope */
99
/** @typedef {{ runFastInstallSmoke: boolean; runFullInstallSmoke: boolean }} InstallSmokeScope */
1010

1111
const FULL_SCOPE = {
1212
runNode: true,
1313
runMacos: true,
14+
runIosBuild: true,
1415
runAndroid: true,
1516
runWindows: true,
1617
runSkillsPython: true,
@@ -21,6 +22,7 @@ const FULL_SCOPE = {
2122
const EMPTY_SCOPE = {
2223
runNode: false,
2324
runMacos: false,
25+
runIosBuild: false,
2426
runAndroid: false,
2527
runWindows: false,
2628
runSkillsPython: false,
@@ -36,6 +38,8 @@ const MACOS_NATIVE_RE =
3638
/^(apps\/macos\/|apps\/macos-mlx-tts\/|apps\/ios\/|apps\/shared\/|apps\/swabble\/|Swabble\/)/;
3739
const MACOS_SCRIPT_SCOPE_RE =
3840
/^(?: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)$/;
41+
const IOS_BUILD_RE =
42+
/^(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)$)/;
3943
const ANDROID_NATIVE_RE = /^(apps\/android\/|apps\/shared\/)/;
4044
const NODE_SCOPE_RE =
4145
/^(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(
7276
*/
7377
export function detectChangedScope(changedPaths) {
7478
if (!Array.isArray(changedPaths) || changedPaths.length === 0) {
75-
return {
76-
runNode: true,
77-
runMacos: true,
78-
runAndroid: true,
79-
runWindows: true,
80-
runSkillsPython: true,
81-
runChangedSmoke: true,
82-
runControlUiI18n: true,
83-
};
79+
return { ...FULL_SCOPE };
8480
}
8581

8682
let runNode = false;
8783
let runMacos = false;
84+
let runIosBuild = false;
8885
let runAndroid = false;
8986
let runWindows = false;
9087
let runSkillsPython = false;
@@ -120,6 +117,10 @@ export function detectChangedScope(changedPaths) {
120117
runMacos = true;
121118
}
122119

120+
if (IOS_BUILD_RE.test(path)) {
121+
runIosBuild = true;
122+
}
123+
123124
if (!NATIVE_PROTOCOL_GEN_RE.test(path) && ANDROID_NATIVE_RE.test(path)) {
124125
runAndroid = true;
125126
}
@@ -155,6 +156,7 @@ export function detectChangedScope(changedPaths) {
155156
return {
156157
runNode,
157158
runMacos,
159+
runIosBuild,
158160
runAndroid,
159161
runWindows,
160162
runSkillsPython,
@@ -298,6 +300,7 @@ export function writeGitHubOutput(
298300
}
299301
appendFileSync(outputPath, `run_node=${scope.runNode}\n`, "utf8");
300302
appendFileSync(outputPath, `run_macos=${scope.runMacos}\n`, "utf8");
303+
appendFileSync(outputPath, `run_ios_build=${scope.runIosBuild}\n`, "utf8");
301304
appendFileSync(outputPath, `run_android=${scope.runAndroid}\n`, "utf8");
302305
appendFileSync(outputPath, `run_windows=${scope.runWindows}\n`, "utf8");
303306
appendFileSync(outputPath, `run_skills_python=${scope.runSkillsPython}\n`, "utf8");

0 commit comments

Comments
 (0)