Skip to content

Commit dd19bfc

Browse files
chrfalchclaude
andcommitted
CI: align the SwiftPM iOS workflows with the repo's CI conventions
Addresses review feedback that the original approach did not follow the patterns already used in CI: - The three workflows no longer trigger prebuild-ios-dependencies / prebuild-ios-core themselves. They are workflow_call-only and consume the ReactCore / ReactNativeDependencies artifacts produced elsewhere in the run, the way e2e-ios-templateapp does. The `slices` input added to the two prebuild workflows is reverted. - They are invoked from test-all.yml instead of carrying their own cron. Note this means they run per-PR (gated on should_test_ios) and on pushes to main, not nightly -- test-all.yml has no schedule trigger. They start as continue-on-error so a brand-new lane cannot hard-block unrelated iOS PRs; remove that once it has proven green on main. - The new-app workflow reuses the react-native-package artifact instead of rebuilding the npm package. - Both helper scripts are deleted. Priming the artifact slots needs no new code: download-spm-artifacts.js already accepts tarball overrides via RN_CORE_TARBALL_PATH / RN_DEPS_TARBALL_PATH, so the workflows call `npx react-native spm download` directly, and the new-app case reuses scripts/e2e/init-project-e2e.js --useHelloWorld. Debug and Release run as parallel matrix cells. Each cell stages both flavor slots because `spm add` validates both (it stages both framework trees and selects one via a per-configuration build setting). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 0df922c commit dd19bfc

8 files changed

Lines changed: 349 additions & 947 deletions

.github/workflows/prebuild-ios-core.yml

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,6 @@ on:
1313
type: boolean
1414
required: false
1515
default: false
16-
slices:
17-
description: 'JSON array of Apple platform slices to build. Defaults to the full set; callers that only consume a subset (e.g. a simulator-only e2e build) can pass fewer to save time.'
18-
type: string
19-
required: false
20-
default: '["ios", "ios-simulator", "mac-catalyst"]'
2116

2217
jobs:
2318
build-rn-slice:
@@ -26,7 +21,7 @@ jobs:
2621
fail-fast: false
2722
matrix:
2823
flavor: ['Debug', 'Release']
29-
slice: ${{ fromJSON(inputs.slices) }}
24+
slice: ['ios', 'ios-simulator', 'mac-catalyst']
3025
steps:
3126
- name: Checkout
3227
uses: actions/checkout@v6

.github/workflows/prebuild-ios-dependencies.yml

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,6 @@ name: Prebuild iOS Dependencies
22

33
on:
44
workflow_call: # this directive allow us to call this workflow from other workflows
5-
inputs:
6-
slices:
7-
description: 'JSON array of Apple platform slices to build. Defaults to the full set; callers that only consume a subset (e.g. a simulator-only e2e build) can pass fewer to save time.'
8-
type: string
9-
required: false
10-
default: '["ios", "ios-simulator", "macos", "mac-catalyst", "tvos", "tvos-simulator", "xros", "xros-simulator"]'
115

126
jobs:
137
prepare_workspace:
@@ -57,7 +51,17 @@ jobs:
5751
fail-fast: false
5852
matrix:
5953
flavor: ['Debug', 'Release']
60-
slice: ${{ fromJSON(inputs.slices) }}
54+
slice:
55+
[
56+
'ios',
57+
'ios-simulator',
58+
'macos',
59+
'mac-catalyst',
60+
'tvos',
61+
'tvos-simulator',
62+
'xros',
63+
'xros-simulator',
64+
]
6165
steps:
6266
- name: Checkout
6367
uses: actions/checkout@v6

.github/workflows/test-all.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,40 @@ jobs:
203203
fail-on-error: true
204204
secrets: inherit
205205

206+
test_ios_spm_rntester:
207+
needs:
208+
[
209+
prebuild_apple_dependencies,
210+
prebuild_react_native_core,
211+
check_code_changes,
212+
]
213+
if: ${{ needs.prebuild_react_native_core.result == 'success' && needs.check_code_changes.outputs.should_test_ios == 'true' }}
214+
uses: ./.github/workflows/test-ios-spm-rntester.yml
215+
secrets: inherit
216+
217+
test_ios_spm_helloworld:
218+
needs:
219+
[
220+
prebuild_apple_dependencies,
221+
prebuild_react_native_core,
222+
check_code_changes,
223+
]
224+
if: ${{ needs.prebuild_react_native_core.result == 'success' && needs.check_code_changes.outputs.should_test_ios == 'true' }}
225+
uses: ./.github/workflows/test-ios-spm-helloworld.yml
226+
secrets: inherit
227+
228+
test_ios_spm_newapp:
229+
needs:
230+
[
231+
build_npm_package,
232+
prebuild_apple_dependencies,
233+
prebuild_react_native_core,
234+
check_code_changes,
235+
]
236+
if: ${{ needs.prebuild_react_native_core.result == 'success' && needs.build_npm_package.result == 'success' && needs.check_code_changes.outputs.should_test_ios == 'true' }}
237+
uses: ./.github/workflows/test-ios-spm-newapp.yml
238+
secrets: inherit
239+
206240
test_e2e_android_templateapp:
207241
needs: [build_npm_package, build_android]
208242
if: ${{ always() && needs.build_android.result == 'success' && needs.build_npm_package.result == 'success' }}

.github/workflows/test-ios-spm-helloworld.yml

Lines changed: 98 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -4,60 +4,22 @@ permissions:
44
contents: read
55

66
on:
7-
workflow_dispatch:
8-
inputs:
9-
flavor:
10-
description: 'Which build configuration(s) to test'
11-
type: choice
12-
options:
13-
- both
14-
- Debug
15-
- Release
16-
default: both
17-
schedule:
18-
# Nightly at 07:00 UTC (low-traffic hour).
19-
- cron: '0 7 * * *'
7+
workflow_call:
208

219
jobs:
22-
prebuild_apple_dependencies:
23-
uses: ./.github/workflows/prebuild-ios-dependencies.yml
24-
with:
25-
# e2e only compiles for the iOS simulator — no need for device/catalyst/tv/xr slices.
26-
slices: '["ios-simulator"]'
27-
secrets: inherit
28-
29-
prebuild_react_native_core:
30-
needs: [prebuild_apple_dependencies]
31-
if: ${{ needs.prebuild_apple_dependencies.result == 'success' }}
32-
uses: ./.github/workflows/prebuild-ios-core.yml
33-
with:
34-
use-hermes-prebuilt: ${{ !endsWith(github.ref_name, '-stable') }}
35-
slices: '["ios-simulator"]'
36-
secrets: inherit
37-
38-
set_flavors:
39-
runs-on: ubuntu-latest
40-
outputs:
41-
flavors: ${{ steps.compute.outputs.flavors }}
42-
steps:
43-
- id: compute
44-
env:
45-
FLAVOR: ${{ github.event.inputs.flavor }}
46-
run: |
47-
case "$FLAVOR" in
48-
Debug) echo 'flavors=["Debug"]' >> "$GITHUB_OUTPUT" ;;
49-
Release) echo 'flavors=["Release"]' >> "$GITHUB_OUTPUT" ;;
50-
*) echo 'flavors=["Debug","Release"]' >> "$GITHUB_OUTPUT" ;;
51-
esac
52-
5310
test:
54-
needs: [prebuild_react_native_core, set_flavors]
55-
if: ${{ needs.prebuild_react_native_core.result == 'success' }}
5611
runs-on: macos-15-large
12+
# TODO: remove once this new lane has proven green on main. Keeps a
13+
# brand-new SwiftPM build from hard-blocking unrelated iOS PRs.
14+
continue-on-error: true
5715
strategy:
5816
fail-fast: false
5917
matrix:
60-
flavor: ${{ fromJSON(needs.set_flavors.outputs.flavors) }}
18+
flavor: [Debug, Release]
19+
env:
20+
APP_IOS_DIR: private/helloworld/ios
21+
XCODE_PROJECT: HelloWorld.xcodeproj
22+
XCODE_SCHEME: HelloWorld
6123
steps:
6224
- name: Checkout
6325
uses: actions/checkout@v6
@@ -72,11 +34,14 @@ jobs:
7234
run: node ./scripts/releases/use-hermes-prebuilt.js
7335
- name: Run yarn install again, with the correct hermes version
7436
uses: ./.github/actions/yarn-install
75-
- name: Ensure CocoaPods (needed by `spm add --deintegrate`)
37+
- name: Ensure CocoaPods (`spm add --deintegrate` shells out to `pod`)
7638
shell: bash
7739
run: pod --version || sudo gem install cocoapods --no-document
78-
# `spm add --artifacts` validates BOTH the debug/ and release/ slots, so
79-
# download both flavors regardless of which one this cell builds.
40+
# Both flavors are needed by every matrix cell, not just the one it builds:
41+
# `spm add` stages both flavor framework trees and lets a per-configuration
42+
# build setting pick one at build time, so it validates the debug/ and
43+
# release/ artifact slots together (and `--download skip` refuses an
44+
# incomplete slot). Please don't "optimise" this down to one flavor.
8045
- name: Download ReactCore (Debug)
8146
uses: actions/download-artifact@v7
8247
with:
@@ -97,19 +62,90 @@ jobs:
9762
with:
9863
name: ReactNativeDependenciesRelease.xcframework.tar.gz
9964
path: /tmp/deps-release
100-
- name: Prime SPM artifacts (Debug + Release)
65+
# `spm download` primes both flavor slots per call and skips any slot that
66+
# already validates, so prime in two passes: the first fills both slots
67+
# with the Release bits, then debug/ is dropped and re-primed from the
68+
# Debug tarballs while release/ validates and is left alone. hermes-engine
69+
# is fetched from Maven by the command itself.
70+
- name: Prime SwiftPM artifacts (Debug + Release)
71+
shell: bash
72+
working-directory: ${{ env.APP_IOS_DIR }}
73+
run: |
74+
RN_CORE_TARBALL_PATH=/tmp/rc-release/ReactCoreRelease.xcframework.tar.gz \
75+
RN_DEPS_TARBALL_PATH=/tmp/deps-release/ReactNativeDependenciesRelease.xcframework.tar.gz \
76+
npx react-native spm download --artifacts /tmp/spm-artifacts
77+
rm -rf /tmp/spm-artifacts/debug
78+
RN_CORE_TARBALL_PATH=/tmp/rc-debug/ReactCoreDebug.xcframework.tar.gz \
79+
RN_DEPS_TARBALL_PATH=/tmp/deps-debug/ReactNativeDependenciesDebug.xcframework.tar.gz \
80+
npx react-native spm download --artifacts /tmp/spm-artifacts
81+
- name: Scaffold Package.swift manifests for community dependencies
82+
shell: bash
83+
working-directory: ${{ env.APP_IOS_DIR }}
84+
run: npx react-native spm scaffold || true
85+
- name: Convert the app to SwiftPM
86+
shell: bash
87+
working-directory: ${{ env.APP_IOS_DIR }}
88+
run: npx react-native spm add --deintegrate --artifacts /tmp/spm-artifacts --download skip
89+
- name: Assert the app is on SwiftPM
90+
shell: bash
91+
working-directory: ${{ env.APP_IOS_DIR }}
92+
run: |
93+
if [[ ! -f "$XCODE_PROJECT/.spm-injected.json" ]]; then
94+
echo "::error::spm add did not inject SwiftPM: $XCODE_PROJECT/.spm-injected.json is missing"
95+
exit 1
96+
fi
97+
if [[ -f Podfile ]] && grep -q 'use_react_native!' Podfile; then
98+
echo "::error::spm add --deintegrate left use_react_native! in the Podfile"
99+
exit 1
100+
fi
101+
echo "SwiftPM injected in place; Podfile de-integrated."
102+
# Under SwiftPM there is no hermes-engine pod to set HERMES_CLI_PATH, so
103+
# resolve hermesc from the app's node_modules for react-native-xcode.sh.
104+
- name: Resolve HERMES_CLI_PATH
105+
if: ${{ matrix.flavor == 'Release' }}
106+
shell: bash
107+
working-directory: ${{ env.APP_IOS_DIR }}
108+
run: |
109+
HERMESC=$(node -e "const p=require('path');console.log(p.join(p.dirname(require.resolve('hermes-compiler/package.json')),'hermesc/osx-bin/hermesc'))" 2>/dev/null || true)
110+
if [[ -z "$HERMESC" || ! -x "$HERMESC" ]]; then
111+
echo "::error::Could not resolve hermes-compiler's hermesc from $PWD. Release builds need it to compile JS to Hermes bytecode; failing here so it is not mistaken for an xcodebuild bundling error."
112+
exit 1
113+
fi
114+
echo "HERMES_CLI_PATH=$HERMESC" >> "$GITHUB_ENV"
115+
echo "Using hermesc at $HERMESC"
116+
- name: Build ${{ env.XCODE_SCHEME }} (${{ matrix.flavor }})
101117
shell: bash
118+
working-directory: ${{ env.APP_IOS_DIR }}
102119
run: |
103-
node scripts/e2e/spm-prime-artifacts.js --artifacts /tmp/spm-artifacts --flavor Debug \
104-
--core-tarball /tmp/rc-debug/ReactCoreDebug.xcframework.tar.gz \
105-
--deps-tarball /tmp/deps-debug/ReactNativeDependenciesDebug.xcframework.tar.gz
106-
node scripts/e2e/spm-prime-artifacts.js --artifacts /tmp/spm-artifacts --flavor Release \
107-
--core-tarball /tmp/rc-release/ReactCoreRelease.xcframework.tar.gz \
108-
--deps-tarball /tmp/deps-release/ReactNativeDependenciesRelease.xcframework.tar.gz
109-
- name: Convert to SwiftPM + build
120+
xcodebuild \
121+
-project "$XCODE_PROJECT" \
122+
-scheme "$XCODE_SCHEME" \
123+
-configuration "${{ matrix.flavor }}" \
124+
-sdk iphonesimulator \
125+
-destination 'generic/platform=iOS Simulator' \
126+
-derivedDataPath build/spm-e2e-dd \
127+
build
128+
- name: Check the embedded React.framework flavor
110129
shell: bash
130+
working-directory: ${{ env.APP_IOS_DIR }}
111131
run: |
112-
node scripts/e2e/spm-ios-e2e.js \
113-
--app helloworld \
114-
--flavor "${{ matrix.flavor }}" \
115-
--artifacts /tmp/spm-artifacts
132+
PRODUCTS="build/spm-e2e-dd/Build/Products/${{ matrix.flavor }}-iphonesimulator"
133+
if [[ ! -d "$PRODUCTS" ]]; then
134+
echo "Skipping flavor check: no build products directory."
135+
exit 0
136+
fi
137+
BINARY=$(find "$PRODUCTS" -maxdepth 4 -path '*.app/Frameworks/React.framework/React' -type f 2>/dev/null | head -1 || true)
138+
if [[ -z "$BINARY" ]] || ! command -v nm >/dev/null; then
139+
echo "Skipping flavor check: no embedded React.framework binary or no nm."
140+
exit 0
141+
fi
142+
COUNT=$(nm "$BINARY" | grep -c getDebugProps || true)
143+
echo "getDebugProps symbols in $BINARY: $COUNT"
144+
if [[ "${{ matrix.flavor }}" == 'Debug' && "$COUNT" -eq 0 ]]; then
145+
echo "::error::Debug build embeds a Release React.framework (expected getDebugProps symbols, found none)"
146+
exit 1
147+
fi
148+
if [[ "${{ matrix.flavor }}" == 'Release' && "$COUNT" -ne 0 ]]; then
149+
echo "::error::Release build embeds a Debug React.framework ($COUNT getDebugProps symbols, expected none)"
150+
exit 1
151+
fi

0 commit comments

Comments
 (0)