From 77d63457f9b0632aa93dbb685e18c7fe22b4127e Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Tue, 3 Nov 2026 17:39:04 +0100 Subject: [PATCH 1/3] Check in CI that `spm deinit` takes the template app back to CocoaPods No CI job tested the CocoaPods opt-out: after the SwiftPM build, nothing ran `spm deinit` or looked at the files it leaves. The template app job now records the Podfile and ios/.gitignore checksums before `spm add`, runs `spm deinit` after the app upload, and checks that the Podfile and .gitignore are unchanged, the marker and ReactNativeSPM/ are gone, and the Xcode project and shared scheme have no SwiftPM references left. The check reads files only; `deinit` does not run `pod install`. --- .github/workflow-scripts/assert-spm-deinit.sh | 82 +++++++++++++++++++ .github/workflows/e2e-ios-templateapp.yml | 14 ++++ 2 files changed, 96 insertions(+) create mode 100755 .github/workflow-scripts/assert-spm-deinit.sh diff --git a/.github/workflow-scripts/assert-spm-deinit.sh b/.github/workflow-scripts/assert-spm-deinit.sh new file mode 100755 index 000000000000..d4d9de66ebfc --- /dev/null +++ b/.github/workflow-scripts/assert-spm-deinit.sh @@ -0,0 +1,82 @@ +#!/bin/bash +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +# Checks that `npx react-native spm deinit` left an iOS app with only its +# CocoaPods files. It checks only what deinit removes; `pod install` restores +# the CocoaPods references, so they are not checked here. +# +# Usage: assert-spm-deinit.sh + +if [ $# -ne 4 ]; then + echo "Usage: $0 " + exit 2 +fi + +IOS_DIR=$1 +XCODEPROJ="$IOS_DIR/$2" +EXPECTED_PODFILE_SHA256=$3 +EXPECTED_GITIGNORE_SHA256=$4 +PBXPROJ="$XCODEPROJ/project.pbxproj" +FAILED=0 + +fail() { + echo "::error::$1" + FAILED=1 +} + +sha256_or_absent() { + if [ -f "$1" ]; then + shasum -a 256 "$1" | cut -d ' ' -f 1 + else + echo absent + fi +} + +ACTUAL_PODFILE_SHA256=$(sha256_or_absent "$IOS_DIR/Podfile") +if [ "$ACTUAL_PODFILE_SHA256" != "$EXPECTED_PODFILE_SHA256" ]; then + fail "Podfile changed: sha256 is '$ACTUAL_PODFILE_SHA256', expected '$EXPECTED_PODFILE_SHA256'." +fi + +ACTUAL_GITIGNORE_SHA256=$(sha256_or_absent "$IOS_DIR/.gitignore") +if [ "$ACTUAL_GITIGNORE_SHA256" != "$EXPECTED_GITIGNORE_SHA256" ]; then + fail ".gitignore changed: sha256 is '$ACTUAL_GITIGNORE_SHA256', expected '$EXPECTED_GITIGNORE_SHA256'." +fi + +if [ -e "$XCODEPROJ/.spm-injected.json" ]; then + fail "$XCODEPROJ/.spm-injected.json is still there." +fi + +if [ -e "$IOS_DIR/ReactNativeSPM" ]; then + fail "$IOS_DIR/ReactNativeSPM is still there." +fi + +if [ ! -f "$PBXPROJ" ]; then + fail "$PBXPROJ is missing." +else + for INJECTED in \ + XCLocalSwiftPackageReference \ + XCSwiftPackageProductDependency \ + packageReferences \ + packageProductDependencies \ + ReactNativeSPM \ + "Sync SPM Autolinking" \ + "Embed React Native Flavored Frameworks"; do + if grep -qF "$INJECTED" "$PBXPROJ"; then + fail "$PBXPROJ still contains '$INJECTED'." + fi + done +fi + +for SCHEME in "$XCODEPROJ"/xcshareddata/xcschemes/*.xcscheme; do + if [ -f "$SCHEME" ] && grep -qF 'title = "Sync SPM Autolinking"' "$SCHEME"; then + fail "$SCHEME still has the Sync SPM Autolinking pre-action." + fi +done + +if [ $FAILED -ne 0 ]; then + exit 1 +fi +echo "spm deinit removed every SwiftPM change." diff --git a/.github/workflows/e2e-ios-templateapp.yml b/.github/workflows/e2e-ios-templateapp.yml index 56a7902a5adf..c3504efb2750 100644 --- a/.github/workflows/e2e-ios-templateapp.yml +++ b/.github/workflows/e2e-ios-templateapp.yml @@ -61,6 +61,15 @@ jobs: uses: ./.github/actions/setup-spm-artifacts with: working-directory: /tmp/RNTestProject/ios + - name: Record the Podfile and .gitignore checksums + working-directory: /tmp/RNTestProject/ios + run: | + echo "PODFILE_SHA256=$(shasum -a 256 Podfile | cut -d ' ' -f 1)" >> "$GITHUB_ENV" + if [ -f .gitignore ]; then + echo "GITIGNORE_SHA256=$(shasum -a 256 .gitignore | cut -d ' ' -f 1)" >> "$GITHUB_ENV" + else + echo "GITIGNORE_SHA256=absent" >> "$GITHUB_ENV" + fi - name: Convert the app to SwiftPM working-directory: /tmp/RNTestProject/ios run: RN_STRICT_API=1 npx react-native spm add --deintegrate --artifacts /tmp/spm-artifacts --download skip @@ -80,6 +89,11 @@ jobs: name: RNTestProject-${{ matrix.flavor }} overwrite: true path: /tmp/RNTestProject/Build/Products/${{ matrix.flavor }}-iphonesimulator/RNTestProject.app + - name: Take the app back to CocoaPods + working-directory: /tmp/RNTestProject/ios + run: npx react-native spm deinit + - name: Check that only the CocoaPods files are left + run: ./.github/workflow-scripts/assert-spm-deinit.sh /tmp/RNTestProject/ios RNTestProject.xcodeproj "$PODFILE_SHA256" "$GITIGNORE_SHA256" - name: Report status id: report-status if: always() From 7e726df11435f70f63d11b5eb5c5e55fd3615690 Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Thu, 5 Nov 2026 16:20:48 +0100 Subject: [PATCH 2/3] Start CI on main as the base From bf87974344e222425afcec9013793519dcb25e92 Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Fri, 6 Nov 2026 10:05:20 +0100 Subject: [PATCH 3/3] Move the deinit check to the SwiftPM new-app job and build the app with CocoaPods after it --- .github/workflow-scripts/assert-spm-deinit.sh | 148 ++++++++++++------ .github/workflows/e2e-ios-templateapp.yml | 14 -- .github/workflows/test-ios-spm-newapp.yml | 52 ++++++ 3 files changed, 149 insertions(+), 65 deletions(-) diff --git a/.github/workflow-scripts/assert-spm-deinit.sh b/.github/workflow-scripts/assert-spm-deinit.sh index d4d9de66ebfc..b8bd6f5072a3 100755 --- a/.github/workflow-scripts/assert-spm-deinit.sh +++ b/.github/workflow-scripts/assert-spm-deinit.sh @@ -8,23 +8,21 @@ # CocoaPods files. It checks only what deinit removes; `pod install` restores # the CocoaPods references, so they are not checked here. # -# Usage: assert-spm-deinit.sh +# It assumes the app had no Swift packages before `spm add`: any +# packageReferences or packageProductDependencies left after deinit fail the +# check. Do not use it on an app that already has Swift packages. +# +# Usage: +# assert-spm-deinit.sh record +# Run before `spm add`. Prints PODFILE_SHA256 and GITIGNORE_SHA256, and +# appends them to $GITHUB_ENV when it is set. +# assert-spm-deinit.sh check +# Run after `spm deinit`, with the values that record printed. -if [ $# -ne 4 ]; then - echo "Usage: $0 " +usage() { + echo "Usage: $0 record " + echo " $0 check " exit 2 -fi - -IOS_DIR=$1 -XCODEPROJ="$IOS_DIR/$2" -EXPECTED_PODFILE_SHA256=$3 -EXPECTED_GITIGNORE_SHA256=$4 -PBXPROJ="$XCODEPROJ/project.pbxproj" -FAILED=0 - -fail() { - echo "::error::$1" - FAILED=1 } sha256_or_absent() { @@ -35,48 +33,96 @@ sha256_or_absent() { fi } -ACTUAL_PODFILE_SHA256=$(sha256_or_absent "$IOS_DIR/Podfile") -if [ "$ACTUAL_PODFILE_SHA256" != "$EXPECTED_PODFILE_SHA256" ]; then - fail "Podfile changed: sha256 is '$ACTUAL_PODFILE_SHA256', expected '$EXPECTED_PODFILE_SHA256'." -fi +record() { + local ios_dir=$1 + if [ ! -f "$ios_dir/Podfile" ]; then + echo "::error::$ios_dir/Podfile is missing. Record the checksums while the app is still on CocoaPods." + exit 1 + fi + + local values + values="PODFILE_SHA256=$(sha256_or_absent "$ios_dir/Podfile") +GITIGNORE_SHA256=$(sha256_or_absent "$ios_dir/.gitignore")" + echo "$values" + if [ -n "$GITHUB_ENV" ]; then + echo "$values" >> "$GITHUB_ENV" + fi +} + +check() { + local ios_dir=$1 + local xcodeproj="$ios_dir/$2" + local expected_podfile_sha256=$3 + local expected_gitignore_sha256=$4 + local pbxproj="$xcodeproj/project.pbxproj" + local failed=0 + + fail() { + echo "::error::$1" + failed=1 + } -ACTUAL_GITIGNORE_SHA256=$(sha256_or_absent "$IOS_DIR/.gitignore") -if [ "$ACTUAL_GITIGNORE_SHA256" != "$EXPECTED_GITIGNORE_SHA256" ]; then - fail ".gitignore changed: sha256 is '$ACTUAL_GITIGNORE_SHA256', expected '$EXPECTED_GITIGNORE_SHA256'." -fi + local actual_podfile_sha256 + actual_podfile_sha256=$(sha256_or_absent "$ios_dir/Podfile") + if [ "$actual_podfile_sha256" != "$expected_podfile_sha256" ]; then + fail "Podfile changed: sha256 is '$actual_podfile_sha256', expected '$expected_podfile_sha256'." + fi -if [ -e "$XCODEPROJ/.spm-injected.json" ]; then - fail "$XCODEPROJ/.spm-injected.json is still there." -fi + local actual_gitignore_sha256 + actual_gitignore_sha256=$(sha256_or_absent "$ios_dir/.gitignore") + if [ "$actual_gitignore_sha256" != "$expected_gitignore_sha256" ]; then + fail ".gitignore changed: sha256 is '$actual_gitignore_sha256', expected '$expected_gitignore_sha256'." + fi -if [ -e "$IOS_DIR/ReactNativeSPM" ]; then - fail "$IOS_DIR/ReactNativeSPM is still there." -fi + if [ -e "$xcodeproj/.spm-injected.json" ]; then + fail "$xcodeproj/.spm-injected.json is still there." + fi + + if [ -e "$ios_dir/ReactNativeSPM" ]; then + fail "$ios_dir/ReactNativeSPM is still there." + fi -if [ ! -f "$PBXPROJ" ]; then - fail "$PBXPROJ is missing." -else - for INJECTED in \ - XCLocalSwiftPackageReference \ - XCSwiftPackageProductDependency \ - packageReferences \ - packageProductDependencies \ - ReactNativeSPM \ - "Sync SPM Autolinking" \ - "Embed React Native Flavored Frameworks"; do - if grep -qF "$INJECTED" "$PBXPROJ"; then - fail "$PBXPROJ still contains '$INJECTED'." + if [ ! -f "$pbxproj" ]; then + fail "$pbxproj is missing." + else + local injected + for injected in \ + XCLocalSwiftPackageReference \ + XCSwiftPackageProductDependency \ + packageReferences \ + packageProductDependencies \ + ReactNativeSPM \ + "Sync SPM Autolinking" \ + "Embed React Native Flavored Frameworks"; do + if grep -qF "$injected" "$pbxproj"; then + fail "$pbxproj still contains '$injected'." + fi + done + fi + + local scheme + for scheme in "$xcodeproj"/xcshareddata/xcschemes/*.xcscheme; do + if [ -f "$scheme" ] && grep -qF 'title = "Sync SPM Autolinking"' "$scheme"; then + fail "$scheme still has the Sync SPM Autolinking pre-action." fi done -fi -for SCHEME in "$XCODEPROJ"/xcshareddata/xcschemes/*.xcscheme; do - if [ -f "$SCHEME" ] && grep -qF 'title = "Sync SPM Autolinking"' "$SCHEME"; then - fail "$SCHEME still has the Sync SPM Autolinking pre-action." + if [ $failed -ne 0 ]; then + exit 1 fi -done + echo "spm deinit removed every SwiftPM change." +} -if [ $FAILED -ne 0 ]; then - exit 1 -fi -echo "spm deinit removed every SwiftPM change." +case "$1" in + record) + [ $# -eq 2 ] || usage + record "$2" + ;; + check) + [ $# -eq 5 ] || usage + check "$2" "$3" "$4" "$5" + ;; + *) + usage + ;; +esac diff --git a/.github/workflows/e2e-ios-templateapp.yml b/.github/workflows/e2e-ios-templateapp.yml index c3504efb2750..56a7902a5adf 100644 --- a/.github/workflows/e2e-ios-templateapp.yml +++ b/.github/workflows/e2e-ios-templateapp.yml @@ -61,15 +61,6 @@ jobs: uses: ./.github/actions/setup-spm-artifacts with: working-directory: /tmp/RNTestProject/ios - - name: Record the Podfile and .gitignore checksums - working-directory: /tmp/RNTestProject/ios - run: | - echo "PODFILE_SHA256=$(shasum -a 256 Podfile | cut -d ' ' -f 1)" >> "$GITHUB_ENV" - if [ -f .gitignore ]; then - echo "GITIGNORE_SHA256=$(shasum -a 256 .gitignore | cut -d ' ' -f 1)" >> "$GITHUB_ENV" - else - echo "GITIGNORE_SHA256=absent" >> "$GITHUB_ENV" - fi - name: Convert the app to SwiftPM working-directory: /tmp/RNTestProject/ios run: RN_STRICT_API=1 npx react-native spm add --deintegrate --artifacts /tmp/spm-artifacts --download skip @@ -89,11 +80,6 @@ jobs: name: RNTestProject-${{ matrix.flavor }} overwrite: true path: /tmp/RNTestProject/Build/Products/${{ matrix.flavor }}-iphonesimulator/RNTestProject.app - - name: Take the app back to CocoaPods - working-directory: /tmp/RNTestProject/ios - run: npx react-native spm deinit - - name: Check that only the CocoaPods files are left - run: ./.github/workflow-scripts/assert-spm-deinit.sh /tmp/RNTestProject/ios RNTestProject.xcodeproj "$PODFILE_SHA256" "$GITIGNORE_SHA256" - name: Report status id: report-status if: always() diff --git a/.github/workflows/test-ios-spm-newapp.yml b/.github/workflows/test-ios-spm-newapp.yml index 5607e8b94804..b21f653f199a 100644 --- a/.github/workflows/test-ios-spm-newapp.yml +++ b/.github/workflows/test-ios-spm-newapp.yml @@ -46,6 +46,14 @@ jobs: uses: ./.github/actions/setup-spm-artifacts with: working-directory: ${{ env.APP_IOS_DIR }} + # The `spm deinit` round trip runs in the Debug cell only: it does not + # depend on the flavor. + - name: Record HelloWorld before SwiftPM + if: matrix.flavor == 'Debug' + shell: bash + run: | + ./.github/workflow-scripts/assert-spm-deinit.sh record "$APP_IOS_DIR" + cp -R "$APP_IOS_DIR" "$RUNNER_TEMP/ios-before-spm" # HelloWorld is a CocoaPods project in git. CI converts it to SwiftPM on every run. - name: Convert HelloWorld to SwiftPM shell: bash @@ -100,3 +108,47 @@ jobs: echo "::error::Release build embeds a Debug React.framework ($COUNT getDebugProps symbols, expected none)" exit 1 fi + - name: Take HelloWorld back to CocoaPods + if: matrix.flavor == 'Debug' + shell: bash + working-directory: ${{ env.APP_IOS_DIR }} + run: npx react-native spm deinit + # Informational only. Expected differences include the CocoaPods + # references and the .xcworkspace, which `pod install` restores, and the + # JavaScriptCore.framework reference and empty Pods group, which deinit + # does not restore on purpose. + - name: Show what changed since before SwiftPM + if: ${{ !cancelled() && matrix.flavor == 'Debug' }} + shell: bash + run: diff -r -x build "$RUNNER_TEMP/ios-before-spm" "$APP_IOS_DIR" || true + - name: Check that only the CocoaPods files are left + if: matrix.flavor == 'Debug' + shell: bash + run: ./.github/workflow-scripts/assert-spm-deinit.sh check "$APP_IOS_DIR" "$XCODE_PROJECT" "$PODFILE_SHA256" "$GITIGNORE_SHA256" + - name: Setup ruby + if: matrix.flavor == 'Debug' + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.6.10 + # The tarballs are the Debug ones that setup-spm-artifacts downloaded. + - name: Install the CocoaPods dependencies + if: matrix.flavor == 'Debug' + shell: bash + working-directory: private/helloworld + run: | + export RCT_USE_LOCAL_RN_DEP=/tmp/deps-debug/ReactNativeDependenciesDebug.xcframework.tar.gz + export RCT_TESTONLY_RNCORE_TARBALL_PATH=/tmp/rc-debug/ReactCoreDebug.xcframework.tar.gz + yarn bootstrap ios | cat + - name: Build ${{ env.XCODE_SCHEME }} with CocoaPods (Debug) + if: matrix.flavor == 'Debug' + shell: bash + working-directory: ${{ env.APP_IOS_DIR }} + run: | + xcodebuild \ + -workspace "${XCODE_PROJECT%.xcodeproj}.xcworkspace" \ + -scheme "$XCODE_SCHEME" \ + -configuration Debug \ + -sdk iphonesimulator \ + -destination 'generic/platform=iOS Simulator' \ + -derivedDataPath build/cocoapods-e2e-dd \ + build