ci: Move iOS runtime tests off macOS-14 (backport 6.7) - #24965
MartinZikmund wants to merge 10 commits into
Conversation
Backport of #24680 (CI part) to release/stable/6.7. The macOS-14 hosted image is retired on 2 Nov 2026. The shards now use the build image and Xcode, take the simulator runtime that Xcode ships, create the 12.9-inch iPad when the image lacks it, and install with xcrun simctl instead of idb. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018riLfyvT4wZYEh8iFjWkKa
Backport of #24961 (cleanup part) to release/stable/6.7. Nothing runs on macOSVMImage_UITests / xCodeRoot_iOS_UITests anymore; the disabled native iOS stage and TestFlight now take the build image and Xcode. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018riLfyvT4wZYEh8iFjWkKa
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
Review effort: Lite
Findings: 3
Open (4)
The runtime selection matches.versionbystartswith($IOS_SDK_VERSION). In practice, the iOS… · New The previous implementation had an explicit wait loop becausesimctl listcould temporarily… · New If the UDID isn’t found in thesimctl list devicesoutput (e.g., transient CoreSimulator state),… · New Using:=inside the exported assignment is functional, but it’s a bit opaque because it both… · New
What changed in this PR
Moves iOS Skia runtime tests off the retiring macOS-14 image by aligning UI test execution with the standard macOS image/Xcode and simplifying the simulator install path.
Changes:
- Switch
ios-uitest-run.shfrom idb-based installs and hardcoded iOS runtime to Xcode-selected runtime discovery +simctl install(with one retry). - Remove macOS-14 / Xcode 15.3 “UITest” image parameters from Azure DevOps templates and route iOS test jobs to the build image/Xcode.
- Drop
macOSVMImage_UITestsandxCodeRoot_iOS_UITestsvariables from the main pipeline variables.
| File | Description |
|---|---|
| build/test-scripts/ios-uitest-run.sh | Updates simulator runtime/device resolution and app install flow to avoid idb and hardcoded iOS 17.5 runtime. |
| build/ci/tests/.azure-devops-tests--skia.yml | Removes test-only macOS/Xcode parameters and uses the shared build image/Xcode for iOS Skia runtime tests. |
| build/ci/tests/.azure-devops-tests-ios-native.yml | Routes native iOS test jobs to the shared image/Xcode settings instead of test-only parameters. |
| build/ci/publish/.azure-devops-publish-ios-testflight.yml | Removes test-only Xcode root parameter to align publishing with build Xcode selection. |
| build/ci/.azure-devops-stages.yml | Stops supplying macOS-14/Xcode 15.3 test-only parameters into downstream templates. |
| .vsts-ci.yml | Removes macOS-14/Xcode 15.3 test-only variables from pipeline configuration. |
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
| UNO_UITEST_SIMULATOR_VERSION=$(xcrun simctl list runtimes --json | jq -r --arg ver "$IOS_SDK_VERSION" ' | ||
| .runtimes | ||
| | map(select(.isAvailable == true and (.identifier | test("SimRuntime\.iOS")) and (.version | startswith($ver)))) | ||
| | sort_by(.version | split(".") | map(tonumber? // 0)) | ||
| | .[-1].identifier // empty') | ||
|
|
||
| if [ -z "$UNO_UITEST_SIMULATOR_VERSION" ]; then | ||
| echo "##vso[task.logissue type=error]UNOBLD008: No iOS $IOS_SDK_VERSION simulator runtime is available on this agent." |
| find_ios_device() { | ||
| xcrun simctl list devices --json | jq -r --arg sim "$UNO_UITEST_SIMULATOR_VERSION" --arg name "$UNO_UITEST_SIMULATOR_NAME" ' | ||
| (.devices[$sim] // []) | ||
| | map(select(.name == $name and .isAvailable == true)) | ||
| | .[0].udid // empty' | ||
| } | ||
|
|
||
| # Check for the presence of idb, and install it if it's not present | ||
| # NOTE: fb-idb currently breaks under Python 3.14 (asyncio get_event_loop change), | ||
| # so we pin fb-idb to Python 3.12 to avoid "There is no current event loop in thread 'MainThread'". | ||
| # Historical context: prior installs referenced an App Center issue/workaround. | ||
| # https://github.com/microsoft/appcenter/issues/2605#issuecomment-1854414963 | ||
| export PATH=$PATH:~/.local/bin | ||
| UITEST_IOSDEVICE_ID=$(find_ios_device) |
| export UITEST_IOSDEVICE_DATA_PATH=$(xcrun simctl list devices --json | jq -r --arg udid "$UITEST_IOSDEVICE_ID" '[.devices[][] | select(.udid == $udid)][0].dataPath') | ||
|
|
||
| export DEVICELIST_FILEPATH=$LOG_FILEPATH/DeviceList-$LOG_PREFIX.json | ||
| echo "Listing iOS simulators to $DEVICELIST_FILEPATH" | ||
| xcrun simctl list devices --json > $DEVICELIST_FILEPATH | ||
|
|
|
|
||
| export UNO_UITEST_SIMULATOR_VERSION="com.apple.CoreSimulator.SimRuntime.iOS-17-5" | ||
| export UNO_UITEST_SIMULATOR_NAME="iPad Pro (12.9-inch) (6th generation)" | ||
| export UNO_UITEST_SIMULATOR_NAME="${UNO_UITEST_SIMULATOR_NAME:=iPad Pro (12.9-inch) (6th generation)}" |
Moves the remaining macOS-15 jobs to macOS-26, as on master. macOS-26 does not ship the iOS 26.1 simulator runtime that Xcode 26.1.1 needs, so Xcode moves to 26.6 (iOS 26.5 runtime preinstalled). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018riLfyvT4wZYEh8iFjWkKa
|
Claude finished @MartinZikmund's task in 3m 13s —— View job PR ReviewVerdict: fix-first — the CI YAML cleanup is correct and complete, but 3 medium issues, 0 blocking:
Cross-cutting notes:
|
| .runtimes | ||
| | map(select(.isAvailable == true and (.identifier | test("SimRuntime\.iOS")) and (.version | startswith($ver)))) | ||
| | sort_by(.version | split(".") | map(tonumber? // 0)) | ||
| | .[-1].identifier // empty') |
There was a problem hiding this comment.
Medium — fragile version match that can produce a false "no runtime" failure.
xcrun --sdk iphonesimulator --show-sdk-version returns a three-part string such as "26.1.0" or a two-part string such as "26.1" depending on the Xcode version. startswith($ver) then matches the runtime .version field, which is always three-part (e.g. "26.1.0"). That is fine when the two strings agree, but it breaks if they diverge even slightly:
- SDK →
"26.1", runtime.version→"26.0.0"→ no match → false failure. - SDK →
"26.1.0", runtime.version→"26.1.1"→startswith("26.1.0")fails → drops a valid runtime.
A more robust approach is to compare only major+minor, which is the meaningful compatibility boundary for simulator runtimes:
| | .[-1].identifier // empty') | |
| | map(select(.isAvailable == true and (.identifier | test("SimRuntime\\.iOS")) and (.version | split(".")[0:2] | join(".") == ($ver | split(".")[0:2] | join("."))))) |
Alternatively, strip both strings to major.minor before the comparison. Either way the error message below should include the full list of discovered runtimes so the build log explains why the match failed.
| # Historical context: prior installs referenced an App Center issue/workaround. | ||
| # https://github.com/microsoft/appcenter/issues/2605#issuecomment-1854414963 | ||
| export PATH=$PATH:~/.local/bin | ||
| UITEST_IOSDEVICE_ID=$(find_ios_device) |
There was a problem hiding this comment.
Medium — removed the CoreSimulator startup poll, reintroducing a source of intermittent flakiness.
The old code looped with sleep 5 until simctl list returned the device. That guard existed because CoreSimulator can take a few seconds to become responsive on a freshly provisioned agent, during which simctl list returns an empty or incomplete device list.
The new code calls find_ios_device() once. If CoreSimulator isn't ready, it returns empty → the code falls into the "create" path → xcrun simctl create may fail or silently duplicate a device, and the agent image already pre-creates this model.
Consider wrapping the initial lookup in a short bounded retry before falling through to device creation, e.g.:
for _attempt in 1 2 3; do
UITEST_IOSDEVICE_ID=$(find_ios_device)
[ -n "$UITEST_IOSDEVICE_ID" ] && break
echo "CoreSimulator not ready yet (attempt $_attempt/3), waiting 5s"
sleep 5
doneIf it's still empty after retries, then proceed with creation — that path is correct for images that genuinely don't ship the device model.
| fi | ||
|
|
||
| export UITEST_IOSDEVICE_ID | ||
| export UITEST_IOSDEVICE_DATA_PATH=$(xcrun simctl list devices --json | jq -r --arg udid "$UITEST_IOSDEVICE_ID" '[.devices[][] | select(.udid == $udid)][0].dataPath') |
There was a problem hiding this comment.
Medium — literal "null" string can leak into downstream path handling.
jq -r returns the string null (not empty) when .[0].dataPath is absent — for example if CoreSimulator is still settling after simctl create and the device doesn't yet appear in the flat devices[][] scan. UITEST_IOSDEVICE_DATA_PATH is later used as a filesystem path; a literal "null" component produces a confusing artifact-upload failure rather than a clear diagnostic.
Use // empty so jq emits nothing on a miss, then guard:
| export UITEST_IOSDEVICE_DATA_PATH=$(xcrun simctl list devices --json | jq -r --arg udid "$UITEST_IOSDEVICE_ID" '[.devices[][] | select(.udid == $udid)][0].dataPath') | |
| export UITEST_IOSDEVICE_DATA_PATH=$(xcrun simctl list devices --json | jq -r --arg udid "$UITEST_IOSDEVICE_ID" '[.devices[][] | select(.udid == $udid)][0].dataPath // empty') |
Add a check after the assignment:
if [ -z "$UITEST_IOSDEVICE_DATA_PATH" ]; then
echo "##vso[task.logissue type=warning]UNOBLD009: Could not resolve dataPath for device $UITEST_IOSDEVICE_ID"
fi|
Claude finished @MartinZikmund's task in 2m 7s —— View job PR ReviewVerdict: fix-first — the CI YAML cleanup is correct and complete, but 3 medium issues, 0 blocking:
Cross-cutting notes:
|
| IOS_SDK_VERSION=$(xcrun --sdk iphonesimulator --show-sdk-version) | ||
| UNO_UITEST_SIMULATOR_VERSION=$(xcrun simctl list runtimes --json | jq -r --arg ver "$IOS_SDK_VERSION" ' | ||
| .runtimes | ||
| | map(select(.isAvailable == true and (.identifier | test("SimRuntime\.iOS")) and (.version | startswith($ver)))) |
There was a problem hiding this comment.
Medium — startswith($ver) can produce a false "no runtime" failure.
xcrun --sdk iphonesimulator --show-sdk-version returns a two-part string ("26.1") on some Xcode versions and three-part ("26.1.0") on others. The runtime .version field from simctl list runtimes is always three-part. When $ver is "26.1.0" and the installed runtime is at patch "26.1.1", startswith("26.1.0") is false — a perfectly valid runtime gets rejected.
The meaningful compatibility boundary for simulator runtimes is major.minor. Compare only that:
| | map(select(.isAvailable == true and (.identifier | test("SimRuntime\.iOS")) and (.version | startswith($ver)))) | |
| | map(select(.isAvailable == true and (.identifier | test("SimRuntime\\.iOS")) and (.version | split(".")[0:2] | join(".") == ($ver | split(".")[0:2] | join("."))))) |
Also worth adding the discovered runtimes to the error message below so the build log explains why the match failed.
| # Historical context: prior installs referenced an App Center issue/workaround. | ||
| # https://github.com/microsoft/appcenter/issues/2605#issuecomment-1854414963 | ||
| export PATH=$PATH:~/.local/bin | ||
| UITEST_IOSDEVICE_ID=$(find_ios_device) |
There was a problem hiding this comment.
Medium — single find_ios_device() call reintroduces CoreSimulator startup flakiness.
The old code polled in a while true loop with sleep 5 until simctl list returned the device, specifically because CoreSimulator can take a few seconds to become responsive on a freshly provisioned agent. The new code calls find_ios_device() exactly once: if CoreSimulator isn't ready, the result is empty, the code enters the "create" path, and simctl create can silently duplicate the pre-created device — or fail outright.
Add a short bounded retry before falling through to device creation:
| UITEST_IOSDEVICE_ID=$(find_ios_device) | |
| for _cs_attempt in 1 2 3; do | |
| UITEST_IOSDEVICE_ID=$(find_ios_device) | |
| [ -n "$UITEST_IOSDEVICE_ID" ] && break | |
| echo "CoreSimulator not ready yet (attempt $_cs_attempt/3), retrying in 5s…" | |
| sleep 5 | |
| done |
If UITEST_IOSDEVICE_ID is still empty after retries, the existing creation path below handles it correctly — that path is valid for images that genuinely don't pre-create the device model.
| fi | ||
|
|
||
| export UITEST_IOSDEVICE_ID | ||
| export UITEST_IOSDEVICE_DATA_PATH=$(xcrun simctl list devices --json | jq -r --arg udid "$UITEST_IOSDEVICE_ID" '[.devices[][] | select(.udid == $udid)][0].dataPath') |
There was a problem hiding this comment.
Medium — literal "null" string can leak into downstream path handling.
jq -r emits the string null (not empty) when .[0].dataPath is absent — for example if CoreSimulator is still settling after simctl create and the device doesn't yet appear in the flat devices[][] scan. UITEST_IOSDEVICE_DATA_PATH is used as a filesystem path later; a literal "null" component produces a confusing artifact-upload failure rather than a clear diagnostic.
Use // empty so jq emits nothing on a miss, then guard:
| export UITEST_IOSDEVICE_DATA_PATH=$(xcrun simctl list devices --json | jq -r --arg udid "$UITEST_IOSDEVICE_ID" '[.devices[][] | select(.udid == $udid)][0].dataPath') | |
| export UITEST_IOSDEVICE_DATA_PATH=$(xcrun simctl list devices --json | jq -r --arg udid "$UITEST_IOSDEVICE_ID" '[.devices[][] | select(.udid == $udid)][0].dataPath // empty') | |
| if [ -z "$UITEST_IOSDEVICE_DATA_PATH" ]; then | |
| echo "##vso[task.logissue type=warning]UNOBLD009: Could not resolve dataPath for device $UITEST_IOSDEVICE_ID" | |
| fi |
The upstream Noto font data moved, so the Font Fallback Maps Validation stage fails on every release/stable/6.7 PR. master and servicing/6.8 regenerate these on a schedule; 6.7 does not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018riLfyvT4wZYEh8iFjWkKa
|
Claude finished @MartinZikmund's task in 2m 33s —— View job PR ReviewVerdict: fix-first — the CI YAML cleanup is correct and complete, but 3 medium issues, 0 blocking:
Cross-cutting notes:
|
| IOS_SDK_VERSION=$(xcrun --sdk iphonesimulator --show-sdk-version) | ||
| UNO_UITEST_SIMULATOR_VERSION=$(xcrun simctl list runtimes --json | jq -r --arg ver "$IOS_SDK_VERSION" ' | ||
| .runtimes | ||
| | map(select(.isAvailable == true and (.identifier | test("SimRuntime\.iOS")) and (.version | startswith($ver)))) |
There was a problem hiding this comment.
Medium — startswith($ver) can produce a false "no runtime" failure.
xcrun --sdk iphonesimulator --show-sdk-version returns a two-part string ("26.1") on some Xcode versions and a three-part string ("26.1.0") on others. The runtime .version field from simctl list runtimes is always three-part. When $ver is "26.1.0" and the installed runtime is at patch "26.1.1", startswith("26.1.0") is false — a valid runtime gets rejected and the job fails with UNOBLD008.
The meaningful compatibility boundary for simulator runtimes is major.minor. Narrow the comparison to that:
| | map(select(.isAvailable == true and (.identifier | test("SimRuntime\.iOS")) and (.version | startswith($ver)))) | |
| | map(select(.isAvailable == true and (.identifier | test("SimRuntime\\.iOS")) and (.version | split(".")[0:2] | join(".") == ($ver | split(".")[0:2] | join("."))))) |
Also worth surfacing the discovered runtimes in the error message below (line 181) so the build log explains why the match failed.
| # Historical context: prior installs referenced an App Center issue/workaround. | ||
| # https://github.com/microsoft/appcenter/issues/2605#issuecomment-1854414963 | ||
| export PATH=$PATH:~/.local/bin | ||
| UITEST_IOSDEVICE_ID=$(find_ios_device) |
There was a problem hiding this comment.
Medium — single find_ios_device() call reintroduces CoreSimulator startup flakiness.
The old code polled with while true; do sleep 5; done specifically because CoreSimulator can take a few seconds to become responsive on a freshly provisioned agent. The new code calls find_ios_device() exactly once — if CoreSimulator isn't ready, the result is empty and execution falls through to the device-creation path, which may silently duplicate a pre-created device or fail outright.
A short bounded retry before the creation fallback preserves the guard without an unbounded loop:
| UITEST_IOSDEVICE_ID=$(find_ios_device) | |
| for _cs_attempt in 1 2 3; do | |
| UITEST_IOSDEVICE_ID=$(find_ios_device) | |
| [ -n "$UITEST_IOSDEVICE_ID" ] && break | |
| echo "CoreSimulator not ready yet (attempt $_cs_attempt/3), retrying in 5s…" | |
| sleep 5 | |
| done |
If UITEST_IOSDEVICE_ID is still empty after retries, the existing creation path below is the correct fallback — that path is valid for images that genuinely don't pre-create the device model.
| fi | ||
|
|
||
| export UITEST_IOSDEVICE_ID | ||
| export UITEST_IOSDEVICE_DATA_PATH=$(xcrun simctl list devices --json | jq -r --arg udid "$UITEST_IOSDEVICE_ID" '[.devices[][] | select(.udid == $udid)][0].dataPath') |
There was a problem hiding this comment.
Medium — literal "null" string can leak into downstream path handling.
jq -r emits the string null (not empty) when .[0].dataPath is absent — for example if CoreSimulator is still settling after simctl create and the device doesn't yet appear in the flat devices[][] scan. UITEST_IOSDEVICE_DATA_PATH is used as a filesystem path later; a literal "null" component produces a confusing artifact-upload failure rather than a clear diagnostic.
Use // empty so jq emits nothing on a miss, then guard:
| export UITEST_IOSDEVICE_DATA_PATH=$(xcrun simctl list devices --json | jq -r --arg udid "$UITEST_IOSDEVICE_ID" '[.devices[][] | select(.udid == $udid)][0].dataPath') | |
| export UITEST_IOSDEVICE_DATA_PATH=$(xcrun simctl list devices --json | jq -r --arg udid "$UITEST_IOSDEVICE_ID" '[.devices[][] | select(.udid == $udid)][0].dataPath // empty') | |
| if [ -z "$UITEST_IOSDEVICE_DATA_PATH" ]; then | |
| echo "##vso[task.logissue type=warning]UNOBLD009: Could not resolve dataPath for device $UITEST_IOSDEVICE_ID" | |
| fi |
Uno.Templates@*-* now resolves to 7.0.0-dev.17, so MyApp is built on 7.0 packages and the locally built 6.x RemoteControl client never connects. Mirrors the master ignore (37dc650). Refs #24800 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014RHVD5sbb5nMHzf8Lu24gZ (cherry picked from commit caa909b)
|
Claude finished @MartinZikmund's task in 1m 59s —— View job PR ReviewVerdict: fix-first — the CI YAML cleanup is correct and complete, but 3 medium issues, 0 blocking:
Each issue has a code suggestion attached to its inline comment — they are independent one-to-three line fixes. Cross-cutting notes:
|
| IOS_SDK_VERSION=$(xcrun --sdk iphonesimulator --show-sdk-version) | ||
| UNO_UITEST_SIMULATOR_VERSION=$(xcrun simctl list runtimes --json | jq -r --arg ver "$IOS_SDK_VERSION" ' | ||
| .runtimes | ||
| | map(select(.isAvailable == true and (.identifier | test("SimRuntime\.iOS")) and (.version | startswith($ver)))) |
There was a problem hiding this comment.
Medium — startswith($ver) can produce a false "no runtime" failure.
xcrun --sdk iphonesimulator --show-sdk-version returns a two-part string ("26.1") on some Xcode versions and three-part ("26.1.0") on others. The runtime .version from simctl list runtimes is always three-part. When $ver is "26.1.0" and the installed runtime is "26.1.1", startswith("26.1.0") is false — a valid runtime gets rejected and the job fails with UNOBLD008.
The meaningful compatibility boundary for simulator runtimes is major.minor. Compare only that:
| | map(select(.isAvailable == true and (.identifier | test("SimRuntime\.iOS")) and (.version | startswith($ver)))) | |
| | map(select(.isAvailable == true and (.identifier | test("SimRuntime\\.iOS")) and (.version | split(".")[0:2] | join(".") == ($ver | split(".")[0:2] | join("."))))) |
Also worth surfacing the discovered runtimes in the error message (line 180) so the build log explains why the match failed.
| # Historical context: prior installs referenced an App Center issue/workaround. | ||
| # https://github.com/microsoft/appcenter/issues/2605#issuecomment-1854414963 | ||
| export PATH=$PATH:~/.local/bin | ||
| UITEST_IOSDEVICE_ID=$(find_ios_device) |
There was a problem hiding this comment.
Medium — single find_ios_device() call reintroduces CoreSimulator startup flakiness.
The old code polled with while true; do sleep 5; done specifically because CoreSimulator can take a few seconds to become responsive on a freshly provisioned agent. The new code calls find_ios_device() exactly once — if CoreSimulator isn't ready, the result is empty, execution falls through to the device-creation path, and simctl create can silently duplicate a pre-created device or fail outright.
A short bounded retry before the creation fallback preserves the guard without an unbounded loop:
| UITEST_IOSDEVICE_ID=$(find_ios_device) | |
| for _cs_attempt in 1 2 3; do | |
| UITEST_IOSDEVICE_ID=$(find_ios_device) | |
| [ -n "$UITEST_IOSDEVICE_ID" ] && break | |
| echo "CoreSimulator not ready yet (attempt $_cs_attempt/3), retrying in 5s…" | |
| sleep 5 | |
| done |
If UITEST_IOSDEVICE_ID is still empty after retries, the existing creation path below is the correct fallback — it's valid for images that genuinely don't pre-create the device model.
| fi | ||
|
|
||
| export UITEST_IOSDEVICE_ID | ||
| export UITEST_IOSDEVICE_DATA_PATH=$(xcrun simctl list devices --json | jq -r --arg udid "$UITEST_IOSDEVICE_ID" '[.devices[][] | select(.udid == $udid)][0].dataPath') |
There was a problem hiding this comment.
Medium — literal "null" string can leak into downstream path handling.
jq -r emits the string null (not empty) when .[0].dataPath is absent — for example if CoreSimulator is still settling after simctl create and the device doesn't yet appear in the flat devices[][] scan. UITEST_IOSDEVICE_DATA_PATH is used as a filesystem path later; a literal "null" component produces a confusing artifact-upload failure rather than a clear diagnostic.
Use // empty so jq emits nothing on a miss, then guard:
| export UITEST_IOSDEVICE_DATA_PATH=$(xcrun simctl list devices --json | jq -r --arg udid "$UITEST_IOSDEVICE_ID" '[.devices[][] | select(.udid == $udid)][0].dataPath') | |
| export UITEST_IOSDEVICE_DATA_PATH=$(xcrun simctl list devices --json | jq -r --arg udid "$UITEST_IOSDEVICE_ID" '[.devices[][] | select(.udid == $udid)][0].dataPath // empty') | |
| if [ -z "$UITEST_IOSDEVICE_DATA_PATH" ]; then | |
| echo "##vso[task.logissue type=warning]UNOBLD009: Could not resolve dataPath for device $UITEST_IOSDEVICE_ID" | |
| fi |
.NET for iOS 26.1 refuses any Xcode but 26.1 ("requires Xcode 26.1. The current version of Xcode is 26.6"), so builds stay on Xcode 26.1.1, which macOS-26 also ships. That image has no iOS 26.1 simulator runtime, so build-only jobs skip the runtime check in Select Xcode, and the iOS runtime-test shards select Xcode 26.6, whose iOS 26.5 runtime is preinstalled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018riLfyvT4wZYEh8iFjWkKa
| fi | ||
|
|
||
| export UITEST_IOSDEVICE_ID | ||
| export UITEST_IOSDEVICE_DATA_PATH=$(xcrun simctl list devices --json | jq -r --arg udid "$UITEST_IOSDEVICE_ID" '[.devices[][] | select(.udid == $udid)][0].dataPath') |
There was a problem hiding this comment.
Medium — literal "null" string can leak into downstream path handling (still unaddressed).
jq -r emits the string null (not empty) when .[0].dataPath is absent — for example if CoreSimulator is still settling after simctl create and the device doesn't yet appear in the flat devices[][] scan. UITEST_IOSDEVICE_DATA_PATH is used as a filesystem path later; a literal "null" component produces a confusing artifact-upload failure rather than a clear diagnostic.
Use // empty so jq emits nothing on a miss, then guard:
| export UITEST_IOSDEVICE_DATA_PATH=$(xcrun simctl list devices --json | jq -r --arg udid "$UITEST_IOSDEVICE_ID" '[.devices[][] | select(.udid == $udid)][0].dataPath') | |
| export UITEST_IOSDEVICE_DATA_PATH=$(xcrun simctl list devices --json | jq -r --arg udid "$UITEST_IOSDEVICE_ID" '[.devices[][] | select(.udid == $udid)][0].dataPath // empty') | |
| if [ -z "$UITEST_IOSDEVICE_DATA_PATH" ]; then | |
| echo "##vso[task.logissue type=warning]UNOBLD009: Could not resolve dataPath for device $UITEST_IOSDEVICE_ID" | |
| fi |
|
🤖 Your WebAssembly Skia Sample App stage site is ready! Visit it here: https://unowasmprstaging.z20.web.core.windows.net/pr-24965/wasm-skia-net9/index.html |
jq rejects "\." in a string literal, so every iOS shard aborted while resolving the simulator. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018riLfyvT4wZYEh8iFjWkKa
|
Claude finished @MartinZikmund's task in 2m 38s —— View job PR ReviewVerdict: fix-first — the CI YAML cleanup is correct and complete, but 3 medium issues, 0 blocking:
Each issue has a one-click code suggestion attached. All three are independent and confined to Cross-cutting notes (unchanged):
|
| IOS_SDK_VERSION=$(xcrun --sdk iphonesimulator --show-sdk-version) | ||
| UNO_UITEST_SIMULATOR_VERSION=$(xcrun simctl list runtimes --json | jq -r --arg ver "$IOS_SDK_VERSION" ' | ||
| .runtimes | ||
| | map(select(.isAvailable == true and (.identifier | test("SimRuntime\\.iOS")) and (.version | startswith($ver)))) |
There was a problem hiding this comment.
Medium — fragile version match (still unaddressed after 6+ review passes).
xcrun --sdk iphonesimulator --show-sdk-version can return either a two-part ("26.5") or three-part ("26.5.0") string depending on the Xcode version. The runtime .version from simctl list runtimes is always three-part. If $ver is "26.5.0" and the installed runtime is "26.5.1", startswith("26.5.0") is false — a perfectly valid runtime gets rejected and the job fails with UNOBLD008.
The meaningful compatibility boundary for simulator runtimes is major.minor. Compare only that:
| | map(select(.isAvailable == true and (.identifier | test("SimRuntime\\.iOS")) and (.version | startswith($ver)))) | |
| | map(select(.isAvailable == true and (.identifier | test("SimRuntime\\.iOS")) and (.version | split(".")[0:2] | join(".") == ($ver | split(".")[0:2] | join("."))))) |
| # Historical context: prior installs referenced an App Center issue/workaround. | ||
| # https://github.com/microsoft/appcenter/issues/2605#issuecomment-1854414963 | ||
| export PATH=$PATH:~/.local/bin | ||
| UITEST_IOSDEVICE_ID=$(find_ios_device) |
There was a problem hiding this comment.
Medium — single find_ios_device() call reintroduces CoreSimulator startup flakiness (still unaddressed after 6+ review passes).
The old code polled with while true; do sleep 5; done specifically because CoreSimulator can take a few seconds to become responsive on a freshly provisioned agent. The new code calls find_ios_device() exactly once — if CoreSimulator isn't ready, the result is empty, execution falls through to the device-creation path, and simctl create can silently duplicate a pre-created device or fail outright.
A short bounded retry before the creation fallback preserves the guard without an unbounded loop:
| UITEST_IOSDEVICE_ID=$(find_ios_device) | |
| for _cs_attempt in 1 2 3; do | |
| UITEST_IOSDEVICE_ID=$(find_ios_device) | |
| [ -n "$UITEST_IOSDEVICE_ID" ] && break | |
| echo "CoreSimulator not ready yet (attempt $_cs_attempt/3), retrying in 5s…" | |
| sleep 5 | |
| done |
If UITEST_IOSDEVICE_ID is still empty after retries, the existing creation path below is the correct fallback — it's valid for images that genuinely don't pre-create the device model.
| fi | ||
|
|
||
| export UITEST_IOSDEVICE_ID | ||
| export UITEST_IOSDEVICE_DATA_PATH=$(xcrun simctl list devices --json | jq -r --arg udid "$UITEST_IOSDEVICE_ID" '[.devices[][] | select(.udid == $udid)][0].dataPath') |
There was a problem hiding this comment.
Medium — literal "null" string can leak into downstream path handling (still unaddressed after 6+ review passes).
jq -r emits the string null (not empty) when .[0].dataPath is absent — for example if CoreSimulator is still settling after simctl create and the device doesn't yet appear in the flat devices[][] scan. UITEST_IOSDEVICE_DATA_PATH is used as a filesystem path later; a literal "null" component produces a confusing artifact-upload failure rather than a clear diagnostic.
Use // empty so jq emits nothing on a miss, then guard:
| export UITEST_IOSDEVICE_DATA_PATH=$(xcrun simctl list devices --json | jq -r --arg udid "$UITEST_IOSDEVICE_ID" '[.devices[][] | select(.udid == $udid)][0].dataPath') | |
| export UITEST_IOSDEVICE_DATA_PATH=$(xcrun simctl list devices --json | jq -r --arg udid "$UITEST_IOSDEVICE_ID" '[.devices[][] | select(.udid == $udid)][0].dataPath // empty') | |
| if [ -z "$UITEST_IOSDEVICE_DATA_PATH" ]; then | |
| echo "##vso[task.logissue type=warning]UNOBLD009: Could not resolve dataPath for device $UITEST_IOSDEVICE_ID" | |
| fi |
|
🤖 Your WebAssembly Skia Sample App stage site is ready! Visit it here: https://unowasmprstaging.z20.web.core.windows.net/pr-24965/wasm-skia-net9/index.html |
The 6.x suite took 26-55 min on macOS-15 against a 60 min job timeout, and ran past it on macOS-26. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018riLfyvT4wZYEh8iFjWkKa
|
Claude finished @MartinZikmund's task in 2m 3s —— View job PR ReviewVerdict: fix-first — the CI YAML cleanup is correct and complete, but 3 medium issues, 0 blocking:
Cross-cutting notes (unchanged from previous passes):
|
| IOS_SDK_VERSION=$(xcrun --sdk iphonesimulator --show-sdk-version) | ||
| UNO_UITEST_SIMULATOR_VERSION=$(xcrun simctl list runtimes --json | jq -r --arg ver "$IOS_SDK_VERSION" ' | ||
| .runtimes | ||
| | map(select(.isAvailable == true and (.identifier | test("SimRuntime\\.iOS")) and (.version | startswith($ver)))) |
There was a problem hiding this comment.
Medium — fragile version match that can produce a false UNOBLD008 failure (flagged in every review pass, still unaddressed).
xcrun --sdk iphonesimulator --show-sdk-version returns a two-part string ("26.5") on some Xcode versions and three-part ("26.5.0") on others. The runtime .version from simctl list runtimes is always three-part. When $ver is "26.5.0" and the installed runtime is "26.5.1", startswith("26.5.0") is false — a valid runtime is rejected and the job fails with UNOBLD008.
Compare only major.minor, which is the meaningful compatibility boundary:
| | map(select(.isAvailable == true and (.identifier | test("SimRuntime\\.iOS")) and (.version | startswith($ver)))) | |
| | map(select(.isAvailable == true and (.identifier | test("SimRuntime\\.iOS")) and (.version | split(".")[0:2] | join(".") == ($ver | split(".")[0:2] | join("."))))) |
| # Historical context: prior installs referenced an App Center issue/workaround. | ||
| # https://github.com/microsoft/appcenter/issues/2605#issuecomment-1854414963 | ||
| export PATH=$PATH:~/.local/bin | ||
| UITEST_IOSDEVICE_ID=$(find_ios_device) |
There was a problem hiding this comment.
Medium — single find_ios_device() call reintroduces CoreSimulator startup flakiness (flagged in every review pass, still unaddressed).
The old code polled with while true; do sleep 5; done specifically because CoreSimulator can take a few seconds to become responsive on a freshly provisioned agent. The new code calls find_ios_device() exactly once — if CoreSimulator isn't ready, the result is empty, execution falls through to the device-creation path, and simctl create can silently duplicate a pre-created device or fail outright.
A bounded retry before the creation fallback preserves the guard without an unbounded loop:
| UITEST_IOSDEVICE_ID=$(find_ios_device) | |
| for _cs_attempt in 1 2 3; do | |
| UITEST_IOSDEVICE_ID=$(find_ios_device) | |
| [ -n "$UITEST_IOSDEVICE_ID" ] && break | |
| echo "CoreSimulator not ready yet (attempt $_cs_attempt/3), retrying in 5s…" | |
| sleep 5 | |
| done |
If UITEST_IOSDEVICE_ID is still empty after retries, the existing creation path below is the correct fallback — valid for images that genuinely don't pre-create the device model.
| fi | ||
|
|
||
| export UITEST_IOSDEVICE_ID | ||
| export UITEST_IOSDEVICE_DATA_PATH=$(xcrun simctl list devices --json | jq -r --arg udid "$UITEST_IOSDEVICE_ID" '[.devices[][] | select(.udid == $udid)][0].dataPath') |
There was a problem hiding this comment.
Medium — literal "null" string can leak into downstream path handling (flagged in every review pass, still unaddressed).
jq -r emits the string null (not empty) when .[0].dataPath is absent — for example if CoreSimulator is still settling after simctl create. UITEST_IOSDEVICE_DATA_PATH is used as a filesystem path later; a literal "null" component produces a confusing artifact-upload failure rather than a clear diagnostic.
Use // empty so jq emits nothing on a miss, then guard:
| export UITEST_IOSDEVICE_DATA_PATH=$(xcrun simctl list devices --json | jq -r --arg udid "$UITEST_IOSDEVICE_ID" '[.devices[][] | select(.udid == $udid)][0].dataPath') | |
| export UITEST_IOSDEVICE_DATA_PATH=$(xcrun simctl list devices --json | jq -r --arg udid "$UITEST_IOSDEVICE_ID" '[.devices[][] | select(.udid == $udid)][0].dataPath // empty') | |
| if [ -z "$UITEST_IOSDEVICE_DATA_PATH" ]; then | |
| echo "##vso[task.logissue type=warning]UNOBLD009: Could not resolve dataPath for device $UITEST_IOSDEVICE_ID" | |
| fi |
|
🤖 Your WebAssembly Skia Sample App stage site is ready! Visit it here: https://unowasmprstaging.z20.web.core.windows.net/pr-24965/wasm-skia-net9/index.html |
|
Claude finished @MartinZikmund's task in 1m 46s —— View job PR ReviewVerdict: fix-first — the CI YAML cleanup is correct and complete, but 3 medium issues, 0 blocking:
Cross-cutting notes (unchanged from previous passes):
|
| IOS_SDK_VERSION=$(xcrun --sdk iphonesimulator --show-sdk-version) | ||
| UNO_UITEST_SIMULATOR_VERSION=$(xcrun simctl list runtimes --json | jq -r --arg ver "$IOS_SDK_VERSION" ' | ||
| .runtimes | ||
| | map(select(.isAvailable == true and (.identifier | test("SimRuntime\\.iOS")) and (.version | startswith($ver)))) |
There was a problem hiding this comment.
Medium — fragile version match that can produce a false UNOBLD008 failure (still unaddressed after 8+ review passes).
xcrun --sdk iphonesimulator --show-sdk-version returns either a two-part string ("26.5") or a three-part string ("26.5.0") depending on the Xcode version. The runtime .version from simctl list runtimes is always three-part. When $ver is "26.5.0" and the installed runtime is at patch "26.5.1", startswith("26.5.0") is false — a valid runtime is rejected and the job fails with UNOBLD008.
The meaningful compatibility boundary for simulator runtimes is major.minor. Compare only that:
| | map(select(.isAvailable == true and (.identifier | test("SimRuntime\\.iOS")) and (.version | startswith($ver)))) | |
| | map(select(.isAvailable == true and (.identifier | test("SimRuntime\\.iOS")) and (.version | split(".")[0:2] | join(".") == ($ver | split(".")[0:2] | join("."))))) |
| # Historical context: prior installs referenced an App Center issue/workaround. | ||
| # https://github.com/microsoft/appcenter/issues/2605#issuecomment-1854414963 | ||
| export PATH=$PATH:~/.local/bin | ||
| UITEST_IOSDEVICE_ID=$(find_ios_device) |
There was a problem hiding this comment.
Medium — single find_ios_device() call reintroduces CoreSimulator startup flakiness (still unaddressed after 8+ review passes).
The old code polled with while true; do sleep 5; done specifically because CoreSimulator can take a few seconds to become responsive on a freshly provisioned agent. The new code calls find_ios_device() exactly once — if CoreSimulator isn't ready, the result is empty, execution falls through to the device-creation path, and simctl create can silently duplicate a pre-created device or fail outright.
A bounded retry before the creation fallback preserves the guard without an unbounded loop:
| UITEST_IOSDEVICE_ID=$(find_ios_device) | |
| for _cs_attempt in 1 2 3; do | |
| UITEST_IOSDEVICE_ID=$(find_ios_device) | |
| [ -n "$UITEST_IOSDEVICE_ID" ] && break | |
| echo "CoreSimulator not ready yet (attempt $_cs_attempt/3), retrying in 5s…" | |
| sleep 5 | |
| done |
If UITEST_IOSDEVICE_ID is still empty after retries, the existing creation path below is the correct fallback — valid for images that genuinely don't pre-create the device model.
| fi | ||
|
|
||
| export UITEST_IOSDEVICE_ID | ||
| export UITEST_IOSDEVICE_DATA_PATH=$(xcrun simctl list devices --json | jq -r --arg udid "$UITEST_IOSDEVICE_ID" '[.devices[][] | select(.udid == $udid)][0].dataPath') |
There was a problem hiding this comment.
Medium — literal "null" string can leak into downstream path handling (still unaddressed after 8+ review passes).
jq -r emits the string null (not empty) when .[0].dataPath is absent — for example if CoreSimulator is still settling after simctl create and the device doesn't yet appear in the flat devices[][] scan. UITEST_IOSDEVICE_DATA_PATH is used as a filesystem path later; a literal "null" component produces a confusing artifact-upload failure rather than a clear diagnostic.
Use // empty so jq emits nothing on a miss, then guard:
| export UITEST_IOSDEVICE_DATA_PATH=$(xcrun simctl list devices --json | jq -r --arg udid "$UITEST_IOSDEVICE_ID" '[.devices[][] | select(.udid == $udid)][0].dataPath') | |
| export UITEST_IOSDEVICE_DATA_PATH=$(xcrun simctl list devices --json | jq -r --arg udid "$UITEST_IOSDEVICE_ID" '[.devices[][] | select(.udid == $udid)][0].dataPath // empty') | |
| if [ -z "$UITEST_IOSDEVICE_DATA_PATH" ]; then | |
| echo "##vso[task.logissue type=warning]UNOBLD009: Could not resolve dataPath for device $UITEST_IOSDEVICE_ID" | |
| fi |
|
🤖 Your WebAssembly Skia Sample App stage site is ready! Visit it here: https://unowasmprstaging.z20.web.core.windows.net/pr-24965/wasm-skia-net9/index.html |
On the macOS-26 image these segfault the whole Desktop Skia macOS run on 6.x, in three out of three runs, taking every later test with it. They are already skipped on iOS and Win32 for #11997; master passes them, so the crash is 6.x-specific and needs a Mac to root-cause. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018riLfyvT4wZYEh8iFjWkKa
61edc86 to
1936306
Compare
|
Claude finished @MartinZikmund's task in 2m 4s —— View job PR ReviewVerdict: fix-first — the CI YAML cleanup and the new 3 medium issues, 0 blocking:
New commit ( Adding Cross-cutting notes (unchanged from previous passes):
|
| IOS_SDK_VERSION=$(xcrun --sdk iphonesimulator --show-sdk-version) | ||
| UNO_UITEST_SIMULATOR_VERSION=$(xcrun simctl list runtimes --json | jq -r --arg ver "$IOS_SDK_VERSION" ' | ||
| .runtimes | ||
| | map(select(.isAvailable == true and (.identifier | test("SimRuntime\\.iOS")) and (.version | startswith($ver)))) |
There was a problem hiding this comment.
Medium — fragile version match that can produce a false UNOBLD008 failure (unaddressed in all previous review passes).
xcrun --sdk iphonesimulator --show-sdk-version returns a two-part string ("26.5") on some Xcode versions and three-part ("26.5.0") on others. The runtime .version field from simctl list runtimes is always three-part. When $ver is "26.5.0" and the installed runtime is "26.5.1", startswith("26.5.0") is false — a valid runtime is rejected and the job fails with UNOBLD008.
The meaningful compatibility boundary for simulator runtimes is major.minor. Compare only that:
| | map(select(.isAvailable == true and (.identifier | test("SimRuntime\\.iOS")) and (.version | startswith($ver)))) | |
| | map(select(.isAvailable == true and (.identifier | test("SimRuntime\\.iOS")) and (.version | split(".")[0:2] | join(".") == ($ver | split(".")[0:2] | join("."))))) |
| # Historical context: prior installs referenced an App Center issue/workaround. | ||
| # https://github.com/microsoft/appcenter/issues/2605#issuecomment-1854414963 | ||
| export PATH=$PATH:~/.local/bin | ||
| UITEST_IOSDEVICE_ID=$(find_ios_device) |
There was a problem hiding this comment.
Medium — single find_ios_device() call reintroduces CoreSimulator startup flakiness (unaddressed in all previous review passes).
The old code polled with while true; do sleep 5; done specifically because CoreSimulator can take a few seconds to become responsive on a freshly provisioned agent. The new code calls find_ios_device() exactly once — if CoreSimulator isn't ready, the result is empty, execution falls through to the device-creation path, and simctl create can silently duplicate a pre-created device or fail outright.
A bounded retry before the creation fallback preserves the guard without an unbounded loop:
| UITEST_IOSDEVICE_ID=$(find_ios_device) | |
| for _cs_attempt in 1 2 3; do | |
| UITEST_IOSDEVICE_ID=$(find_ios_device) | |
| [ -n "$UITEST_IOSDEVICE_ID" ] && break | |
| echo "CoreSimulator not ready yet (attempt $_cs_attempt/3), retrying in 5s…" | |
| sleep 5 | |
| done |
| fi | ||
|
|
||
| export UITEST_IOSDEVICE_ID | ||
| export UITEST_IOSDEVICE_DATA_PATH=$(xcrun simctl list devices --json | jq -r --arg udid "$UITEST_IOSDEVICE_ID" '[.devices[][] | select(.udid == $udid)][0].dataPath') |
There was a problem hiding this comment.
Medium — literal "null" string can leak into downstream path handling (unaddressed in all previous review passes).
jq -r emits the string null (not empty) when .[0].dataPath is absent — for example if CoreSimulator is still settling after simctl create and the device doesn't yet appear in the flat devices[][] scan. UITEST_IOSDEVICE_DATA_PATH is used as a filesystem path later; a literal "null" component produces a confusing artifact-upload failure rather than a clear diagnostic.
Use // empty so jq emits nothing on a miss, then guard:
| export UITEST_IOSDEVICE_DATA_PATH=$(xcrun simctl list devices --json | jq -r --arg udid "$UITEST_IOSDEVICE_ID" '[.devices[][] | select(.udid == $udid)][0].dataPath') | |
| export UITEST_IOSDEVICE_DATA_PATH=$(xcrun simctl list devices --json | jq -r --arg udid "$UITEST_IOSDEVICE_ID" '[.devices[][] | select(.udid == $udid)][0].dataPath // empty') | |
| if [ -z "$UITEST_IOSDEVICE_DATA_PATH" ]; then | |
| echo "##vso[task.logissue type=warning]UNOBLD009: Could not resolve dataPath for device $UITEST_IOSDEVICE_ID" | |
| fi |
|
🤖 Your WebAssembly Skia Sample App stage site is ready! Visit it here: https://unowasmprstaging.z20.web.core.windows.net/pr-24965/wasm-skia-net9/index.html |
|
The build 237295 did not find any UI Test snapshots differences. |
|
|


GitHub Issue: part of #24959
Backport of #24680 and #24961 to
release/stable/6.7, in one PR.PR Type:
🏗️ Build or CI related changes
What changed? 🚀
The hosted
macOS-14image is retired on 2 Nov 2026, with outage windows from 5 Oct. On this branch the iOS Skia runtime tests still ran on it (macOSVMImage_UITests/ Xcode 15.3), and every other Mac job ran onmacOS-15. This branch has no tvOS stage.macOS-15tomacOS-26. The DevServer CLI template-test jobs that hardcodedmacOS-15move too.xCodeRootSimulator), whose iOS 26.5 runtime is preinstalled, so they skip that download. An app built with the 26.1 SDK runs on the newer simulator.macOS-26, iOS 26.5 simulator).ios-uitest-run.shuses the simulator runtime that ships with the selected Xcode instead of the hardcodediOS-17-5, createsiPad Pro (12.9-inch) (6th generation)when the image lacks it (same screen size as before), and installs withxcrun simctl install(one retry). idb and its Homebrew tap / Python pins are gone.macOSVMImage_UITests,xCodeRoot_iOS_UITestsand thevmMacImageTest/vmImageTest/xCodeRootTesttemplate parameters are removed. The disabled native iOS stage and TestFlight now take the build image and Xcode.Also regenerates
FontFallbackMaps.skia.cs. The upstream Noto data moved, soSetup - Font Fallback Maps Validationfails on every 6.7 PR (master and 6.8 regenerate it on a schedule; 6.7 doesn't).Also cherry-picks
caa909b46c8fromservicing/6.8, which ignoresRealAppLaunchIntegrationTests.WhenRealAppBuiltAndRunWithDevServer_RealConnectionEstablished.Uno.Templates@*-*now scaffolds MyApp on 7.0 packages, which a 6.x DevServer can't connect to, so the test fails onrelease/stable/6.7itself (build 236027).Also skips the five WebView2
ExecuteScriptAsyncruntime tests onSkiaMacOS, next to their existing iOS/Win32 #11997 exclusion. On the macOS-26 image they segfault the whole Desktop Skia macOS process on 6.x (3 out of 3 runs on 6.7), so every later test is lost. Master passes them on macOS-26, and backporting the #24112 native-peer fix did not help, so the root cause is 6.x-specific and needs a Mac to chase.The iOS multi-window scene fix from #24680 isn't needed here: this branch doesn't support multiple windows on iOS (
SupportsMultipleWindowsisfalse), so closing a window can't background the app.Risk: the simulator moves from iOS 17.5 to iOS 26.5, so outcomes can shift. This PR's CI run is the check.
PR Checklist ✅
Screenshots Compare Test Runresults.🤖 Generated with Claude Code
https://claude.ai/code/session_018riLfyvT4wZYEh8iFjWkKa