Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
09bfb26
test(ios): expand simulator e2e coverage
thymikee Jul 26, 2026
c77aabe
test(ios): make coverage checks host portable
thymikee Jul 26, 2026
cb2f29e
test(ios): handle deep link confirmation
thymikee Jul 27, 2026
e9ed9a4
test(ios): fix deep link prompt selector
thymikee Jul 27, 2026
4b026dd
ci: stabilize full simulator nightly
thymikee Jul 27, 2026
521ada1
test(ios): stabilize permission prompt lifecycle
thymikee Jul 27, 2026
0e4adac
test(e2e): wait for route-specific landmarks
thymikee Jul 27, 2026
14af835
test(e2e): reset permissions from inactive app
thymikee Jul 27, 2026
44bea69
test(e2e): redeliver trusted cold deep links
thymikee Jul 27, 2026
1bbb4fa
test(ios): verify orientation native readback
thymikee Jul 27, 2026
97d3d8b
test(ios): stabilize simulator permission coverage
thymikee Jul 27, 2026
ebff914
test(ios): wait for tab target after deep link
thymikee Jul 27, 2026
682efc8
test(ios): paginate full event timeline
thymikee Jul 27, 2026
76ad9b6
test(ios): simplify event pagination coverage
thymikee Jul 27, 2026
a8e7ffe
test(ios): stabilize simulator e2e coverage
thymikee Jul 27, 2026
388b2b4
test(ios): model simulator recorder lifetime
thymikee Jul 27, 2026
5a046f3
ci(test-app): cache fixture dependencies
thymikee Jul 27, 2026
f77b5e8
fix(ci): isolate test app cache by node
thymikee Jul 27, 2026
f254d37
fix(ios): settle fixture route navigation
thymikee Jul 27, 2026
4079c0b
test(ci): waive unbenchmarked ios system UI help
thymikee Jul 27, 2026
fa7993a
fix(ios): tolerate delayed simulator scale lookup
thymikee Jul 27, 2026
d85ce42
0.20.1
thymikee Jul 27, 2026
22d8a76
test(ci): remove superseded system UI waiver
thymikee Jul 27, 2026
d6ee49f
fix(ios): harden simulator e2e reliability
thymikee Jul 27, 2026
4b55472
chore: clarify Apple runner CI steps
thymikee Jul 27, 2026
9f77bca
fix(ios): wait before fixture home snapshot
thymikee Jul 28, 2026
5c88d2a
fix(ios): require exact catalog navigation
thymikee Jul 28, 2026
11f440e
chore(ci): format rebased workflows
thymikee Jul 28, 2026
4d00897
fix(ios): retry unobserved fixture navigation
thymikee Jul 28, 2026
4bc4ea6
refactor(test): remove iOS e2e workarounds
thymikee Jul 28, 2026
24abda0
fix(ci): verify fixture artifact provenance
thymikee Jul 28, 2026
634ae40
fix(ci): align fixture artifact fingerprints
thymikee Jul 28, 2026
c57a29f
fix(ci): use unified Android helper packager
thymikee Jul 28, 2026
cd8009f
perf(ci): scope fixture build concurrency
thymikee Jul 28, 2026
7527c09
chore: format fixture artifact tests
thymikee Jul 28, 2026
4f55910
test(ios): update split Apple coverage owner
thymikee Jul 28, 2026
2887483
fix(ios): accept deep-link confirmation alerts
thymikee Jul 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions .github/actions/setup-android-replay-host/action.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
name: 'Setup Android Replay Host'
description: 'Prepare the Android CI host for replay tests and expose the agent-device home path'

inputs:
package-helpers:
description: 'Restore or package the npm-bundled Android snapshot and IME helpers'
required: false
default: 'false'

outputs:
agent-home-dir:
description: 'Resolved agent-device home directory'
value: ${{ steps.agent-home.outputs.dir }}
helpers-cache-hit:
description: 'Whether packaged Android helpers were restored from cache'
value: ${{ steps.android-helpers-cache.outputs.cache-hit }}

runs:
using: 'composite'
Expand All @@ -20,3 +29,64 @@ runs:
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
shell: bash

- name: Resolve Android helper source hash
if: inputs.package-helpers == 'true'
id: android-helper-source
run: |
VERSION="$(node -p 'require("./package.json").version')"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "value=${{ hashFiles('android/snapshot-helper/AndroidManifest.xml', 'android/snapshot-helper/debug.keystore', 'android/snapshot-helper/src/**', 'android/ime-helper/AndroidManifest.xml', 'android/ime-helper/res/**', 'android/ime-helper/src/**', 'scripts/build-android-helper.sh', 'scripts/package-android-helper.sh', '.github/actions/setup-android-replay-host/action.yml') }}" >> "$GITHUB_OUTPUT"
shell: bash

- name: Restore packaged Android helpers
if: inputs.package-helpers == 'true'
id: android-helpers-cache
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.2.3
with:
path: |
android/snapshot-helper/dist
android/ime-helper/dist
key: android-helpers-${{ runner.os }}-${{ runner.arch }}-api36-v${{ steps.android-helper-source.outputs.version }}-${{ steps.android-helper-source.outputs.value }}

- name: Install Android helper SDK packages
if: inputs.package-helpers == 'true' && steps.android-helpers-cache.outputs.cache-hit != 'true'
run: |
SDK_ROOT="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-/usr/local/lib/android/sdk}}"
SDKMANAGER="$SDK_ROOT/cmdline-tools/latest/bin/sdkmanager"
if [ ! -x "$SDKMANAGER" ]; then
SDKMANAGER="$SDK_ROOT/cmdline-tools/bin/sdkmanager"
fi
if [ ! -x "$SDKMANAGER" ]; then
echo "sdkmanager not found under $SDK_ROOT" >&2
exit 1
fi
yes | "$SDKMANAGER" --licenses >/dev/null || true
"$SDKMANAGER" "platforms;android-36" "build-tools;36.0.0"
shell: bash

- name: Package npm-bundled Android helpers
if: inputs.package-helpers == 'true' && steps.android-helpers-cache.outputs.cache-hit != 'true'
run: |
VERSION="${{ steps.android-helper-source.outputs.version }}"
rm -rf android/snapshot-helper/dist android/ime-helper/dist
sh ./scripts/package-android-helper.sh snapshot "$VERSION" "v$VERSION" android/snapshot-helper/dist
sh ./scripts/package-android-helper.sh ime "$VERSION" android/ime-helper/dist
shell: bash

- name: Verify packaged Android helpers
if: inputs.package-helpers == 'true'
run: |
VERSION="$(node -p 'require("./package.json").version')"
test -f "android/snapshot-helper/dist/agent-device-android-snapshot-helper-$VERSION.manifest.json"
test -f "android/ime-helper/dist/agent-device-android-ime-helper-$VERSION.manifest.json"
shell: bash

- name: Save packaged Android helpers
if: inputs.package-helpers == 'true' && steps.android-helpers-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.2.3
with:
path: |
android/snapshot-helper/dist
android/ime-helper/dist
key: android-helpers-${{ runner.os }}-${{ runner.arch }}-api36-v${{ steps.android-helper-source.outputs.version }}-${{ steps.android-helper-source.outputs.value }}
77 changes: 0 additions & 77 deletions .github/actions/setup-apple-replay/action.yml

This file was deleted.

87 changes: 87 additions & 0 deletions .github/actions/setup-apple-runner-build/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: 'Set Up Apple Runner Build'
description: 'Restore an exact Apple runner build or build and cache it on a miss'

inputs:
derived-path:
description: 'DerivedData path used by the Apple runner'
required: true
cache-key-prefix:
description: 'Prefix for the cache key'
required: true
cache-key-suffix:
description: 'Optional suffix for the cache key'
required: false
default: ''
build-command:
description: 'Command used to build Apple runner artifacts'
required: true
xcuitest-platform:
description: 'Optional AGENT_DEVICE_XCUITEST_PLATFORM value'
required: false
default: ''
xcuitest-destination:
description: 'Optional AGENT_DEVICE_XCUITEST_DESTINATION value'
required: false
default: ''
outputs:
cache-hit:
description: 'Whether an exact Apple runner build cache was restored'
value: ${{ steps.restore-runner-build.outputs.cache-hit }}

runs:
using: 'composite'
steps:
- name: Resolve Xcode cache identity
id: xcode
run: |
set -euo pipefail
XCODE_VERSION="$(xcodebuild -version | tr '\n' ' ' | sed -E 's/[[:space:]]+/ /g; s/[[:space:]]$//')"
XCODE_KEY="$(echo "$XCODE_VERSION" | tr ' ' '-' | tr -cd '[:alnum:]._-')"
echo "key=$XCODE_KEY" >> "$GITHUB_OUTPUT"
shell: bash

- name: Resolve Apple runner source hash
id: source-hash
run: |
set -euo pipefail
echo "value=${{ hashFiles('apple/runner/**', 'scripts/build-xcuitest-apple.sh', 'scripts/patch-xcuitest-runner-icon.ts', 'scripts/write-xcuitest-cache-metadata.mjs', 'src/platforms/apple/core/apple-runner-platform.ts', 'src/platforms/apple/core/runner/runner-cache-metadata.ts', 'src/platforms/apple/core/runner/runner-icon.ts', 'src/platforms/apple/core/runner/runner-xctestrun.ts', 'src/platforms/apple/core/runner/runner-xctestrun-products.ts', '.github/actions/setup-apple-runner-build/action.yml', 'package.json', 'pnpm-lock.yaml') }}" >> "$GITHUB_OUTPUT"
shell: bash

- name: Resolve Apple runner build variant
id: build-variant
run: |
set -euo pipefail
VARIANT="$(
printf '%s\n' \
'${{ inputs.build-command }}' \
'${{ inputs.xcuitest-platform }}' \
'${{ inputs.xcuitest-destination }}' \
"${AGENT_DEVICE_XCUITEST_INCLUDE_UNIT_TESTS:-0}" \
| shasum -a 256 \
| cut -c1-16
)"
echo "key=$VARIANT" >> "$GITHUB_OUTPUT"
shell: bash

- name: Restore Apple runner build cache
id: restore-runner-build
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.2.3
with:
path: ${{ inputs.derived-path }}
key: ${{ inputs.cache-key-prefix }}-${{ steps.xcode.outputs.key }}${{ inputs.cache-key-suffix }}-${{ steps.build-variant.outputs.key }}-${{ steps.source-hash.outputs.value }}

- name: Build Apple runner artifacts on cache miss
if: steps.restore-runner-build.outputs.cache-hit != 'true'
run: ${{ inputs.build-command }}
shell: bash
env:
AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH: ${{ inputs.derived-path }}
AGENT_DEVICE_XCUITEST_PLATFORM: ${{ inputs.xcuitest-platform }}
AGENT_DEVICE_XCUITEST_DESTINATION: ${{ inputs.xcuitest-destination }}

- name: Save Apple runner build cache
if: steps.restore-runner-build.outputs.cache-hit != 'true'
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.2.3
with:
path: ${{ inputs.derived-path }}
key: ${{ inputs.cache-key-prefix }}-${{ steps.xcode.outputs.key }}${{ inputs.cache-key-suffix }}-${{ steps.build-variant.outputs.key }}-${{ steps.source-hash.outputs.value }}
96 changes: 81 additions & 15 deletions .github/actions/setup-fixture-app/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ description: 'Install a ready Release build of examples/test-app on the booted i
# Release build embedded may be stale — @expo/repack-app swaps in a bundle built
# from the current source. If no artifact exists yet (the producer has not run
# for this fingerprint), it falls back to building inline, so a caller is never
# left without an app.
# left without an app. Callers that run alongside the producer can opt into a
# bounded wait first, avoiding two simultaneous native builds on a cold key.
# Artifact hits are accepted only from this repository's designated producer
# workflow at the current head or from a default-branch push.
#
# The caller's job needs `permissions: actions: read` for the artifact lookup.
# Relevant to #320 (move replay coverage off system apps onto a stable fixture).
Expand All @@ -25,7 +28,10 @@ inputs:
description: 'Install the app onto the booted simulator'
required: false
default: 'true'

wait-for-artifact-seconds:
description: 'How long to poll for a concurrently produced artifact before building inline'
required: false
default: '0'
outputs:
app-path:
description: 'Path to the ready .app bundle'
Expand All @@ -40,11 +46,10 @@ outputs:
runs:
using: 'composite'
steps:
# Installed even on a hit, because the fingerprint is derived from the
# dependency graph and cannot be computed without it.
- name: Install test app dependencies
shell: bash
run: pnpm test-app:install
# The fingerprint/repack commands need the dependency graph. This shared
# action avoids recreating it on every warm native-artifact consumer.
- name: Setup test app dependencies
uses: ./.github/actions/setup-test-app-dependencies

- name: Fetch the cached Release binary
id: fetch
Expand All @@ -53,19 +58,80 @@ runs:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
HASH="$(pnpm --dir examples/test-app exec fingerprint fingerprint:generate | jq -r .hash)"
NAME="fingerprint.${HASH}.ios"
NAME="$(sh "$GITHUB_ACTION_PATH/resolve-artifact-name.sh" ios)"
DEST="${{ github.workspace }}/.tmp/fixture-app"
REPOSITORY="${{ github.repository }}"
EXPECTED_HEAD_SHA="${{ github.event.pull_request.head.sha || github.sha }}"
TRUSTED_ARTIFACT="$GITHUB_ACTION_PATH/trusted-artifact.mjs"
rm -rf "$DEST"; mkdir -p "$DEST"
WAIT_SECONDS="${{ inputs.wait-for-artifact-seconds }}"
case "$WAIT_SECONDS" in
''|*[!0-9]*)
echo "::error::wait-for-artifact-seconds must be a non-negative integer."
exit 1
;;
esac

# A lookup failure (API outage, auth, transient 5xx) must not fail the
# caller: it degrades to an inline build exactly as a cache miss does. The
# assignment sits in an `if` so `set -e` cannot exit on it, and an empty
# ART_ID then takes the same path as "no artifact found".
if ! ART_ID="$(gh api "repos/${{ github.repository }}/actions/artifacts?name=${NAME}&per_page=1" \
--jq '[.artifacts[] | select(.expired == false)][0].id // empty' 2>/dev/null)"; then
# caller or trigger a pointless wait: it takes the inline-build path.
query_artifact() {
node "$TRUSTED_ARTIFACT" find "$REPOSITORY" "$NAME" "$EXPECTED_HEAD_SHA" 2>/dev/null
}
query_producer_state() {
node "$TRUSTED_ARTIFACT" producer-state "$REPOSITORY" "$EXPECTED_HEAD_SHA" 2>/dev/null
}
if ! ART_ID="$(query_artifact)"; then
echo "::warning::Could not query the build cache for $NAME; building inline."
ART_ID=""
WAIT_SECONDS=0
fi
if [ -z "$ART_ID" ] && [ "$WAIT_SECONDS" -gt 0 ]; then
DEADLINE=$((SECONDS + WAIT_SECONDS))
MISSING_PRODUCER_POLLS=0
echo "$NAME is not available; waiting up to ${WAIT_SECONDS}s for its producer."
while [ -z "$ART_ID" ] && [ "$SECONDS" -lt "$DEADLINE" ]; do
if ! PRODUCER_STATE="$(query_producer_state)"; then
echo "::warning::Could not inspect the fixture producer; building inline."
break
fi
case "$PRODUCER_STATE" in
queued)
echo "Fixture producer is queued; building inline instead of waiting for a macOS runner."
break
;;
failed)
echo "Fixture producer failed; building inline."
break
;;
success)
echo "Fixture producer completed without $NAME; building inline."
break
;;
absent)
MISSING_PRODUCER_POLLS=$((MISSING_PRODUCER_POLLS + 1))
if [ "$MISSING_PRODUCER_POLLS" -ge 3 ]; then
echo "No fixture producer appeared for $EXPECTED_HEAD_SHA; building inline."
break
fi
;;
in_progress)
MISSING_PRODUCER_POLLS=0
;;
*)
echo "::warning::Fixture producer returned unknown state '$PRODUCER_STATE'; building inline."
break
;;
esac
sleep 30
if ! ART_ID="$(query_artifact)"; then
echo "::warning::Build cache polling failed for $NAME; building inline."
ART_ID=""
break
fi
if [ -n "$ART_ID" ]; then
break
fi
done
fi

SOURCE=build
Expand All @@ -84,7 +150,7 @@ runs:
fi
rm -rf "$STAGE"
else
echo "$NAME not in the cache yet; building inline."
echo "$NAME not in the cache after the configured wait; building inline."
fi
echo "source=$SOURCE" >> "$GITHUB_OUTPUT"

Expand Down
Loading
Loading