Skip to content

Commit 768a96d

Browse files
ksroda-saclaude
andauthored
feat(ci): extend test-ios.yml to cover React Native iOS samples (#163)
* feat(ci): extend test-ios.yml to cover React Native iOS samples Previously test-ios.yml only built native iOS samples (samples/ios/*). RN iOS sub-apps (samples/react-native/*/ios) had no CI coverage — the gap that justified the dependabot manual-review exception for RN bumps. find-projects now discovers both: - native iOS: project.yml at samples/ios/<flow>/ - RN iOS: Podfile at samples/react-native/<flow>/ios/ Per-entry steps branch on project type: - native iOS: existing flow (xcconfig + SPM + `xcodebuild test`) - RN iOS: setup-node + yarn install (walk up to RN root) + pod install + `xcodebuild build` against the generated xcworkspace `build` (not `test`) for RN — RN samples don't ship XCTest bundles; their tests run via Jest, exercised by test-js.yml. The build alone validates pod install, native module autolinking, and the iOS compile path. The watch_root walk-up to package.json (same pattern test-android.yml uses for RN Android) ensures JS-side changes correctly trigger the iOS build for RN samples. Once green, the dependabot manual-review exception for RN can be dropped in a follow-up PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: cache SwiftPM and CocoaPods source archives in iOS workflow `xcodebuild -resolvePackageDependencies` (native iOS) and `pod install` (RN iOS) both pay a non-trivial cold-fetch cost on every matrix entry. Cache the global source caches keyed on each project's lockfile / config: - Native iOS: ~/Library/Caches/org.swift.swiftpm and DerivedData SourcePackages, keyed on project.yml content. - RN iOS: ~/Library/Caches/CocoaPods (just the source archives, not the per-project Pods/ dir which `pod install` regenerates deterministically), keyed on Podfile.lock content. Saves ~30s per native iOS entry and ~90s per RN iOS entry on warm cache. Also bumps actions/cache to @v5 to match the rest of the repo. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(samples/rn): regenerate login-pkce Podfile.lock for react-native 0.85.3 The login-pkce sample's package.json was bumped to react-native@0.85.3 but Podfile.lock still pinned 0.85.2 — drift that the new RN iOS CI coverage (test-ios.yml) caught on the first run. Regenerated via `yarn install && cd ios && pod install`. Also includes incidental quote-stripping in token-refresh's project.pbxproj (PRODUCT_BUNDLE_IDENTIFIER) that Xcode produced as a side effect. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: fail-fast on iOS matrix + clean login-pkce Podfile.lock 1. fail-fast: true on the iOS test matrix. macOS minutes are ~10× Ubuntu's, so cancelling in-flight entries on the first failure saves real money. Other test workflows keep fail-fast: false (Linux runner cost is small). 2. login-pkce Podfile.lock fully regenerated after nuking Pods/. The earlier regeneration left Pods/Local Podspecs/React-Core-prebuilt.podspec.json stale at 0.85.2 because CocoaPods didn't invalidate that one cached spec, so the lockfile inherited 0.85.2 for React-Core-prebuilt while every other React pod was 0.85.3 — the drift that broke CI. Fresh install produces a uniformly 0.85.3 lock. pbxproj is incidental Xcode normalization. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f100770 commit 768a96d

4 files changed

Lines changed: 460 additions & 341 deletions

File tree

.github/workflows/test-ios.yml

Lines changed: 109 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,8 @@ name: Test iOS Builds
33
on:
44
push:
55
branches: [main]
6-
# No paths filter on pull_request — required status checks need this workflow to
7-
# always trigger so the aggregator can report. find-projects below narrows the
8-
# matrix to only projects whose subtree changed, so docs-only / other-framework
9-
# PRs produce an empty matrix → test job skipped → aggregator passes via
10-
# `result == 'skipped'` (no macOS minutes burned).
6+
# No paths filter — required status checks need this workflow to always trigger.
7+
# find-projects below narrows the matrix to only changed projects.
118
pull_request:
129
schedule:
1310
- cron: "0 10 * * 1"
@@ -25,8 +22,6 @@ jobs:
2522
matrix: ${{ steps.find.outputs.matrix }}
2623
steps:
2724
- uses: actions/checkout@v6
28-
# Default shallow checkout — we only need the working tree for `find`
29-
# below, not git history. The PR's changed files come from gh api.
3025
- id: find
3126
env:
3227
EVENT_NAME: ${{ github.event_name }}
@@ -35,18 +30,33 @@ jobs:
3530
GH_TOKEN: ${{ github.token }}
3631
run: |
3732
set -euo pipefail
38-
# Each iOS sample is identified by its project.yml (xcodegen input) —
39-
# that's the authoritative source; the generated .xcodeproj comes and goes.
40-
ALL=$(find samples/ios -mindepth 2 -maxdepth 2 -name "project.yml" -exec dirname {} \; 2>/dev/null | sort)
33+
# Native iOS samples have project.yml; RN iOS samples have Podfile.
34+
ALL=$(
35+
{
36+
find samples/ios -mindepth 2 -maxdepth 2 -name "project.yml" -exec dirname {} \; 2>/dev/null
37+
find samples/react-native -name "Podfile" -not -path "*/Pods/*" -not -path "*/node_modules/*" -exec dirname {} \; 2>/dev/null
38+
} | sort
39+
)
4140
4241
if [ "$EVENT_NAME" = "pull_request" ]; then
4342
CHANGED=$(gh api --paginate "/repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" --jq '.[].filename' 2>/dev/null || echo "")
4443
if printf '%s\n' "$CHANGED" | grep -qFx "$WORKFLOW_FILE"; then
4544
FILTERED="$ALL"
4645
else
46+
# For RN samples the JS-side deps that drive native autolinking
47+
# live one level up, so walk up to package.json for watch_root.
4748
FILTERED=$(while IFS= read -r proj; do
4849
[ -z "$proj" ] && continue
49-
if printf '%s\n' "$CHANGED" | grep -q "^${proj}/"; then
50+
watch_root="$proj"
51+
d="$proj"
52+
while [ "$d" != "." ] && [ "$d" != "/" ]; do
53+
if [ -f "$d/package.json" ]; then
54+
watch_root="$d"
55+
break
56+
fi
57+
d=$(dirname "$d")
58+
done
59+
if printf '%s\n' "$CHANGED" | grep -q "^${watch_root}/"; then
5060
echo "$proj"
5161
fi
5262
done <<< "$ALL")
@@ -63,31 +73,86 @@ jobs:
6373
if: ${{ needs.find-projects.outputs.matrix != '[]' }}
6474
runs-on: macos-latest
6575
strategy:
66-
fail-fast: false
76+
# Cancel in-flight matrix entries on first failure — macOS minutes are
77+
# ~10× Ubuntu's, so save the cost over seeing every failure at once.
78+
fail-fast: true
6779
matrix:
6880
project: ${{ fromJson(needs.find-projects.outputs.matrix) }}
6981
steps:
7082
- uses: actions/checkout@v6
7183

72-
# Config.xcconfig is gitignored — copy the example so build settings have
73-
# placeholder values. No real CIAM workspace is needed for build + unit tests
74-
# (network-touching paths only execute in the running app).
75-
- name: Provision example xcconfig
84+
- name: Detect project type
85+
id: type
86+
run: |
87+
if [ -f "${{ matrix.project }}/project.yml" ]; then
88+
echo "type=native" >> "$GITHUB_OUTPUT"
89+
else
90+
echo "type=rn" >> "$GITHUB_OUTPUT"
91+
fi
92+
93+
# ── Native iOS prep ───────────────────────────────────────────────────
94+
- name: Provision example xcconfig (native iOS)
95+
if: steps.type.outputs.type == 'native'
7696
working-directory: ${{ matrix.project }}
7797
run: cp Config.example.xcconfig Config.xcconfig
7898

79-
# Pre-fetch Swift Package Manager deps (AppAuth-iOS) so the test step doesn't
80-
# also pay the resolution cost — makes failures during dep resolution easier
81-
# to spot in logs.
82-
- name: Resolve SPM dependencies
99+
# Cache the SPM source archives so cold resolves stay fast.
100+
- name: Cache SwiftPM (native iOS)
101+
if: steps.type.outputs.type == 'native'
102+
uses: actions/cache@v5
103+
with:
104+
path: |
105+
~/Library/Caches/org.swift.swiftpm
106+
~/Library/Developer/Xcode/DerivedData/**/SourcePackages
107+
key: spm-${{ runner.os }}-${{ hashFiles(format('{0}/project.yml', matrix.project)) }}
108+
restore-keys: spm-${{ runner.os }}-
109+
110+
- name: Resolve SPM dependencies (native iOS)
111+
if: steps.type.outputs.type == 'native'
83112
working-directory: ${{ matrix.project }}
84113
run: xcodebuild -resolvePackageDependencies -project Quickstart.xcodeproj -scheme Quickstart
85114

86-
# GitHub-hosted macOS runners can ship multiple iOS runtimes (especially
87-
# during macOS version transitions), each with overlapping device names
88-
# like "iPhone 16 Pro". Capture the UDID — globally unique — rather than
89-
# the name, and pass `id=` to xcodebuild for an unambiguous destination.
90-
# We pick the newest iOS runtime by sorting runtime keys.
115+
# ── RN iOS prep ───────────────────────────────────────────────────────
116+
# `pod install` resolves native modules from node_modules, so JS deps
117+
# must be installed first.
118+
- name: Set up Node (RN iOS)
119+
if: steps.type.outputs.type == 'rn'
120+
uses: actions/setup-node@v6
121+
with:
122+
node-version: "22"
123+
- name: Enable Corepack (RN iOS)
124+
if: steps.type.outputs.type == 'rn'
125+
run: corepack enable
126+
- name: Install JS dependencies (RN iOS)
127+
if: steps.type.outputs.type == 'rn'
128+
run: |
129+
dir="${{ matrix.project }}"
130+
while [ "$dir" != "." ] && [ "$dir" != "/" ]; do
131+
if [ -f "$dir/package.json" ]; then
132+
(cd "$dir" && yarn install --immutable)
133+
break
134+
fi
135+
dir=$(dirname "$dir")
136+
done
137+
# Cache the global CocoaPods source archives. Not the project's Pods/ dir —
138+
# `pod install` is fast when the source cache is warm and still regenerates
139+
# the right Pods/ for the matrix entry's Podfile.lock.
140+
- name: Cache CocoaPods (RN iOS)
141+
if: steps.type.outputs.type == 'rn'
142+
uses: actions/cache@v5
143+
with:
144+
path: ~/Library/Caches/CocoaPods
145+
key: pods-${{ runner.os }}-${{ hashFiles(format('{0}/Podfile.lock', matrix.project)) }}
146+
restore-keys: pods-${{ runner.os }}-
147+
148+
- name: Install CocoaPods (RN iOS)
149+
if: steps.type.outputs.type == 'rn'
150+
working-directory: ${{ matrix.project }}
151+
run: pod install
152+
153+
# ── Common: simulator selection ───────────────────────────────────────
154+
# Pick UDID rather than name — multiple runtimes can share a device name
155+
# like "iPhone 16 Pro" during macOS image transitions.
91156
- name: Pick iOS simulator
92157
id: sim
93158
run: |
@@ -103,18 +168,15 @@ jobs:
103168
if [ -z "$UDID" ]; then
104169
echo "No iPhone simulator found"; exit 1
105170
fi
106-
echo "Selected UDID: $UDID"
107171
echo "udid=$UDID" >> "$GITHUB_OUTPUT"
108172
109-
- name: Build and test
173+
# ── Native iOS: build + run unit tests ────────────────────────────────
174+
# Ad-hoc signing gives the app an application-identifier entitlement so
175+
# Keychain APIs work on the simulator. GENERATE_INFOPLIST_FILE=YES
176+
# auto-generates QuickstartTests' Info.plist (project.yml doesn't declare one).
177+
- name: Test (native iOS)
178+
if: steps.type.outputs.type == 'native'
110179
working-directory: ${{ matrix.project }}
111-
# - Ad-hoc signing (`CODE_SIGN_IDENTITY=-`) gives the app an
112-
# `application-identifier` entitlement so Keychain APIs work on the
113-
# simulator. `CODE_SIGNING_REQUIRED=NO` keeps things permissive.
114-
# - `GENERATE_INFOPLIST_FILE=YES` lets xcodebuild auto-generate the
115-
# QuickstartTests target's Info.plist, which the project.yml doesn't
116-
# declare explicitly (Xcode's GUI builds it implicitly, but xcodebuild
117-
# demands one once we ad-hoc sign).
118180
run: |
119181
xcodebuild test \
120182
-project Quickstart.xcodeproj \
@@ -124,6 +186,19 @@ jobs:
124186
CODE_SIGNING_REQUIRED=NO \
125187
GENERATE_INFOPLIST_FILE=YES
126188
189+
# ── RN iOS: build only ────────────────────────────────────────────────
190+
# No XCTests on the RN iOS side; tests live in JS (test-js.yml runs Jest).
191+
- name: Build (RN iOS)
192+
if: steps.type.outputs.type == 'rn'
193+
working-directory: ${{ matrix.project }}
194+
run: |
195+
xcodebuild build \
196+
-workspace Quickstart.xcworkspace \
197+
-scheme Quickstart \
198+
-destination "platform=iOS Simulator,id=${{ steps.sim.outputs.udid }}" \
199+
CODE_SIGN_IDENTITY=- \
200+
CODE_SIGNING_REQUIRED=NO
201+
127202
# Stable aggregator for branch protection.
128203
all-tests-passed:
129204
name: iOS tests passed

0 commit comments

Comments
 (0)