fix(remote): renew the lease while an artifact upload runs (#2946) #9305
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: 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 }} |