Skip to content

fix(remote): renew the lease while an artifact upload runs (#2946) #9305

fix(remote): renew the lease while an artifact upload runs (#2946)

fix(remote): renew the lease while an artifact upload runs (#2946) #9305

Workflow file for this run

name: iOS
on:
pull_request:
paths-ignore:
- 'docs/**'
- 'website/**'
- 'README.md'
- 'AGENTS.md'
- 'CONTEXT.md'
- 'CONTRIBUTING.md'
- 'LICENSE'
- 'SECURITY.md'
- '.github/actions/build-docs/action.yml'
- '.github/workflows/deploy.yml'
- '.github/workflows/pr-preview.yml'
- '.github/workflows/pr-preview-cleanup.yml'
# Routing (#1781 A9-2). Everything below is derived from the affected selector's
# device-lane ownership (scripts/check-affected/device-lanes.ts): unit tests no device
# lane runs, and trees another platform family owns. `pnpm check:gate-manifest` asserts
# this list against the selector over every tracked path, both ways — a path the selector
# routes to the iOS lane (or fails open on: tooling, workflows, package manifests) must not
# appear here, and a path it classifies as another family's or as a unit test must. The
# `push` trigger has no filter: main runs every lane unconditionally.
- '**/*.md'
- 'src/**/*.test.ts'
- 'src/**/__tests__/**/*.ts'
- 'packages/*/src/**/*.test.ts'
- 'packages/*/src/**/__tests__/**/*.ts'
- 'packages/platform-android/src/**'
- 'packages/capture-kit/src/snapshot/snapshot-presentation/android/**'
- 'packages/platform-harmonyos/src/**'
- 'packages/platform-linux/src/**'
- 'packages/platform-vega/src/**'
- 'packages/platform-web/src/**'
- 'android/**'
- 'linux/**'
- 'test/integration/replays/android/**'
- 'test/integration/replays/linux/**'
- 'test/integration/replays/macos/**'
- 'test/integration/android-**'
- 'test/integration/smoke-android-**'
- 'test/integration/smoke-linux-**'
- 'test/integration/linux-e2e/**'
- 'test/integration/smoke-macos-**'
- 'test/integration/smoke-web-**'
- 'test/integration/macos-e2e/**'
- 'test/integration/web-e2e/**'
push:
branches:
- main
permissions:
contents: read
actions: read
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
smoke-ios:
name: Smoke Tests
runs-on: macos-26
timeout-minutes: 80
env:
IOS_RUNTIME_VERSION: '26.2'
AGENT_DEVICE_STATE_DIR: ${{ github.workspace }}/.tmp/agent-device-state
AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH: ${{ github.workspace }}/.tmp/ios-runner-derived
AGENT_DEVICE_IOS_PREPARE_TIMEOUT_MS: '420000'
AGENT_DEVICE_XCUITEST_INCLUDE_UNIT_TESTS: '1'
# A generic simulator destination leaves the active arch undefined and Xcode 26.6
# defaults it to x86_64, which runs the whole runner under Rosetta on arm64 hosts.
# Pin the native slice: measured ~30% faster commits on identical CI hardware
# (#1874 diagnosis, PR #1941).
AGENT_DEVICE_XCUITEST_ARCHS: 'arm64'
AGENT_DEVICE_IOS_APP_EVENT_URL_TEMPLATE: agent-device-test-app:///automation?event={event}&payload={payload}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set fixture producer wait policy
id: fixture-producer
run: |
set -euo pipefail
WAIT_SECONDS=1800
if [ "${{ github.event_name }}" = "pull_request" ] && [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then
echo "Fork pull request cannot publish the shared fixture artifact; a cache miss will build inline."
WAIT_SECONDS=0
fi
echo "wait-seconds=$WAIT_SECONDS" >> "$GITHUB_OUTPUT"
- name: Setup toolchain
uses: ./.github/actions/setup-node-pnpm
with:
cache-dependency-path: |
pnpm-lock.yaml
examples/test-app/pnpm-lock.yaml
- name: Select iOS runner XCTests
id: xctest-impact
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
if [ "${{ github.event_name }}" = 'pull_request' ]; then
git fetch origin "$BASE_SHA" --depth=1 || echo 'Base fetch failed; XCTest selection will fail open.'
fi
node --experimental-strip-types scripts/apple-ci-impact.ts xctest
- name: Establish host focus canary
id: host-focus-canary
run: |
set -euo pipefail
osascript -e 'tell application "Finder" to activate'
sleep 1
FRONTMOST="$(osascript -e 'tell application "System Events" to get bundle identifier of first application process whose frontmost is true')"
test "$FRONTMOST" = "com.apple.finder"
mkdir -p .tmp
echo "$FRONTMOST" > .tmp/ios-host-focus-before
- name: Restore and build iOS XCTest runner
uses: ./.github/actions/setup-apple-runner-build
with:
derived-path: ${{ env.AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH }}
cache-key-prefix: ios-runner-prebuilt
cache-key-suffix: -ios-${{ env.IOS_RUNTIME_VERSION }}-arm64
gate: swift-runner-ios
xcuitest-platform: ios
xcuitest-destination: generic/platform=iOS Simulator
- name: Boot iOS test simulator
id: ios-simulator
uses: ./.github/actions/boot-ios-test-simulator
with:
runtime-version: ${{ env.IOS_RUNTIME_VERSION }}
preferred-device-name: iPhone 17 Pro
- name: Run targeted iOS runner XCTest regressions
id: ios-xctest
if: steps.xctest-impact.outputs.run != 'false'
run: |
set -euo pipefail
XCTESTRUN_PATH="$(find "$AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH/Build/Products" -maxdepth 1 -name '*.xctestrun' -print -quit)"
test -n "$XCTESTRUN_PATH"
IOS_PR_TEST_ARGS=()
IOS_PR_TEST_IDS="$(node --experimental-strip-types scripts/check-xctest-selection.ts --ios-pr-tests)"
while IFS= read -r test_id; do
IOS_PR_TEST_ARGS+=("-only-testing:$test_id")
done <<< "$IOS_PR_TEST_IDS"
test "${#IOS_PR_TEST_ARGS[@]}" -gt 0
xcodebuild test-without-building \
-xctestrun "$XCTESTRUN_PATH" \
-destination "platform=iOS Simulator,id=${{ steps.ios-simulator.outputs.simulator-udid }}" \
"${IOS_PR_TEST_ARGS[@]}" 2>&1 | tee /tmp/agent-device-runner-regressions.log
node --input-type=module -e '
import { readFileSync } from "node:fs";
const log = readFileSync("/tmp/agent-device-runner-regressions.log", "utf8");
if (!/\] AGENT_DEVICE_RUNNER_OPTIONAL_PROBE_WAIT_COMPLETED$/m.test(log)) {
throw new Error("Optional observation ended the runner test before its wait completed");
}
'
- name: Preflight iOS runner through public CLI
run: |
pnpm clean:daemon
node --experimental-strip-types src/bin.ts prepare ios-runner --platform ios --udid "${{ steps.ios-simulator.outputs.simulator-udid }}" --timeout "$AGENT_DEVICE_IOS_PREPARE_TIMEOUT_MS" --json
# The replay reuses the prepared runner in the same daemon.
- name: Run iOS Settings replay smoke test
run: |
node --experimental-strip-types src/bin.ts test test/integration/replays/ios/simulator/01-settings.ad --udid "${{ steps.ios-simulator.outputs.simulator-udid }}" --retries 2 --artifacts-dir test/artifacts/replays-ios-simulator-smoke --report-junit test/artifacts/replays-ios-simulator-smoke.junit.xml
- name: Fetch current fixture app
id: fixture-app
uses: ./.github/actions/setup-fixture-app
with:
install: 'false'
wait-for-artifact-seconds: ${{ steps.fixture-producer.outputs.wait-seconds }}
- name: Report fixture cache source
run: |
echo "Fixture app source: ${{ steps.fixture-app.outputs.source }}" >> "$GITHUB_STEP_SUMMARY"
# #1584: isolated, cheap automatic guard for the #1562 class of regression (iOS silently
# ignoring a `gesture pan` duration). Split out of gesture-lab.ad, which stays full-tier
# (dispatch-only via replays-manual.yml) because its multi-touch commands are a separate,
# heavier concern. This is the only automatic lane this assertion runs in today.
# --timeout raises the per-attempt budget past the script's own 60s: it absorbs an
# in-replay runner rebuild (measured 48-61s build + ~40s launch) on a runner cache miss;
# the normal path finishes in 21-34s.
- name: Run gesture pan-duration smoke replay
run: |
xcrun simctl install "${{ steps.ios-simulator.outputs.simulator-udid }}" "${{ steps.fixture-app.outputs.app-path }}"
node --experimental-strip-types src/bin.ts test examples/test-app/replays/gesture-pan-duration.ad --udid "${{ steps.ios-simulator.outputs.simulator-udid }}" --timeout 180000 --retries 2 --artifacts-dir test/artifacts/replays-ios-gesture-pan-duration --report-junit test/artifacts/replays-ios-gesture-pan-duration.junit.xml
pnpm clean:daemon
- name: Run fixture-backed iOS simulator E2E smoke
env:
AGENT_DEVICE_FIXTURE_APP_ID: ${{ steps.fixture-app.outputs.app-id }}
AGENT_DEVICE_FIXTURE_APP_PATH: ${{ steps.fixture-app.outputs.app-path }}
AGENT_DEVICE_IOS_E2E: '1'
AGENT_DEVICE_IOS_E2E_TIER: smoke
AGENT_DEVICE_IOS_UDID: ${{ steps.ios-simulator.outputs.simulator-udid }}
run: |
pnpm gate build
node --experimental-strip-types scripts/node-test-tmpdir.ts --test test/integration/smoke-ios-simulator-coverage.test.ts test/integration/smoke-ios-simulator.test.ts
- name: Assert simulator automation preserved host focus
if: ${{ always() }}
run: |
set -euo pipefail
test "$(cat .tmp/ios-host-focus-before)" = "com.apple.finder"
FRONTMOST="$(osascript -e 'tell application "System Events" to get bundle identifier of first application process whose frontmost is true')"
test "$FRONTMOST" = "com.apple.finder"
- name: Run iOS physical device smoke replay
if: env.IOS_UDID != ''
env:
IOS_UDID: ${{ vars.IOS_UDID }}
run: |
pnpm clean:daemon
node --experimental-strip-types src/bin.ts test test/integration/replays/ios/device/01-physical-lifecycle.ad --udid "$IOS_UDID" --retries 2 --artifacts-dir test/artifacts/replays-ios-device-smoke --report-junit test/artifacts/replays-ios-device-smoke.junit.xml
- name: Assert iOS XCTest selection was honored
if: always()
env:
SELECTED: ${{ steps.xctest-impact.outputs.run }}
OUTCOME: ${{ steps.ios-xctest.outcome }}
run: |
if [ "$SELECTED" = 'true' ] && [ "$OUTCOME" = 'skipped' ]; then
echo '::error::iOS runner XCTest was selected but its step was skipped.'
exit 1
fi
- name: Upload iOS artifacts
if: always()
uses: ./.github/actions/upload-agent-device-artifacts
with:
artifact-name: ios-artifacts
agent-state-dir: ${{ env.AGENT_DEVICE_STATE_DIR }}
runner-derived-path: ${{ env.AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH }}