From ee4e8878d43bfce3a210dd83cfb0eb2fc2e006f2 Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Wed, 22 Apr 2026 13:10:15 +0200 Subject: [PATCH 01/18] Introduce iOS RN artifacts workflow and adjust existing code --- .github/scripts/getNewPatchedRNVersion.sh | 7 +- .../publishReactNativeAndroidArtifacts.yml | 25 +- .../publishReactNativeiOSArtifacts.yml | 341 ++++++++++++++++++ android/build.gradle | 2 +- android/settings.gradle | 2 +- gradleUtils/ExpensiLog.gradle | 26 -- gradleUtils/PatchedArtifactsSettings.gradle | 176 --------- rock.config.mjs | 2 +- .../android/ExpensiUtils.gradle | 152 ++++++++ .../android/PatchedArtifactsSettings.gradle | 46 +++ .../artifacts-utils/compute-patches-hash.sh | 27 ++ .../artifacts-utils/ios/publish/build.gradle | 82 +++++ .../ios/publish/settings.gradle | 1 + scripts/compute-patches-hash.sh | 26 -- 14 files changed, 679 insertions(+), 236 deletions(-) create mode 100644 .github/workflows/publishReactNativeiOSArtifacts.yml delete mode 100644 gradleUtils/ExpensiLog.gradle delete mode 100644 gradleUtils/PatchedArtifactsSettings.gradle create mode 100644 scripts/artifacts-utils/android/ExpensiUtils.gradle create mode 100644 scripts/artifacts-utils/android/PatchedArtifactsSettings.gradle create mode 100755 scripts/artifacts-utils/compute-patches-hash.sh create mode 100644 scripts/artifacts-utils/ios/publish/build.gradle create mode 100644 scripts/artifacts-utils/ios/publish/settings.gradle delete mode 100755 scripts/compute-patches-hash.sh diff --git a/.github/scripts/getNewPatchedRNVersion.sh b/.github/scripts/getNewPatchedRNVersion.sh index caac4124c57d..8a77b98d330c 100755 --- a/.github/scripts/getNewPatchedRNVersion.sh +++ b/.github/scripts/getNewPatchedRNVersion.sh @@ -5,6 +5,11 @@ if [ -z "$GITHUB_TOKEN" ]; then exit 1 fi +if [ -z "$ARTIFACT_ID" ]; then + echo "ARTIFACT_ID env variable is not set" + exit 1 +fi + if [[ "$IS_HYBRID_BUILD" == "true" ]]; then readonly PACKAGE="react-hybrid" else @@ -15,7 +20,7 @@ VERSION="$(jq -r '.dependencies["react-native"]' package.json)" readonly VERSION # List all versions of the package -PACKAGE_VERSIONS="$(gh api "/orgs/Expensify/packages/maven/com.expensify.${PACKAGE}.react-android/versions" --paginate --jq '.[].name')" +PACKAGE_VERSIONS="$(gh api "/orgs/Expensify/packages/maven/com.expensify.${PACKAGE}.${ARTIFACT_ID}/versions" --paginate --jq '.[].name')" # Filter only versions matching the base React Native version PACKAGE_VERSIONS="$(echo "$PACKAGE_VERSIONS" | grep "$VERSION")" diff --git a/.github/workflows/publishReactNativeAndroidArtifacts.yml b/.github/workflows/publishReactNativeAndroidArtifacts.yml index bf8fec797008..ce4cc732a5e1 100644 --- a/.github/workflows/publishReactNativeAndroidArtifacts.yml +++ b/.github/workflows/publishReactNativeAndroidArtifacts.yml @@ -43,8 +43,16 @@ jobs: if: ${{ github.event_name != 'workflow_dispatch' }} id: getOldPatchesHash run: | - echo "HYBRID_APP_HASH=$(./scripts/compute-patches-hash.sh patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" - echo "STANDALONE_APP_HASH=$(./scripts/compute-patches-hash.sh patches)" >> "$GITHUB_OUTPUT" + HYBRID_HERMES_FLAG="" + if grep -q '^[[:space:]]*hermesV1Enabled[[:space:]]*=[[:space:]]*true' Mobile-Expensify/Android/gradle.properties 2>/dev/null; then + HYBRID_HERMES_FLAG="--hermes-v1" + fi + STANDALONE_HERMES_FLAG="" + if grep -q '^[[:space:]]*hermesV1Enabled[[:space:]]*=[[:space:]]*true' android/gradle.properties 2>/dev/null; then + STANDALONE_HERMES_FLAG="--hermes-v1" + fi + echo "HYBRID_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh $HYBRID_HERMES_FLAG patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" + echo "STANDALONE_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh $STANDALONE_HERMES_FLAG patches)" >> "$GITHUB_OUTPUT" - name: Get previous react-native version if: ${{ github.event_name != 'workflow_dispatch' }} @@ -61,8 +69,16 @@ jobs: - name: Get new patches hash id: getNewPatchesHash run: | - echo "HYBRID_APP_HASH=$(./scripts/compute-patches-hash.sh patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" - echo "STANDALONE_APP_HASH=$(./scripts/compute-patches-hash.sh patches)" >> "$GITHUB_OUTPUT" + HYBRID_HERMES_FLAG="" + if grep -q '^[[:space:]]*hermesV1Enabled[[:space:]]*=[[:space:]]*true' Mobile-Expensify/Android/gradle.properties 2>/dev/null; then + HYBRID_HERMES_FLAG="--hermes-v1" + fi + STANDALONE_HERMES_FLAG="" + if grep -q '^[[:space:]]*hermesV1Enabled[[:space:]]*=[[:space:]]*true' android/gradle.properties 2>/dev/null; then + STANDALONE_HERMES_FLAG="--hermes-v1" + fi + echo "HYBRID_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh $HYBRID_HERMES_FLAG patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" + echo "STANDALONE_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh $STANDALONE_HERMES_FLAG patches)" >> "$GITHUB_OUTPUT" - name: Get new react-native version if: ${{ github.event_name != 'workflow_dispatch' }} @@ -169,6 +185,7 @@ jobs: env: GITHUB_TOKEN: ${{ github.token }} IS_HYBRID_BUILD: ${{ matrix.is_hybrid }} + ARTIFACT_ID: react-android - name: Build and publish React Native artifacts working-directory: ${{ matrix.is_hybrid == 'true' && 'Mobile-Expensify/Android' || 'android' }} diff --git a/.github/workflows/publishReactNativeiOSArtifacts.yml b/.github/workflows/publishReactNativeiOSArtifacts.yml new file mode 100644 index 000000000000..f7a0dcf0d332 --- /dev/null +++ b/.github/workflows/publishReactNativeiOSArtifacts.yml @@ -0,0 +1,341 @@ +name: Publish React Native iOS Artifacts + +on: + workflow_dispatch: + inputs: + build_standalone: + description: 'Build Standalone artifacts' + required: false + type: boolean + build_hybridapp: + description: 'Build HybridApp artifacts' + required: false + type: boolean + push: + branches: + - main + paths: + - package.json + - patches/react-native+*.patch + - patches/@react-native+*.patch + - patches/react-native/react-native+*.patch + - patches/react-native/@react-native+*.patch + - Mobile-Expensify + +jobs: + verifyPatches: + name: Verify React Native Patches + runs-on: ubuntu-latest + outputs: + build_targets: ${{ steps.getArtifactBuildTargets.outputs.BUILD_TARGETS }} + hybrid_app_patches_hash: ${{ steps.getNewPatchesHash.outputs.HYBRID_APP_HASH }} + standalone_patches_hash: ${{ steps.getNewPatchesHash.outputs.STANDALONE_APP_HASH }} + steps: + - name: Checkout + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + with: + submodules: true + ref: ${{ github.event.before || 'main' }} + token: ${{ secrets.OS_BOTIFY_TOKEN }} + + - name: Get previous patches hash + if: ${{ github.event_name != 'workflow_dispatch' }} + id: getOldPatchesHash + run: | + HYBRID_HERMES_FLAG="" + if grep -q "RCT_HERMES_V1_ENABLED.*=.*'1'" Mobile-Expensify/iOS/Podfile 2>/dev/null; then + HYBRID_HERMES_FLAG="--hermes-v1" + fi + STANDALONE_HERMES_FLAG="" + if grep -q "RCT_HERMES_V1_ENABLED.*=.*'1'" ios/Podfile 2>/dev/null; then + STANDALONE_HERMES_FLAG="--hermes-v1" + fi + echo "HYBRID_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh $HYBRID_HERMES_FLAG patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" + echo "STANDALONE_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh $STANDALONE_HERMES_FLAG patches)" >> "$GITHUB_OUTPUT" + + - name: Get previous react-native version + if: ${{ github.event_name != 'workflow_dispatch' }} + id: getOldVersion + run: echo "VERSION=$(jq -r '.dependencies["react-native"]' package.json)" >> "$GITHUB_OUTPUT" + + - name: Checkout new ref + if: ${{ github.event_name != 'workflow_dispatch' }} + run: | + git fetch origin ${{ github.event.after }} --depth=1 + git checkout ${{ github.event.after }} + git submodule update + + - name: Get new patches hash + id: getNewPatchesHash + run: | + HYBRID_HERMES_FLAG="" + if grep -q "RCT_HERMES_V1_ENABLED.*=.*'1'" Mobile-Expensify/iOS/Podfile 2>/dev/null; then + HYBRID_HERMES_FLAG="--hermes-v1" + fi + STANDALONE_HERMES_FLAG="" + if grep -q "RCT_HERMES_V1_ENABLED.*=.*'1'" ios/Podfile 2>/dev/null; then + STANDALONE_HERMES_FLAG="--hermes-v1" + fi + echo "HYBRID_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh $HYBRID_HERMES_FLAG patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" + echo "STANDALONE_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh $STANDALONE_HERMES_FLAG patches)" >> "$GITHUB_OUTPUT" + + - name: Get new react-native version + if: ${{ github.event_name != 'workflow_dispatch' }} + id: getNewVersion + run: echo "VERSION=$(jq -r '.dependencies["react-native"]' package.json)" >> "$GITHUB_OUTPUT" + + - name: Check if version changed + if: ${{ github.event_name != 'workflow_dispatch' }} + id: didVersionChange + run: | + readonly DID_VERSION_CHANGE=${{ steps.getOldVersion.outputs.VERSION != steps.getNewVersion.outputs.VERSION && 'true' || 'false' }} + echo "DID_VERSION_CHANGE=$DID_VERSION_CHANGE" >> "$GITHUB_OUTPUT" + if [[ "$DID_VERSION_CHANGE" == 'true' ]]; then + echo "::notice::Detected react-native version bump (${{ steps.getOldVersion.outputs.VERSION }} -> ${{ steps.getNewVersion.outputs.VERSION }})" + fi + + - name: Check if patches changed + if: ${{ github.event_name != 'workflow_dispatch' }} + id: didPatchesChange + run: | + readonly DID_HYBRID_APP_PATCHES_CHANGE=${{ steps.getOldPatchesHash.outputs.HYBRID_APP_HASH != steps.getNewPatchesHash.outputs.HYBRID_APP_HASH && 'true' || 'false' }} + readonly DID_STANDALONE_APP_PATCHES_CHANGE=${{ steps.getOldPatchesHash.outputs.STANDALONE_APP_HASH != steps.getNewPatchesHash.outputs.STANDALONE_APP_HASH && 'true' || 'false' }} + echo "DID_HYBRID_APP_PATCHES_CHANGE=$DID_HYBRID_APP_PATCHES_CHANGE" >> "$GITHUB_OUTPUT" + echo "DID_STANDALONE_APP_PATCHES_CHANGE=$DID_STANDALONE_APP_PATCHES_CHANGE" >> "$GITHUB_OUTPUT" + + if [[ "$DID_HYBRID_APP_PATCHES_CHANGE" == 'true' ]]; then + echo "::notice::Detected changes in HybridApp patches (${{ steps.getOldPatchesHash.outputs.HYBRID_APP_HASH }} -> ${{ steps.getNewPatchesHash.outputs.HYBRID_APP_HASH }})" + fi + + if [[ "$DID_STANDALONE_APP_PATCHES_CHANGE" == 'true' ]]; then + echo "::notice::Detected changes in Standalone NewDot patches (${{ steps.getOldPatchesHash.outputs.STANDALONE_APP_HASH }} -> ${{ steps.getNewPatchesHash.outputs.STANDALONE_APP_HASH }})" + fi + + - name: Get artifact build targets + id: getArtifactBuildTargets + run: | + if [[ '${{ github.event_name }}' == 'workflow_dispatch' ]]; then + BUILD_TARGETS=() + + if [[ '${{ inputs.build_standalone }}' == 'true' ]]; then + BUILD_TARGETS+=(false) + fi + + if [[ '${{ inputs.build_hybridapp }}' == 'true' ]]; then + BUILD_TARGETS+=(true) + fi + + if [[ ${#BUILD_TARGETS[@]} -ne 0 ]]; then + echo "BUILD_TARGETS=$(printf '%s\n' "${BUILD_TARGETS[@]}" | jq -R . | jq -c -s .)" >> "$GITHUB_OUTPUT" + fi + exit 0 + fi + + # When there is a version change or standalone app patches change, we need to build for both hybrid and standalone + if [[ '${{ steps.didVersionChange.outputs.DID_VERSION_CHANGE }}' == 'true' || '${{ steps.didPatchesChange.outputs.DID_STANDALONE_APP_PATCHES_CHANGE }}' == 'true' ]]; then + echo "BUILD_TARGETS=[\"true\", \"false\"]" >> "$GITHUB_OUTPUT" + elif [[ '${{ steps.didPatchesChange.outputs.DID_HYBRID_APP_PATCHES_CHANGE }}' == 'true' ]]; then + echo "BUILD_TARGETS=[\"true\"]" >> "$GITHUB_OUTPUT" + fi + + buildSlice: + name: Build iOS Slice + runs-on: macos-15 + needs: verifyPatches + if: needs.verifyPatches.outputs.build_targets != '' + strategy: + fail-fast: false + matrix: + is_hybrid: ${{ fromJSON(needs.verifyPatches.outputs.build_targets) }} + flavor: ['Debug', 'Release'] + platform: ['ios', 'ios-simulator'] + concurrency: + group: ${{ github.workflow }}-slice-${{ matrix.is_hybrid }}-${{ matrix.flavor }}-${{ matrix.platform }} + cancel-in-progress: true + env: + DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer + steps: + - name: Checkout Code + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + with: + submodules: ${{ matrix.is_hybrid }} + token: ${{ secrets.OS_BOTIFY_TOKEN }} + + - name: Setup Node + uses: ./.github/actions/composite/setupNode + with: + IS_HYBRID_BUILD: ${{ matrix.is_hybrid }} + + - name: Resolve Hermes version + id: hermes + run: echo "VERSION=$(sed 's/^hermes-v\{0,1\}//' node_modules/react-native/sdks/.hermesversion)" >> "$GITHUB_OUTPUT" + + - name: Fetch prebuild files from upstream RN + run: | + RN_VERSION="v$(jq -r '.dependencies["react-native"]' package.json)" + COMMIT=$(git ls-remote https://github.com/facebook/react-native.git "${RN_VERSION}^{}" | cut -f1) + git clone --depth=1 --filter=blob:none --sparse --no-checkout \ + https://github.com/facebook/react-native.git /tmp/rn-prebuild + git -C /tmp/rn-prebuild sparse-checkout set \ + packages/react-native/scripts \ + packages/react-native/Package.swift + git -C /tmp/rn-prebuild checkout "${COMMIT}" + cp -r /tmp/rn-prebuild/packages/react-native/scripts/ios-prebuild* node_modules/react-native/scripts/ + cp /tmp/rn-prebuild/packages/react-native/Package.swift node_modules/react-native/ + rm -rf /tmp/rn-prebuild + + - name: Setup prebuild workspace + working-directory: node_modules/react-native + run: node scripts/ios-prebuild.js -s -f ${{ matrix.flavor }} + env: + HERMES_VERSION: ${{ steps.hermes.outputs.VERSION }} + + - name: Build slice + working-directory: node_modules/react-native + run: node scripts/ios-prebuild.js -b -f ${{ matrix.flavor }} -p ${{ matrix.platform }} + + - name: Upload slice artifact + uses: actions/upload-artifact@v4 + with: + name: slice-${{ matrix.is_hybrid }}-${{ matrix.flavor }}-${{ matrix.platform }} + path: node_modules/react-native/.build/output/spm/${{ matrix.flavor }}/Build/Products + + - name: Upload headers + uses: actions/upload-artifact@v4 + with: + name: headers-${{ matrix.is_hybrid }}-${{ matrix.flavor }}-${{ matrix.platform }} + path: node_modules/react-native/.build/headers + + composeXCFramework: + name: Compose XCFramework + runs-on: macos-15 + needs: [verifyPatches, buildSlice] + strategy: + fail-fast: false + matrix: + is_hybrid: ${{ fromJSON(needs.verifyPatches.outputs.build_targets) }} + flavor: ['Debug', 'Release'] + concurrency: + group: ${{ github.workflow }}-compose-${{ matrix.is_hybrid }}-${{ matrix.flavor }} + cancel-in-progress: true + env: + DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer + steps: + - name: Checkout Code + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + with: + submodules: ${{ matrix.is_hybrid }} + token: ${{ secrets.OS_BOTIFY_TOKEN }} + + - name: Setup Node + uses: ./.github/actions/composite/setupNode + with: + IS_HYBRID_BUILD: ${{ matrix.is_hybrid }} + + - name: Fetch prebuild files from upstream RN + run: | + RN_VERSION="v$(jq -r '.dependencies["react-native"]' package.json)" + COMMIT=$(git ls-remote https://github.com/facebook/react-native.git "${RN_VERSION}^{}" | cut -f1) + git clone --depth=1 --filter=blob:none --sparse --no-checkout \ + https://github.com/facebook/react-native.git /tmp/rn-prebuild + git -C /tmp/rn-prebuild sparse-checkout set \ + packages/react-native/scripts \ + packages/react-native/Package.swift + git -C /tmp/rn-prebuild checkout "${COMMIT}" + cp -r /tmp/rn-prebuild/packages/react-native/scripts/ios-prebuild* node_modules/react-native/scripts/ + cp /tmp/rn-prebuild/packages/react-native/Package.swift node_modules/react-native/ + rm -rf /tmp/rn-prebuild + + - name: Download slice artifacts + uses: actions/download-artifact@v4 + with: + pattern: slice-${{ matrix.is_hybrid }}-${{ matrix.flavor }}-* + path: node_modules/react-native/.build/output/spm/${{ matrix.flavor }}/Build/Products + merge-multiple: true + + - name: Download headers + uses: actions/download-artifact@v4 + with: + pattern: headers-${{ matrix.is_hybrid }}-${{ matrix.flavor }}-* + path: node_modules/react-native/.build/headers + merge-multiple: true + + - name: Compose XCFramework + working-directory: node_modules/react-native + run: node scripts/ios-prebuild.js -c -f ${{ matrix.flavor }} + + - name: Compress and rename XCFramework + run: | + cd node_modules/react-native/.build/output/xcframeworks/${{ matrix.flavor }} + tar -czf reactnative-core-${{ matrix.flavor == 'Debug' && 'debug' || 'release' }}.tar.gz React.xcframework + + - name: Compress and rename dSYM + run: | + cd node_modules/react-native/.build/output/xcframeworks/${{ matrix.flavor }}/Symbols + tar -czf ../reactnative-core-dSYM-${{ matrix.flavor == 'Debug' && 'debug' || 'release' }}.tar.gz . + + - name: Upload XCFramework artifact + uses: actions/upload-artifact@v4 + with: + name: reactnative-core-${{ matrix.flavor == 'Debug' && 'debug' || 'release' }}-${{ matrix.is_hybrid }} + path: node_modules/react-native/.build/output/xcframeworks/${{ matrix.flavor }}/reactnative-core-${{ matrix.flavor == 'Debug' && 'debug' || 'release' }}.tar.gz + + - name: Upload dSYM artifact + uses: actions/upload-artifact@v4 + with: + name: reactnative-core-dSYM-${{ matrix.flavor == 'Debug' && 'debug' || 'release' }}-${{ matrix.is_hybrid }} + path: node_modules/react-native/.build/output/xcframeworks/${{ matrix.flavor }}/reactnative-core-dSYM-${{ matrix.flavor == 'Debug' && 'debug' || 'release' }}.tar.gz + + publishReactNativeiOSArtifacts: + name: Publish React Native iOS Artifacts + runs-on: ${{ github.repository_owner == 'Expensify' && 'ubuntu-latest-xl' || 'ubuntu-latest' }} + needs: [verifyPatches, composeXCFramework] + strategy: + fail-fast: false + matrix: + is_hybrid: ${{ fromJSON(needs.verifyPatches.outputs.build_targets) }} + concurrency: + group: ${{ github.workflow }}-publish-${{ matrix.is_hybrid }} + cancel-in-progress: false + steps: + - name: Checkout Code + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + with: + submodules: ${{ matrix.is_hybrid }} + token: ${{ secrets.OS_BOTIFY_TOKEN }} + + - name: Setup Node + uses: ./.github/actions/composite/setupNode + with: + IS_HYBRID_BUILD: ${{ matrix.is_hybrid }} + + - name: Determine new patched RN version + id: getNewPatchedVersion + run: echo "NEW_PATCHED_VERSION=$(./.github/scripts/getNewPatchedRNVersion.sh)" >> "$GITHUB_OUTPUT" + env: + GITHUB_TOKEN: ${{ github.token }} + IS_HYBRID_BUILD: ${{ matrix.is_hybrid }} + ARTIFACT_ID: react-native-artifacts + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + pattern: reactnative-core-*-${{ matrix.is_hybrid }} + path: /tmp/ios-artifacts + merge-multiple: true + + - name: Publish to GitHub Packages Maven + run: ./android/gradlew -p scripts/artifacts-utils/ios/publish publish -PartifactsDir="/tmp/ios-artifacts" -PpatchedVersion="${PATCHED_VERSION}" -PpatchesHash="${PATCHES_HASH}" -PpackageName="${PACKAGE_NAME}" + env: + GH_PUBLISH_ACTOR: ${{ github.actor }} + GH_PUBLISH_TOKEN: ${{ github.token }} + PATCHED_VERSION: ${{ steps.getNewPatchedVersion.outputs.NEW_PATCHED_VERSION }} + PATCHES_HASH: ${{ matrix.is_hybrid == 'true' && needs.verifyPatches.outputs.hybrid_app_patches_hash || needs.verifyPatches.outputs.standalone_patches_hash }} + PACKAGE_NAME: ${{ matrix.is_hybrid == 'true' && 'react-hybrid' || 'react-standalone' }} + + - name: Announce failed workflow in Slack + if: ${{ failure() }} + uses: ./.github/actions/composite/announceFailedWorkflowInSlack + with: + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} + CHANNEL: '#expensify-open-source' diff --git a/android/build.gradle b/android/build.gradle index 599b8e3ef529..7df762250985 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -16,7 +16,7 @@ buildscript { // "mapbox" indicates the usage of the Mapbox SDK. RNMapboxMapsImpl = "mapbox" reactNativeIncludedBuild = gradle.getIncludedBuilds().find { it.name == 'react-native' } - // This is our custom extension that we defined in gradleUtils/PatchedArtifactsSettings.gradle + // This is our custom extension that we defined in scripts/artifacts-utils/android/PatchedArtifactsSettings.gradle // It enables us to use custom artifacts of patched react-native patchedArtifactsConfig = project.gradle.settings.extensions.findByName('patchedArtifacts') } diff --git a/android/settings.gradle b/android/settings.gradle index b41080ec1d7a..e1e1b28c975c 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -6,7 +6,7 @@ plugins { id("com.facebook.react.settings") id("expo-autolinking-settings") } -apply from: "${rootDir}/../gradleUtils/PatchedArtifactsSettings.gradle" +apply from: "${rootDir}/../scripts/artifacts-utils/android/PatchedArtifactsSettings.gradle" extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand(['npx', 'rock', 'config', '-p', 'android']) } rootProject.name = 'NewExpensify' diff --git a/gradleUtils/ExpensiLog.gradle b/gradleUtils/ExpensiLog.gradle deleted file mode 100644 index fcf15c98dc1a..000000000000 --- a/gradleUtils/ExpensiLog.gradle +++ /dev/null @@ -1,26 +0,0 @@ -ext.ExpensiLog = new Object() { - private final String BLUE = "\u001B[34m" - private final String YELLOW = "\u001B[33m" - private final String RED = "\u001B[31m" - private final String RESET = "\u001B[0m" - - private String prefix = "" - void setPrefix(String newPrefix) { this.prefix = newPrefix } - void clearPrefix() { this.prefix = "" } - - private String format(String color, String message) { - return "${color}[${prefix}] ${message}${this.RESET}" - } - - void lifecycle(String message) { - logger.lifecycle(format(this.BLUE, message)) - } - - void warn(String message) { - logger.warn(format(this.YELLOW, message)) - } - - void error(String message) { - logger.error(format(this.RED, message)) - } -} diff --git a/gradleUtils/PatchedArtifactsSettings.gradle b/gradleUtils/PatchedArtifactsSettings.gradle deleted file mode 100644 index 066a70ef982a..000000000000 --- a/gradleUtils/PatchedArtifactsSettings.gradle +++ /dev/null @@ -1,176 +0,0 @@ -import groovy.xml.XmlSlurper -import groovy.json.JsonSlurper -import java.util.concurrent.TimeUnit - -apply from: "ExpensiLog.gradle" - -class PatchedArtifactsConfig { - String version = null - String packageName = null - String githubUsername = null - String githubToken = null - Boolean buildFromSource = true -} - -def patchedArtifacts = settings.getExtensions().create('patchedArtifacts', PatchedArtifactsConfig) - -def warnIfNotConfigured(reason) { - ExpensiLog.warn("$reason") - ExpensiLog.warn("For setup instructions, refer to: https://github.com/Expensify/App/blob/main/contributingGuides/SETUP_ANDROID.md#enabling-prebuilt-react-native-artifacts") -} - -def getNewDotRootDir() { - return "${rootDir}/${hasProperty('newDotRoot') ? getProperty('newDotRoot') : "/.."}" -} - -def runCommand(command) { - def process = command.execute() - process.waitFor(10, TimeUnit.SECONDS) - return [output: process.in.text?.trim(), exitCode: process.exitValue()] -} - -def hasGithubCLI() { - return runCommand(["which", "gh"]).exitCode == 0 -} - -def isCIEnvironment() { - return System.getenv("CI") != null -} - -def hasRequiredScopes() { - def scopes = runCommand(["gh", "auth", "status"]).output - return ['read:packages', 'write:packages'].any { scope -> scopes.contains(scope) } -} - -def getCIGithubActor() { - return System.getenv("GITHUB_ACTOR") -} - -def getCIGithubToken() { - return System.getenv("GITHUB_TOKEN") -} - -def hasRequiredCIEnvs() { - return getCIGithubActor() != null && getCIGithubToken() != null -} - -def getGithubUsername() { - if (isCIEnvironment()) { - return getCIGithubActor() - } - return new JsonSlurper().parseText(runCommand(["gh", "api", "user"]).output).login -} - -def getGithubToken() { - if (isCIEnvironment()) { - return getCIGithubToken() - } - return runCommand(["gh", "auth", "token"]).output -} - -def validateCredentialsContext() { - if (!hasGithubCLI()) { - return "No Github CLI found." - } - - if (isCIEnvironment() && !hasRequiredCIEnvs()) { - return "Missing required CI environment variables GITHUB_ACTOR and/or GITHUB_TOKEN." - } - - if (!isCIEnvironment() && !hasRequiredScopes()) { - return "Github token does not have required scope read:packages." - } - - return null -} - -def getCredentials() { - try { - def validationErrorMessage = validateCredentialsContext() - if (validationErrorMessage != null) { - warnIfNotConfigured(validationErrorMessage) - return null - } - - return [ - githubUsername: getGithubUsername(), - githubToken: getGithubToken(), - ] - } catch (Exception e) { - warnIfNotConfigured("Failed to get Github credentials. This might be due to an expired token or not being logged in.") - return null - } -} - -def getReactNativeVersion() { - def packageJsonPath = "${getNewDotRootDir()}/package.json" - def packageJson = file(packageJsonPath) - return new JsonSlurper().parse(packageJson).dependencies.'react-native' -} - -def getArtifactsCandidates(packageName) { - def reactNativeVersion = getReactNativeVersion() - def patchedVersions = runCommand(["gh", "api", "/orgs/Expensify/packages/maven/com.expensify.${packageName}.react-android/versions", "--jq", ".[].name"]).output - return patchedVersions.split("\n").findAll { it.startsWith(reactNativeVersion) } -} - -def getPomFile(version, packageName, githubToken) { - return new XmlSlurper().parse( - new URL("https://maven.pkg.github.com/Expensify/App/com/expensify/${packageName}/react-android/${version}/react-android-${version}.pom").openConnection().with { - setRequestProperty("Authorization", "Bearer ${githubToken}") - getInputStream() - } - ) -} - -def getLocalPatchesHash() { - def newDotRootDir = getNewDotRootDir() - def computePatchesScript = "${getNewDotRootDir()}/scripts/compute-patches-hash.sh" - def arguments = hasProperty('newDotRoot') ? ["${newDotRootDir}/patches", "${newDotRootDir}/Mobile-Expensify/patches"] : ["${newDotRootDir}/patches"] - def localPatchesHash = runCommand([computePatchesScript] + arguments).output.trim() - - return localPatchesHash -} - -def findMatchingArtifactsVersion(packageName, githubToken) { - try { - def localPatchesHash = getLocalPatchesHash() - def candidates = getArtifactsCandidates(packageName) - for (candidate in candidates) { - def patchesHashFromPomFile = getPomFile(candidate, packageName, githubToken).properties.patchesHash.text().trim() - if (patchesHashFromPomFile == localPatchesHash) { - return candidate - } - } - } catch (Exception e) { - ExpensiLog.error("Failed to find matching artifacts version for ${packageName}. Reason: $e") - return null - } -} - -settings.extensions.configure(PatchedArtifactsConfig) { config -> - ExpensiLog.setPrefix("PatchedArtifacts") - config.buildFromSource = getProperty('patchedArtifacts.forceBuildFromSource') == 'true' - if(config.buildFromSource) { - ExpensiLog.lifecycle("Forcing build from source.") - return - } - - def credentials = getCredentials() - config.packageName = getProperty('patchedArtifacts.packageName') - - if(credentials != null) { - config.githubUsername = credentials.githubUsername - config.githubToken = credentials.githubToken - config.version = findMatchingArtifactsVersion(config.packageName, config.githubToken) - } - - if(config.version == null || config.packageName == null || config.githubUsername == null || config.githubToken == null) { - config.buildFromSource = true - ExpensiLog.error("No matching artifacts version found for ${config.packageName}. Building react-native from source.") - } - else { - config.buildFromSource = false - ExpensiLog.lifecycle("Using patched react-native artifacts: ${config.packageName}:${config.version}") - } -} diff --git a/rock.config.mjs b/rock.config.mjs index 3f441f1ac5a3..8c8748132dde 100644 --- a/rock.config.mjs +++ b/rock.config.mjs @@ -22,7 +22,7 @@ export default { android: platformAndroid({sourceDir: isHybrid ? './Mobile-Expensify/Android' : './android'}), }, fingerprint: { - extraSources: ['android/gradle.properties', 'ios/Podfile', 'scripts/compute-patches-hash.sh'], + extraSources: ['android/gradle.properties', 'ios/Podfile', 'scripts/artifacts-utils/compute-patches-hash.sh'], env: ['USE_WEB_PROXY', 'PUSHER_DEV_SUFFIX', 'SECURE_NGROK_URL', 'NGROK_URL', 'USE_NGROK', 'FORCE_NATIVE_BUILD'], ignorePaths: ['Mobile-Expensify/Android/assets/app/shared/bundle.js'], }, diff --git a/scripts/artifacts-utils/android/ExpensiUtils.gradle b/scripts/artifacts-utils/android/ExpensiUtils.gradle new file mode 100644 index 000000000000..3624b58bab61 --- /dev/null +++ b/scripts/artifacts-utils/android/ExpensiUtils.gradle @@ -0,0 +1,152 @@ +import groovy.json.JsonSlurper +import groovy.xml.XmlSlurper +import java.util.concurrent.TimeUnit + +ext.ExpensiLog = new Object() { + private final String BLUE = "\u001B[34m" + private final String YELLOW = "\u001B[33m" + private final String RED = "\u001B[31m" + private final String RESET = "\u001B[0m" + + private String prefix = "" + void setPrefix(String newPrefix) { this.prefix = newPrefix } + + private String format(String color, String message) { + return "${color}[${prefix}] ${message}${this.RESET}" + } + + void lifecycle(String message) { logger.lifecycle(format(this.BLUE, message)) } + void warn(String message) { logger.warn(format(this.YELLOW, message)) } + void error(String message) { logger.error(format(this.RED, message)) } +} + +ext.ExpensiUtils = new Object() { + String GITHUB_REPO = 'Expensify/App' + String GITHUB_OWNER = 'Expensify' + String ARTIFACT_ID = 'react-android' + + String mavenPomUrl(String packageName, String version) { + return "https://maven.pkg.github.com/${GITHUB_REPO}/com/expensify/${packageName}/${ARTIFACT_ID}/${version}/${ARTIFACT_ID}-${version}.pom" + } + + String mavenVersionsApiUrl(String packageName) { + return "/orgs/${GITHUB_OWNER}/packages/maven/com.expensify.${packageName}.${ARTIFACT_ID}/versions" + } + + Map runCommand(List command) { + def process = command.execute() + process.waitFor(10, TimeUnit.SECONDS) + return [output: process.in.text?.trim(), exitCode: process.exitValue()] + } + + // GitHub Packages returns 302 to a signed S3 URL, don't forward the token there. + InputStream fetch(String url, String githubToken) { + def conn = new URL(url).openConnection() + conn.setInstanceFollowRedirects(false) + conn.setRequestProperty("Authorization", "Bearer ${githubToken}") + if (conn.responseCode == 302) { + return new URL(conn.getHeaderField("Location")).openConnection().getInputStream() + } + return conn.getInputStream() + } + + boolean hasGithubCLI() { + return runCommand(["which", "gh"]).exitCode == 0 + } + + boolean isCIEnvironment() { + return System.getenv("CI") != null + } + + boolean hasRequiredScopes() { + def scopes = runCommand(["gh", "auth", "status"]).output + return ['read:packages', 'write:packages'].any { scope -> scopes.contains(scope) } + } + + String getGithubUsername() { + if (isCIEnvironment()) return System.getenv("GITHUB_ACTOR") + return new JsonSlurper().parseText(runCommand(["gh", "api", "user"]).output).login + } + + String getGithubToken() { + if (isCIEnvironment()) return System.getenv("GITHUB_TOKEN") + return runCommand(["gh", "auth", "token"]).output + } + + Map getCredentials() { + try { + if (!hasGithubCLI()) { + warnNotConfigured("No Github CLI found.") + return null + } + if (isCIEnvironment() && !(System.getenv("GITHUB_ACTOR") && System.getenv("GITHUB_TOKEN"))) { + warnNotConfigured("Missing required CI environment variables GITHUB_ACTOR and/or GITHUB_TOKEN.") + return null + } + if (!isCIEnvironment() && !hasRequiredScopes()) { + warnNotConfigured("Github token does not have required scope read:packages.") + return null + } + return [githubUsername: getGithubUsername(), githubToken: getGithubToken()] + } catch (Exception e) { + warnNotConfigured("Failed to get Github credentials. This might be due to an expired token or not being logged in.") + return null + } + } + + private void warnNotConfigured(String reason) { + ExpensiLog.warn(reason) + ExpensiLog.warn("For setup instructions, refer to: https://github.com/${GITHUB_REPO}/blob/main/contributingGuides/SETUP_ANDROID.md#enabling-prebuilt-react-native-artifacts") + } + + String getReactNativeVersion(String newDotRootDir) { + def packageJson = new File("${newDotRootDir}/package.json") + return new JsonSlurper().parse(packageJson).dependencies.'react-native' + } + + String computePatchesHash(String newDotRootDir, boolean isHybrid) { + def script = "${newDotRootDir}/scripts/artifacts-utils/compute-patches-hash.sh" + def args = [script] + + def propsFile = isHybrid + ? new File("${newDotRootDir}/Mobile-Expensify/Android/gradle.properties") + : new File("${newDotRootDir}/android/gradle.properties") + def props = new Properties() + if (propsFile.exists()) { propsFile.withInputStream { props.load(it) } } + if (props.getProperty('hermesV1Enabled') == 'true') { + args << "--hermes-v1" + } + + args += isHybrid + ? ["${newDotRootDir}/patches", "${newDotRootDir}/Mobile-Expensify/patches"] + : ["${newDotRootDir}/patches"] + return runCommand(args).output.trim() + } + + List getArtifactsCandidates(String packageName, String newDotRootDir) { + def rnVersion = getReactNativeVersion(newDotRootDir) + def result = runCommand(["gh", "api", "--paginate", mavenVersionsApiUrl(packageName), "--jq", ".[].name"]) + return result.output.split("\n").findAll { it.startsWith(rnVersion) } as List + } + + def getPomFile(String packageName, String version, String githubToken) { + return new XmlSlurper().parse(fetch(mavenPomUrl(packageName, version), githubToken)) + } + + String findMatchingArtifactsVersion(String packageName, String githubToken, String newDotRootDir, boolean isHybrid) { + try { + def localPatchesHash = computePatchesHash(newDotRootDir, isHybrid) + def candidates = getArtifactsCandidates(packageName, newDotRootDir) + for (candidate in candidates) { + def remoteHash = getPomFile(packageName, candidate, githubToken).properties.patchesHash.text().trim() + if (remoteHash == localPatchesHash) { + return candidate + } + } + return null + } catch (Exception e) { + ExpensiLog.error("Failed to find matching artifacts version for ${packageName}. Reason: $e") + return null + } + } +} diff --git a/scripts/artifacts-utils/android/PatchedArtifactsSettings.gradle b/scripts/artifacts-utils/android/PatchedArtifactsSettings.gradle new file mode 100644 index 000000000000..58cd23c85f3c --- /dev/null +++ b/scripts/artifacts-utils/android/PatchedArtifactsSettings.gradle @@ -0,0 +1,46 @@ +apply from: "ExpensiUtils.gradle" + +class PatchedArtifactsConfig { + String version = null + String packageName = null + String githubUsername = null + String githubToken = null + Boolean buildFromSource = true +} + +settings.getExtensions().create('patchedArtifacts', PatchedArtifactsConfig) + +def getNewDotRootDir() { + return "${rootDir}/${hasProperty('newDotRoot') ? getProperty('newDotRoot') : "/.."}" +} + +settings.extensions.configure(PatchedArtifactsConfig) { config -> + ExpensiLog.setPrefix("PatchedArtifacts") + config.buildFromSource = getProperty('patchedArtifacts.forceBuildFromSource') == 'true' + if (config.buildFromSource) { + ExpensiLog.lifecycle("Forcing build from source.") + return + } + + def credentials = ExpensiUtils.getCredentials() + config.packageName = getProperty('patchedArtifacts.packageName') + + if (credentials != null) { + config.githubUsername = credentials.githubUsername + config.githubToken = credentials.githubToken + config.version = ExpensiUtils.findMatchingArtifactsVersion( + config.packageName, + config.githubToken, + getNewDotRootDir(), + hasProperty('newDotRoot') + ) + } + + if (config.version == null || config.packageName == null || config.githubUsername == null || config.githubToken == null) { + config.buildFromSource = true + ExpensiLog.error("No matching artifacts version found for ${config.packageName}. Building react-native from source.") + } else { + config.buildFromSource = false + ExpensiLog.lifecycle("Using patched react-native artifacts: ${config.packageName}:${config.version}") + } +} diff --git a/scripts/artifacts-utils/compute-patches-hash.sh b/scripts/artifacts-utils/compute-patches-hash.sh new file mode 100755 index 000000000000..b63b10981de1 --- /dev/null +++ b/scripts/artifacts-utils/compute-patches-hash.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)" +readonly SCRIPT_DIR +source "$SCRIPT_DIR/../shellUtils.sh" + +HERMES_V1=false +PATCH_DIRS=() +for arg in "$@"; do + case "$arg" in + --hermes-v1) HERMES_V1=true ;; + *) PATCH_DIRS+=("$arg") ;; + esac +done + +if [ ${#PATCH_DIRS[@]} -eq 0 ]; then + error "At least one patch directory argument is required" + exit 1 +fi + +PATCHES_HASH=$(find "${PATCH_DIRS[@]}" -type f \( -name "react-native+*.patch" -o -name "@react-native+*.patch" \) -exec sha256sum {} \; | awk '{split($2, pathParts, "/"); print pathParts[length(pathParts)], $1 }' | sort | sha256sum | awk '{print $1}') + +if [[ "$HERMES_V1" == "true" ]]; then + PATCHES_HASH=$(printf "%s" "${PATCHES_HASH}-hermesV1" | sha256sum | awk '{print $1}') +fi + +echo "$PATCHES_HASH" diff --git a/scripts/artifacts-utils/ios/publish/build.gradle b/scripts/artifacts-utils/ios/publish/build.gradle new file mode 100644 index 000000000000..3419b6c11b30 --- /dev/null +++ b/scripts/artifacts-utils/ios/publish/build.gradle @@ -0,0 +1,82 @@ +apply plugin: 'maven-publish' + +def artifactsDir = findProperty('artifactsDir') ?: error('artifactsDir property is required') +def patchedVersion = findProperty('patchedVersion') ?: error('patchedVersion property is required') +def patchesHash = findProperty('patchesHash') ?: error('patchesHash property is required') +def packageName = findProperty('packageName') ?: error('packageName property is required') + +configurations.maybeCreate('iosArtifacts') + +def coreDebugArtifact = artifacts.add('iosArtifacts', file("${artifactsDir}/reactnative-core-debug.tar.gz")) { + type = 'tgz' + extension = 'tar.gz' + classifier = 'reactnative-core-debug' +} + +def coreReleaseArtifact = artifacts.add('iosArtifacts', file("${artifactsDir}/reactnative-core-release.tar.gz")) { + type = 'tgz' + extension = 'tar.gz' + classifier = 'reactnative-core-release' +} + +def coreDsymDebugArtifact = artifacts.add('iosArtifacts', file("${artifactsDir}/reactnative-core-dSYM-debug.tar.gz")) { + type = 'tgz' + extension = 'tar.gz' + classifier = 'reactnative-core-dSYM-debug' +} + +def coreDsymReleaseArtifact = artifacts.add('iosArtifacts', file("${artifactsDir}/reactnative-core-dSYM-release.tar.gz")) { + type = 'tgz' + extension = 'tar.gz' + classifier = 'reactnative-core-dSYM-release' +} + +publishing { + publications { + release(MavenPublication) { + groupId = "com.expensify.${packageName}" + artifactId = 'react-native-artifacts' + version = patchedVersion + + artifact(coreDebugArtifact) + artifact(coreReleaseArtifact) + artifact(coreDsymDebugArtifact) + artifact(coreDsymReleaseArtifact) + + pom { + name = 'react-native-artifacts' + description = 'Patched React Native iOS prebuilt artifacts for Expensify' + url = 'https://github.com/Expensify/App' + + properties = [ + 'patchesHash': patchesHash, + ] + + developers { + developer { + id = 'expensify' + name = 'Expensify' + } + } + + licenses { + license { + name = 'MIT License' + url = 'https://github.com/facebook/react-native/blob/HEAD/LICENSE' + distribution = 'repo' + } + } + } + } + } + + repositories { + maven { + url = 'https://maven.pkg.github.com/Expensify/App' + credentials { + username = System.getenv('GH_PUBLISH_ACTOR') + password = System.getenv('GH_PUBLISH_TOKEN') + } + } + } +} diff --git a/scripts/artifacts-utils/ios/publish/settings.gradle b/scripts/artifacts-utils/ios/publish/settings.gradle new file mode 100644 index 000000000000..82e1f0703c8d --- /dev/null +++ b/scripts/artifacts-utils/ios/publish/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'ios-artifacts-publish' diff --git a/scripts/compute-patches-hash.sh b/scripts/compute-patches-hash.sh deleted file mode 100755 index fc8c23549bd5..000000000000 --- a/scripts/compute-patches-hash.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)" -readonly SCRIPT_DIR -source "$SCRIPT_DIR/shellUtils.sh" - -if [ $# -eq 0 ]; then - error "Please provide at least one path as an argument" - exit 1 -fi - -PATCH_DIRS=("$@") -readonly PATCH_DIRS - -# Find all patches, compute their hash, put filename before hash, sort, compute hash of hashes -PATCHES_HASH=$(find "${PATCH_DIRS[@]}" -type f \( -name "react-native+*.patch" -o -name "@react-native+*.patch" \) -exec sha256sum {} \; | awk '{split($2, pathParts, "/"); print pathParts[length(pathParts)], $1 }' | sort | sha256sum | awk '{print $1}') - -# Include hermesV1Enabled flag in the hash so that enabling Hermes V1 invalidates -# prebuilt artifacts and triggers a build from source. -ANDROID_GRADLE_PROPS="$SCRIPT_DIR/../android/gradle.properties" - -if [ -f "$ANDROID_GRADLE_PROPS" ] && grep -q '^[[:space:]]*hermesV1Enabled[[:space:]]*=[[:space:]]*true' "$ANDROID_GRADLE_PROPS"; then - PATCHES_HASH=$(printf "%s" "${PATCHES_HASH}-hermesV1" | sha256sum | awk '{print $1}') -fi - -echo "$PATCHES_HASH" From ce5494537298908fecf7cb14ea6950d7e899af58 Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Wed, 22 Apr 2026 14:02:41 +0200 Subject: [PATCH 02/18] Make both workflows more DRY --- .../determineNewPatchedRNVersion/action.yml | 29 +++ .../fetchReactNativePrebuild/action.yml | 23 +++ .../publishReactNativeAndroidArtifacts.yml | 132 ++----------- .../publishReactNativeiOSArtifacts.yml | 180 +++--------------- .../workflows/verifyReactNativePatches.yml | 152 +++++++++++++++ .../artifacts-utils/resolve-hermes-flag.sh | 43 +++++ 6 files changed, 288 insertions(+), 271 deletions(-) create mode 100644 .github/actions/composite/determineNewPatchedRNVersion/action.yml create mode 100644 .github/actions/composite/fetchReactNativePrebuild/action.yml create mode 100644 .github/workflows/verifyReactNativePatches.yml create mode 100755 scripts/artifacts-utils/resolve-hermes-flag.sh diff --git a/.github/actions/composite/determineNewPatchedRNVersion/action.yml b/.github/actions/composite/determineNewPatchedRNVersion/action.yml new file mode 100644 index 000000000000..e804287350d0 --- /dev/null +++ b/.github/actions/composite/determineNewPatchedRNVersion/action.yml @@ -0,0 +1,29 @@ +name: Determine new patched React Native version +description: > + Runs getNewPatchedRNVersion.sh with the required env wiring + (GITHUB_TOKEN, IS_HYBRID_BUILD, ARTIFACT_ID) and exposes the + resulting version string as an output. + +inputs: + artifact_id: + description: Artifact id registered in GitHub Packages Maven (e.g. react-android, react-native-artifacts) + required: true + is_hybrid_build: + description: Whether this is a HybridApp build (true) or Standalone (false) + required: true + +outputs: + NEW_PATCHED_VERSION: + description: The computed next patched React Native version (e.g. 0.83.1-3) + value: ${{ steps.run.outputs.NEW_PATCHED_VERSION }} + +runs: + using: composite + steps: + - id: run + shell: bash + run: echo "NEW_PATCHED_VERSION=$(./.github/scripts/getNewPatchedRNVersion.sh)" >> "$GITHUB_OUTPUT" + env: + GITHUB_TOKEN: ${{ github.token }} + IS_HYBRID_BUILD: ${{ inputs.is_hybrid_build }} + ARTIFACT_ID: ${{ inputs.artifact_id }} diff --git a/.github/actions/composite/fetchReactNativePrebuild/action.yml b/.github/actions/composite/fetchReactNativePrebuild/action.yml new file mode 100644 index 000000000000..096d7cd3547c --- /dev/null +++ b/.github/actions/composite/fetchReactNativePrebuild/action.yml @@ -0,0 +1,23 @@ +name: Fetch React Native prebuild files +description: > + Copies the `ios-prebuild*` scripts and `Package.swift` from the upstream + facebook/react-native repo (matching the version pinned in package.json) + into the local node_modules/react-native/ folder. + +runs: + using: composite + steps: + - name: Fetch prebuild files from upstream React Native + shell: bash + run: | + RN_VERSION="v$(jq -r '.dependencies["react-native"]' package.json)" + COMMIT=$(git ls-remote https://github.com/facebook/react-native.git "${RN_VERSION}^{}" | cut -f1) + git clone --depth=1 --filter=blob:none --sparse --no-checkout \ + https://github.com/facebook/react-native.git /tmp/rn-prebuild + git -C /tmp/rn-prebuild sparse-checkout set \ + packages/react-native/scripts \ + packages/react-native/Package.swift + git -C /tmp/rn-prebuild checkout "${COMMIT}" + cp -r /tmp/rn-prebuild/packages/react-native/scripts/ios-prebuild* node_modules/react-native/scripts/ + cp /tmp/rn-prebuild/packages/react-native/Package.swift node_modules/react-native/ + rm -rf /tmp/rn-prebuild diff --git a/.github/workflows/publishReactNativeAndroidArtifacts.yml b/.github/workflows/publishReactNativeAndroidArtifacts.yml index ce4cc732a5e1..b68b6c79cd00 100644 --- a/.github/workflows/publishReactNativeAndroidArtifacts.yml +++ b/.github/workflows/publishReactNativeAndroidArtifacts.yml @@ -24,120 +24,15 @@ on: jobs: verifyPatches: - name: Verify React Native Patches - runs-on: 'blacksmith-4vcpu-ubuntu-2404' - outputs: - build_targets: ${{ steps.getArtifactBuildTargets.outputs.BUILD_TARGETS }} - hybrid_app_patches_hash: ${{ steps.getNewPatchesHash.outputs.HYBRID_APP_HASH }} - standalone_patches_hash: ${{ steps.getNewPatchesHash.outputs.STANDALONE_APP_HASH }} - steps: - - name: Checkout - # v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - with: - submodules: true - ref: ${{ github.event.before || 'main' }} - token: ${{ secrets.OS_BOTIFY_TOKEN }} - - - name: Get previous patches hash - if: ${{ github.event_name != 'workflow_dispatch' }} - id: getOldPatchesHash - run: | - HYBRID_HERMES_FLAG="" - if grep -q '^[[:space:]]*hermesV1Enabled[[:space:]]*=[[:space:]]*true' Mobile-Expensify/Android/gradle.properties 2>/dev/null; then - HYBRID_HERMES_FLAG="--hermes-v1" - fi - STANDALONE_HERMES_FLAG="" - if grep -q '^[[:space:]]*hermesV1Enabled[[:space:]]*=[[:space:]]*true' android/gradle.properties 2>/dev/null; then - STANDALONE_HERMES_FLAG="--hermes-v1" - fi - echo "HYBRID_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh $HYBRID_HERMES_FLAG patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" - echo "STANDALONE_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh $STANDALONE_HERMES_FLAG patches)" >> "$GITHUB_OUTPUT" - - - name: Get previous react-native version - if: ${{ github.event_name != 'workflow_dispatch' }} - id: getOldVersion - run: echo "VERSION=$(jq -r '.dependencies["react-native"]' package.json)" >> "$GITHUB_OUTPUT" - - - name: Checkout new ref - if: ${{ github.event_name != 'workflow_dispatch' }} - run: | - git fetch origin ${{ github.event.after }} --depth=1 - git checkout ${{ github.event.after }} - git submodule update - - - name: Get new patches hash - id: getNewPatchesHash - run: | - HYBRID_HERMES_FLAG="" - if grep -q '^[[:space:]]*hermesV1Enabled[[:space:]]*=[[:space:]]*true' Mobile-Expensify/Android/gradle.properties 2>/dev/null; then - HYBRID_HERMES_FLAG="--hermes-v1" - fi - STANDALONE_HERMES_FLAG="" - if grep -q '^[[:space:]]*hermesV1Enabled[[:space:]]*=[[:space:]]*true' android/gradle.properties 2>/dev/null; then - STANDALONE_HERMES_FLAG="--hermes-v1" - fi - echo "HYBRID_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh $HYBRID_HERMES_FLAG patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" - echo "STANDALONE_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh $STANDALONE_HERMES_FLAG patches)" >> "$GITHUB_OUTPUT" - - - name: Get new react-native version - if: ${{ github.event_name != 'workflow_dispatch' }} - id: getNewVersion - run: echo "VERSION=$(jq -r '.dependencies["react-native"]' package.json)" >> "$GITHUB_OUTPUT" - - - name: Check if version changed - if: ${{ github.event_name != 'workflow_dispatch' }} - id: didVersionChange - run: | - readonly DID_VERSION_CHANGE=${{ steps.getOldVersion.outputs.VERSION != steps.getNewVersion.outputs.VERSION && 'true' || 'false' }} - echo "DID_VERSION_CHANGE=$DID_VERSION_CHANGE" >> "$GITHUB_OUTPUT" - if [[ "$DID_VERSION_CHANGE" == 'true' ]]; then - echo "::notice::Detected react-native version bump (${{ steps.getOldVersion.outputs.VERSION }} -> ${{ steps.getNewVersion.outputs.VERSION }})" - fi - - - name: Check if patches changed - if: ${{ github.event_name != 'workflow_dispatch' }} - id: didPatchesChange - run: | - readonly DID_HYBRID_APP_PATCHES_CHANGE=${{ steps.getOldPatchesHash.outputs.HYBRID_APP_HASH != steps.getNewPatchesHash.outputs.HYBRID_APP_HASH && 'true' || 'false' }} - readonly DID_STANDALONE_APP_PATCHES_CHANGE=${{ steps.getOldPatchesHash.outputs.STANDALONE_APP_HASH != steps.getNewPatchesHash.outputs.STANDALONE_APP_HASH && 'true' || 'false' }} - echo "DID_HYBRID_APP_PATCHES_CHANGE=$DID_HYBRID_APP_PATCHES_CHANGE" >> "$GITHUB_OUTPUT" - echo "DID_STANDALONE_APP_PATCHES_CHANGE=$DID_STANDALONE_APP_PATCHES_CHANGE" >> "$GITHUB_OUTPUT" - - if [[ "$DID_HYBRID_APP_PATCHES_CHANGE" == 'true' ]]; then - echo "::notice::Detected changes in HybridApp patches (${{ steps.getOldPatchesHash.outputs.HYBRID_APP_HASH }} -> ${{ steps.getNewPatchesHash.outputs.HYBRID_APP_HASH }})" - fi - - if [[ "$DID_STANDALONE_APP_PATCHES_CHANGE" == 'true' ]]; then - echo "::notice::Detected changes in Standalone NewDot patches (${{ steps.getOldPatchesHash.outputs.STANDALONE_APP_HASH }} -> ${{ steps.getNewPatchesHash.outputs.STANDALONE_APP_HASH }})" - fi - - - name: Get artifact build targets - id: getArtifactBuildTargets - run: | - if [[ '${{ github.event_name }}' == 'workflow_dispatch' ]]; then - BUILD_TARGETS=() - - if [[ '${{ inputs.build_standalone }}' == 'true' ]]; then - BUILD_TARGETS+=(false) - fi - - if [[ '${{ inputs.build_hybridapp }}' == 'true' ]]; then - BUILD_TARGETS+=(true) - fi - - if [[ ${#BUILD_TARGETS[@]} -ne 0 ]]; then - echo "BUILD_TARGETS=$(printf '%s\n' "${BUILD_TARGETS[@]}" | jq -R . | jq -c -s .)" >> "$GITHUB_OUTPUT" - fi - exit 0 - fi - - # When there is a version change or standalone app patches change, we need to build for both hybrid and standalone - if [[ '${{ steps.didVersionChange.outputs.DID_VERSION_CHANGE }}' == 'true' || '${{ steps.didPatchesChange.outputs.DID_STANDALONE_APP_PATCHES_CHANGE }}' == 'true' ]]; then - echo "BUILD_TARGETS=[\"true\", \"false\"]" >> "$GITHUB_OUTPUT" - elif [[ '${{ steps.didPatchesChange.outputs.DID_HYBRID_APP_PATCHES_CHANGE }}' == 'true' ]]; then - echo "BUILD_TARGETS=[\"true\"]" >> "$GITHUB_OUTPUT" - fi + uses: ./.github/workflows/verifyReactNativePatches.yml + with: + platform: android + ref_before: ${{ github.event.before || '' }} + ref_after: ${{ github.event.after || '' }} + build_standalone: ${{ github.event_name == 'workflow_dispatch' && inputs.build_standalone || false }} + build_hybridapp: ${{ github.event_name == 'workflow_dispatch' && inputs.build_hybridapp || false }} + secrets: + OS_BOTIFY_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} buildAndPublishReactNativeArtifacts: name: Build and Publish React Native Artifacts @@ -181,11 +76,10 @@ jobs: - name: Determine new patched RN version id: getNewPatchedVersion - run: echo "NEW_PATCHED_VERSION=$(./.github/scripts/getNewPatchedRNVersion.sh)" >> "$GITHUB_OUTPUT" - env: - GITHUB_TOKEN: ${{ github.token }} - IS_HYBRID_BUILD: ${{ matrix.is_hybrid }} - ARTIFACT_ID: react-android + uses: ./.github/actions/composite/determineNewPatchedRNVersion + with: + artifact_id: react-android + is_hybrid_build: ${{ matrix.is_hybrid }} - name: Build and publish React Native artifacts working-directory: ${{ matrix.is_hybrid == 'true' && 'Mobile-Expensify/Android' || 'android' }} diff --git a/.github/workflows/publishReactNativeiOSArtifacts.yml b/.github/workflows/publishReactNativeiOSArtifacts.yml index f7a0dcf0d332..e19b15253733 100644 --- a/.github/workflows/publishReactNativeiOSArtifacts.yml +++ b/.github/workflows/publishReactNativeiOSArtifacts.yml @@ -24,119 +24,15 @@ on: jobs: verifyPatches: - name: Verify React Native Patches - runs-on: ubuntu-latest - outputs: - build_targets: ${{ steps.getArtifactBuildTargets.outputs.BUILD_TARGETS }} - hybrid_app_patches_hash: ${{ steps.getNewPatchesHash.outputs.HYBRID_APP_HASH }} - standalone_patches_hash: ${{ steps.getNewPatchesHash.outputs.STANDALONE_APP_HASH }} - steps: - - name: Checkout - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 - with: - submodules: true - ref: ${{ github.event.before || 'main' }} - token: ${{ secrets.OS_BOTIFY_TOKEN }} - - - name: Get previous patches hash - if: ${{ github.event_name != 'workflow_dispatch' }} - id: getOldPatchesHash - run: | - HYBRID_HERMES_FLAG="" - if grep -q "RCT_HERMES_V1_ENABLED.*=.*'1'" Mobile-Expensify/iOS/Podfile 2>/dev/null; then - HYBRID_HERMES_FLAG="--hermes-v1" - fi - STANDALONE_HERMES_FLAG="" - if grep -q "RCT_HERMES_V1_ENABLED.*=.*'1'" ios/Podfile 2>/dev/null; then - STANDALONE_HERMES_FLAG="--hermes-v1" - fi - echo "HYBRID_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh $HYBRID_HERMES_FLAG patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" - echo "STANDALONE_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh $STANDALONE_HERMES_FLAG patches)" >> "$GITHUB_OUTPUT" - - - name: Get previous react-native version - if: ${{ github.event_name != 'workflow_dispatch' }} - id: getOldVersion - run: echo "VERSION=$(jq -r '.dependencies["react-native"]' package.json)" >> "$GITHUB_OUTPUT" - - - name: Checkout new ref - if: ${{ github.event_name != 'workflow_dispatch' }} - run: | - git fetch origin ${{ github.event.after }} --depth=1 - git checkout ${{ github.event.after }} - git submodule update - - - name: Get new patches hash - id: getNewPatchesHash - run: | - HYBRID_HERMES_FLAG="" - if grep -q "RCT_HERMES_V1_ENABLED.*=.*'1'" Mobile-Expensify/iOS/Podfile 2>/dev/null; then - HYBRID_HERMES_FLAG="--hermes-v1" - fi - STANDALONE_HERMES_FLAG="" - if grep -q "RCT_HERMES_V1_ENABLED.*=.*'1'" ios/Podfile 2>/dev/null; then - STANDALONE_HERMES_FLAG="--hermes-v1" - fi - echo "HYBRID_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh $HYBRID_HERMES_FLAG patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" - echo "STANDALONE_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh $STANDALONE_HERMES_FLAG patches)" >> "$GITHUB_OUTPUT" - - - name: Get new react-native version - if: ${{ github.event_name != 'workflow_dispatch' }} - id: getNewVersion - run: echo "VERSION=$(jq -r '.dependencies["react-native"]' package.json)" >> "$GITHUB_OUTPUT" - - - name: Check if version changed - if: ${{ github.event_name != 'workflow_dispatch' }} - id: didVersionChange - run: | - readonly DID_VERSION_CHANGE=${{ steps.getOldVersion.outputs.VERSION != steps.getNewVersion.outputs.VERSION && 'true' || 'false' }} - echo "DID_VERSION_CHANGE=$DID_VERSION_CHANGE" >> "$GITHUB_OUTPUT" - if [[ "$DID_VERSION_CHANGE" == 'true' ]]; then - echo "::notice::Detected react-native version bump (${{ steps.getOldVersion.outputs.VERSION }} -> ${{ steps.getNewVersion.outputs.VERSION }})" - fi - - - name: Check if patches changed - if: ${{ github.event_name != 'workflow_dispatch' }} - id: didPatchesChange - run: | - readonly DID_HYBRID_APP_PATCHES_CHANGE=${{ steps.getOldPatchesHash.outputs.HYBRID_APP_HASH != steps.getNewPatchesHash.outputs.HYBRID_APP_HASH && 'true' || 'false' }} - readonly DID_STANDALONE_APP_PATCHES_CHANGE=${{ steps.getOldPatchesHash.outputs.STANDALONE_APP_HASH != steps.getNewPatchesHash.outputs.STANDALONE_APP_HASH && 'true' || 'false' }} - echo "DID_HYBRID_APP_PATCHES_CHANGE=$DID_HYBRID_APP_PATCHES_CHANGE" >> "$GITHUB_OUTPUT" - echo "DID_STANDALONE_APP_PATCHES_CHANGE=$DID_STANDALONE_APP_PATCHES_CHANGE" >> "$GITHUB_OUTPUT" - - if [[ "$DID_HYBRID_APP_PATCHES_CHANGE" == 'true' ]]; then - echo "::notice::Detected changes in HybridApp patches (${{ steps.getOldPatchesHash.outputs.HYBRID_APP_HASH }} -> ${{ steps.getNewPatchesHash.outputs.HYBRID_APP_HASH }})" - fi - - if [[ "$DID_STANDALONE_APP_PATCHES_CHANGE" == 'true' ]]; then - echo "::notice::Detected changes in Standalone NewDot patches (${{ steps.getOldPatchesHash.outputs.STANDALONE_APP_HASH }} -> ${{ steps.getNewPatchesHash.outputs.STANDALONE_APP_HASH }})" - fi - - - name: Get artifact build targets - id: getArtifactBuildTargets - run: | - if [[ '${{ github.event_name }}' == 'workflow_dispatch' ]]; then - BUILD_TARGETS=() - - if [[ '${{ inputs.build_standalone }}' == 'true' ]]; then - BUILD_TARGETS+=(false) - fi - - if [[ '${{ inputs.build_hybridapp }}' == 'true' ]]; then - BUILD_TARGETS+=(true) - fi - - if [[ ${#BUILD_TARGETS[@]} -ne 0 ]]; then - echo "BUILD_TARGETS=$(printf '%s\n' "${BUILD_TARGETS[@]}" | jq -R . | jq -c -s .)" >> "$GITHUB_OUTPUT" - fi - exit 0 - fi - - # When there is a version change or standalone app patches change, we need to build for both hybrid and standalone - if [[ '${{ steps.didVersionChange.outputs.DID_VERSION_CHANGE }}' == 'true' || '${{ steps.didPatchesChange.outputs.DID_STANDALONE_APP_PATCHES_CHANGE }}' == 'true' ]]; then - echo "BUILD_TARGETS=[\"true\", \"false\"]" >> "$GITHUB_OUTPUT" - elif [[ '${{ steps.didPatchesChange.outputs.DID_HYBRID_APP_PATCHES_CHANGE }}' == 'true' ]]; then - echo "BUILD_TARGETS=[\"true\"]" >> "$GITHUB_OUTPUT" - fi + uses: ./.github/workflows/verifyReactNativePatches.yml + with: + platform: ios + ref_before: ${{ github.event.before || '' }} + ref_after: ${{ github.event.after || '' }} + build_standalone: ${{ github.event_name == 'workflow_dispatch' && inputs.build_standalone || false }} + build_hybridapp: ${{ github.event_name == 'workflow_dispatch' && inputs.build_hybridapp || false }} + secrets: + OS_BOTIFY_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} buildSlice: name: Build iOS Slice @@ -156,7 +52,8 @@ jobs: DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer steps: - name: Checkout Code - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + # v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: submodules: ${{ matrix.is_hybrid }} token: ${{ secrets.OS_BOTIFY_TOKEN }} @@ -171,18 +68,7 @@ jobs: run: echo "VERSION=$(sed 's/^hermes-v\{0,1\}//' node_modules/react-native/sdks/.hermesversion)" >> "$GITHUB_OUTPUT" - name: Fetch prebuild files from upstream RN - run: | - RN_VERSION="v$(jq -r '.dependencies["react-native"]' package.json)" - COMMIT=$(git ls-remote https://github.com/facebook/react-native.git "${RN_VERSION}^{}" | cut -f1) - git clone --depth=1 --filter=blob:none --sparse --no-checkout \ - https://github.com/facebook/react-native.git /tmp/rn-prebuild - git -C /tmp/rn-prebuild sparse-checkout set \ - packages/react-native/scripts \ - packages/react-native/Package.swift - git -C /tmp/rn-prebuild checkout "${COMMIT}" - cp -r /tmp/rn-prebuild/packages/react-native/scripts/ios-prebuild* node_modules/react-native/scripts/ - cp /tmp/rn-prebuild/packages/react-native/Package.swift node_modules/react-native/ - rm -rf /tmp/rn-prebuild + uses: ./.github/actions/composite/fetchReactNativePrebuild - name: Setup prebuild workspace working-directory: node_modules/react-native @@ -195,13 +81,13 @@ jobs: run: node scripts/ios-prebuild.js -b -f ${{ matrix.flavor }} -p ${{ matrix.platform }} - name: Upload slice artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f with: name: slice-${{ matrix.is_hybrid }}-${{ matrix.flavor }}-${{ matrix.platform }} path: node_modules/react-native/.build/output/spm/${{ matrix.flavor }}/Build/Products - name: Upload headers - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f with: name: headers-${{ matrix.is_hybrid }}-${{ matrix.flavor }}-${{ matrix.platform }} path: node_modules/react-native/.build/headers @@ -222,7 +108,8 @@ jobs: DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer steps: - name: Checkout Code - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + # v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: submodules: ${{ matrix.is_hybrid }} token: ${{ secrets.OS_BOTIFY_TOKEN }} @@ -233,28 +120,17 @@ jobs: IS_HYBRID_BUILD: ${{ matrix.is_hybrid }} - name: Fetch prebuild files from upstream RN - run: | - RN_VERSION="v$(jq -r '.dependencies["react-native"]' package.json)" - COMMIT=$(git ls-remote https://github.com/facebook/react-native.git "${RN_VERSION}^{}" | cut -f1) - git clone --depth=1 --filter=blob:none --sparse --no-checkout \ - https://github.com/facebook/react-native.git /tmp/rn-prebuild - git -C /tmp/rn-prebuild sparse-checkout set \ - packages/react-native/scripts \ - packages/react-native/Package.swift - git -C /tmp/rn-prebuild checkout "${COMMIT}" - cp -r /tmp/rn-prebuild/packages/react-native/scripts/ios-prebuild* node_modules/react-native/scripts/ - cp /tmp/rn-prebuild/packages/react-native/Package.swift node_modules/react-native/ - rm -rf /tmp/rn-prebuild + uses: ./.github/actions/composite/fetchReactNativePrebuild - name: Download slice artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 with: pattern: slice-${{ matrix.is_hybrid }}-${{ matrix.flavor }}-* path: node_modules/react-native/.build/output/spm/${{ matrix.flavor }}/Build/Products merge-multiple: true - name: Download headers - uses: actions/download-artifact@v4 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 with: pattern: headers-${{ matrix.is_hybrid }}-${{ matrix.flavor }}-* path: node_modules/react-native/.build/headers @@ -275,13 +151,13 @@ jobs: tar -czf ../reactnative-core-dSYM-${{ matrix.flavor == 'Debug' && 'debug' || 'release' }}.tar.gz . - name: Upload XCFramework artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f with: name: reactnative-core-${{ matrix.flavor == 'Debug' && 'debug' || 'release' }}-${{ matrix.is_hybrid }} path: node_modules/react-native/.build/output/xcframeworks/${{ matrix.flavor }}/reactnative-core-${{ matrix.flavor == 'Debug' && 'debug' || 'release' }}.tar.gz - name: Upload dSYM artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f with: name: reactnative-core-dSYM-${{ matrix.flavor == 'Debug' && 'debug' || 'release' }}-${{ matrix.is_hybrid }} path: node_modules/react-native/.build/output/xcframeworks/${{ matrix.flavor }}/reactnative-core-dSYM-${{ matrix.flavor == 'Debug' && 'debug' || 'release' }}.tar.gz @@ -299,7 +175,8 @@ jobs: cancel-in-progress: false steps: - name: Checkout Code - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 + # v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd with: submodules: ${{ matrix.is_hybrid }} token: ${{ secrets.OS_BOTIFY_TOKEN }} @@ -311,14 +188,13 @@ jobs: - name: Determine new patched RN version id: getNewPatchedVersion - run: echo "NEW_PATCHED_VERSION=$(./.github/scripts/getNewPatchedRNVersion.sh)" >> "$GITHUB_OUTPUT" - env: - GITHUB_TOKEN: ${{ github.token }} - IS_HYBRID_BUILD: ${{ matrix.is_hybrid }} - ARTIFACT_ID: react-native-artifacts + uses: ./.github/actions/composite/determineNewPatchedRNVersion + with: + artifact_id: react-native-artifacts + is_hybrid_build: ${{ matrix.is_hybrid }} - name: Download artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 with: pattern: reactnative-core-*-${{ matrix.is_hybrid }} path: /tmp/ios-artifacts diff --git a/.github/workflows/verifyReactNativePatches.yml b/.github/workflows/verifyReactNativePatches.yml new file mode 100644 index 000000000000..9f39aeac89b5 --- /dev/null +++ b/.github/workflows/verifyReactNativePatches.yml @@ -0,0 +1,152 @@ +name: Verify React Native Patches + +on: + workflow_call: + inputs: + platform: + description: Platform being verified (android or ios) + required: true + type: string + ref_before: + description: Git ref to compare against (typically github.event.before); leave empty for workflow_dispatch + required: false + type: string + default: '' + ref_after: + description: Git ref to compare to (typically github.event.after); leave empty for workflow_dispatch + required: false + type: string + default: '' + build_standalone: + description: Force Standalone build (from workflow_dispatch) + required: false + type: boolean + default: false + build_hybridapp: + description: Force HybridApp build (from workflow_dispatch) + required: false + type: boolean + default: false + + outputs: + build_targets: + description: JSON array of `is_hybrid` values to build (empty string when nothing to build) + value: ${{ jobs.verify.outputs.build_targets }} + hybrid_app_patches_hash: + description: Patches hash for HybridApp + value: ${{ jobs.verify.outputs.hybrid_app_patches_hash }} + standalone_patches_hash: + description: Patches hash for Standalone + value: ${{ jobs.verify.outputs.standalone_patches_hash }} + + secrets: + OS_BOTIFY_TOKEN: + description: Token used to checkout private submodules + required: true + +jobs: + verify: + name: Verify React Native Patches + runs-on: ${{ github.repository_owner == 'Expensify' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} + outputs: + build_targets: ${{ steps.getArtifactBuildTargets.outputs.BUILD_TARGETS }} + hybrid_app_patches_hash: ${{ steps.getNewPatchesHash.outputs.HYBRID_APP_HASH }} + standalone_patches_hash: ${{ steps.getNewPatchesHash.outputs.STANDALONE_APP_HASH }} + steps: + - name: Checkout + # v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + submodules: true + ref: ${{ inputs.ref_before || 'main' }} + token: ${{ secrets.OS_BOTIFY_TOKEN }} + + - name: Get previous patches hash + if: ${{ inputs.ref_after != '' }} + id: getOldPatchesHash + run: | + HYBRID_HERMES_FLAG=$(./scripts/artifacts-utils/resolve-hermes-flag.sh "${{ inputs.platform }}" hybrid) + STANDALONE_HERMES_FLAG=$(./scripts/artifacts-utils/resolve-hermes-flag.sh "${{ inputs.platform }}" standalone) + echo "HYBRID_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh $HYBRID_HERMES_FLAG patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" + echo "STANDALONE_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh $STANDALONE_HERMES_FLAG patches)" >> "$GITHUB_OUTPUT" + + - name: Get previous react-native version + if: ${{ inputs.ref_after != '' }} + id: getOldVersion + run: echo "VERSION=$(jq -r '.dependencies["react-native"]' package.json)" >> "$GITHUB_OUTPUT" + + - name: Checkout new ref + if: ${{ inputs.ref_after != '' }} + run: | + git fetch origin ${{ inputs.ref_after }} --depth=1 + git checkout ${{ inputs.ref_after }} + git submodule update + + - name: Get new patches hash + id: getNewPatchesHash + run: | + HYBRID_HERMES_FLAG=$(./scripts/artifacts-utils/resolve-hermes-flag.sh "${{ inputs.platform }}" hybrid) + STANDALONE_HERMES_FLAG=$(./scripts/artifacts-utils/resolve-hermes-flag.sh "${{ inputs.platform }}" standalone) + echo "HYBRID_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh $HYBRID_HERMES_FLAG patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" + echo "STANDALONE_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh $STANDALONE_HERMES_FLAG patches)" >> "$GITHUB_OUTPUT" + + - name: Get new react-native version + if: ${{ inputs.ref_after != '' }} + id: getNewVersion + run: echo "VERSION=$(jq -r '.dependencies["react-native"]' package.json)" >> "$GITHUB_OUTPUT" + + - name: Check if version changed + if: ${{ inputs.ref_after != '' }} + id: didVersionChange + run: | + readonly DID_VERSION_CHANGE=${{ steps.getOldVersion.outputs.VERSION != steps.getNewVersion.outputs.VERSION && 'true' || 'false' }} + echo "DID_VERSION_CHANGE=$DID_VERSION_CHANGE" >> "$GITHUB_OUTPUT" + if [[ "$DID_VERSION_CHANGE" == 'true' ]]; then + echo "::notice::Detected react-native version bump (${{ steps.getOldVersion.outputs.VERSION }} -> ${{ steps.getNewVersion.outputs.VERSION }})" + fi + + - name: Check if patches changed + if: ${{ inputs.ref_after != '' }} + id: didPatchesChange + run: | + readonly DID_HYBRID_APP_PATCHES_CHANGE=${{ steps.getOldPatchesHash.outputs.HYBRID_APP_HASH != steps.getNewPatchesHash.outputs.HYBRID_APP_HASH && 'true' || 'false' }} + readonly DID_STANDALONE_APP_PATCHES_CHANGE=${{ steps.getOldPatchesHash.outputs.STANDALONE_APP_HASH != steps.getNewPatchesHash.outputs.STANDALONE_APP_HASH && 'true' || 'false' }} + echo "DID_HYBRID_APP_PATCHES_CHANGE=$DID_HYBRID_APP_PATCHES_CHANGE" >> "$GITHUB_OUTPUT" + echo "DID_STANDALONE_APP_PATCHES_CHANGE=$DID_STANDALONE_APP_PATCHES_CHANGE" >> "$GITHUB_OUTPUT" + + if [[ "$DID_HYBRID_APP_PATCHES_CHANGE" == 'true' ]]; then + echo "::notice::Detected changes in HybridApp patches (${{ steps.getOldPatchesHash.outputs.HYBRID_APP_HASH }} -> ${{ steps.getNewPatchesHash.outputs.HYBRID_APP_HASH }})" + fi + + if [[ "$DID_STANDALONE_APP_PATCHES_CHANGE" == 'true' ]]; then + echo "::notice::Detected changes in Standalone NewDot patches (${{ steps.getOldPatchesHash.outputs.STANDALONE_APP_HASH }} -> ${{ steps.getNewPatchesHash.outputs.STANDALONE_APP_HASH }})" + fi + + - name: Get artifact build targets + id: getArtifactBuildTargets + run: | + # workflow_dispatch path: caller leaves ref_after empty and passes explicit toggles. + if [[ '${{ inputs.ref_after }}' == '' ]]; then + BUILD_TARGETS=() + + if [[ '${{ inputs.build_standalone }}' == 'true' ]]; then + BUILD_TARGETS+=(false) + fi + + if [[ '${{ inputs.build_hybridapp }}' == 'true' ]]; then + BUILD_TARGETS+=(true) + fi + + if [[ ${#BUILD_TARGETS[@]} -ne 0 ]]; then + echo "BUILD_TARGETS=$(printf '%s\n' "${BUILD_TARGETS[@]}" | jq -R . | jq -c -s .)" >> "$GITHUB_OUTPUT" + fi + exit 0 + fi + + # push path: derive targets from what actually changed between ref_before and ref_after. + # Version change or standalone patch change forces a full rebuild of both hybrid and standalone. + if [[ '${{ steps.didVersionChange.outputs.DID_VERSION_CHANGE }}' == 'true' || '${{ steps.didPatchesChange.outputs.DID_STANDALONE_APP_PATCHES_CHANGE }}' == 'true' ]]; then + echo "BUILD_TARGETS=[\"true\", \"false\"]" >> "$GITHUB_OUTPUT" + elif [[ '${{ steps.didPatchesChange.outputs.DID_HYBRID_APP_PATCHES_CHANGE }}' == 'true' ]]; then + echo "BUILD_TARGETS=[\"true\"]" >> "$GITHUB_OUTPUT" + fi diff --git a/scripts/artifacts-utils/resolve-hermes-flag.sh b/scripts/artifacts-utils/resolve-hermes-flag.sh new file mode 100755 index 000000000000..2ed78a8c0beb --- /dev/null +++ b/scripts/artifacts-utils/resolve-hermes-flag.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +# Resolves the hermes flag argument ("--hermes-v1" or empty) for a given +# platform and build type, based on the relevant config file +# (gradle.properties for Android, Podfile for iOS). +# +# Used by CI workflows to forward the right flag to compute-patches-hash.sh. +# +# Usage: resolve-hermes-flag.sh + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)" +readonly SCRIPT_DIR +source "$SCRIPT_DIR/../shellUtils.sh" + +PLATFORM="${1:-}" +BUILD_TYPE="${2:-}" + +case "$PLATFORM:$BUILD_TYPE" in + android:standalone) + FILE="android/gradle.properties" + PATTERN='^[[:space:]]*hermesV1Enabled[[:space:]]*=[[:space:]]*true' + ;; + android:hybrid) + FILE="Mobile-Expensify/Android/gradle.properties" + PATTERN='^[[:space:]]*hermesV1Enabled[[:space:]]*=[[:space:]]*true' + ;; + ios:standalone) + FILE="ios/Podfile" + PATTERN="RCT_HERMES_V1_ENABLED.*=.*'1'" + ;; + ios:hybrid) + FILE="Mobile-Expensify/iOS/Podfile" + PATTERN="RCT_HERMES_V1_ENABLED.*=.*'1'" + ;; + *) + error "Usage: $0 " + exit 1 + ;; +esac + +if grep -q "$PATTERN" "$FILE" 2>/dev/null; then + echo "--hermes-v1" +fi From 0845c4a03877a5bf744b1f570552bc458ad87f0f Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Wed, 22 Apr 2026 14:11:27 +0200 Subject: [PATCH 03/18] Use blacksmith runners --- .github/workflows/publishReactNativeiOSArtifacts.yml | 6 +++--- .github/workflows/verifyReactNativePatches.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publishReactNativeiOSArtifacts.yml b/.github/workflows/publishReactNativeiOSArtifacts.yml index e19b15253733..546c3366458d 100644 --- a/.github/workflows/publishReactNativeiOSArtifacts.yml +++ b/.github/workflows/publishReactNativeiOSArtifacts.yml @@ -36,7 +36,7 @@ jobs: buildSlice: name: Build iOS Slice - runs-on: macos-15 + runs-on: blacksmith-12vcpu-macos-latest needs: verifyPatches if: needs.verifyPatches.outputs.build_targets != '' strategy: @@ -94,7 +94,7 @@ jobs: composeXCFramework: name: Compose XCFramework - runs-on: macos-15 + runs-on: blacksmith-12vcpu-macos-latest needs: [verifyPatches, buildSlice] strategy: fail-fast: false @@ -164,7 +164,7 @@ jobs: publishReactNativeiOSArtifacts: name: Publish React Native iOS Artifacts - runs-on: ${{ github.repository_owner == 'Expensify' && 'ubuntu-latest-xl' || 'ubuntu-latest' }} + runs-on: blacksmith-4vcpu-ubuntu-2404 needs: [verifyPatches, composeXCFramework] strategy: fail-fast: false diff --git a/.github/workflows/verifyReactNativePatches.yml b/.github/workflows/verifyReactNativePatches.yml index 9f39aeac89b5..13c7a6fd8f53 100644 --- a/.github/workflows/verifyReactNativePatches.yml +++ b/.github/workflows/verifyReactNativePatches.yml @@ -47,7 +47,7 @@ on: jobs: verify: name: Verify React Native Patches - runs-on: ${{ github.repository_owner == 'Expensify' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} + runs-on: blacksmith-4vcpu-ubuntu-2404 outputs: build_targets: ${{ steps.getArtifactBuildTargets.outputs.BUILD_TARGETS }} hybrid_app_patches_hash: ${{ steps.getNewPatchesHash.outputs.HYBRID_APP_HASH }} From bf4b546ab1fb490c276cd2bf6e1d76eccc4aa7f2 Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Wed, 22 Apr 2026 15:49:55 +0200 Subject: [PATCH 04/18] Cleanup workflow files --- .../publishReactNativeAndroidArtifacts.yml | 2 +- .../workflows/publishReactNativeiOSArtifacts.yml | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publishReactNativeAndroidArtifacts.yml b/.github/workflows/publishReactNativeAndroidArtifacts.yml index b68b6c79cd00..30bc6392d465 100644 --- a/.github/workflows/publishReactNativeAndroidArtifacts.yml +++ b/.github/workflows/publishReactNativeAndroidArtifacts.yml @@ -36,7 +36,7 @@ jobs: buildAndPublishReactNativeArtifacts: name: Build and Publish React Native Artifacts - runs-on: ${{ github.repository_owner == 'Expensify' && 'blacksmith-16vcpu-ubuntu-2404' || 'blacksmith-2vcpu-ubuntu-2404' }} + runs-on: blacksmith-16vcpu-ubuntu-2404 needs: verifyPatches if: needs.verifyPatches.outputs.build_targets != '' strategy: diff --git a/.github/workflows/publishReactNativeiOSArtifacts.yml b/.github/workflows/publishReactNativeiOSArtifacts.yml index 546c3366458d..dbbcff8b87d2 100644 --- a/.github/workflows/publishReactNativeiOSArtifacts.yml +++ b/.github/workflows/publishReactNativeiOSArtifacts.yml @@ -22,6 +22,9 @@ on: - patches/react-native/@react-native+*.patch - Mobile-Expensify +env: + DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer + jobs: verifyPatches: uses: ./.github/workflows/verifyReactNativePatches.yml @@ -48,8 +51,6 @@ jobs: concurrency: group: ${{ github.workflow }}-slice-${{ matrix.is_hybrid }}-${{ matrix.flavor }}-${{ matrix.platform }} cancel-in-progress: true - env: - DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer steps: - name: Checkout Code # v6 @@ -85,12 +86,14 @@ jobs: with: name: slice-${{ matrix.is_hybrid }}-${{ matrix.flavor }}-${{ matrix.platform }} path: node_modules/react-native/.build/output/spm/${{ matrix.flavor }}/Build/Products + retention-days: 1 - name: Upload headers uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f with: name: headers-${{ matrix.is_hybrid }}-${{ matrix.flavor }}-${{ matrix.platform }} path: node_modules/react-native/.build/headers + retention-days: 1 composeXCFramework: name: Compose XCFramework @@ -104,8 +107,6 @@ jobs: concurrency: group: ${{ github.workflow }}-compose-${{ matrix.is_hybrid }}-${{ matrix.flavor }} cancel-in-progress: true - env: - DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer steps: - name: Checkout Code # v6 @@ -155,12 +156,14 @@ jobs: with: name: reactnative-core-${{ matrix.flavor == 'Debug' && 'debug' || 'release' }}-${{ matrix.is_hybrid }} path: node_modules/react-native/.build/output/xcframeworks/${{ matrix.flavor }}/reactnative-core-${{ matrix.flavor == 'Debug' && 'debug' || 'release' }}.tar.gz + retention-days: 1 - name: Upload dSYM artifact uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f with: name: reactnative-core-dSYM-${{ matrix.flavor == 'Debug' && 'debug' || 'release' }}-${{ matrix.is_hybrid }} path: node_modules/react-native/.build/output/xcframeworks/${{ matrix.flavor }}/reactnative-core-dSYM-${{ matrix.flavor == 'Debug' && 'debug' || 'release' }}.tar.gz + retention-days: 1 publishReactNativeiOSArtifacts: name: Publish React Native iOS Artifacts @@ -171,6 +174,9 @@ jobs: matrix: is_hybrid: ${{ fromJSON(needs.verifyPatches.outputs.build_targets) }} concurrency: + # Publish runs sequentially per is_hybrid. Cancelling an in-flight publish + # can leave Maven metadata out of sync with the uploaded artifact, so we + # let the current run finish and queue the next one instead. group: ${{ github.workflow }}-publish-${{ matrix.is_hybrid }} cancel-in-progress: false steps: From 808d17cf7ccba9204bf12bfbe63ad171f013e1f1 Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Wed, 22 Apr 2026 15:58:16 +0200 Subject: [PATCH 05/18] Refactor resolve-hermes-flag and move script to .github --- .github/scripts/resolve-hermes-flag.sh | 38 ++++++++++++++++ .../workflows/verifyReactNativePatches.yml | 8 ++-- .../artifacts-utils/resolve-hermes-flag.sh | 43 ------------------- 3 files changed, 42 insertions(+), 47 deletions(-) create mode 100755 .github/scripts/resolve-hermes-flag.sh delete mode 100755 scripts/artifacts-utils/resolve-hermes-flag.sh diff --git a/.github/scripts/resolve-hermes-flag.sh b/.github/scripts/resolve-hermes-flag.sh new file mode 100755 index 000000000000..63bf38c182fb --- /dev/null +++ b/.github/scripts/resolve-hermes-flag.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# Resolves the hermes flag argument ("--hermes-v1" or empty) for a given +# platform and build type, based on the relevant config file +# (gradle.properties for Android, Podfile for iOS). +# +# Used by CI workflows to forward the right flag to compute-patches-hash.sh. +# +# Usage: resolve-hermes-flag.sh + +set -euo pipefail + +source scripts/shellUtils.sh + +PLATFORM="${1:-}" +BUILD_TYPE="${2:-}" + +case "$PLATFORM:$BUILD_TYPE" in + android:standalone) FILE="android/gradle.properties" ;; + android:hybrid) FILE="Mobile-Expensify/Android/gradle.properties" ;; + ios:standalone) FILE="ios/Podfile" ;; + ios:hybrid) FILE="Mobile-Expensify/iOS/Podfile" ;; + *) error "Usage: $0 "; exit 1 ;; +esac + +if [[ ! -f "$FILE" ]]; then + error "Config file not found: $FILE" + exit 1 +fi + +case "$PLATFORM" in + android) PATTERN='^[[:space:]]*hermesV1Enabled[[:space:]]*=[[:space:]]*true' ;; + ios) PATTERN="^[[:space:]]*[^#[:space:]].*RCT_HERMES_V1_ENABLED[[:space:]]*=[[:space:]]*'1'" ;; +esac + +if grep -Eq "$PATTERN" "$FILE"; then + echo "--hermes-v1" +fi diff --git a/.github/workflows/verifyReactNativePatches.yml b/.github/workflows/verifyReactNativePatches.yml index 13c7a6fd8f53..783ee5d6488e 100644 --- a/.github/workflows/verifyReactNativePatches.yml +++ b/.github/workflows/verifyReactNativePatches.yml @@ -65,8 +65,8 @@ jobs: if: ${{ inputs.ref_after != '' }} id: getOldPatchesHash run: | - HYBRID_HERMES_FLAG=$(./scripts/artifacts-utils/resolve-hermes-flag.sh "${{ inputs.platform }}" hybrid) - STANDALONE_HERMES_FLAG=$(./scripts/artifacts-utils/resolve-hermes-flag.sh "${{ inputs.platform }}" standalone) + HYBRID_HERMES_FLAG=$(./.github/scripts/resolve-hermes-flag.sh "${{ inputs.platform }}" hybrid) + STANDALONE_HERMES_FLAG=$(./.github/scripts/resolve-hermes-flag.sh "${{ inputs.platform }}" standalone) echo "HYBRID_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh $HYBRID_HERMES_FLAG patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" echo "STANDALONE_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh $STANDALONE_HERMES_FLAG patches)" >> "$GITHUB_OUTPUT" @@ -85,8 +85,8 @@ jobs: - name: Get new patches hash id: getNewPatchesHash run: | - HYBRID_HERMES_FLAG=$(./scripts/artifacts-utils/resolve-hermes-flag.sh "${{ inputs.platform }}" hybrid) - STANDALONE_HERMES_FLAG=$(./scripts/artifacts-utils/resolve-hermes-flag.sh "${{ inputs.platform }}" standalone) + HYBRID_HERMES_FLAG=$(./.github/scripts/resolve-hermes-flag.sh "${{ inputs.platform }}" hybrid) + STANDALONE_HERMES_FLAG=$(./.github/scripts/resolve-hermes-flag.sh "${{ inputs.platform }}" standalone) echo "HYBRID_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh $HYBRID_HERMES_FLAG patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" echo "STANDALONE_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh $STANDALONE_HERMES_FLAG patches)" >> "$GITHUB_OUTPUT" diff --git a/scripts/artifacts-utils/resolve-hermes-flag.sh b/scripts/artifacts-utils/resolve-hermes-flag.sh deleted file mode 100755 index 2ed78a8c0beb..000000000000 --- a/scripts/artifacts-utils/resolve-hermes-flag.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash - -# Resolves the hermes flag argument ("--hermes-v1" or empty) for a given -# platform and build type, based on the relevant config file -# (gradle.properties for Android, Podfile for iOS). -# -# Used by CI workflows to forward the right flag to compute-patches-hash.sh. -# -# Usage: resolve-hermes-flag.sh - -SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)" -readonly SCRIPT_DIR -source "$SCRIPT_DIR/../shellUtils.sh" - -PLATFORM="${1:-}" -BUILD_TYPE="${2:-}" - -case "$PLATFORM:$BUILD_TYPE" in - android:standalone) - FILE="android/gradle.properties" - PATTERN='^[[:space:]]*hermesV1Enabled[[:space:]]*=[[:space:]]*true' - ;; - android:hybrid) - FILE="Mobile-Expensify/Android/gradle.properties" - PATTERN='^[[:space:]]*hermesV1Enabled[[:space:]]*=[[:space:]]*true' - ;; - ios:standalone) - FILE="ios/Podfile" - PATTERN="RCT_HERMES_V1_ENABLED.*=.*'1'" - ;; - ios:hybrid) - FILE="Mobile-Expensify/iOS/Podfile" - PATTERN="RCT_HERMES_V1_ENABLED.*=.*'1'" - ;; - *) - error "Usage: $0 " - exit 1 - ;; -esac - -if grep -q "$PATTERN" "$FILE" 2>/dev/null; then - echo "--hermes-v1" -fi From 4dad8812583d304148ece6e6176b7efb9591e5a6 Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Wed, 22 Apr 2026 16:12:10 +0200 Subject: [PATCH 06/18] Update comment in resolve-hermes-flag --- .github/scripts/resolve-hermes-flag.sh | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/scripts/resolve-hermes-flag.sh b/.github/scripts/resolve-hermes-flag.sh index 63bf38c182fb..4d9d0a159756 100755 --- a/.github/scripts/resolve-hermes-flag.sh +++ b/.github/scripts/resolve-hermes-flag.sh @@ -1,12 +1,9 @@ #!/bin/bash # Resolves the hermes flag argument ("--hermes-v1" or empty) for a given -# platform and build type, based on the relevant config file -# (gradle.properties for Android, Podfile for iOS). -# -# Used by CI workflows to forward the right flag to compute-patches-hash.sh. -# -# Usage: resolve-hermes-flag.sh +# platform and build type by inspecting the relevant config file +# (gradle.properties for Android, Podfile for iOS). Used by CI workflows +# to forward the right flag to compute-patches-hash.sh. set -euo pipefail From c6e45a3358034ea42af3435d5af106f01baa06d8 Mon Sep 17 00:00:00 2001 From: mateuuszzzzz Date: Tue, 14 Jul 2026 13:14:49 +0200 Subject: [PATCH 07/18] fix types and lint --- .../artifacts-utils/lib/artifactsResolver.ts | 26 +++++++++----- scripts/artifacts-utils/resolve-artifacts.ts | 13 +++++-- tests/unit/ArtifactsResolverTest.ts | 35 ++++++++++--------- 3 files changed, 47 insertions(+), 27 deletions(-) diff --git a/scripts/artifacts-utils/lib/artifactsResolver.ts b/scripts/artifacts-utils/lib/artifactsResolver.ts index b591397e1ce1..ab913f9ebc09 100644 --- a/scripts/artifacts-utils/lib/artifactsResolver.ts +++ b/scripts/artifacts-utils/lib/artifactsResolver.ts @@ -82,18 +82,20 @@ function getCredentials(): Credentials | null { return null; } if (isCI()) { - if (!process.env.GITHUB_ACTOR || !process.env.GITHUB_TOKEN) { - logError('Missing required CI environment variables GITHUB_ACTOR and/or GITHUB_TOKEN.'); - return null; + // typeof narrows the (loosely-typed) process.env members to string without assigning an any value. + if (typeof process.env.GITHUB_ACTOR === 'string' && typeof process.env.GITHUB_TOKEN === 'string') { + return {githubUsername: process.env.GITHUB_ACTOR, githubToken: process.env.GITHUB_TOKEN}; } - return {githubUsername: process.env.GITHUB_ACTOR, githubToken: process.env.GITHUB_TOKEN}; + logError('Missing required CI environment variables GITHUB_ACTOR and/or GITHUB_TOKEN.'); + return null; } if (!hasRequiredScopes()) { logError('GitHub token does not have required scope read:packages.'); return null; } + // `--jq .login` returns the login string directly, so there is no untyped JSON to assert on. return { - githubUsername: (JSON.parse(runGh(['api', 'user'])) as {login: string}).login, + githubUsername: runGh(['api', 'user', '--jq', '.login']), githubToken: runGh(['auth', 'token']), }; } catch { @@ -147,8 +149,17 @@ function fetchTokenSafe(url: string, githubToken: string | null, maxRedirects = } function getReactNativeVersion(newDotRoot: string): string { - const pkg = JSON.parse(fs.readFileSync(path.join(newDotRoot, 'package.json'), 'utf8')) as {dependencies: Record}; - return pkg.dependencies['react-native']; + const parsed: unknown = JSON.parse(fs.readFileSync(path.join(newDotRoot, 'package.json'), 'utf8')); + if (typeof parsed === 'object' && parsed !== null && 'dependencies' in parsed) { + const deps: unknown = parsed.dependencies; + if (typeof deps === 'object' && deps !== null && 'react-native' in deps) { + const version: unknown = deps['react-native']; + if (typeof version === 'string') { + return version; + } + } + } + throw new Error('Could not read react-native version from package.json'); } function computePatchesHash(newDotRoot: string, isHybrid: boolean): string { @@ -183,7 +194,6 @@ async function findMatchingArtifactsVersion(options: ResolveOptions, artifactId: const rnVersion = getReactNativeVersion(newDotRoot); const candidates = getArtifactsCandidates(packageName, artifactId, rnVersion); for (const candidate of candidates) { - // eslint-disable-next-line no-await-in-loop const remoteHash = await getRemotePatchesHash(packageName, artifactId, candidate, githubToken); if (remoteHash != null && remoteHash === localPatchesHash) { return candidate; diff --git a/scripts/artifacts-utils/resolve-artifacts.ts b/scripts/artifacts-utils/resolve-artifacts.ts index 3a5543ee90f8..2734bb495572 100644 --- a/scripts/artifacts-utils/resolve-artifacts.ts +++ b/scripts/artifacts-utils/resolve-artifacts.ts @@ -1,5 +1,5 @@ import parseCommandLineArguments from '../utils/parseCommandLineArguments'; -import resolveArtifacts, {type Platform} from './lib/artifactsResolver'; +import resolveArtifacts from './lib/artifactsResolver'; /** * Thin CLI entry point around the shared artifacts resolver, invoked by the @@ -11,12 +11,19 @@ import resolveArtifacts, {type Platform} from './lib/artifactsResolver'; * --platform=ios --package=react-hybrid --hybrid=true --new-dot-root=. * * Prints the resolution result as JSON to stdout (logs go to stderr). It always - * exits 0 with a valid JSON payload — a failure resolves to `buildFromSource`. + * exits 0 with a valid JSON payload — any failure resolves to `buildFromSource`. */ const args = parseCommandLineArguments(); -const platform = args.platform as Platform; +const platform = args.platform; const packageName = args.package ?? ''; +// Validate rather than assert: an invalid/missing platform falls back to source. +if (platform !== 'ios' && platform !== 'android') { + process.stderr.write(`[PatchedArtifacts] Invalid or missing --platform "${platform ?? ''}" (expected "ios" or "android"); building from source.\n`); + process.stdout.write(JSON.stringify({buildFromSource: true, version: null, packageName, artifactId: ''})); + process.exit(0); +} + resolveArtifacts({ platform, packageName, diff --git a/tests/unit/ArtifactsResolverTest.ts b/tests/unit/ArtifactsResolverTest.ts index 9b1c41b0787d..19ef993eb4ac 100644 --- a/tests/unit/ArtifactsResolverTest.ts +++ b/tests/unit/ArtifactsResolverTest.ts @@ -1,5 +1,7 @@ import resolveArtifacts, {ARTIFACT_IDS, fetchTokenSafe} from '@scripts/artifacts-utils/lib/artifactsResolver'; +import type {ClientRequest, IncomingMessage} from 'http'; + /** * @jest-environment node */ @@ -11,19 +13,15 @@ import https from 'https'; jest.mock('child_process'); jest.mock('https'); -const mockExecFileSync = execFileSync as jest.MockedFunction; -const mockGet = https.get as jest.MockedFunction; +const mockExecFileSync = jest.mocked(execFileSync); +const mockGet = jest.mocked(https.get); const NEW_DOT_ROOT = '/repo'; const LOCAL_HASH = 'abc123hash'; -/** Build a fake IncomingMessage that emits the given body then ends. */ +/** Builds a fake IncomingMessage that emits the given body then ends. */ function fakeResponse(statusCode: number, headers: Record, body = '') { - const res = new EventEmitter() as EventEmitter & {statusCode: number; headers: Record; resume: () => void; setEncoding: () => void}; - res.statusCode = statusCode; - res.headers = headers; - res.resume = () => undefined; - res.setEncoding = () => undefined; + const res = Object.assign(new EventEmitter(), {statusCode, headers, resume: () => undefined, setEncoding: () => undefined}); process.nextTick(() => { if (body) { res.emit('data', body); @@ -40,14 +38,19 @@ function fakeResponse(statusCode: number, headers: Record, body function mockHttpsSequence(responses: Array<{statusCode: number; headers?: Record; body?: string}>) { const seenAuthHeaders: Array = []; let call = 0; - mockGet.mockImplementation((_url: string | URL, options: https.RequestOptions, callback?: (res: unknown) => void) => { - const headers = (options?.headers ?? {}) as Record; - seenAuthHeaders.push(headers.Authorization); + const impl = (_url: string | URL, options: {headers?: Record}, callback?: (res: IncomingMessage) => void): ClientRequest => { + const auth = options.headers?.Authorization; + seenAuthHeaders.push(typeof auth === 'string' ? auth : undefined); const spec = responses.at(call++) ?? {statusCode: 500}; - callback?.(fakeResponse(spec.statusCode, spec.headers ?? {}, spec.body)); - const req = new EventEmitter(); - return req as unknown as ReturnType; - }); + // Faking Node's IncomingMessage / ClientRequest requires narrowing assertions in a unit test. + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + callback?.(fakeResponse(spec.statusCode, spec.headers ?? {}, spec.body) as unknown as IncomingMessage); + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + return new EventEmitter() as unknown as ClientRequest; + }; + // https.get is overloaded; cast the single fake impl to its type. + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + mockGet.mockImplementation(impl as typeof https.get); return seenAuthHeaders; } @@ -71,7 +74,7 @@ function mockResolveExec(candidates: string) { } return ''; }); - jest.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify({dependencies: {'react-native': '0.85.3'}})); + jest.spyOn(fs, 'readFileSync').mockReturnValue('{"dependencies":{"react-native":"0.85.3"}}'); } describe('artifactsResolver', () => { From 0ec0c25f44f13d63a0818742bb094ed41b1f8e50 Mon Sep 17 00:00:00 2001 From: mateuuszzzzz Date: Tue, 14 Jul 2026 13:22:40 +0200 Subject: [PATCH 08/18] Remove redundant gradle utils --- .../android/ExpensiUtils.gradle | 43 ++----------------- .../android/PatchedArtifactsSettings.gradle | 27 ++++++------ 2 files changed, 15 insertions(+), 55 deletions(-) diff --git a/scripts/artifacts-utils/android/ExpensiUtils.gradle b/scripts/artifacts-utils/android/ExpensiUtils.gradle index f53c488324bb..a63e4990958d 100644 --- a/scripts/artifacts-utils/android/ExpensiUtils.gradle +++ b/scripts/artifacts-utils/android/ExpensiUtils.gradle @@ -20,30 +20,21 @@ ext.ExpensiLog = new Object() { } ext.ExpensiUtils = new Object() { - String GITHUB_REPO = 'Expensify/App' - Map runCommand(List command, long timeoutSeconds = 10, File workingDir = null) { def process = workingDir ? command.execute(null, workingDir) : command.execute() process.waitFor(timeoutSeconds, TimeUnit.SECONDS) return [output: process.in.text?.trim(), exitCode: process.exitValue()] } - boolean hasGithubCLI() { - return runCommand(["which", "gh"]).exitCode == 0 - } - boolean isCIEnvironment() { return System.getenv("CI") != null } - boolean hasRequiredScopes() { - def scopes = runCommand(["gh", "auth", "status"]).output - return ['read:packages', 'write:packages'].any { scope -> scopes.contains(scope) } - } - + // Credential read for the authenticated Maven download. Validation lives in the shared + // TS resolver, so these are only called once it has resolved a version. String getGithubUsername() { if (isCIEnvironment()) return System.getenv("GITHUB_ACTOR") - return new JsonSlurper().parseText(runCommand(["gh", "api", "user"]).output).login + return runCommand(["gh", "api", "user", "--jq", ".login"]).output } String getGithubToken() { @@ -51,34 +42,6 @@ ext.ExpensiUtils = new Object() { return runCommand(["gh", "auth", "token"]).output } - // Credentials are resolved here (not in the shared resolver) because the - // authenticated Maven download in android/build.gradle needs them Gradle-side. - Map getCredentials() { - try { - if (!hasGithubCLI()) { - warnNotConfigured("No Github CLI found.") - return null - } - if (isCIEnvironment() && !(System.getenv("GITHUB_ACTOR") && System.getenv("GITHUB_TOKEN"))) { - warnNotConfigured("Missing required CI environment variables GITHUB_ACTOR and/or GITHUB_TOKEN.") - return null - } - if (!isCIEnvironment() && !hasRequiredScopes()) { - warnNotConfigured("Github token does not have required scope read:packages.") - return null - } - return [githubUsername: getGithubUsername(), githubToken: getGithubToken()] - } catch (Exception e) { - warnNotConfigured("Failed to get Github credentials. This might be due to an expired token or not being logged in.") - return null - } - } - - private void warnNotConfigured(String reason) { - ExpensiLog.warn(reason) - ExpensiLog.warn("For setup instructions, refer to: https://github.com/${GITHUB_REPO}/blob/main/contributingGuides/SETUP_ANDROID.md#enabling-prebuilt-react-native-artifacts") - } - // Resolve the prebuilt artifact version matching the local patches via the shared // TS resolver (credentials + patches-hash match live there). Returns a map: // [buildFromSource: Boolean, version: String|null]. Any failure -> build from source. diff --git a/scripts/artifacts-utils/android/PatchedArtifactsSettings.gradle b/scripts/artifacts-utils/android/PatchedArtifactsSettings.gradle index 58cd23c85f3c..3878eb5c2fda 100644 --- a/scripts/artifacts-utils/android/PatchedArtifactsSettings.gradle +++ b/scripts/artifacts-utils/android/PatchedArtifactsSettings.gradle @@ -22,25 +22,22 @@ settings.extensions.configure(PatchedArtifactsConfig) { config -> return } - def credentials = ExpensiUtils.getCredentials() config.packageName = getProperty('patchedArtifacts.packageName') - if (credentials != null) { - config.githubUsername = credentials.githubUsername - config.githubToken = credentials.githubToken - config.version = ExpensiUtils.findMatchingArtifactsVersion( - config.packageName, - config.githubToken, - getNewDotRootDir(), - hasProperty('newDotRoot') - ) - } + // The shared TS resolver owns credential validation + patches-hash matching. + def resolution = ExpensiUtils.resolveArtifacts(config.packageName, getNewDotRootDir(), hasProperty('newDotRoot')) + config.version = resolution.version + config.buildFromSource = resolution.buildFromSource - if (config.version == null || config.packageName == null || config.githubUsername == null || config.githubToken == null) { + if (config.buildFromSource || config.version == null) { config.buildFromSource = true ExpensiLog.error("No matching artifacts version found for ${config.packageName}. Building react-native from source.") - } else { - config.buildFromSource = false - ExpensiLog.lifecycle("Using patched react-native artifacts: ${config.packageName}:${config.version}") + return } + + // Credentials for the authenticated Maven download. The resolver already validated + // them (it returned a version), so we only need to read them here. + config.githubUsername = ExpensiUtils.getGithubUsername() + config.githubToken = ExpensiUtils.getGithubToken() + ExpensiLog.lifecycle("Using patched react-native artifacts: ${config.packageName}:${config.version}") } From c6c760613c20fdc4744187292157eaadf9ac2009 Mon Sep 17 00:00:00 2001 From: mateuuszzzzz Date: Tue, 14 Jul 2026 16:21:19 +0200 Subject: [PATCH 09/18] Use octokit and adjust old paths --- .../publishReactNativeAndroidArtifacts.yml | 4 +- .../publishReactNativeiOSArtifacts.yml | 4 +- .../android/ExpensiUtils.gradle | 22 +- .../android/PatchedArtifactsSettings.gradle | 8 +- .../ios/patched_ios_artifacts.rb | 43 +-- .../artifacts-utils/lib/artifactsResolver.ts | 265 ++++++++++-------- scripts/artifacts-utils/resolve-artifacts.ts | 20 +- tests/unit/ArtifactsResolverTest.ts | 139 ++++----- 8 files changed, 224 insertions(+), 281 deletions(-) diff --git a/.github/workflows/publishReactNativeAndroidArtifacts.yml b/.github/workflows/publishReactNativeAndroidArtifacts.yml index 5a75a189856c..95250a652b5a 100644 --- a/.github/workflows/publishReactNativeAndroidArtifacts.yml +++ b/.github/workflows/publishReactNativeAndroidArtifacts.yml @@ -100,9 +100,9 @@ jobs: id: computePatchesHash run: | if [[ '${{ matrix.is_hybrid }}' == 'true' ]]; then - echo "PATCHES_HASH=$(./scripts/compute-patches-hash.sh patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" + echo "PATCHES_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" else - echo "PATCHES_HASH=$(./scripts/compute-patches-hash.sh patches)" >> "$GITHUB_OUTPUT" + echo "PATCHES_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh patches)" >> "$GITHUB_OUTPUT" fi - name: Setup Node diff --git a/.github/workflows/publishReactNativeiOSArtifacts.yml b/.github/workflows/publishReactNativeiOSArtifacts.yml index 010d5c560aeb..ed331af1ebd2 100644 --- a/.github/workflows/publishReactNativeiOSArtifacts.yml +++ b/.github/workflows/publishReactNativeiOSArtifacts.yml @@ -250,9 +250,9 @@ jobs: id: computePatchesHash run: | if [[ '${{ matrix.is_hybrid }}' == 'true' ]]; then - echo "PATCHES_HASH=$(./scripts/compute-patches-hash.sh patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" + echo "PATCHES_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" else - echo "PATCHES_HASH=$(./scripts/compute-patches-hash.sh patches)" >> "$GITHUB_OUTPUT" + echo "PATCHES_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh patches)" >> "$GITHUB_OUTPUT" fi - name: Determine new patched RN version diff --git a/scripts/artifacts-utils/android/ExpensiUtils.gradle b/scripts/artifacts-utils/android/ExpensiUtils.gradle index a63e4990958d..50ad887cf8ca 100644 --- a/scripts/artifacts-utils/android/ExpensiUtils.gradle +++ b/scripts/artifacts-utils/android/ExpensiUtils.gradle @@ -26,33 +26,13 @@ ext.ExpensiUtils = new Object() { return [output: process.in.text?.trim(), exitCode: process.exitValue()] } - boolean isCIEnvironment() { - return System.getenv("CI") != null - } - - // Credential read for the authenticated Maven download. Validation lives in the shared - // TS resolver, so these are only called once it has resolved a version. - String getGithubUsername() { - if (isCIEnvironment()) return System.getenv("GITHUB_ACTOR") - return runCommand(["gh", "api", "user", "--jq", ".login"]).output - } - - String getGithubToken() { - if (isCIEnvironment()) return System.getenv("GITHUB_TOKEN") - return runCommand(["gh", "auth", "token"]).output - } - - // Resolve the prebuilt artifact version matching the local patches via the shared - // TS resolver (credentials + patches-hash match live there). Returns a map: - // [buildFromSource: Boolean, version: String|null]. Any failure -> build from source. + // Returns [buildFromSource, version, githubUsername, githubToken]; any failure -> build from source. Map resolveArtifacts(String packageName, String newDotRootDir, boolean isHybrid) { def cmd = [ "npx", "tsx", "${newDotRootDir}/scripts/artifacts-utils/resolve-artifacts.ts", "--platform=android", "--package=${packageName}", "--hybrid=${isHybrid}", "--new-dot-root=${newDotRootDir}" ] try { - // Runs from the repo root so `npx tsx` resolves node_modules. Generous - // timeout: npx/tsx startup + network POM lookups. def result = runCommand(cmd, 120, new File(newDotRootDir)) if (result.exitCode != 0) { ExpensiLog.error("Artifacts resolver failed (exit ${result.exitCode}). Building react-native from source.") diff --git a/scripts/artifacts-utils/android/PatchedArtifactsSettings.gradle b/scripts/artifacts-utils/android/PatchedArtifactsSettings.gradle index 3878eb5c2fda..7c33fe57b95d 100644 --- a/scripts/artifacts-utils/android/PatchedArtifactsSettings.gradle +++ b/scripts/artifacts-utils/android/PatchedArtifactsSettings.gradle @@ -24,7 +24,6 @@ settings.extensions.configure(PatchedArtifactsConfig) { config -> config.packageName = getProperty('patchedArtifacts.packageName') - // The shared TS resolver owns credential validation + patches-hash matching. def resolution = ExpensiUtils.resolveArtifacts(config.packageName, getNewDotRootDir(), hasProperty('newDotRoot')) config.version = resolution.version config.buildFromSource = resolution.buildFromSource @@ -35,9 +34,8 @@ settings.extensions.configure(PatchedArtifactsConfig) { config -> return } - // Credentials for the authenticated Maven download. The resolver already validated - // them (it returned a version), so we only need to read them here. - config.githubUsername = ExpensiUtils.getGithubUsername() - config.githubToken = ExpensiUtils.getGithubToken() + // The resolver returns the credentials the Maven download needs alongside the version. + config.githubUsername = resolution.githubUsername + config.githubToken = resolution.githubToken ExpensiLog.lifecycle("Using patched react-native artifacts: ${config.packageName}:${config.version}") } diff --git a/scripts/artifacts-utils/ios/patched_ios_artifacts.rb b/scripts/artifacts-utils/ios/patched_ios_artifacts.rb index ab7775635fc1..e796a88e1a2a 100644 --- a/scripts/artifacts-utils/ios/patched_ios_artifacts.rb +++ b/scripts/artifacts-utils/ios/patched_ios_artifacts.rb @@ -1,19 +1,7 @@ # Consumer for Expensify's patched React Native iOS prebuilt artifacts. -# -# React Native already ships a full mechanism for consuming prebuilt RNCore -# artifacts (`ReactNativeCoreUtils` in -# node_modules/react-native/scripts/cocoapods/rncore.rb): version resolution -> -# podspec source -> download -> VFS overlay / xcconfig / dSYM wiring. -# -# We don't reimplement any of that. Ruby classes are open, so this file reopens -# `ReactNativeCoreUtils` and rewrites only the few seams that decide *which* -# artifact to fetch and *how* to authenticate — pointing them at OUR private -# GitHub Packages Maven repo (matched by patches hash). Everything else (VFS -# overlay, xcconfig, pod dependency, dSYM handling) is reused unchanged. -# -# IMPORTANT: this file MUST be required AFTER `react_native_pods.rb` (which -# defines ReactNativeCoreUtils), so our method definitions override the -# originals. The Podfile requires them in that order. +# Reopens ReactNativeCoreUtils (rncore.rb) to point RNCore resolution and +# download at our private GitHub Packages Maven repo (matched by patches hash). +# Must be required after react_native_pods.rb, which defines ReactNativeCoreUtils. require 'json' @@ -26,21 +14,14 @@ def self.setup is_hybrid = ENV['IS_HYBRID_APP'] == 'true' package_name = is_hybrid ? 'react-hybrid' : 'react-standalone' - # Custom RN core prebuilt is ON by default. Honor an explicit opt-out - # (RCT_USE_PREBUILT_RNCORE=0 -> build from source, skip resolution). - # Otherwise resolve a version matching the local patches; no match -> source. forced_source = ENV['RCT_USE_PREBUILT_RNCORE'] == '0' resolution = forced_source ? {'buildFromSource' => true, 'version' => nil} : resolve(package_name, is_hybrid) - # Always stash our state as class variables — the reopened - # ReactNativeCoreUtils methods below read them, including on the source path. ReactNativeCoreUtils.class_variable_set(:@@patched_version, resolution['version']) ReactNativeCoreUtils.class_variable_set(:@@patched_package_name, package_name) - ReactNativeCoreUtils.class_variable_set(:@@patched_github_token, github_token) + ReactNativeCoreUtils.class_variable_set(:@@patched_github_token, resolution['githubToken']) ReactNativeCoreUtils.class_variable_set(:@@patched_build_from_source, resolution['buildFromSource']) - # Keep RN's podspec gate consistent with our decision (replaces the old - # hardcoded `ENV['RCT_USE_PREBUILT_RNCORE'] = '1'`). ENV['RCT_USE_PREBUILT_RNCORE'] = resolution['buildFromSource'] ? '0' : '1' end @@ -57,35 +38,20 @@ def self.resolve(package_name, is_hybrid) Pod::UI.warn("[PatchedIOSArtifacts] Resolver failed (#{e.message}); building from source.") if defined?(Pod::UI) {'buildFromSource' => true, 'version' => nil} end - - def self.github_token - token = ENV['GITHUB_TOKEN'] - return token if token && !token.empty? - `gh auth token`.strip - rescue - nil - end end -# --- Rewrite the ReactNativeCoreUtils seams to consume OUR artifacts. --- class ReactNativeCoreUtils - # Force our resolved decision; skip Meta's Maven-Central existence probe. def self.setup_rncore(react_native_path, _react_native_version) @@react_native_path = react_native_path @@build_from_source = @@patched_build_from_source @@download_dsyms = ENV['RCT_SYMBOLICATE_PREBUILT_FRAMEWORKS'] == '1' end - # Point the tarball URL at our private GitHub Packages Maven. The file-name - # layout matches Meta's, so only base URL / group / version change. def self.stable_tarball_url(_version, build_type, dsyms = false) classifier = "reactnative-core-#{dsyms ? 'dSYM-' : ''}#{build_type}" "#{PatchedIOSArtifacts::GITHUB_PACKAGES_BASE}/com/expensify/#{@@patched_package_name}/react-native-artifacts/#{@@patched_version}/react-native-artifacts-#{@@patched_version}-#{classifier}.tar.gz" end - # Authenticated, redirect-safe download: curl (>=7.58) drops the custom - # Authorization header on the cross-host 302 to the object store, so the token - # never leaks. Never pass --location-trusted. def self.download_rncore_tarball(_react_native_path, tarball_url, version, configuration, dsyms = false) dir = artifacts_dir destination = configuration.nil? ? @@ -94,6 +60,7 @@ def self.download_rncore_tarball(_react_native_path, tarball_url, version, confi unless File.exist?(destination) tmp = "#{dir}/reactnative-core.download" + # curl drops the Authorization header on the cross-host redirect to the object store. header = @@patched_github_token ? %(-H "Authorization: Bearer #{@@patched_github_token}") : '' ok = system(%(mkdir -p "#{dir}" && curl --fail --location --proto '=https' #{header} "#{tarball_url}" -o "#{tmp}" && mv "#{tmp}" "#{destination}")) raise "[PatchedIOSArtifacts] Failed to download #{tarball_url}" unless ok diff --git a/scripts/artifacts-utils/lib/artifactsResolver.ts b/scripts/artifacts-utils/lib/artifactsResolver.ts index ab913f9ebc09..623fceac39a5 100644 --- a/scripts/artifacts-utils/lib/artifactsResolver.ts +++ b/scripts/artifacts-utils/lib/artifactsResolver.ts @@ -1,29 +1,23 @@ +import {getOctokit} from '@actions/github'; import {execFileSync} from 'child_process'; import fs from 'fs'; -import https from 'https'; import path from 'path'; /** * Shared resolver for Expensify's patched React Native prebuilt artifacts. - * - * This is the platform-agnostic "brain" ported from the Android-only - * `ExpensiUtils.gradle`. It ONLY resolves which artifact version to use - * (by matching the local patches hash against the `patchesHash` recorded in - * each candidate's Maven POM). It never downloads the artifact itself — that - * stays native to each build system (Gradle Maven for Android, curl for iOS). - * - * All runtime-specific inputs (the Hermes V1 flag, isHybrid, package name) are - * passed IN by the caller — the caller's build system already knows them, so we - * never parse `gradle.properties`/`Podfile` here. - * - * Consumed by: - * - Android: `scripts/artifacts-utils/android/ExpensiUtils.gradle` (via exec) - * - iOS: `scripts/artifacts-utils/ios/patched_ios_artifacts.rb` (via exec) + * Resolves which artifact version to use by matching the local patches hash + * against the `patchesHash` recorded in each candidate's Maven POM. Consumed via + * `resolve-artifacts.ts` by Gradle (Android) and `patched_ios_artifacts.rb` (iOS). */ type Platform = 'ios' | 'android'; -type Credentials = {githubUsername: string; githubToken: string}; +/** Base credentials — a token authenticates every download. */ +type Credentials = {githubToken: string}; +/** iOS's curl Bearer download needs only the token. */ +type IosCredentials = Credentials; +/** Android's Gradle Maven `credentials {}` block additionally requires the username. */ +type AndroidCredentials = Credentials & {githubUsername: string}; type ResolveOptions = { platform: Platform; @@ -32,35 +26,62 @@ type ResolveOptions = { isHybrid: boolean; }; -type ResolveResult = { - buildFromSource: boolean; - version: string | null; +/** No prebuilt match (or no credentials): the caller builds react-native from source. Carries no secrets. */ +type SourceBuild = { + buildFromSource: true; + version: null; packageName: string; artifactId: string; }; +/** A matching prebuilt artifact was found; carries the credentials the native download needs. */ +type Prebuilt = { + buildFromSource: false; + version: string; + packageName: string; + artifactId: string; +} & Creds; + +type IosResult = SourceBuild | Prebuilt; +type AndroidResult = SourceBuild | Prebuilt; +type ResolveResult = IosResult | AndroidResult; + const GITHUB_REPO = 'Expensify/App'; const GITHUB_OWNER = 'Expensify'; -/** The only Maven coordinate that differs per platform. */ const ARTIFACT_IDS: Record = { android: 'react-android', ios: 'react-native-artifacts', }; -/** All human-facing logs MUST go to stderr — stdout is reserved for the JSON result. */ +/** Logs go to stderr; stdout is reserved for the JSON result. */ function logError(message: string) { process.stderr.write(`[PatchedArtifacts] ${message}\n`); } -function runGh(args: string[]): string { - return execFileSync('gh', args, {encoding: 'utf8'}).trim(); -} +/** Credentials as read from the source; fields are validated per platform by the callers. */ +type RawCredentials = {githubToken: string | null; githubUsername: string | null}; function isCI(): boolean { return process.env.CI != null; } +/** Reads a non-empty environment variable, or null. */ +function getEnvVar(name: string): string | null { + const value: unknown = process.env[name]; + return typeof value === 'string' && value.length > 0 ? value : null; +} + +/** Runs a `gh` command and returns its trimmed output, or null on failure/empty. */ +function getGh(args: string[]): string | null { + try { + const output = execFileSync('gh', args, {encoding: 'utf8'}).trim(); + return output.length > 0 ? output : null; + } catch { + return null; + } +} + function hasGithubCLI(): boolean { try { execFileSync('which', ['gh'], {stdio: 'ignore'}); @@ -71,95 +92,84 @@ function hasGithubCLI(): boolean { } function hasRequiredScopes(): boolean { - const status = execFileSync('gh', ['auth', 'status'], {encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']}).toString(); - return status.includes('read:packages'); -} - -function getCredentials(): Credentials | null { try { - if (!hasGithubCLI()) { - logError('No GitHub CLI found.'); - return null; - } - if (isCI()) { - // typeof narrows the (loosely-typed) process.env members to string without assigning an any value. - if (typeof process.env.GITHUB_ACTOR === 'string' && typeof process.env.GITHUB_TOKEN === 'string') { - return {githubUsername: process.env.GITHUB_ACTOR, githubToken: process.env.GITHUB_TOKEN}; - } - logError('Missing required CI environment variables GITHUB_ACTOR and/or GITHUB_TOKEN.'); - return null; - } - if (!hasRequiredScopes()) { - logError('GitHub token does not have required scope read:packages.'); - return null; - } - // `--jq .login` returns the login string directly, so there is no untyped JSON to assert on. - return { - githubUsername: runGh(['api', 'user', '--jq', '.login']), - githubToken: runGh(['auth', 'token']), - }; + const status = execFileSync('gh', ['auth', 'status'], {encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']}).toString(); + return status.includes('read:packages'); } catch { - logError('Failed to get GitHub credentials. This might be due to an expired token or not being logged in.'); + return false; + } +} + +/** + * In CI credentials come from the environment (no gh CLI). Locally they come from + * the gh CLI, which must be installed and scoped for read:packages. + */ +function readCredentials(): RawCredentials | null { + if (isCI()) { + return {githubToken: getEnvVar('GITHUB_TOKEN'), githubUsername: getEnvVar('GITHUB_ACTOR')}; + } + if (!hasGithubCLI()) { + logError('No GitHub CLI found.'); return null; } + if (!hasRequiredScopes()) { + logError('GitHub token does not have required scope read:packages.'); + return null; + } + return {githubToken: getGh(['auth', 'token']), githubUsername: getGh(['api', 'user', '--jq', '.login'])}; +} + +function getIosCredentials(): IosCredentials | null { + const credentials = readCredentials(); + if (credentials == null || credentials.githubToken == null) { + logError('Missing GitHub token.'); + return null; + } + return {githubToken: credentials.githubToken}; +} + +function getAndroidCredentials(): AndroidCredentials | null { + const credentials = readCredentials(); + if (credentials == null || credentials.githubToken == null || credentials.githubUsername == null) { + logError('Missing GitHub credentials (username and/or token).'); + return null; + } + return {githubToken: credentials.githubToken, githubUsername: credentials.githubUsername}; } function mavenPomUrl(packageName: string, artifactId: string, version: string): string { return `https://maven.pkg.github.com/${GITHUB_REPO}/com/expensify/${packageName}/${artifactId}/${version}/${artifactId}-${version}.pom`; } -function mavenVersionsApiUrl(packageName: string, artifactId: string): string { - return `/orgs/${GITHUB_OWNER}/packages/maven/com.expensify.${packageName}.${artifactId}/versions`; +function buildAuthHeaders(githubToken: string | null): Record { + return githubToken ? {Authorization: `Bearer ${githubToken}`} : {}; } /** - * GitHub Packages returns a 302 to a signed object-store URL. We send the token - * ONLY to maven.pkg.github.com and never follow the redirect with it attached, - * so the token is never leaked to the redirect host. + * GitHub Packages 302-redirects to a signed object-store URL on a different host. + * `fetch` follows the redirect and drops the Authorization header on the + * cross-origin hop, so the token reaches only the initial host, never the object store. */ -function fetchTokenSafe(url: string, githubToken: string | null, maxRedirects = 5): Promise { - return new Promise((resolve, reject) => { - if (maxRedirects < 0) { - reject(new Error(`Too many redirects fetching ${url}`)); - return; - } - const headers = githubToken ? {Authorization: `Bearer ${githubToken}`} : {}; - https - .get(url, {headers}, (res) => { - const {statusCode = 0, headers: resHeaders} = res; - if (statusCode >= 300 && statusCode < 400 && resHeaders.location) { - res.resume(); // drain - // Follow the redirect WITHOUT the token. - const nextUrl = new URL(resHeaders.location, url).toString(); - fetchTokenSafe(nextUrl, null, maxRedirects - 1).then(resolve, reject); - return; - } - if (statusCode < 200 || statusCode >= 300) { - res.resume(); - reject(new Error(`Request to ${url} failed with status ${statusCode}`)); - return; - } - let body = ''; - res.setEncoding('utf8'); - res.on('data', (chunk: string) => (body += chunk)); - res.on('end', () => resolve(body)); - }) - .on('error', reject); - }); +async function fetchTokenSafe(url: string, githubToken: string | null): Promise { + const response = await fetch(url, {headers: buildAuthHeaders(githubToken)}); + if (!response.ok) { + throw new Error(`Request to ${url} failed with status ${response.status}`); + } + return response.text(); +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null; } function getReactNativeVersion(newDotRoot: string): string { const parsed: unknown = JSON.parse(fs.readFileSync(path.join(newDotRoot, 'package.json'), 'utf8')); - if (typeof parsed === 'object' && parsed !== null && 'dependencies' in parsed) { - const deps: unknown = parsed.dependencies; - if (typeof deps === 'object' && deps !== null && 'react-native' in deps) { - const version: unknown = deps['react-native']; - if (typeof version === 'string') { - return version; - } - } + const dependencies = isRecord(parsed) ? parsed.dependencies : undefined; + const version = isRecord(dependencies) ? dependencies['react-native'] : undefined; + if (typeof version !== 'string') { + throw new Error('Could not read react-native version from package.json'); } - throw new Error('Could not read react-native version from package.json'); + return version; } function computePatchesHash(newDotRoot: string, isHybrid: boolean): string { @@ -173,17 +183,20 @@ function computePatchesHash(newDotRoot: string, isHybrid: boolean): string { return execFileSync('bash', args, {encoding: 'utf8'}).trim(); } -function getArtifactsCandidates(packageName: string, artifactId: string, rnVersion: string): string[] { - const output = runGh(['api', '--paginate', mavenVersionsApiUrl(packageName, artifactId), '--jq', '.[].name']); - return output - .split('\n') - .map((line) => line.trim()) - .filter((name) => name.startsWith(rnVersion)); +async function getArtifactsCandidates(packageName: string, artifactId: string, rnVersion: string, githubToken: string): Promise { + const octokit = getOctokit(githubToken); + /* eslint-disable @typescript-eslint/naming-convention -- GitHub REST API params are snake_case */ + const versions = await octokit.paginate(octokit.rest.packages.getAllPackageVersionsForPackageOwnedByOrg, { + package_type: 'maven', + org: GITHUB_OWNER, + package_name: `com.expensify.${packageName}.${artifactId}`, + per_page: 100, + }); /* eslint-enable @typescript-eslint/naming-convention */ + return versions.map((version) => version.name).filter((name) => name.startsWith(rnVersion)); } async function getRemotePatchesHash(packageName: string, artifactId: string, version: string, githubToken: string): Promise { const pom = await fetchTokenSafe(mavenPomUrl(packageName, artifactId, version), githubToken); - // The publish step writes into the POM block. return pom.match(/([^<]+)<\/patchesHash>/)?.[1]?.trim() ?? null; } @@ -192,7 +205,7 @@ async function findMatchingArtifactsVersion(options: ResolveOptions, artifactId: try { const localPatchesHash = computePatchesHash(newDotRoot, isHybrid); const rnVersion = getReactNativeVersion(newDotRoot); - const candidates = getArtifactsCandidates(packageName, artifactId, rnVersion); + const candidates = await getArtifactsCandidates(packageName, artifactId, rnVersion, githubToken); for (const candidate of candidates) { const remoteHash = await getRemotePatchesHash(packageName, artifactId, candidate, githubToken); if (remoteHash != null && remoteHash === localPatchesHash) { @@ -206,30 +219,42 @@ async function findMatchingArtifactsVersion(options: ResolveOptions, artifactId: } } -/** - * Resolves whether a matching prebuilt artifact exists for the current patches. - * Returns `buildFromSource: true` (a normal outcome, not an error) whenever no - * match is found or credentials are unavailable. - */ -async function resolveArtifacts(options: ResolveOptions): Promise { - const artifactId = ARTIFACT_IDS[options.platform]; - const base: ResolveResult = {buildFromSource: true, version: null, packageName: options.packageName, artifactId}; - - const credentials = getCredentials(); +async function resolveWithCredentials( + options: ResolveOptions, + artifactId: string, + credentials: Creds | null, + sourceBuild: SourceBuild, +): Promise> { if (!credentials) { - return base; + return sourceBuild; } - const version = await findMatchingArtifactsVersion(options, artifactId, credentials.githubToken); - if (!version) { + if (version == null) { logError(`No matching artifacts version found for ${options.packageName}. Building react-native from source.`); - return base; + return sourceBuild; } - logError(`Using patched react-native artifacts: ${options.packageName}:${version}`); - return {...base, buildFromSource: false, version}; + return {buildFromSource: false, version, packageName: options.packageName, artifactId, ...credentials}; +} + +/** + * Resolves whether a matching prebuilt artifact exists for the current patches. + * Returns a `SourceBuild` (no secrets) when no match is found or credentials are + * unavailable, otherwise a `Prebuilt` carrying the credentials the native + * download needs — token only for iOS, username + token for Android. + */ +function resolveArtifacts(options: ResolveOptions & {platform: 'ios'}): Promise; +function resolveArtifacts(options: ResolveOptions & {platform: 'android'}): Promise; +function resolveArtifacts(options: ResolveOptions): Promise { + const artifactId = ARTIFACT_IDS[options.platform]; + const sourceBuild: SourceBuild = {buildFromSource: true, version: null, packageName: options.packageName, artifactId}; + + if (options.platform === 'ios') { + return resolveWithCredentials(options, artifactId, getIosCredentials(), sourceBuild); + } + return resolveWithCredentials(options, artifactId, getAndroidCredentials(), sourceBuild); } export default resolveArtifacts; -export {ARTIFACT_IDS, fetchTokenSafe}; -export type {Platform, ResolveOptions, ResolveResult}; +export {ARTIFACT_IDS}; +export type {Platform, ResolveOptions, ResolveResult, IosResult, AndroidResult}; diff --git a/scripts/artifacts-utils/resolve-artifacts.ts b/scripts/artifacts-utils/resolve-artifacts.ts index 2734bb495572..f8de81456808 100644 --- a/scripts/artifacts-utils/resolve-artifacts.ts +++ b/scripts/artifacts-utils/resolve-artifacts.ts @@ -2,36 +2,30 @@ import parseCommandLineArguments from '../utils/parseCommandLineArguments'; import resolveArtifacts from './lib/artifactsResolver'; /** - * Thin CLI entry point around the shared artifacts resolver, invoked by the - * native build systems (Gradle `ExpensiUtils.gradle` and iOS - * `patched_ios_artifacts.rb`) which cannot import the TS module directly. + * CLI wrapper around the shared artifacts resolver for the native build systems + * (Gradle and `patched_ios_artifacts.rb`), which cannot import the TS module. * * Usage: * tsx scripts/artifacts-utils/resolve-artifacts.ts \ * --platform=ios --package=react-hybrid --hybrid=true --new-dot-root=. * - * Prints the resolution result as JSON to stdout (logs go to stderr). It always - * exits 0 with a valid JSON payload — any failure resolves to `buildFromSource`. + * Prints the result as JSON to stdout (logs go to stderr) and always exits 0. */ const args = parseCommandLineArguments(); const platform = args.platform; const packageName = args.package ?? ''; -// Validate rather than assert: an invalid/missing platform falls back to source. if (platform !== 'ios' && platform !== 'android') { process.stderr.write(`[PatchedArtifacts] Invalid or missing --platform "${platform ?? ''}" (expected "ios" or "android"); building from source.\n`); process.stdout.write(JSON.stringify({buildFromSource: true, version: null, packageName, artifactId: ''})); process.exit(0); } -resolveArtifacts({ - platform, - packageName, - newDotRoot: args['new-dot-root'] ?? '.', - isHybrid: args.hybrid === 'true', -}) +const options = {packageName, newDotRoot: args['new-dot-root'] ?? '.', isHybrid: args.hybrid === 'true'}; +const resolution = platform === 'ios' ? resolveArtifacts({...options, platform: 'ios'}) : resolveArtifacts({...options, platform: 'android'}); + +resolution .then((result) => process.stdout.write(JSON.stringify(result))) .catch(() => { - // Never fail the build over resolution — fall back to building from source. process.stdout.write(JSON.stringify({buildFromSource: true, version: null, packageName, artifactId: ''})); }); diff --git a/tests/unit/ArtifactsResolverTest.ts b/tests/unit/ArtifactsResolverTest.ts index 19ef993eb4ac..9ff5e0eff0d9 100644 --- a/tests/unit/ArtifactsResolverTest.ts +++ b/tests/unit/ArtifactsResolverTest.ts @@ -1,61 +1,46 @@ -import resolveArtifacts, {ARTIFACT_IDS, fetchTokenSafe} from '@scripts/artifacts-utils/lib/artifactsResolver'; - -import type {ClientRequest, IncomingMessage} from 'http'; +import resolveArtifacts, {ARTIFACT_IDS} from '@scripts/artifacts-utils/lib/artifactsResolver'; /** * @jest-environment node */ +import {getOctokit} from '@actions/github'; import {execFileSync} from 'child_process'; -import {EventEmitter} from 'events'; import fs from 'fs'; -import https from 'https'; jest.mock('child_process'); -jest.mock('https'); +jest.mock('@actions/github'); const mockExecFileSync = jest.mocked(execFileSync); -const mockGet = jest.mocked(https.get); +const mockGetOctokit = jest.mocked(getOctokit); +const mockPaginate = jest.fn(); const NEW_DOT_ROOT = '/repo'; const LOCAL_HASH = 'abc123hash'; -/** Builds a fake IncomingMessage that emits the given body then ends. */ -function fakeResponse(statusCode: number, headers: Record, body = '') { - const res = Object.assign(new EventEmitter(), {statusCode, headers, resume: () => undefined, setEncoding: () => undefined}); - process.nextTick(() => { - if (body) { - res.emit('data', body); - } - res.emit('end'); - }); - return res; +/** A minimal fetch Response stub — only the members the resolver reads. */ +function fakeFetchResponse(body: string) { + return {ok: true, status: 200, text: () => Promise.resolve(body)}; } -/** - * Queue-based https.get mock. Records the `Authorization` header seen on every - * request so we can assert the token is never forwarded to a redirect host. - */ -function mockHttpsSequence(responses: Array<{statusCode: number; headers?: Record; body?: string}>) { - const seenAuthHeaders: Array = []; +/** Replaces global fetch with a queue of POM responses (one per candidate lookup). */ +function mockFetchBodies(bodies: string[]) { let call = 0; - const impl = (_url: string | URL, options: {headers?: Record}, callback?: (res: IncomingMessage) => void): ClientRequest => { - const auth = options.headers?.Authorization; - seenAuthHeaders.push(typeof auth === 'string' ? auth : undefined); - const spec = responses.at(call++) ?? {statusCode: 500}; - // Faking Node's IncomingMessage / ClientRequest requires narrowing assertions in a unit test. - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - callback?.(fakeResponse(spec.statusCode, spec.headers ?? {}, spec.body) as unknown as IncomingMessage); - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - return new EventEmitter() as unknown as ClientRequest; - }; - // https.get is overloaded; cast the single fake impl to its type. + global.fetch = jest.fn().mockImplementation(() => Promise.resolve(fakeFetchResponse(bodies.at(call++) ?? ''))); +} + +/** Makes the Octokit package-versions API return the given version names. */ +function mockVersions(names: string[]) { + mockPaginate.mockResolvedValue(names.map((name) => ({name}))); + // Faking a minimal Octokit surface in a unit test. // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - mockGet.mockImplementation(impl as typeof https.get); - return seenAuthHeaders; + mockGetOctokit.mockReturnValue({ + paginate: mockPaginate, + rest: {packages: {getAllPackageVersionsForPackageOwnedByOrg: jest.fn()}}, + } as unknown as ReturnType); } -/** Mocks the exec calls needed for a full resolve: credentials + hash + candidate list. */ -function mockResolveExec(candidates: string) { +/** Mocks the gh CLI calls needed for credentials + the local patches hash. */ +function mockResolveExec() { mockExecFileSync.mockImplementation((cmd: string, args?: readonly string[]) => { if (cmd === 'bash') { return LOCAL_HASH; @@ -64,57 +49,33 @@ function mockResolveExec(candidates: string) { return 'Token scopes: read:packages'; } if (cmd === 'gh' && args?.includes('user')) { - return JSON.stringify({login: 'me'}); + // `gh api user --jq .login` returns the bare login string. + return 'me'; } if (cmd === 'gh' && args?.includes('token')) { return 'tok'; } - if (cmd === 'gh' && args?.includes('--paginate')) { - return candidates; - } return ''; }); jest.spyOn(fs, 'readFileSync').mockReturnValue('{"dependencies":{"react-native":"0.85.3"}}'); } describe('artifactsResolver', () => { + const ORIGINAL_CI = typeof process.env.CI === 'string' ? process.env.CI : undefined; + beforeEach(() => { jest.clearAllMocks(); + // Force the local (gh) credential path deterministically, regardless of the runner. + delete process.env.CI; }); afterEach(() => { jest.restoreAllMocks(); - }); - - describe('fetchTokenSafe (security)', () => { - it('does NOT forward the token when following a cross-host 302 redirect', async () => { - const seenAuth = mockHttpsSequence([ - {statusCode: 302, headers: {location: 'https://objectstorage.example.com/signed?sig=xyz'}}, - {statusCode: 200, body: ''}, - ]); - - const body = await fetchTokenSafe('https://maven.pkg.github.com/pom', 'secret-token'); - - expect(body).toBe(''); - // First hop (GitHub Packages) carries the token; the redirect hop (S3) must NOT. - expect(seenAuth.at(0)).toBe('Bearer secret-token'); - expect(seenAuth.at(1)).toBeUndefined(); - }); - - it('returns the body directly on a 200 with no redirect', async () => { - mockHttpsSequence([{statusCode: 200, body: 'hello'}]); - await expect(fetchTokenSafe('https://maven.pkg.github.com/pom', 'tok')).resolves.toBe('hello'); - }); - - it('rejects on a non-2xx status', async () => { - mockHttpsSequence([{statusCode: 404}]); - await expect(fetchTokenSafe('https://maven.pkg.github.com/pom', 'tok')).rejects.toThrow('status 404'); - }); - - it('rejects when the redirect chain is too long', async () => { - mockHttpsSequence(Array.from({length: 10}, () => ({statusCode: 302, headers: {location: 'https://a.example.com/next'}}))); - await expect(fetchTokenSafe('https://maven.pkg.github.com/pom', 'tok')).rejects.toThrow('Too many redirects'); - }); + if (ORIGINAL_CI === undefined) { + delete process.env.CI; + } else { + process.env.CI = ORIGINAL_CI; + } }); describe('ARTIFACT_IDS', () => { @@ -139,21 +100,39 @@ describe('artifactsResolver', () => { }); it('resolves a matching version and does not build from source', async () => { - mockResolveExec('0.85.3-nomatch\n0.85.3-match'); - mockHttpsSequence([ - {statusCode: 200, body: 'differenthash'}, - {statusCode: 200, body: `${LOCAL_HASH}`}, - ]); + mockResolveExec(); + mockVersions(['0.85.3-nomatch', '0.85.3-match']); + mockFetchBodies(['differenthash', `${LOCAL_HASH}`]); const result = await resolveArtifacts({platform: 'ios', packageName: 'react-hybrid', newDotRoot: NEW_DOT_ROOT, isHybrid: true}); expect(result.buildFromSource).toBe(false); expect(result.version).toBe('0.85.3-match'); + if (!result.buildFromSource) { + expect(result.githubToken).toBe('tok'); + // iOS carries no username — its result type doesn't even include the field. + expect('githubUsername' in result).toBe(false); + } + }); + + it('returns the username alongside the token for a matching Android artifact', async () => { + mockResolveExec(); + mockVersions(['0.85.3-match']); + mockFetchBodies([`${LOCAL_HASH}`]); + + const result = await resolveArtifacts({platform: 'android', packageName: 'react-standalone', newDotRoot: NEW_DOT_ROOT, isHybrid: false}); + + expect(result.buildFromSource).toBe(false); + if (!result.buildFromSource) { + expect(result.githubToken).toBe('tok'); + expect(result.githubUsername).toBe('me'); + } }); it('falls back to source build when no candidate matches the local patches hash', async () => { - mockResolveExec('0.85.3-other'); - mockHttpsSequence([{statusCode: 200, body: 'nomatch'}]); + mockResolveExec(); + mockVersions(['0.85.3-other']); + mockFetchBodies(['nomatch']); const result = await resolveArtifacts({platform: 'android', packageName: 'react-standalone', newDotRoot: NEW_DOT_ROOT, isHybrid: false}); From 1e65a3101f0cff7c9c6853ef7fe5e2fbb49b7983 Mon Sep 17 00:00:00 2001 From: mateuuszzzzz Date: Tue, 14 Jul 2026 16:32:04 +0200 Subject: [PATCH 10/18] Use isRecord guard from libs --- scripts/artifacts-utils/lib/artifactsResolver.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/scripts/artifacts-utils/lib/artifactsResolver.ts b/scripts/artifacts-utils/lib/artifactsResolver.ts index 623fceac39a5..b58ff7330e5f 100644 --- a/scripts/artifacts-utils/lib/artifactsResolver.ts +++ b/scripts/artifacts-utils/lib/artifactsResolver.ts @@ -1,3 +1,5 @@ +import {isRecord} from '@libs/ObjectUtils'; + import {getOctokit} from '@actions/github'; import {execFileSync} from 'child_process'; import fs from 'fs'; @@ -49,10 +51,10 @@ type ResolveResult = IosResult | AndroidResult; const GITHUB_REPO = 'Expensify/App'; const GITHUB_OWNER = 'Expensify'; -const ARTIFACT_IDS: Record = { +const ARTIFACT_IDS = { android: 'react-android', ios: 'react-native-artifacts', -}; +} satisfies Record; /** Logs go to stderr; stdout is reserved for the JSON result. */ function logError(message: string) { @@ -158,10 +160,6 @@ async function fetchTokenSafe(url: string, githubToken: string | null): Promise< return response.text(); } -function isRecord(value: unknown): value is Record { - return typeof value === 'object' && value !== null; -} - function getReactNativeVersion(newDotRoot: string): string { const parsed: unknown = JSON.parse(fs.readFileSync(path.join(newDotRoot, 'package.json'), 'utf8')); const dependencies = isRecord(parsed) ? parsed.dependencies : undefined; From 7aea5b94e983e836885a83c7d9efce5be5bd3de0 Mon Sep 17 00:00:00 2001 From: mateuuszzzzz Date: Tue, 14 Jul 2026 17:38:53 +0200 Subject: [PATCH 11/18] Fix type error in PatchedArtifactsSettings --- .../artifacts-utils/android/PatchedArtifactsSettings.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/artifacts-utils/android/PatchedArtifactsSettings.gradle b/scripts/artifacts-utils/android/PatchedArtifactsSettings.gradle index 7c33fe57b95d..5654e57838d6 100644 --- a/scripts/artifacts-utils/android/PatchedArtifactsSettings.gradle +++ b/scripts/artifacts-utils/android/PatchedArtifactsSettings.gradle @@ -24,7 +24,8 @@ settings.extensions.configure(PatchedArtifactsConfig) { config -> config.packageName = getProperty('patchedArtifacts.packageName') - def resolution = ExpensiUtils.resolveArtifacts(config.packageName, getNewDotRootDir(), hasProperty('newDotRoot')) + def isHybrid = config.packageName == 'react-hybrid' + def resolution = ExpensiUtils.resolveArtifacts(config.packageName, getNewDotRootDir().toString(), isHybrid) config.version = resolution.version config.buildFromSource = resolution.buildFromSource From 1e69fdb99cf922531968c5d635f5a427a30b41ce Mon Sep 17 00:00:00 2001 From: mateuuszzzzz Date: Tue, 14 Jul 2026 18:52:09 +0200 Subject: [PATCH 12/18] Create one workflow that orchestrates iOS and Android builds --- .../publishReactNativeAndroidArtifacts.yml | 87 ++++---------- .../workflows/publishReactNativeArtifacts.yml | 103 +++++++++++++++++ .../publishReactNativeiOSArtifacts.yml | 109 ++++++------------ .../workflows/verifyReactNativePatches.yml | 4 - 4 files changed, 160 insertions(+), 143 deletions(-) create mode 100644 .github/workflows/publishReactNativeArtifacts.yml diff --git a/.github/workflows/publishReactNativeAndroidArtifacts.yml b/.github/workflows/publishReactNativeAndroidArtifacts.yml index 95250a652b5a..413643c504d5 100644 --- a/.github/workflows/publishReactNativeAndroidArtifacts.yml +++ b/.github/workflows/publishReactNativeAndroidArtifacts.yml @@ -1,98 +1,57 @@ name: Publish React Native Android Artifacts on: - workflow_dispatch: + workflow_call: inputs: - build_standalone: - description: 'Build Standalone artifacts' - required: false - type: boolean - build_hybridapp: - description: 'Build HybridApp artifacts' - required: false - type: boolean - app_pull_request_url: - description: 'The Expensify/App pull request URL.' + build_targets: + description: JSON array of `is_hybrid` values to build (e.g. ["true","false"]). + required: true + type: string + app_ref: + description: Expensify/App ref to checkout (empty = triggering ref). required: false + type: string default: '' - mobile_expensify_pull_request_url: - description: 'The Expensify/Mobile-Expensify pull request URL to check out the submodule.' + mobile_expensify_ref: + description: Mobile-Expensify ref to checkout for hybrid builds (empty = pinned submodule). required: false + type: string default: '' - reviewed_code: - description: I reviewed this pull request and verified that it does not contain any malicious code. - type: boolean + secrets: + OS_BOTIFY_TOKEN: + description: Token used to checkout private submodules. + required: true + SLACK_WEBHOOK: + description: Webhook used to announce failures. required: true - default: false - push: - branches: - - main - paths: - - package.json - - patches/react-native+*.patch - - patches/@react-native+*.patch - - patches/react-native/react-native+*.patch - - patches/react-native/@react-native+*.patch - - Mobile-Expensify jobs: - verifyPatches: - uses: ./.github/workflows/verifyReactNativePatches.yml - with: - platform: android - ref_before: ${{ github.event.before || '' }} - ref_after: ${{ github.event.after || '' }} - build_standalone: ${{ github.event_name == 'workflow_dispatch' && inputs.build_standalone || false }} - build_hybridapp: ${{ github.event_name == 'workflow_dispatch' && inputs.build_hybridapp || false }} - secrets: - OS_BOTIFY_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} - - # Validates the actor and that they reviewed the PR before any token is used or untrusted App PR code is checked out. - prep: - uses: ./.github/workflows/validateBuildRequest.yml - with: - REVIEWED_CODE: ${{ inputs.reviewed_code || false }} - secrets: - OS_BOTIFY_COMMIT_TOKEN: ${{ secrets.OS_BOTIFY_COMMIT_TOKEN }} - - resolveRefs: - name: Resolve build refs - needs: [prep] - uses: ./.github/workflows/resolveBuildRefs.yml - with: - APP_PULL_REQUEST_URL: ${{ inputs.app_pull_request_url }} - MOBILE_EXPENSIFY_PULL_REQUEST_URL: ${{ inputs.mobile_expensify_pull_request_url }} - secrets: - OS_BOTIFY_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} - buildAndPublishReactNativeArtifacts: name: Build and Publish React Native Artifacts runs-on: ${{ github.repository_owner == 'Expensify' && 'blacksmith-16vcpu-ubuntu-2404' || 'blacksmith-2vcpu-ubuntu-2404' }} - needs: [verifyPatches, prep, resolveRefs] - if: needs.verifyPatches.outputs.build_targets != '' strategy: # Disable fail-fast to prevent cancelling both jobs when only one needs to be stopped due to concurrency limits fail-fast: false matrix: # Dynamically build the matrix based on the build targets - is_hybrid: ${{ fromJSON(needs.verifyPatches.outputs.build_targets) }} + is_hybrid: ${{ fromJSON(inputs.build_targets) }} concurrency: - group: ${{ github.workflow }}-${{ github.job }}-${{ matrix.is_hybrid }} + group: ${{ github.workflow }}-android-build-${{ matrix.is_hybrid }} cancel-in-progress: true steps: - name: Checkout Code uses: useblacksmith/checkout@c9796daa2a4bdebdab5bd16be2c09a70cd4e1121 # v1 with: - ref: ${{ needs.resolveRefs.outputs.APP_REF }} + ref: ${{ inputs.app_ref }} submodules: ${{ matrix.is_hybrid }} token: ${{ secrets.OS_BOTIFY_TOKEN }} - name: Checkout Mobile-Expensify to specified branch or commit - if: ${{ matrix.is_hybrid == 'true' && needs.resolveRefs.outputs.MOBILE_EXPENSIFY_REF != '' }} + if: ${{ matrix.is_hybrid == 'true' && inputs.mobile_expensify_ref != '' }} run: | cd Mobile-Expensify - git fetch origin ${{ needs.resolveRefs.outputs.MOBILE_EXPENSIFY_REF }} - git checkout ${{ needs.resolveRefs.outputs.MOBILE_EXPENSIFY_REF }} + git fetch origin ${{ inputs.mobile_expensify_ref }} + git checkout ${{ inputs.mobile_expensify_ref }} # Recompute the patches hash from the actually checked-out tree (which may include a custom Mobile-Expensify # ref) so the published artifact is tagged with a hash that consumers will match against. diff --git a/.github/workflows/publishReactNativeArtifacts.yml b/.github/workflows/publishReactNativeArtifacts.yml new file mode 100644 index 000000000000..2abbe1b7b7db --- /dev/null +++ b/.github/workflows/publishReactNativeArtifacts.yml @@ -0,0 +1,103 @@ +name: Publish React Native Artifacts + +# Single entry point for building + publishing our prebuilt React Native artifacts. +# Runs the platform-independent phases (verify patches / validate request / resolve refs) ONCE, +# then fans out to the iOS and Android publish workflows in parallel. Always builds both platforms. + +on: + workflow_dispatch: + inputs: + build_standalone: + description: 'Build Standalone artifacts' + required: false + type: boolean + build_hybridapp: + description: 'Build HybridApp artifacts' + required: false + type: boolean + app_pull_request_url: + description: 'The Expensify/App pull request URL.' + required: false + default: '' + mobile_expensify_pull_request_url: + description: 'The Expensify/Mobile-Expensify pull request URL to check out the submodule.' + required: false + default: '' + reviewed_code: + description: I reviewed this pull request and verified that it does not contain any malicious code. + type: boolean + required: true + default: false + push: + branches: + - main + paths: + - package.json + - patches/react-native+*.patch + - patches/@react-native+*.patch + - patches/react-native/react-native+*.patch + - patches/react-native/@react-native+*.patch + - Mobile-Expensify + +jobs: + verifyPatches: + uses: ./.github/workflows/verifyReactNativePatches.yml + with: + ref_before: ${{ github.event.before || '' }} + ref_after: ${{ github.event.after || '' }} + build_standalone: ${{ github.event_name == 'workflow_dispatch' && inputs.build_standalone || false }} + build_hybridapp: ${{ github.event_name == 'workflow_dispatch' && inputs.build_hybridapp || false }} + secrets: + OS_BOTIFY_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} + + # Validates the actor and that they reviewed the PR before any token is used or untrusted App PR code is checked out. + prep: + uses: ./.github/workflows/validateBuildRequest.yml + with: + REVIEWED_CODE: ${{ inputs.reviewed_code || false }} + secrets: + OS_BOTIFY_COMMIT_TOKEN: ${{ secrets.OS_BOTIFY_COMMIT_TOKEN }} + + resolveRefs: + name: Resolve build refs + needs: [prep] + uses: ./.github/workflows/resolveBuildRefs.yml + with: + APP_PULL_REQUEST_URL: ${{ inputs.app_pull_request_url }} + MOBILE_EXPENSIFY_PULL_REQUEST_URL: ${{ inputs.mobile_expensify_pull_request_url }} + secrets: + OS_BOTIFY_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} + + # Fail-fast: a manual dispatch that ticks neither variant yields no build targets. Without this the run + # would be a confusing green no-op. On push an empty build_targets is a legitimate "nothing changed". + assertHasTargets: + needs: [verifyPatches] + if: github.event_name == 'workflow_dispatch' && needs.verifyPatches.outputs.build_targets == '' + runs-on: ubuntu-latest + steps: + - name: Fail when nothing was selected to build + run: | + echo "::error::No build targets selected — tick build_standalone and/or build_hybridapp." + exit 1 + + ios: + name: iOS + needs: [verifyPatches, prep, resolveRefs] + if: needs.verifyPatches.outputs.build_targets != '' + uses: ./.github/workflows/publishReactNativeiOSArtifacts.yml + with: + build_targets: ${{ needs.verifyPatches.outputs.build_targets }} + app_ref: ${{ needs.resolveRefs.outputs.APP_REF }} + mobile_expensify_ref: ${{ needs.resolveRefs.outputs.MOBILE_EXPENSIFY_REF }} + secrets: inherit + + android: + name: Android + needs: [verifyPatches, prep, resolveRefs] + if: needs.verifyPatches.outputs.build_targets != '' + uses: ./.github/workflows/publishReactNativeAndroidArtifacts.yml + with: + build_targets: ${{ needs.verifyPatches.outputs.build_targets }} + app_ref: ${{ needs.resolveRefs.outputs.APP_REF }} + mobile_expensify_ref: ${{ needs.resolveRefs.outputs.MOBILE_EXPENSIFY_REF }} + secrets: inherit diff --git a/.github/workflows/publishReactNativeiOSArtifacts.yml b/.github/workflows/publishReactNativeiOSArtifacts.yml index ed331af1ebd2..3d3987303df0 100644 --- a/.github/workflows/publishReactNativeiOSArtifacts.yml +++ b/.github/workflows/publishReactNativeiOSArtifacts.yml @@ -1,82 +1,41 @@ name: Publish React Native iOS Artifacts on: - workflow_dispatch: + workflow_call: inputs: - build_standalone: - description: 'Build Standalone artifacts' - required: false - type: boolean - build_hybridapp: - description: 'Build HybridApp artifacts' - required: false - type: boolean - app_pull_request_url: - description: 'The Expensify/App pull request URL.' + build_targets: + description: JSON array of `is_hybrid` values to build (e.g. ["true","false"]). + required: true + type: string + app_ref: + description: Expensify/App ref to checkout (empty = triggering ref). required: false + type: string default: '' - mobile_expensify_pull_request_url: - description: 'The Expensify/Mobile-Expensify pull request URL to check out the submodule.' + mobile_expensify_ref: + description: Mobile-Expensify ref to checkout for hybrid builds (empty = pinned submodule). required: false + type: string default: '' - reviewed_code: - description: I reviewed this pull request and verified that it does not contain any malicious code. - type: boolean + secrets: + OS_BOTIFY_TOKEN: + description: Token used to checkout private submodules. + required: true + SLACK_WEBHOOK: + description: Webhook used to announce failures. required: true - default: false - push: - branches: - - main - paths: - - package.json - - patches/react-native+*.patch - - patches/@react-native+*.patch - - patches/react-native/react-native+*.patch - - patches/react-native/@react-native+*.patch - - Mobile-Expensify env: DEVELOPER_DIR: /Applications/Xcode_26.2.app/Contents/Developer jobs: - verifyPatches: - uses: ./.github/workflows/verifyReactNativePatches.yml - with: - platform: ios - ref_before: ${{ github.event.before || '' }} - ref_after: ${{ github.event.after || '' }} - build_standalone: ${{ github.event_name == 'workflow_dispatch' && inputs.build_standalone || false }} - build_hybridapp: ${{ github.event_name == 'workflow_dispatch' && inputs.build_hybridapp || false }} - secrets: - OS_BOTIFY_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} - - # Validates the actor and that they reviewed the PR before any token is used or untrusted App PR code is checked out. - prep: - uses: ./.github/workflows/validateBuildRequest.yml - with: - REVIEWED_CODE: ${{ inputs.reviewed_code || false }} - secrets: - OS_BOTIFY_COMMIT_TOKEN: ${{ secrets.OS_BOTIFY_COMMIT_TOKEN }} - - resolveRefs: - name: Resolve build refs - needs: [prep] - uses: ./.github/workflows/resolveBuildRefs.yml - with: - APP_PULL_REQUEST_URL: ${{ inputs.app_pull_request_url }} - MOBILE_EXPENSIFY_PULL_REQUEST_URL: ${{ inputs.mobile_expensify_pull_request_url }} - secrets: - OS_BOTIFY_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} - buildSlice: name: Build iOS Slice runs-on: blacksmith-12vcpu-macos-latest - needs: [verifyPatches, prep, resolveRefs] - if: needs.verifyPatches.outputs.build_targets != '' strategy: fail-fast: false matrix: - is_hybrid: ${{ fromJSON(needs.verifyPatches.outputs.build_targets) }} + is_hybrid: ${{ fromJSON(inputs.build_targets) }} flavor: ['Debug', 'Release'] platform: ['ios', 'ios-simulator'] concurrency: @@ -86,16 +45,16 @@ jobs: - name: Checkout Code uses: useblacksmith/checkout@c9796daa2a4bdebdab5bd16be2c09a70cd4e1121 # v1 with: - ref: ${{ needs.resolveRefs.outputs.APP_REF }} + ref: ${{ inputs.app_ref }} submodules: ${{ matrix.is_hybrid }} token: ${{ secrets.OS_BOTIFY_TOKEN }} - name: Checkout Mobile-Expensify to specified branch or commit - if: ${{ matrix.is_hybrid == 'true' && needs.resolveRefs.outputs.MOBILE_EXPENSIFY_REF != '' }} + if: ${{ matrix.is_hybrid == 'true' && inputs.mobile_expensify_ref != '' }} run: | cd Mobile-Expensify - git fetch origin ${{ needs.resolveRefs.outputs.MOBILE_EXPENSIFY_REF }} - git checkout ${{ needs.resolveRefs.outputs.MOBILE_EXPENSIFY_REF }} + git fetch origin ${{ inputs.mobile_expensify_ref }} + git checkout ${{ inputs.mobile_expensify_ref }} - name: Setup Node uses: ./.github/actions/composite/setupNode @@ -136,11 +95,11 @@ jobs: composeXCFramework: name: Compose XCFramework runs-on: blacksmith-12vcpu-macos-latest - needs: [verifyPatches, buildSlice, resolveRefs] + needs: [buildSlice] strategy: fail-fast: false matrix: - is_hybrid: ${{ fromJSON(needs.verifyPatches.outputs.build_targets) }} + is_hybrid: ${{ fromJSON(inputs.build_targets) }} flavor: ['Debug', 'Release'] concurrency: group: ${{ github.workflow }}-compose-${{ matrix.is_hybrid }}-${{ matrix.flavor }} @@ -149,16 +108,16 @@ jobs: - name: Checkout Code uses: useblacksmith/checkout@c9796daa2a4bdebdab5bd16be2c09a70cd4e1121 # v1 with: - ref: ${{ needs.resolveRefs.outputs.APP_REF }} + ref: ${{ inputs.app_ref }} submodules: ${{ matrix.is_hybrid }} token: ${{ secrets.OS_BOTIFY_TOKEN }} - name: Checkout Mobile-Expensify to specified branch or commit - if: ${{ matrix.is_hybrid == 'true' && needs.resolveRefs.outputs.MOBILE_EXPENSIFY_REF != '' }} + if: ${{ matrix.is_hybrid == 'true' && inputs.mobile_expensify_ref != '' }} run: | cd Mobile-Expensify - git fetch origin ${{ needs.resolveRefs.outputs.MOBILE_EXPENSIFY_REF }} - git checkout ${{ needs.resolveRefs.outputs.MOBILE_EXPENSIFY_REF }} + git fetch origin ${{ inputs.mobile_expensify_ref }} + git checkout ${{ inputs.mobile_expensify_ref }} - name: Setup Node uses: ./.github/actions/composite/setupNode @@ -213,11 +172,11 @@ jobs: publishReactNativeiOSArtifacts: name: Publish React Native iOS Artifacts runs-on: blacksmith-4vcpu-ubuntu-2404 - needs: [verifyPatches, composeXCFramework, resolveRefs] + needs: [composeXCFramework] strategy: fail-fast: false matrix: - is_hybrid: ${{ fromJSON(needs.verifyPatches.outputs.build_targets) }} + is_hybrid: ${{ fromJSON(inputs.build_targets) }} concurrency: # Publish runs sequentially per is_hybrid. Cancelling an in-flight publish # can leave Maven metadata out of sync with the uploaded artifact, so we @@ -228,16 +187,16 @@ jobs: - name: Checkout Code uses: useblacksmith/checkout@c9796daa2a4bdebdab5bd16be2c09a70cd4e1121 # v1 with: - ref: ${{ needs.resolveRefs.outputs.APP_REF }} + ref: ${{ inputs.app_ref }} submodules: ${{ matrix.is_hybrid }} token: ${{ secrets.OS_BOTIFY_TOKEN }} - name: Checkout Mobile-Expensify to specified branch or commit - if: ${{ matrix.is_hybrid == 'true' && needs.resolveRefs.outputs.MOBILE_EXPENSIFY_REF != '' }} + if: ${{ matrix.is_hybrid == 'true' && inputs.mobile_expensify_ref != '' }} run: | cd Mobile-Expensify - git fetch origin ${{ needs.resolveRefs.outputs.MOBILE_EXPENSIFY_REF }} - git checkout ${{ needs.resolveRefs.outputs.MOBILE_EXPENSIFY_REF }} + git fetch origin ${{ inputs.mobile_expensify_ref }} + git checkout ${{ inputs.mobile_expensify_ref }} - name: Setup Node uses: ./.github/actions/composite/setupNode diff --git a/.github/workflows/verifyReactNativePatches.yml b/.github/workflows/verifyReactNativePatches.yml index d018cd892bcf..b6d02dfc54db 100644 --- a/.github/workflows/verifyReactNativePatches.yml +++ b/.github/workflows/verifyReactNativePatches.yml @@ -3,10 +3,6 @@ name: Verify React Native Patches on: workflow_call: inputs: - platform: - description: Platform being verified (android or ios) - required: true - type: string ref_before: description: Git ref to compare against (typically github.event.before); leave empty for workflow_dispatch required: false From b2e3b693aefa26faa7734299961b9d5dadd1ca33 Mon Sep 17 00:00:00 2001 From: mateuuszzzzz Date: Thu, 16 Jul 2026 12:52:08 +0200 Subject: [PATCH 13/18] Make iOS builds verbose --- scripts/run-build.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/run-build.sh b/scripts/run-build.sh index bd81a64e7657..f4f97d4a92c2 100755 --- a/scripts/run-build.sh +++ b/scripts/run-build.sh @@ -65,13 +65,13 @@ fi # Check if the argument is one of the desired values case "$BUILD" in --ios) - npx rock run:ios --configuration $IOS_MODE --scheme "$SCHEME" --dev-server "${ROCK_FLAGS[@]}" + npx rock run:ios --configuration $IOS_MODE --scheme "$SCHEME" --verbose --dev-server "${ROCK_FLAGS[@]}" ;; --ipad) - npx rock run:ios --simulator "iPad Pro (12.9-inch) (6th generation)" --configuration $IOS_MODE --scheme "$SCHEME" --dev-server "${ROCK_FLAGS[@]}" + npx rock run:ios --simulator "iPad Pro (12.9-inch) (6th generation)" --configuration $IOS_MODE --scheme "$SCHEME" --verbose --dev-server "${ROCK_FLAGS[@]}" ;; --ipad-sm) - npx rock run:ios --simulator "iPad Pro (11-inch) (4th generation)" --configuration $IOS_MODE --scheme "$SCHEME" --dev-server "${ROCK_FLAGS[@]}" + npx rock run:ios --simulator "iPad Pro (11-inch) (4th generation)" --configuration $IOS_MODE --scheme "$SCHEME" --verbose --dev-server "${ROCK_FLAGS[@]}" ;; --android) # Check if this is an Expensify developer with WARP (only they need cert import) From 579a4e61d95843a9387634e66c62c5206c0a6283 Mon Sep 17 00:00:00 2001 From: mateuuszzzzz Date: Thu, 16 Jul 2026 12:53:05 +0200 Subject: [PATCH 14/18] Fix artifacts resolver test --- tests/unit/ArtifactsResolverTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/ArtifactsResolverTest.ts b/tests/unit/ArtifactsResolverTest.ts index 9ff5e0eff0d9..a160d8cdb143 100644 --- a/tests/unit/ArtifactsResolverTest.ts +++ b/tests/unit/ArtifactsResolverTest.ts @@ -102,7 +102,7 @@ describe('artifactsResolver', () => { it('resolves a matching version and does not build from source', async () => { mockResolveExec(); mockVersions(['0.85.3-nomatch', '0.85.3-match']); - mockFetchBodies(['differenthash', `${LOCAL_HASH}`]); + mockFetchBodies(['differentHash', `${LOCAL_HASH}`]); const result = await resolveArtifacts({platform: 'ios', packageName: 'react-hybrid', newDotRoot: NEW_DOT_ROOT, isHybrid: true}); From 2b2eaef6ce245392d9eeaa0f0013b25a40ac702d Mon Sep 17 00:00:00 2001 From: mateuuszzzzz Date: Thu, 16 Jul 2026 12:53:28 +0200 Subject: [PATCH 15/18] Fix ruby consumer to pin proper RN version --- scripts/artifacts-utils/ios/patched_ios_artifacts.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/artifacts-utils/ios/patched_ios_artifacts.rb b/scripts/artifacts-utils/ios/patched_ios_artifacts.rb index 7ccca7d63110..fec1448b7eb0 100644 --- a/scripts/artifacts-utils/ios/patched_ios_artifacts.rb +++ b/scripts/artifacts-utils/ios/patched_ios_artifacts.rb @@ -41,8 +41,11 @@ def self.resolve(package_name, is_hybrid) end class ReactNativeCoreUtils - def self.setup_rncore(react_native_path, _react_native_version) + def self.setup_rncore(react_native_path, react_native_version) @@react_native_path = react_native_path + # Base RN version (e.g. 0.85.3) — used by RN's install flow as a non-empty guard. The actual + # download URL uses @@patched_version via our stable_tarball_url override, so this stays the plain version. + @@react_native_version = react_native_version @@build_from_source = @@patched_build_from_source @@download_dsyms = ENV['RCT_SYMBOLICATE_PREBUILT_FRAMEWORKS'] == '1' end From fb074b6eb4db9877d026d5b87e5d78d31ce0b6a3 Mon Sep 17 00:00:00 2001 From: mateuuszzzzz Date: Thu, 16 Jul 2026 13:45:18 +0200 Subject: [PATCH 16/18] Restrict this PR to building iOS artifacts --- .../publishReactNativeAndroidArtifacts.yml | 4 +- .../publishReactNativeiOSArtifacts.yml | 4 +- .../workflows/verifyReactNativePatches.yml | 8 +- android/build.gradle | 2 +- android/settings.gradle | 2 +- gradleUtils/ExpensiLog.gradle | 26 + gradleUtils/PatchedArtifactsSettings.gradle | 176 ++ ios/NewExpensify.xcodeproj/project.pbxproj | 8 +- ios/Podfile | 9 - ios/Podfile.lock | 1982 +++++++++++++---- rock.config.mjs | 2 +- .../android/ExpensiUtils.gradle | 47 - .../android/PatchedArtifactsSettings.gradle | 42 - .../ios/patched_ios_artifacts.rb | 73 - .../artifacts-utils/lib/artifactsResolver.ts | 258 --- scripts/artifacts-utils/resolve-artifacts.ts | 31 - .../compute-patches-hash.sh | 12 +- scripts/pod-install.sh | 4 +- scripts/run-build.sh | 6 +- tests/unit/ArtifactsResolverTest.ts | 143 -- 20 files changed, 1819 insertions(+), 1020 deletions(-) create mode 100644 gradleUtils/ExpensiLog.gradle create mode 100644 gradleUtils/PatchedArtifactsSettings.gradle delete mode 100644 scripts/artifacts-utils/android/ExpensiUtils.gradle delete mode 100644 scripts/artifacts-utils/android/PatchedArtifactsSettings.gradle delete mode 100644 scripts/artifacts-utils/ios/patched_ios_artifacts.rb delete mode 100644 scripts/artifacts-utils/lib/artifactsResolver.ts delete mode 100644 scripts/artifacts-utils/resolve-artifacts.ts rename scripts/{artifacts-utils => }/compute-patches-hash.sh (64%) delete mode 100644 tests/unit/ArtifactsResolverTest.ts diff --git a/.github/workflows/publishReactNativeAndroidArtifacts.yml b/.github/workflows/publishReactNativeAndroidArtifacts.yml index 413643c504d5..b471e12a2997 100644 --- a/.github/workflows/publishReactNativeAndroidArtifacts.yml +++ b/.github/workflows/publishReactNativeAndroidArtifacts.yml @@ -59,9 +59,9 @@ jobs: id: computePatchesHash run: | if [[ '${{ matrix.is_hybrid }}' == 'true' ]]; then - echo "PATCHES_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" + echo "PATCHES_HASH=$(./scripts/compute-patches-hash.sh patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" else - echo "PATCHES_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh patches)" >> "$GITHUB_OUTPUT" + echo "PATCHES_HASH=$(./scripts/compute-patches-hash.sh patches)" >> "$GITHUB_OUTPUT" fi - name: Setup Node diff --git a/.github/workflows/publishReactNativeiOSArtifacts.yml b/.github/workflows/publishReactNativeiOSArtifacts.yml index 3d3987303df0..b4347a53e5d0 100644 --- a/.github/workflows/publishReactNativeiOSArtifacts.yml +++ b/.github/workflows/publishReactNativeiOSArtifacts.yml @@ -209,9 +209,9 @@ jobs: id: computePatchesHash run: | if [[ '${{ matrix.is_hybrid }}' == 'true' ]]; then - echo "PATCHES_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" + echo "PATCHES_HASH=$(./scripts/compute-patches-hash.sh patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" else - echo "PATCHES_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh patches)" >> "$GITHUB_OUTPUT" + echo "PATCHES_HASH=$(./scripts/compute-patches-hash.sh patches)" >> "$GITHUB_OUTPUT" fi - name: Determine new patched RN version diff --git a/.github/workflows/verifyReactNativePatches.yml b/.github/workflows/verifyReactNativePatches.yml index b6d02dfc54db..4485f8135ab0 100644 --- a/.github/workflows/verifyReactNativePatches.yml +++ b/.github/workflows/verifyReactNativePatches.yml @@ -53,8 +53,8 @@ jobs: if: ${{ inputs.ref_after != '' }} id: getOldPatchesHash run: | - echo "HYBRID_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" - echo "STANDALONE_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh patches)" >> "$GITHUB_OUTPUT" + echo "HYBRID_APP_HASH=$(./scripts/compute-patches-hash.sh patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" + echo "STANDALONE_APP_HASH=$(./scripts/compute-patches-hash.sh patches)" >> "$GITHUB_OUTPUT" - name: Get previous react-native version if: ${{ inputs.ref_after != '' }} @@ -71,8 +71,8 @@ jobs: - name: Get new patches hash id: getNewPatchesHash run: | - echo "HYBRID_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" - echo "STANDALONE_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh patches)" >> "$GITHUB_OUTPUT" + echo "HYBRID_APP_HASH=$(./scripts/compute-patches-hash.sh patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" + echo "STANDALONE_APP_HASH=$(./scripts/compute-patches-hash.sh patches)" >> "$GITHUB_OUTPUT" - name: Get new react-native version if: ${{ inputs.ref_after != '' }} diff --git a/android/build.gradle b/android/build.gradle index 7ab81991995f..6a970ae008aa 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -16,7 +16,7 @@ buildscript { // "mapbox" indicates the usage of the Mapbox SDK. RNMapboxMapsImpl = "mapbox" reactNativeIncludedBuild = gradle.getIncludedBuilds().find { it.name == 'react-native' } - // This is our custom extension that we defined in scripts/artifacts-utils/android/PatchedArtifactsSettings.gradle + // This is our custom extension that we defined in gradleUtils/PatchedArtifactsSettings.gradle // It enables us to use custom artifacts of patched react-native patchedArtifactsConfig = project.gradle.settings.extensions.findByName('patchedArtifacts') } diff --git a/android/settings.gradle b/android/settings.gradle index e1e1b28c975c..b41080ec1d7a 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -6,7 +6,7 @@ plugins { id("com.facebook.react.settings") id("expo-autolinking-settings") } -apply from: "${rootDir}/../scripts/artifacts-utils/android/PatchedArtifactsSettings.gradle" +apply from: "${rootDir}/../gradleUtils/PatchedArtifactsSettings.gradle" extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand(['npx', 'rock', 'config', '-p', 'android']) } rootProject.name = 'NewExpensify' diff --git a/gradleUtils/ExpensiLog.gradle b/gradleUtils/ExpensiLog.gradle new file mode 100644 index 000000000000..fcf15c98dc1a --- /dev/null +++ b/gradleUtils/ExpensiLog.gradle @@ -0,0 +1,26 @@ +ext.ExpensiLog = new Object() { + private final String BLUE = "\u001B[34m" + private final String YELLOW = "\u001B[33m" + private final String RED = "\u001B[31m" + private final String RESET = "\u001B[0m" + + private String prefix = "" + void setPrefix(String newPrefix) { this.prefix = newPrefix } + void clearPrefix() { this.prefix = "" } + + private String format(String color, String message) { + return "${color}[${prefix}] ${message}${this.RESET}" + } + + void lifecycle(String message) { + logger.lifecycle(format(this.BLUE, message)) + } + + void warn(String message) { + logger.warn(format(this.YELLOW, message)) + } + + void error(String message) { + logger.error(format(this.RED, message)) + } +} diff --git a/gradleUtils/PatchedArtifactsSettings.gradle b/gradleUtils/PatchedArtifactsSettings.gradle new file mode 100644 index 000000000000..066a70ef982a --- /dev/null +++ b/gradleUtils/PatchedArtifactsSettings.gradle @@ -0,0 +1,176 @@ +import groovy.xml.XmlSlurper +import groovy.json.JsonSlurper +import java.util.concurrent.TimeUnit + +apply from: "ExpensiLog.gradle" + +class PatchedArtifactsConfig { + String version = null + String packageName = null + String githubUsername = null + String githubToken = null + Boolean buildFromSource = true +} + +def patchedArtifacts = settings.getExtensions().create('patchedArtifacts', PatchedArtifactsConfig) + +def warnIfNotConfigured(reason) { + ExpensiLog.warn("$reason") + ExpensiLog.warn("For setup instructions, refer to: https://github.com/Expensify/App/blob/main/contributingGuides/SETUP_ANDROID.md#enabling-prebuilt-react-native-artifacts") +} + +def getNewDotRootDir() { + return "${rootDir}/${hasProperty('newDotRoot') ? getProperty('newDotRoot') : "/.."}" +} + +def runCommand(command) { + def process = command.execute() + process.waitFor(10, TimeUnit.SECONDS) + return [output: process.in.text?.trim(), exitCode: process.exitValue()] +} + +def hasGithubCLI() { + return runCommand(["which", "gh"]).exitCode == 0 +} + +def isCIEnvironment() { + return System.getenv("CI") != null +} + +def hasRequiredScopes() { + def scopes = runCommand(["gh", "auth", "status"]).output + return ['read:packages', 'write:packages'].any { scope -> scopes.contains(scope) } +} + +def getCIGithubActor() { + return System.getenv("GITHUB_ACTOR") +} + +def getCIGithubToken() { + return System.getenv("GITHUB_TOKEN") +} + +def hasRequiredCIEnvs() { + return getCIGithubActor() != null && getCIGithubToken() != null +} + +def getGithubUsername() { + if (isCIEnvironment()) { + return getCIGithubActor() + } + return new JsonSlurper().parseText(runCommand(["gh", "api", "user"]).output).login +} + +def getGithubToken() { + if (isCIEnvironment()) { + return getCIGithubToken() + } + return runCommand(["gh", "auth", "token"]).output +} + +def validateCredentialsContext() { + if (!hasGithubCLI()) { + return "No Github CLI found." + } + + if (isCIEnvironment() && !hasRequiredCIEnvs()) { + return "Missing required CI environment variables GITHUB_ACTOR and/or GITHUB_TOKEN." + } + + if (!isCIEnvironment() && !hasRequiredScopes()) { + return "Github token does not have required scope read:packages." + } + + return null +} + +def getCredentials() { + try { + def validationErrorMessage = validateCredentialsContext() + if (validationErrorMessage != null) { + warnIfNotConfigured(validationErrorMessage) + return null + } + + return [ + githubUsername: getGithubUsername(), + githubToken: getGithubToken(), + ] + } catch (Exception e) { + warnIfNotConfigured("Failed to get Github credentials. This might be due to an expired token or not being logged in.") + return null + } +} + +def getReactNativeVersion() { + def packageJsonPath = "${getNewDotRootDir()}/package.json" + def packageJson = file(packageJsonPath) + return new JsonSlurper().parse(packageJson).dependencies.'react-native' +} + +def getArtifactsCandidates(packageName) { + def reactNativeVersion = getReactNativeVersion() + def patchedVersions = runCommand(["gh", "api", "/orgs/Expensify/packages/maven/com.expensify.${packageName}.react-android/versions", "--jq", ".[].name"]).output + return patchedVersions.split("\n").findAll { it.startsWith(reactNativeVersion) } +} + +def getPomFile(version, packageName, githubToken) { + return new XmlSlurper().parse( + new URL("https://maven.pkg.github.com/Expensify/App/com/expensify/${packageName}/react-android/${version}/react-android-${version}.pom").openConnection().with { + setRequestProperty("Authorization", "Bearer ${githubToken}") + getInputStream() + } + ) +} + +def getLocalPatchesHash() { + def newDotRootDir = getNewDotRootDir() + def computePatchesScript = "${getNewDotRootDir()}/scripts/compute-patches-hash.sh" + def arguments = hasProperty('newDotRoot') ? ["${newDotRootDir}/patches", "${newDotRootDir}/Mobile-Expensify/patches"] : ["${newDotRootDir}/patches"] + def localPatchesHash = runCommand([computePatchesScript] + arguments).output.trim() + + return localPatchesHash +} + +def findMatchingArtifactsVersion(packageName, githubToken) { + try { + def localPatchesHash = getLocalPatchesHash() + def candidates = getArtifactsCandidates(packageName) + for (candidate in candidates) { + def patchesHashFromPomFile = getPomFile(candidate, packageName, githubToken).properties.patchesHash.text().trim() + if (patchesHashFromPomFile == localPatchesHash) { + return candidate + } + } + } catch (Exception e) { + ExpensiLog.error("Failed to find matching artifacts version for ${packageName}. Reason: $e") + return null + } +} + +settings.extensions.configure(PatchedArtifactsConfig) { config -> + ExpensiLog.setPrefix("PatchedArtifacts") + config.buildFromSource = getProperty('patchedArtifacts.forceBuildFromSource') == 'true' + if(config.buildFromSource) { + ExpensiLog.lifecycle("Forcing build from source.") + return + } + + def credentials = getCredentials() + config.packageName = getProperty('patchedArtifacts.packageName') + + if(credentials != null) { + config.githubUsername = credentials.githubUsername + config.githubToken = credentials.githubToken + config.version = findMatchingArtifactsVersion(config.packageName, config.githubToken) + } + + if(config.version == null || config.packageName == null || config.githubUsername == null || config.githubToken == null) { + config.buildFromSource = true + ExpensiLog.error("No matching artifacts version found for ${config.packageName}. Building react-native from source.") + } + else { + config.buildFromSource = false + ExpensiLog.lifecycle("Using patched react-native artifacts: ${config.packageName}:${config.version}") + } +} diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index 668366eb685a..061c25eb409c 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -695,7 +695,6 @@ "${PODS_XCFRAMEWORKS_BUILD_DIR}/MapboxCoreMaps/MapboxCoreMaps.framework/MapboxCoreMaps", "${PODS_XCFRAMEWORKS_BUILD_DIR}/Onfido/Onfido.framework/Onfido", "${PODS_XCFRAMEWORKS_BUILD_DIR}/Plaid/LinkKit.framework/LinkKit", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/ReactNativeDependencies/ReactNativeDependencies.framework/ReactNativeDependencies", "${PODS_XCFRAMEWORKS_BUILD_DIR}/Turf/Turf.framework/Turf", "${PODS_XCFRAMEWORKS_BUILD_DIR}/group-ib-fp/GIBMobileSdk.framework/GIBMobileSdk", "${PODS_XCFRAMEWORKS_BUILD_DIR}/group-ib-fp/FPAppsCapability.framework/FPAppsCapability", @@ -711,7 +710,6 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MapboxCoreMaps.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Onfido.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/LinkKit.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactNativeDependencies.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Turf.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GIBMobileSdk.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FPAppsCapability.framework", @@ -810,6 +808,7 @@ "${PODS_CONFIGURATION_BUILD_DIR}/GoogleSignIn/GoogleSignIn.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/GoogleUtilities/GoogleUtilities_Privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/RNPermissions/RNPermissionsPrivacyInfo.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle", @@ -817,6 +816,8 @@ "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/Sentry/Sentry.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/TrustKit/TrustKit.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/lottie-ios/LottiePrivacyInfo.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/lottie-react-native/Lottie_React_Native_Privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/nanopb/nanopb_Privacy.bundle", @@ -840,6 +841,7 @@ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleUtilities_Privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNPermissionsPrivacyInfo.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle", @@ -847,6 +849,8 @@ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Sentry.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/TrustKit.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/LottiePrivacyInfo.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Lottie_React_Native_Privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/nanopb_Privacy.bundle", diff --git a/ios/Podfile b/ios/Podfile index 9bb6d570364d..eae847823bae 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -16,15 +16,6 @@ end node_require('react-native/scripts/react_native_pods.rb') node_require('react-native-permissions/scripts/setup.rb') -# Use prebuilt React Native Dependencies and Core by default. Set either to '0' to build from source. -ENV['RCT_USE_RN_DEP'] = '1' -ENV['RCT_USE_PREBUILT_RNCORE'] = '1' - -# Consume our patched React Native core prebuilt artifacts. Must be required after -# react_native_pods.rb (which defines ReactNativeCoreUtils); setup runs before use_react_native! below. -require_relative '../scripts/artifacts-utils/ios/patched_ios_artifacts.rb' -PatchedIOSArtifacts.setup - MIN_IOS_VERSION = '16.4' platform :ios, MIN_IOS_VERSION prepare_react_native_project! diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b97afd7f61e4..58f1e7dcf7eb 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -27,10 +27,19 @@ PODS: - AppAuth/ExternalUserAgent (1.7.6): - AppAuth/Core - AppLogs (0.1.0) + - boost (1.84.0) + - DoubleConversion (1.1.6) - EXConstants (56.0.16): - ExpoModulesCore - expensify-react-native-background-task (0.0.0): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -48,11 +57,18 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - ExpensifyNitroUtils (0.0.1): + - boost + - DoubleConversion + - fast_float + - fmt + - glog - hermes-engine - NitroModules + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -70,12 +86,19 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - Expo (56.0.8): + - boost + - DoubleConversion - ExpoModulesCore - ExpoModulesJSI + - fast_float + - fmt + - glog - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -95,7 +118,7 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - ExpoAsset (56.0.15): - ExpoModulesCore @@ -120,8 +143,15 @@ PODS: - ExpoLogBox (56.0.12): - React-Core - ExpoModulesCore (56.0.14): + - boost + - DoubleConversion - ExpoModulesJSI + - fast_float + - fmt + - glog - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -140,7 +170,7 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - ExpoModulesJSI (56.0.7): - React-Core @@ -162,6 +192,7 @@ PODS: - ExpoModulesCore - ExpoWebBrowser (56.0.5): - ExpoModulesCore + - fast_float (8.0.0) - FBLazyVector (0.85.3) - Firebase/Analytics (11.13.0): - Firebase/Core @@ -199,11 +230,19 @@ PODS: - GoogleUtilities/Environment (~> 8.1) - GoogleUtilities/UserDefaults (~> 8.1) - PromisesObjC (~> 2.4) + - fmt (12.1.0) - ForkInputMask (7.3.3) - FullStory (1.70.1) - fullstory_react-native (1.9.0): + - boost + - DoubleConversion + - fast_float + - fmt - FullStory (~> 1.14) + - glog - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -221,8 +260,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga + - glog (0.3.5) - GoogleAppMeasurement (11.13.0): - GoogleAppMeasurement/AdIdSupport (= 11.13.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.1) @@ -275,7 +315,14 @@ PODS: - GoogleUtilities/Logger - GoogleUtilities/Privacy - group-ib-fp (2.6.0): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -293,7 +340,7 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - GTMAppAuth (4.1.1): - AppAuth/Core (~> 1.7) @@ -322,8 +369,15 @@ PODS: - libwebp/sharpyuv - lottie-ios (4.6.0) - lottie-react-native (7.3.8): + - boost + - DoubleConversion + - fast_float + - fmt + - glog - hermes-engine - lottie-ios (= 4.6.0) + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -341,7 +395,7 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - MapboxCommon (24.23.1): - Turf (= 4.0.0) @@ -357,7 +411,14 @@ PODS: - nanopb/decode (3.30910.0) - nanopb/encode (3.30910.0) - NitroModules (0.35.0): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-callinvoker @@ -376,7 +437,7 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - NWWebSocket (0.5.7) - Onfido (32.6.2) @@ -391,6 +452,25 @@ PODS: - PusherSwift (10.1.6): - NWWebSocket (~> 0.5.7) - TweetNacl (~> 1.0.0) + - RCT-Folly (2024.11.18.00): + - boost + - DoubleConversion + - fast_float (= 8.0.0) + - fmt (= 12.1.0) + - glog + - RCT-Folly/Default (= 2024.11.18.00) + - RCT-Folly/Default (2024.11.18.00): + - boost + - DoubleConversion + - fast_float (= 8.0.0) + - fmt (= 12.1.0) + - glog + - RCT-Folly/Fabric (2024.11.18.00): + - boost + - DoubleConversion + - fast_float (= 8.0.0) + - fmt (= 12.1.0) + - glog - RCTDeprecation (0.85.3) - RCTRequired (0.85.3) - RCTSwiftUI (0.85.3) @@ -415,7 +495,14 @@ PODS: - React-RCTVibration (= 0.85.3) - React-callinvoker (0.85.3) - React-Core (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTDeprecation - React-Core/Default (= 0.85.3) - React-cxxreact @@ -427,13 +514,21 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger + - React-rendererconsistency - React-runtimeexecutor - React-runtimescheduler - React-utils - - ReactNativeDependencies + - SocketRocket - Yoga - React-Core/CoreModulesHeaders (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTDeprecation - React-Core/Default - React-cxxreact @@ -445,13 +540,21 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger + - React-rendererconsistency - React-runtimeexecutor - React-runtimescheduler - React-utils - - ReactNativeDependencies + - SocketRocket - Yoga - React-Core/Default (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTDeprecation - React-cxxreact - React-featureflags @@ -462,13 +565,21 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger + - React-rendererconsistency - React-runtimeexecutor - React-runtimescheduler - React-utils - - ReactNativeDependencies + - SocketRocket - Yoga - React-Core/DevSupport (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTDeprecation - React-Core/Default (= 0.85.3) - React-Core/RCTWebSocket (= 0.85.3) @@ -481,13 +592,21 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger + - React-rendererconsistency - React-runtimeexecutor - React-runtimescheduler - React-utils - - ReactNativeDependencies + - SocketRocket - Yoga - React-Core/RCTActionSheetHeaders (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTDeprecation - React-Core/Default - React-cxxreact @@ -499,13 +618,21 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger + - React-rendererconsistency - React-runtimeexecutor - React-runtimescheduler - React-utils - - ReactNativeDependencies + - SocketRocket - Yoga - React-Core/RCTAnimationHeaders (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTDeprecation - React-Core/Default - React-cxxreact @@ -517,13 +644,21 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger + - React-rendererconsistency - React-runtimeexecutor - React-runtimescheduler - React-utils - - ReactNativeDependencies + - SocketRocket - Yoga - React-Core/RCTBlobHeaders (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTDeprecation - React-Core/Default - React-cxxreact @@ -535,13 +670,21 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger + - React-rendererconsistency - React-runtimeexecutor - React-runtimescheduler - React-utils - - ReactNativeDependencies + - SocketRocket - Yoga - React-Core/RCTImageHeaders (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTDeprecation - React-Core/Default - React-cxxreact @@ -553,13 +696,21 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger + - React-rendererconsistency - React-runtimeexecutor - React-runtimescheduler - React-utils - - ReactNativeDependencies + - SocketRocket - Yoga - React-Core/RCTLinkingHeaders (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTDeprecation - React-Core/Default - React-cxxreact @@ -571,13 +722,21 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger + - React-rendererconsistency - React-runtimeexecutor - React-runtimescheduler - React-utils - - ReactNativeDependencies + - SocketRocket - Yoga - React-Core/RCTNetworkHeaders (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTDeprecation - React-Core/Default - React-cxxreact @@ -589,13 +748,21 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger + - React-rendererconsistency - React-runtimeexecutor - React-runtimescheduler - React-utils - - ReactNativeDependencies + - SocketRocket - Yoga - React-Core/RCTSettingsHeaders (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTDeprecation - React-Core/Default - React-cxxreact @@ -607,13 +774,21 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger + - React-rendererconsistency - React-runtimeexecutor - React-runtimescheduler - React-utils - - ReactNativeDependencies + - SocketRocket - Yoga - React-Core/RCTTextHeaders (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTDeprecation - React-Core/Default - React-cxxreact @@ -625,13 +800,21 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger + - React-rendererconsistency - React-runtimeexecutor - React-runtimescheduler - React-utils - - ReactNativeDependencies + - SocketRocket - Yoga - React-Core/RCTVibrationHeaders (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTDeprecation - React-Core/Default - React-cxxreact @@ -643,13 +826,21 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger + - React-rendererconsistency - React-runtimeexecutor - React-runtimescheduler - React-utils - - ReactNativeDependencies + - SocketRocket - Yoga - React-Core/RCTWebSocket (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTDeprecation - React-Core/Default (= 0.85.3) - React-cxxreact @@ -661,12 +852,20 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger + - React-rendererconsistency - React-runtimeexecutor - React-runtimescheduler - React-utils - - ReactNativeDependencies + - SocketRocket - Yoga - React-CoreModules (0.85.3): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - RCT-Folly + - RCT-Folly/Fabric - RCTTypeSafety (= 0.85.3) - React-Core/CoreModulesHeaders (= 0.85.3) - React-debug @@ -682,9 +881,16 @@ PODS: - React-runtimeexecutor - React-utils - ReactCommon - - ReactNativeDependencies + - SocketRocket - React-cxxreact (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - React-callinvoker (= 0.85.3) - React-debug (= 0.85.3) - React-jsi (= 0.85.3) @@ -696,12 +902,19 @@ PODS: - React-runtimeexecutor - React-timing (= 0.85.3) - React-utils - - ReactNativeDependencies + - SocketRocket - React-debug (0.85.3): - React-debug/redbox (= 0.85.3) - React-debug/redbox (0.85.3) - React-defaultsnativemodule (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - React-domnativemodule - React-Fabric/animated - React-featureflags @@ -714,10 +927,17 @@ PODS: - React-mutationobservernativemodule - React-RCTFBReactNativeSpec - React-webperformancenativemodule - - ReactNativeDependencies + - SocketRocket - Yoga - React-domnativemodule (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - React-Fabric - React-Fabric/bridging - React-FabricComponents @@ -727,10 +947,17 @@ PODS: - React-RCTFBReactNativeSpec - React-runtimeexecutor - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - React-Fabric (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -764,9 +991,16 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - React-Fabric/animated (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -783,9 +1017,16 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - React-Fabric/animationbackend (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -801,9 +1042,16 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - React-Fabric/animations (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -819,9 +1067,16 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - React-Fabric/attributedstring (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -837,9 +1092,16 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - React-Fabric/bridging (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -855,9 +1117,16 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - React-Fabric/componentregistry (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -873,9 +1142,16 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - React-Fabric/componentregistrynative (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -891,9 +1167,16 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - React-Fabric/components (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -913,9 +1196,16 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - React-Fabric/components/legacyviewmanagerinterop (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -931,9 +1221,16 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - React-Fabric/components/root (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -949,9 +1246,16 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - React-Fabric/components/scrollview (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -967,9 +1271,16 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - React-Fabric/components/view (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -986,10 +1297,17 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - React-Fabric/consistency (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1005,9 +1323,16 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - React-Fabric/core (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1023,9 +1348,16 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - React-Fabric/dom (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1041,9 +1373,16 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - React-Fabric/imagemanager (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1059,9 +1398,16 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - React-Fabric/leakchecker (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1077,9 +1423,16 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - React-Fabric/mounting (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1095,9 +1448,16 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - React-Fabric/observers (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1116,9 +1476,16 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - React-Fabric/observers/events (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1134,9 +1501,16 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - React-Fabric/observers/intersection (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1152,9 +1526,16 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - React-Fabric/observers/mutation (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1170,9 +1551,16 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - React-Fabric/scheduler (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1192,9 +1580,16 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - React-Fabric/telemetry (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1210,9 +1605,16 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - React-Fabric/uimanager (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1230,9 +1632,16 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - React-Fabric/uimanager/consistency (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1249,9 +1658,16 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - React-FabricComponents (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1270,10 +1686,17 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - React-FabricComponents/components (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1301,10 +1724,17 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - React-FabricComponents/components/inputaccessory (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1321,10 +1751,17 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - React-FabricComponents/components/iostextinput (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1341,10 +1778,17 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - React-FabricComponents/components/modal (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1361,10 +1805,17 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - React-FabricComponents/components/rncore (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1381,10 +1832,17 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - React-FabricComponents/components/safeareaview (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1401,10 +1859,17 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - React-FabricComponents/components/scrollview (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1421,10 +1886,17 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - React-FabricComponents/components/switch (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1441,10 +1913,17 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - React-FabricComponents/components/text (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1461,10 +1940,17 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - React-FabricComponents/components/textinput (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1481,10 +1967,17 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - React-FabricComponents/components/unimplementedview (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1501,10 +1994,17 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - React-FabricComponents/components/virtualview (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1521,10 +2021,17 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - React-FabricComponents/textlayoutmanager (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1541,10 +2048,17 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - React-FabricImage (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired (= 0.85.3) - RCTTypeSafety (= 0.85.3) - React-Fabric @@ -1557,27 +2071,55 @@ PODS: - React-rendererdebug - React-utils - ReactCommon - - ReactNativeDependencies + - SocketRocket - Yoga - React-featureflags (0.85.3): - - ReactNativeDependencies + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - RCT-Folly + - RCT-Folly/Fabric + - SocketRocket - React-featureflagsnativemodule (0.85.3): + - boost + - DoubleConversion + - fast_float + - fmt + - glog - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - React-featureflags - React-jsi - React-jsiexecutor - React-RCTFBReactNativeSpec - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - React-graphics (0.85.3): + - boost + - DoubleConversion + - fast_float + - fmt + - glog - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - React-featureflags - React-jsi - React-jsiexecutor - React-utils - - ReactNativeDependencies + - SocketRocket - React-hermes (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - React-cxxreact (= 0.85.3) - React-jsi - React-jsiexecutor (= 0.85.3) @@ -1588,26 +2130,47 @@ PODS: - React-oscompat - React-perflogger (= 0.85.3) - React-runtimeexecutor - - ReactNativeDependencies + - SocketRocket - React-idlecallbacksnativemodule (0.85.3): + - boost + - DoubleConversion + - fast_float + - fmt + - glog - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - React-jsi - React-jsiexecutor - React-RCTFBReactNativeSpec - React-runtimeexecutor - React-runtimescheduler - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - React-ImageManager (0.85.3): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - RCT-Folly + - RCT-Folly/Fabric - React-Core/Default - React-debug - React-Fabric - React-graphics - React-rendererdebug - React-utils - - ReactNativeDependencies + - SocketRocket - React-intersectionobservernativemodule (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - React-cxxreact - React-Fabric - React-Fabric/bridging @@ -1618,21 +2181,42 @@ PODS: - React-runtimeexecutor - React-runtimescheduler - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - React-jserrorhandler (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - React-cxxreact - React-debug - React-featureflags - React-jsi - ReactCommon/turbomodule/bridging - - ReactNativeDependencies + - SocketRocket - React-jsi (0.85.3): - - hermes-engine - - ReactNativeDependencies + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - SocketRocket - React-jsiexecutor (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - React-cxxreact - React-debug - React-jserrorhandler @@ -1644,9 +2228,16 @@ PODS: - React-perflogger - React-runtimeexecutor - React-utils - - ReactNativeDependencies + - SocketRocket - React-jsinspector (0.85.3): + - boost + - DoubleConversion + - fast_float + - fmt + - glog - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - React-featureflags - React-jsi - React-jsinspectorcdp @@ -1656,22 +2247,50 @@ PODS: - React-perflogger (= 0.85.3) - React-runtimeexecutor - React-utils - - ReactNativeDependencies + - SocketRocket - React-jsinspectorcdp (0.85.3): - - ReactNativeDependencies + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - RCT-Folly + - RCT-Folly/Fabric + - SocketRocket - React-jsinspectornetwork (0.85.3): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - RCT-Folly + - RCT-Folly/Fabric - React-jsinspectorcdp - - ReactNativeDependencies + - SocketRocket - React-jsinspectortracing (0.85.3): + - boost + - DoubleConversion + - fast_float + - fmt + - glog - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - React-jsi - React-jsinspectornetwork - React-oscompat - React-timing - React-utils - - ReactNativeDependencies + - SocketRocket - React-jsitooling (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - React-cxxreact (= 0.85.3) - React-debug - React-jsi (= 0.85.3) @@ -1680,23 +2299,51 @@ PODS: - React-jsinspectortracing - React-runtimeexecutor - React-utils - - ReactNativeDependencies + - SocketRocket - React-jsitracing (0.85.3): - React-jsi - React-logger (0.85.3): - - ReactNativeDependencies + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - RCT-Folly + - RCT-Folly/Fabric + - SocketRocket - React-Mapbuffer (0.85.3): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - RCT-Folly + - RCT-Folly/Fabric - React-debug - - ReactNativeDependencies + - SocketRocket - React-microtasksnativemodule (0.85.3): + - boost + - DoubleConversion + - fast_float + - fmt + - glog - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - React-jsi - React-jsiexecutor - React-RCTFBReactNativeSpec - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - React-mutationobservernativemodule (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - React-cxxreact - React-Fabric - React-Fabric/bridging @@ -1707,11 +2354,18 @@ PODS: - React-RCTFBReactNativeSpec - React-runtimeexecutor - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - react-native-advanced-input-mask (1.4.6): + - boost + - DoubleConversion + - fast_float + - fmt - ForkInputMask (~> 7.3.2) + - glog - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1729,11 +2383,18 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - react-native-airship (26.5.0): - AirshipFrameworkProxy (= 15.8.0) - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1751,10 +2412,17 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - react-native-app-logs (0.3.1): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1772,10 +2440,17 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - react-native-blob-util (0.24.9): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1793,10 +2468,17 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - react-native-cameraroll (7.4.0): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1814,12 +2496,19 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - react-native-config (1.5.3): - react-native-config/App (= 1.5.3) - react-native-config/App (1.5.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1837,10 +2526,17 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - react-native-document-picker (10.1.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1858,10 +2554,17 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - react-native-image-picker (7.1.2): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1879,10 +2582,17 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - react-native-key-command (1.0.14): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1900,10 +2610,17 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - react-native-keyboard-controller (1.21.0-beta.1): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1922,10 +2639,17 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - react-native-keyboard-controller/common (1.21.0-beta.1): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1943,10 +2667,17 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - react-native-netinfo (11.4.1): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1964,10 +2695,17 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - react-native-pager-view (8.0.0): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -1985,11 +2723,18 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - SwiftUIIntrospect (~> 1.0) - Yoga - react-native-pdf (7.0.2): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -2007,11 +2752,18 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - react-native-plaid-link-sdk (12.5.3): + - boost + - DoubleConversion + - fast_float + - fmt + - glog - hermes-engine - Plaid (~> 6.4.0) + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -2029,10 +2781,17 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - react-native-safe-area-context (5.6.2): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -2052,10 +2811,17 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - react-native-safe-area-context/common (5.6.2): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -2073,10 +2839,17 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - react-native-safe-area-context/fabric (5.6.2): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -2095,10 +2868,17 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - react-native-skia (2.4.14): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React @@ -2118,10 +2898,17 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - react-native-view-shot (5.1.0): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -2139,10 +2926,17 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - react-native-wallet (0.1.22): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -2160,10 +2954,17 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - react-native-webview (13.16.0): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -2181,10 +2982,17 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - React-NativeModulesApple (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - React-callinvoker - React-Core - React-cxxreact @@ -2196,33 +3004,68 @@ PODS: - React-runtimeexecutor - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - React-networking (0.85.3): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - RCT-Folly + - RCT-Folly/Fabric - React-jsinspectornetwork - React-jsinspectortracing - React-performancetimeline - React-timing - - ReactNativeDependencies + - SocketRocket - React-oscompat (0.85.3) - React-perflogger (0.85.3): - - ReactNativeDependencies + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - RCT-Folly + - RCT-Folly/Fabric + - SocketRocket - React-performancecdpmetrics (0.85.3): + - boost + - DoubleConversion + - fast_float + - fmt + - glog - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - React-jsi - React-performancetimeline - React-runtimeexecutor - React-timing - - ReactNativeDependencies + - SocketRocket - React-performancetimeline (0.85.3): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - RCT-Folly + - RCT-Folly/Fabric - React-featureflags - React-jsinspector - React-jsinspectortracing - React-perflogger - React-timing - - ReactNativeDependencies + - SocketRocket - React-RCTActionSheet (0.85.3): - React-Core/RCTActionSheetHeaders (= 0.85.3) - React-RCTAnimation (0.85.3): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - RCT-Folly + - RCT-Folly/Fabric - RCTTypeSafety - React-Core/RCTAnimationHeaders - React-debug @@ -2231,9 +3074,16 @@ PODS: - React-NativeModulesApple - React-RCTFBReactNativeSpec - ReactCommon - - ReactNativeDependencies + - SocketRocket - React-RCTAppDelegate (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -2258,9 +3108,16 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon - - ReactNativeDependencies + - SocketRocket - React-RCTBlob (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - React-Core/RCTBlobHeaders - React-Core/RCTWebSocket - React-jsi @@ -2270,9 +3127,16 @@ PODS: - React-RCTFBReactNativeSpec - React-RCTNetwork - ReactCommon - - ReactNativeDependencies + - SocketRocket - React-RCTFabric (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTSwiftUIWrapper - React-Core - React-debug @@ -2299,10 +3163,17 @@ PODS: - React-runtimeexecutor - React-runtimescheduler - React-utils - - ReactNativeDependencies + - SocketRocket - Yoga - React-RCTFBReactNativeSpec (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -2310,9 +3181,16 @@ PODS: - React-NativeModulesApple - React-RCTFBReactNativeSpec/components (= 0.85.3) - ReactCommon - - ReactNativeDependencies + - SocketRocket - React-RCTFBReactNativeSpec/components (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -2325,9 +3203,16 @@ PODS: - React-rendererdebug - React-utils - ReactCommon - - ReactNativeDependencies + - SocketRocket - Yoga - React-RCTImage (0.85.3): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - RCT-Folly + - RCT-Folly/Fabric - RCTTypeSafety - React-Core/RCTImageHeaders - React-jsi @@ -2335,7 +3220,7 @@ PODS: - React-RCTFBReactNativeSpec - React-RCTNetwork - ReactCommon - - ReactNativeDependencies + - SocketRocket - React-RCTLinking (0.85.3): - React-Core/RCTLinkingHeaders (= 0.85.3) - React-jsi (= 0.85.3) @@ -2344,6 +3229,13 @@ PODS: - ReactCommon - ReactCommon/turbomodule/core (= 0.85.3) - React-RCTNetwork (0.85.3): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - RCT-Folly + - RCT-Folly/Fabric - RCTTypeSafety - React-Core/RCTNetworkHeaders - React-debug @@ -2355,9 +3247,16 @@ PODS: - React-networking - React-RCTFBReactNativeSpec - ReactCommon - - ReactNativeDependencies + - SocketRocket - React-RCTRuntime (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - React-Core - React-debug - React-jsi @@ -2370,34 +3269,62 @@ PODS: - React-runtimeexecutor - React-RuntimeHermes - React-utils - - ReactNativeDependencies + - SocketRocket - React-RCTSettings (0.85.3): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - RCT-Folly + - RCT-Folly/Fabric - RCTTypeSafety - React-Core/RCTSettingsHeaders - React-jsi - React-NativeModulesApple - React-RCTFBReactNativeSpec - ReactCommon - - ReactNativeDependencies + - SocketRocket - React-RCTText (0.85.3): - React-Core/RCTTextHeaders (= 0.85.3) - Yoga - React-RCTVibration (0.85.3): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - RCT-Folly + - RCT-Folly/Fabric - React-Core/RCTVibrationHeaders - React-jsi - React-NativeModulesApple - React-RCTFBReactNativeSpec - ReactCommon - - ReactNativeDependencies + - SocketRocket - React-rendererconsistency (0.85.3) - React-renderercss (0.85.3): - React-debug - React-utils - React-rendererdebug (0.85.3): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - RCT-Folly + - RCT-Folly/Fabric - React-debug - - ReactNativeDependencies + - SocketRocket - React-RuntimeApple (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - React-callinvoker - React-Core/Default - React-CoreModules @@ -2417,9 +3344,16 @@ PODS: - React-RuntimeHermes - React-runtimescheduler - React-utils - - ReactNativeDependencies + - SocketRocket - React-RuntimeCore (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - React-cxxreact - React-Fabric - React-featureflags @@ -2432,15 +3366,29 @@ PODS: - React-runtimeexecutor - React-runtimescheduler - React-utils - - ReactNativeDependencies + - SocketRocket - React-runtimeexecutor (0.85.3): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - RCT-Folly + - RCT-Folly/Fabric - React-debug - React-featureflags - React-jsi (= 0.85.3) - React-utils - - ReactNativeDependencies + - SocketRocket - React-RuntimeHermes (0.85.3): + - boost + - DoubleConversion + - fast_float + - fmt + - glog - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - React-featureflags - React-hermes - React-jsi @@ -2452,9 +3400,16 @@ PODS: - React-RuntimeCore - React-runtimeexecutor - React-utils - - ReactNativeDependencies + - SocketRocket - React-runtimescheduler (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - React-callinvoker - React-cxxreact - React-debug @@ -2467,16 +3422,30 @@ PODS: - React-runtimeexecutor - React-timing - React-utils - - ReactNativeDependencies + - SocketRocket - React-timing (0.85.3): - React-debug - React-utils (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - React-debug - React-jsi (= 0.85.3) - - ReactNativeDependencies + - SocketRocket - React-webperformancenativemodule (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - React-cxxreact - React-jsi - React-jsiexecutor @@ -2484,11 +3453,18 @@ PODS: - React-RCTFBReactNativeSpec - React-runtimeexecutor - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - ReactAppDependencyProvider (0.85.3): - ReactCodegen - ReactCodegen (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -2505,12 +3481,26 @@ PODS: - React-utils - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - ReactCommon (0.85.3): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - RCT-Folly + - RCT-Folly/Fabric - ReactCommon/turbomodule (= 0.85.3) - - ReactNativeDependencies + - SocketRocket - ReactCommon/turbomodule (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - React-callinvoker (= 0.85.3) - React-cxxreact (= 0.85.3) - React-jsi (= 0.85.3) @@ -2518,17 +3508,31 @@ PODS: - React-perflogger (= 0.85.3) - ReactCommon/turbomodule/bridging (= 0.85.3) - ReactCommon/turbomodule/core (= 0.85.3) - - ReactNativeDependencies + - SocketRocket - ReactCommon/turbomodule/bridging (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - React-callinvoker (= 0.85.3) - React-cxxreact (= 0.85.3) - React-jsi (= 0.85.3) - React-logger (= 0.85.3) - React-perflogger (= 0.85.3) - - ReactNativeDependencies + - SocketRocket - ReactCommon/turbomodule/core (0.85.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - React-callinvoker (= 0.85.3) - React-cxxreact (= 0.85.3) - React-debug (= 0.85.3) @@ -2537,9 +3541,16 @@ PODS: - React-logger (= 0.85.3) - React-perflogger (= 0.85.3) - React-utils (= 0.85.3) - - ReactNativeDependencies + - SocketRocket - ReactNativeBiometrics (0.15.0): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -2557,11 +3568,17 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - - ReactNativeDependencies (0.85.3) - ReactNativeHybridApp (0.0.0): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -2579,12 +3596,19 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - RNAppleAuthentication (2.5.0): - React-Core - RNCClipboard (1.16.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -2602,10 +3626,17 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - RNCPicker (2.11.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -2623,10 +3654,17 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - RNDeviceInfo (10.3.1): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -2644,7 +3682,7 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - RNFBAnalytics (22.2.1): - Firebase/Analytics (= 11.13.0) @@ -2656,7 +3694,14 @@ PODS: - RNFS (2.20.0): - React-Core - RNGestureHandler (2.28.0): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -2675,13 +3720,20 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - RNLiveMarkdown (0.1.329): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -2701,11 +3753,18 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies - RNWorklets + - SocketRocket - Yoga - RNLocalize (3.5.4): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -2723,7 +3782,7 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - rnmapbox-maps (10.3.2): - MapboxMaps (~> 11.23.1) @@ -2732,8 +3791,14 @@ PODS: - rnmapbox-maps/DynamicLibrary (= 10.3.2) - Turf - rnmapbox-maps/DynamicLibrary (10.3.2): + - boost + - DoubleConversion + - fast_float + - fmt - hermes-engine - MapboxMaps (~> 11.23.1) + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React @@ -2748,12 +3813,19 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Turf - Yoga - RNNitroSQLite (9.6.0): + - boost + - DoubleConversion + - fast_float + - fmt + - glog - hermes-engine - NitroModules + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -2771,10 +3843,17 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - RNPermissions (5.4.0): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -2792,10 +3871,17 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - RNReactNativeHapticFeedback (2.3.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -2813,10 +3899,17 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - RNReanimated (4.3.1): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -2835,13 +3928,20 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies - RNReanimated/apple (= 4.3.1) - RNReanimated/common (= 4.3.1) - RNWorklets + - SocketRocket - Yoga - RNReanimated/apple (4.3.1): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -2860,11 +3960,18 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies - RNWorklets + - SocketRocket - Yoga - RNReanimated/common (4.3.1): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -2883,11 +3990,18 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies - RNWorklets + - SocketRocket - Yoga - RNScreens (4.25.0): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -2906,11 +4020,18 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies - RNScreens/common (= 4.25.0) + - SocketRocket - Yoga - RNScreens/common (4.25.0): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -2929,10 +4050,17 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - RNSentry (8.2.0): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -2951,11 +4079,18 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies - Sentry (= 9.5.1) + - SocketRocket - Yoga - RNShare (11.0.2): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -2973,10 +4108,17 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - RNSVG (15.15.5): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -2994,11 +4136,18 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies - RNSVG/common (= 15.15.5) + - SocketRocket - Yoga - RNSVG/common (15.15.5): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -3016,10 +4165,17 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - RNWorklets (0.8.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -3038,12 +4194,19 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies - RNWorklets/apple (= 0.8.3) - RNWorklets/common (= 0.8.3) + - SocketRocket - Yoga - RNWorklets/apple (0.8.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -3062,10 +4225,17 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - RNWorklets/common (0.8.3): - - hermes-engine + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric - RCTRequired - RCTTypeSafety - React-Core @@ -3084,7 +4254,7 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactNativeDependencies + - SocketRocket - Yoga - SDWebImage (5.21.3): - SDWebImage/Core (= 5.21.3) @@ -3100,6 +4270,7 @@ PODS: - Sentry (9.5.1): - Sentry/Core (= 9.5.1) - Sentry/Core (9.5.1) + - SocketRocket (0.7.1) - SwiftUIIntrospect (1.3.0) - TrustKit (3.0.7) - Turf (4.0.0) @@ -3116,6 +4287,8 @@ PODS: DEPENDENCIES: - AirshipServiceExtension - AppLogs (from `../node_modules/react-native-app-logs/AppLogsPod`) + - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) + - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - EXConstants (from `../node_modules/expo-constants/ios`) - "expensify-react-native-background-task (from `../node_modules/@expensify/react-native-background-task`)" - "ExpensifyNitroUtils (from `../node_modules/@expensify/nitro-utils`)" @@ -3136,9 +4309,12 @@ DEPENDENCIES: - ExpoTaskManager (from `../node_modules/expo-task-manager/ios`) - ExpoVideo (from `../node_modules/expo-video/ios`) - ExpoWebBrowser (from `../node_modules/expo-web-browser/ios`) + - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) + - fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`) - "FullStory (from `{http: \"https://ios-releases.fullstory.com/fullstory-1.70.1-xcframework.tar.gz\"}`)" - "fullstory_react-native (from `../node_modules/@fullstory/react-native`)" + - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) - group-ib-fp (from `../node_modules/group-ib-fp`) - GzipSwift - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) @@ -3146,6 +4322,7 @@ DEPENDENCIES: - NitroModules (from `../node_modules/react-native-nitro-modules`) - "onfido-react-native-sdk (from `../node_modules/@onfido/react-native-sdk`)" - "pusher-websocket-react-native (from `../node_modules/@pusher/pusher-websocket-react-native`)" + - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`) - RCTRequired (from `../node_modules/react-native/Libraries/Required`) - RCTSwiftUI (from `../node_modules/react-native/ReactApple/RCTSwiftUI`) @@ -3236,7 +4413,6 @@ DEPENDENCIES: - ReactCodegen (from `build/generated/ios/ReactCodegen`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) - "ReactNativeBiometrics (from `../node_modules/@sbaiahmed1/react-native-biometrics`)" - - ReactNativeDependencies (from `../node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec`) - "ReactNativeHybridApp (from `../node_modules/@expensify/react-native-hybrid-app`)" - "RNAppleAuthentication (from `../node_modules/@invertase/react-native-apple-authentication`)" - "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)" @@ -3259,6 +4435,7 @@ DEPENDENCIES: - RNShare (from `../node_modules/react-native-share`) - RNSVG (from `../node_modules/react-native-svg`) - RNWorklets (from `../node_modules/react-native-worklets`) + - SocketRocket (~> 0.7.1) - TrustKit (~> 3.0) - UMAppLoader (from `../node_modules/unimodules-app-loader/ios`) - VisionCamera (from `../node_modules/react-native-vision-camera`) @@ -3300,6 +4477,7 @@ SPEC REPOS: - SDWebImageSVGCoder - SDWebImageWebPCoder - Sentry + - SocketRocket - SwiftUIIntrospect - TrustKit - Turf @@ -3308,6 +4486,10 @@ SPEC REPOS: EXTERNAL SOURCES: AppLogs: :path: "../node_modules/react-native-app-logs/AppLogsPod" + boost: + :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" + DoubleConversion: + :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" EXConstants: :path: "../node_modules/expo-constants/ios" expensify-react-native-background-task: @@ -3348,12 +4530,18 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-video/ios" ExpoWebBrowser: :path: "../node_modules/expo-web-browser/ios" + fast_float: + :podspec: "../node_modules/react-native/third-party-podspecs/fast_float.podspec" FBLazyVector: :path: "../node_modules/react-native/Libraries/FBLazyVector" + fmt: + :podspec: "../node_modules/react-native/third-party-podspecs/fmt.podspec" FullStory: :http: https://ios-releases.fullstory.com/fullstory-1.70.1-xcframework.tar.gz fullstory_react-native: :path: "../node_modules/@fullstory/react-native" + glog: + :podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec" group-ib-fp: :path: "../node_modules/group-ib-fp" hermes-engine: @@ -3367,6 +4555,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@onfido/react-native-sdk" pusher-websocket-react-native: :path: "../node_modules/@pusher/pusher-websocket-react-native" + RCT-Folly: + :podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec" RCTDeprecation: :path: "../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation" RCTRequired: @@ -3545,8 +4735,6 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon" ReactNativeBiometrics: :path: "../node_modules/@sbaiahmed1/react-native-biometrics" - ReactNativeDependencies: - :podspec: "../node_modules/react-native/third-party-podspecs/ReactNativeDependencies.podspec" ReactNativeHybridApp: :path: "../node_modules/@expensify/react-native-hybrid-app" RNAppleAuthentication: @@ -3608,10 +4796,12 @@ SPEC CHECKSUMS: AirshipServiceExtension: 50d11b2f62c4a490d4e81a1c36f70e2ecb70a27e AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 AppLogs: 3bc4e9b141dbf265b9464409caaa40416a9ee0e0 + boost: 659a89341ea4ab3df8259733813b52f26d8be9a5 + DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb EXConstants: 1c400bb9969f4c9e5ab324553138b74ae47b9efe - expensify-react-native-background-task: f1ed62aef927a06b7c5dbfdfceea0499d03939fc - ExpensifyNitroUtils: 490e097653e2f67f45415e2df725b012fd747a41 - Expo: c4746f2a519c92a8af37f06e43194e303703a40d + expensify-react-native-background-task: 03c640e1f5649692d058cba48c0a138f024a6dd3 + ExpensifyNitroUtils: 86109fe1ab88351ed63ffe11b760d537c70019d7 + Expo: 844d649bc6228d8fa118f29984565b58745bc1d8 ExpoAsset: c2e7b5ba1fe75be683282d6264e38fd7cd8defbb ExpoAudio: 7774082d316ceecc2b26efeb10480bea2fa4d67e ExpoDomWebView: 27205be8754f9992913b8a9a08e65019f2207075 @@ -3620,7 +4810,7 @@ SPEC CHECKSUMS: ExpoImageManipulator: 4a7d8efe364d3db8698507177a75eb16b51bc104 ExpoLocation: ed45ae27d6eb115a84b16713f3e5a9c5822a5b24 ExpoLogBox: 7aa03244fe5eeced5129e4ec7ad5bd9a3994378e - ExpoModulesCore: 57fdaecbef24e245cb1fb066b3549f244766b34c + ExpoModulesCore: 368126ccb297fd4e498aa85c8fd97ce84b9b4d91 ExpoModulesJSI: 31051e6e3d88a96cc48747eabad1d4a6a29c554e ExpoModulesWorklets: 3f847eb6ba544bd4b55d585f89b3b18f8cd73a72 ExpoModulesWorkletsAdapter: 4ced459d91107d4f32a8decf8b7827f58153436a @@ -3628,33 +4818,36 @@ SPEC CHECKSUMS: ExpoTaskManager: f9511a17a4528adb8da94efb639ad592eef8bc53 ExpoVideo: 8ebbbf65778c5f986d65fc17bb60b79e9d7624e6 ExpoWebBrowser: 6e3d90e3fe1952d21a8494872b1e1a485cd50e2f + fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6 FBLazyVector: 473b935415b82ae4f7f9aa9d5b3378491143ccbf Firebase: 3435bc66b4d494c2f22c79fd3aae4c1db6662327 FirebaseAnalytics: 630349facf4a114a0977e5d7570e104261973287 FirebaseCore: c692c7f1c75305ab6aff2b367f25e11d73aa8bd0 FirebaseCoreInternal: 29d7b3af4aaf0b8f3ed20b568c13df399b06f68c FirebaseInstallations: 0ee9074f2c1e86561ace168ee1470dc67aabaf02 + fmt: 530618a01105dae0fa3a2f27c81ae11fa8f67eac ForkInputMask: 55e3fbab504b22da98483e9f9a6514b98fdd2f3c FullStory: 8f87790ed749aa66497e739b065932852e57077d - fullstory_react-native: 0dde80c0899e605f7f6f1bb40841d9ba47acdcae + fullstory_react-native: 1374480e8734b0b9ad7a5565cd94aa1215416368 + glog: e56ede4028c4b7418e6b1195a36b1656bb35e225 GoogleAppMeasurement: 0dfca1a4b534d123de3945e28f77869d10d0d600 GoogleSignIn: d4281ab6cf21542b1cfaff85c191f230b399d2db GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 - group-ib-fp: d0377224e81ebf3c5429e4f5e4ccfe70f3470f45 + group-ib-fp: 42bc98218d2b4b18eac9a2dd0156a3074e0df082 GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 GzipSwift: 893f3e48e597a1a4f62fafcb6514220fcf8287fa - hermes-engine: a4b7496e8b67572f7a6f1dac138391412c70453d + hermes-engine: 1a37d030ed54575ddb733c6484f4b70fbcda5d87 libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7 libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 lottie-ios: 8f959969761e9c45d70353667d00af0e5b9cadb3 - lottie-react-native: 5c0c4fc0211179e7b5c0e660089f31c27128a577 + lottie-react-native: 7696ee54db539844f215c7e1f37372ae3138f37d MapboxCommon: d006256c4643860a7b3046af45d1671569b6c5ac MapboxCoreMaps: de1a4461d0ab5d65d907f06e8d82fdbe06bb3213 MapboxMaps: 22340ae0e0d47b2e4761019b1d6c569ab04e088d nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 - NitroModules: 8cb714abfae488c6a56470ef6c64137b5070010d + NitroModules: f8c2cc3025e4550aee15ff77c525622bf98e774a NWWebSocket: b4741420f1976e1dff4da3edad00c401e4f1d769 Onfido: 65454f91d10758193c857fd149417f6efbea84c5 onfido-react-native-sdk: bb8cfd9198e2e97978461d969497d18b37e45ca7 @@ -3662,6 +4855,7 @@ SPEC CHECKSUMS: PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 pusher-websocket-react-native: 31b5fdd632bfa6d417f9ad0bceefe403ab41825d PusherSwift: fa3d5f6587c20ad5790de87a5c9b150367b5f0f5 + RCT-Folly: 36c4f904fb6cd0219dcb76b94e9502d2a72fab0b RCTDeprecation: 225e022b85a206cf0883a909c4a114159783363c RCTRequired: 827a95c181af0886a11e21bc66c084088cf87839 RCTSwiftUI: 7babb07156ae0a8e5ea97f77e43959ddaca2c6f2 @@ -3669,116 +4863,116 @@ SPEC CHECKSUMS: RCTTypeSafety: 47f936a87875e1a08ccf1ffe34e8bf29f1d85567 React: e2dc35338068bbd299c66f043ae0d7f25de8499e React-callinvoker: d3163d80425735b095cff517da3b8631691fd734 - React-Core: efe6d8298e794a1f6ee1e018a0ee93789c152703 - React-CoreModules: 1c097c5155f4345bccfb00a9a7309952b393f366 - React-cxxreact: 1b54e6919cef2a406a79e5d350cd71bb83d6e49d + React-Core: 4b12e8863a200ff319a724cdf3f7bb461ac30fbd + React-CoreModules: f732869c77b076b962a4e3ee5becd704a87d2817 + React-cxxreact: 84b7c9400c1c84bde649d06755b9c64b48114904 React-debug: f6293d03b643d4bcc487c7da7c745a75fd48ad7d - React-defaultsnativemodule: 7c85b45aa855dfa4e9433e38486270156fb0e47d - React-domnativemodule: 9f22fb114ed6ae763b6d5b746e4916868a5b45ff - React-Fabric: e5dd4f25178e0fce4163d8996e9ecdc3e17e561c - React-FabricComponents: b3d1cf73b5044ecb68f025934a94edc924d7196e - React-FabricImage: 25794f6479f8c4c157a9c8e1f35b85991b766909 - React-featureflags: 6144f7ea6915ffec18197635491d018fb88c7a4d - React-featureflagsnativemodule: 6b0f8bd54c3b071b91ea829987f3e21caa20edf3 - React-graphics: a9af5e6b9a0789a569d8e324457d3829f30dbbe7 - React-hermes: 06b3d795c3ff087060a493520d74fa94d2c25879 - React-idlecallbacksnativemodule: 131c08ace67fc983ba6bf68e2062ef904caa0dee - React-ImageManager: 8c2c188e89da4867ed44fdb59ed658649c0da1a2 - React-intersectionobservernativemodule: a887b9a4c81d8d7f76b7d6406586ffe24e0bab61 - React-jserrorhandler: 8906d74d18fe766a39661b5931a73bb1383d30ed - React-jsi: 4ff0905b76d3b5d6bd7062821ff9fd7bf2371dc2 - React-jsiexecutor: 8026d7cbef72b274838308759345d6208cd7be54 - React-jsinspector: 8d272a45c8f0b332939bccb5816dd9c3a725d02b - React-jsinspectorcdp: 06f43621e2deca92673dd85ae98b0419528c6b7e - React-jsinspectornetwork: 780db9c48e7c8deca3e0cece61c10f593db6acf7 - React-jsinspectortracing: e491e0c354b5b6d2d12f05e11a2c2452db450096 - React-jsitooling: 6ee9b191c2c4fe55224a1b01c756c5839b277ee0 + React-defaultsnativemodule: c412503114bae304c667d832bf838619f82e6937 + React-domnativemodule: c4fd125653b3753879e75adf5aa7c8438d9a9142 + React-Fabric: c6bfb98333f99e6c81916ffa4705326e1716745c + React-FabricComponents: 8b869a560a7db0b208ae33550f0aa6f9ce446b27 + React-FabricImage: e78a213d6a359c9927de1eb4f81444c5bc65cf7a + React-featureflags: bb7e30ca0fe9a8421e094536a25650c6ce9e5212 + React-featureflagsnativemodule: 1a09ad358e71d6d69b1d7a4b54fd93a0f8acd5cf + React-graphics: 268f4c2718610747287d32d25287d8b50d303e7e + React-hermes: c14b3df7089e74ebe956636598734f6996985641 + React-idlecallbacksnativemodule: dfd9165a261c4b5c7cb4ed741642bce63fdbea7c + React-ImageManager: 3fdbb547b886755b08eb4cc20c3087292adb4d1d + React-intersectionobservernativemodule: cd85dd1826f19f2473d83f100e18c5f5860c515e + React-jserrorhandler: ee2d3ffb79eabd6663a908fd5a5cab8ff9d2363d + React-jsi: d2616066314453f8059b4e2f5f7967b926ba5bf6 + React-jsiexecutor: f9c7fba6062530924ae402fd72789dd7479002dd + React-jsinspector: 34e269fd0bf776dabaa62678b25c0be73670c31a + React-jsinspectorcdp: 98fd3b1d42052813788a1a06a3cbbf84c2f4a5e9 + React-jsinspectornetwork: b3f3af03024340c1403a79dc4cc65fa9e13a7bec + React-jsinspectortracing: 2c8b432316abf2b089f6e6c108b7ddd82e3d335d + React-jsitooling: 22410dec7b0df633e0635b7607971175bca736ea React-jsitracing: cb1c2483e0263022c88359a8438cf44711197342 - React-logger: 611b08f6f0055e92c308679b4eb571e61004c8e0 - React-Mapbuffer: 1d2390b72497301218b7d4347dfeef8ccb7a1e3f - React-microtasksnativemodule: 0f9a67dd2b5f0d7555babfd10970ad829b5ce3ad - React-mutationobservernativemodule: f0f1c1b1b04d262804190152295b4c74a586db69 - react-native-advanced-input-mask: ba0ae151c4287b21666c14abef645a349e2d7360 - react-native-airship: 89657796be5f6fb30fd67d6bcf4f30fb498f93ec - react-native-app-logs: b1ed772ab0f760f9fd5ad32f7ee617de9744d660 - react-native-blob-util: ea51a8787d20d1b5f76390cc65bfde599577c33d - react-native-cameraroll: 04488d0fec08f939049e9c99d280930db55296a5 - react-native-config: b36f099a2c7a7dc201ff148ae5c18ab2f2f129c7 - react-native-document-picker: eb1608dbbd4ed32666fbb9a08b7f733bdaecc6ba - react-native-image-picker: 91e8eac0c800912ef8f86ff7d489df29ab81f168 - react-native-key-command: 7aa560672ed9697e7bb05b3c6382bb3b4afe2e6c - react-native-keyboard-controller: 2d60672c8956a1f5517aee8a18abd8da91841503 - react-native-netinfo: 947c73098a6b7d6f4fd41aa4ff8bdfe9c5a7a6a5 - react-native-pager-view: 3940c389055f6b9b1d2559a73725765f9fdf61bc - react-native-pdf: 4b46342e2890b1eb006e044abd4916c6edcb8a27 - react-native-plaid-link-sdk: 2fc7e2549c3ce4403af33868522eded43ef52204 - react-native-safe-area-context: 5e7b91d61e13a350104ba65887415e0f86b1fab1 - react-native-skia: 0303456e14d1a2c614a17daf87f979f163056ffb - react-native-view-shot: 3f63bf155f961829ce65aa472bcba640b33e7108 - react-native-wallet: 50c325265356e1f1dbab0d3a798840fbd601d528 - react-native-webview: 4f00ce23bf30a7102479cd1f3ca90b692d364be9 - React-NativeModulesApple: 0fa5a20ef85fadf1fae5da3ee5c434e2eea6c0c1 - React-networking: 008ca1fb7936195f6ae4f3fedae3596e44b9e62b + React-logger: 65c987d6465d254c08c9eb0fb6145be6eb8ca9ba + React-Mapbuffer: 9c9e5f0871d36f59b5b3421b6121596a6a74e67b + React-microtasksnativemodule: c8315d5f4ae7188f30cbfa499d8ec2e9281b97b1 + React-mutationobservernativemodule: 9bb7f57044059bcaf28e0287154942acd9b9b6dc + react-native-advanced-input-mask: 9c2b52e8d7b09d5df6a6d0f6aafaa46926e39df5 + react-native-airship: ea7daec2c47c80182bc13c836d3f961dbb937678 + react-native-app-logs: 8609315e85ebedb9da47120e5c0bcea5bd6e9d75 + react-native-blob-util: 871805e786e7c8e4a8c482e34ec6640ab19038fe + react-native-cameraroll: 8e83b1a4f8abb386b37599918a412add4a87c5e7 + react-native-config: 62c1dce61201623334f751c71c7b12c4accbdff8 + react-native-document-picker: 18a9d68e6117d37ac5facd87619cbc3ff17575a7 + react-native-image-picker: 1722f5d3dc5de5039d423e290dc31a717ba9605e + react-native-key-command: 7538df85ed26502b2a929c0584235459b26c7a91 + react-native-keyboard-controller: 0619a578e4d79a38dc4b1bef3b89cee6525bc1c7 + react-native-netinfo: f94b3a0fc305e812f3f615989d99299d7110c2ae + react-native-pager-view: c62ab82db28e2d11e7809c59085e2984a45979c5 + react-native-pdf: 6a09a9be0e7ee954ea671437483316f9a28f8572 + react-native-plaid-link-sdk: 425c0a3a923310fcd8489142209ff1508372a7bf + react-native-safe-area-context: 0a3b034bb63a5b684dd2f5fffd3c90ef6ed41ee8 + react-native-skia: 51f30133876025c83e933f4f7253479e6de5d937 + react-native-view-shot: 1a01c96f6fb09b99539142749cd93f685af7fe48 + react-native-wallet: 2ea467e76031a61105416df760bfd591b602cd17 + react-native-webview: cdce419e8022d0ef6f07db21890631258e7a9e6e + React-NativeModulesApple: 67c09c715a8763a07a8a241a3019661ffc3f71d6 + React-networking: 72b8973a280b6a54e46930717e6c15f7c389d76e React-oscompat: 563c536fc2d9cece1f54442fa2a3d9f68a9621e3 - React-perflogger: 2f9377d019d75920cb22572abcc53f770c56b276 - React-performancecdpmetrics: 493da00461051b195fe82bc952bdbf5b76ae31fa - React-performancetimeline: 34fd2c9f2a1960c38e098ea386e1eb07ea1677dc + React-perflogger: 5bfe39e71aa4432d197003142eaef4158c03b69d + React-performancecdpmetrics: dc08b5f87fa0c9c43915bfcd58b8124d2d07c8f9 + React-performancetimeline: 773a4631cf1a12499d25e5afe356c0d4934cb509 React-RCTActionSheet: ea8200cac284d410090bd780baf729903912e4e6 - React-RCTAnimation: 1cd96318d09def3c9b4892a30bcbbf16b421b44b - React-RCTAppDelegate: 4e5703bda7aed317e93fc1388543bde281991f11 - React-RCTBlob: 317a92a3f23cc82035cc381d95bea09425dca95f - React-RCTFabric: 27f96bcc230587e472da7491636712ed08352432 - React-RCTFBReactNativeSpec: 98d34530e32422f1c3d16470cb7cd79e3f42bc40 - React-RCTImage: 30d09cd35be7d3876b7fda2cf7026a3edbd1c3ba + React-RCTAnimation: 2612fabae23447a1afa7ac7405fbb4d0f6fa52b0 + React-RCTAppDelegate: cf9ecbc46ee5d6aa9f06c60d8395aa13495d1368 + React-RCTBlob: 7b4d7420a6d46327eeeeacdbd54a392253517772 + React-RCTFabric: bcd85c8a26c49635e8056af7bdb480969244ae9d + React-RCTFBReactNativeSpec: 68368860f257ac2ec24a8584b933a9b10dd5dc4a + React-RCTImage: 9424c680b0866903ff65a9a093641f3d1e6ce85d React-RCTLinking: c5dd340d5fee2fa01fee28750ae4211449fd3904 - React-RCTNetwork: 86aac9640b06b1934b4bf943fa8c01f1f221bcd5 - React-RCTRuntime: 99a681438773620bd3a418a8cdf34426571b17df - React-RCTSettings: e1f2e8f15859662cbda170f0e9bc061e1e877f14 + React-RCTNetwork: 3fe661f94974cfba5126800d834f98e4608b2d68 + React-RCTRuntime: 1d8ae939d193598ff7de5043196cdc1b90135b71 + React-RCTSettings: 1ba76e945f541c1c246618d2775caea0c6a76e26 React-RCTText: 1d7a4f263266efbd5fa5335a107fbd00ff94ba9e - React-RCTVibration: f0b6e37ad82d6d3ef3f632619bd9d30a2f8bd850 - React-rendererconsistency: e90b9e1bab21c3fabbaa220c3fab820ee81c59d9 + React-RCTVibration: 0de7b5cc470892e4a65358ae7853f117fe5c2173 + React-rendererconsistency: 88c6e0d56897a13f9254a1fd14d909a99d29486c React-renderercss: 41759ec6208f3afe6f7cd0205d4358a983f7ac6d - React-rendererdebug: b1fb291972738cf530a87bb9cf74bb0329195a3a - React-RuntimeApple: 848eadab016fcdad207336793b54309c60cc0135 - React-RuntimeCore: f5ec22d7b25a158970e16d52885a0b637464e23f - React-runtimeexecutor: cba02001a29c853d29d09a200ba28f2721d8ba53 - React-RuntimeHermes: 8dbd458e250523e65c5e9e372595895587116646 - React-runtimescheduler: 95bbb3f154e7d94a38f4f0e6efc32d11ccca6f9e + React-rendererdebug: 0ddb9d40c7e40d47b03e7ef76b8ea02157ccf83c + React-RuntimeApple: 345c69430aba5a1f958962e7ea03d2bd88f017d0 + React-RuntimeCore: 189d7e79c2c57cc292973c9a21b595b5ab32f5ee + React-runtimeexecutor: 07bf5c4b99250eeb87d11c6a7525b2e23c4d9335 + React-RuntimeHermes: 7ab04f8717cb8cb4a95788d822e1d52c17b59f7c + React-runtimescheduler: 3a5dfc9754d01a25573a7814e204ce2285d10290 React-timing: 52725a5eac08312f63ddf2fcedb11887249e62dc - React-utils: c87352bc9b44f0c64f268c6e1463dd8283b09069 - React-webperformancenativemodule: cc6c7dd09c5930910f98ef298e43618948b13d9d + React-utils: 13434fd45de041d5170cdb0a8e7fa593d9c3dc13 + React-webperformancenativemodule: 7c40fad73f558c60f3101e4b3fed0a3e66f7a312 ReactAppDependencyProvider: 25c9c516839be2c5e3d3344f95dc7da5f7e63fc2 - ReactCodegen: 01edc8d202005315c06ca3826d1eaad2c619cae7 - ReactCommon: ced9cfa50957cb9e6b7a1a7cd4ee20f5da6fa3a4 - ReactNativeBiometrics: 8f68cff6f8167c9a34e911eb589b8bc2e39a8e13 - ReactNativeDependencies: 14eafe8d1d2e886394c6787edb10f4b526accb7b - ReactNativeHybridApp: d5b22c484da298f8444d8f23c072431205a0c646 + ReactCodegen: 3a96f3289dc44bc80ba172d16419c058874afa20 + ReactCommon: f7748c97e657e7be0df505a5fc51c4ace9791a32 + ReactNativeBiometrics: a0cbd0e25b86964883d38331f32c7643022b7284 + ReactNativeHybridApp: 16ebccf5382436fcb9303ab5f4b50d9942bccf5c RNAppleAuthentication: 9027af8aa92b4719ef1b6030a8e954d37079473a - RNCClipboard: d73fd4b2ac59e792596e7cd615b6321d577c1a2b - RNCPicker: c61a6d23143b93246e536e5f7f6fcd578a0ab4c5 - RNDeviceInfo: 36bcc76b2a2914b9438732e24dfbf768f9abb6c5 + RNCClipboard: e560338bf6cc4656a09ff90610b62ddc0dbdad65 + RNCPicker: eb89e9d8bbd728bc00c48d294553f5968900f874 + RNDeviceInfo: f4e8dcaba08a5d3a703d30ad41e964e21148d7e2 RNFBAnalytics: 2f451df50833a890974d55a93557da878a85b29e RNFBApp: db9c2e6d36fe579ab19b82c0a4a417ff7569db7e RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8 - RNGestureHandler: 65c63283cff80384282a68091f8dd5e4bf04dfb8 + RNGestureHandler: 5d0f1950e26f5f71085eaacf1598ea38e9549ac0 RNGoogleSignin: 89877c73f0fbf6af2038fbdb7b73b5a25b8330cc - RNLiveMarkdown: 20a8588c3999fcfbf67fb635886c236eaf3029b9 - RNLocalize: 81b376a55d6cb014bfb296d16b3ae0c41dba24d9 - rnmapbox-maps: e8692f8dc284b6dc8b86355615721cd6671f3103 - RNNitroSQLite: d588ba6bd84db154da58fd74a71cb840708c16b1 - RNPermissions: 9d1096e3344a71a076577ddb46887b012323a2ca - RNReactNativeHapticFeedback: 71068ee2dd8b54feae784437353342c57c1cffa9 - RNReanimated: 669dcac7a77261e2c491333be34dd2eb9a5206c3 - RNScreens: 058db6732e139e842b7b1c5c58eb9b1cb79f84b7 - RNSentry: aef25491aaf828e4c0620f13e21e2d9d54af8871 - RNShare: 232e543e178b77459bb9589fd6251165fccf2287 - RNSVG: 8013e66805d1d992d30ffc28150fcc35c1b1c2f9 - RNWorklets: b1ca1c26210b8be65e4036bd114259d1c85e5283 + RNLiveMarkdown: a368618d21adf1269daf840645f0fc1b6b141003 + RNLocalize: 05e367a873223683f0e268d0af9a8a8e6aed3b26 + rnmapbox-maps: 8b7629ef3ae59dd96340470e568cbc7c08c54ff9 + RNNitroSQLite: a9b5965d511ed6e99ce903380e64934d043a0d2c + RNPermissions: 518f0a0c439acc74e2b9937e0e7d29e5031ae949 + RNReactNativeHapticFeedback: 5f1542065f0b24c9252bd8cf3e83bc9c548182e4 + RNReanimated: 920c33f677549b725b3e4a571fa8eeda790703d0 + RNScreens: 2b6107925ee4e14a9b2eb0dfb52fe25223aa64d7 + RNSentry: f73f4da92e4c20841ab16e1fa22fc289bc2f9f4e + RNShare: 1c1fde2c4134b9cf220ffebbd6df9c414036d382 + RNSVG: 0b9792eb17fbbc8e6d186581cb2cf712998dcc2a + RNWorklets: 8598e72b87947ae2f8428a541440851eb50579c3 SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a SDWebImageAVIFCoder: afe194a084e851f70228e4be35ef651df0fc5c57 SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 Sentry: 7475eb7bf6a41d7505f46341706015ad2d1766b9 + SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 SwiftUIIntrospect: fee9aa07293ee280373a591e1824e8ddc869ba5d TrustKit: 1050f3ce0595f8ed775f1237b7f590e3a4f93e91 Turf: c9eb11a65d96af58cac523460fd40fec5061b081 @@ -3787,6 +4981,6 @@ SPEC CHECKSUMS: VisionCamera: 30b358b807324c692064f78385e9a732ce1bebfe Yoga: 9d8e8094a2bed6a175d9e6001a0546d5dae663ee -PODFILE CHECKSUM: cbbbe89535ea02bc8e53033692514f55affcbdd6 +PODFILE CHECKSUM: b14033bdc31e26edfbbc81b22b06ad2c642bfc5f COCOAPODS: 1.16.2 diff --git a/rock.config.mjs b/rock.config.mjs index 533c0a3203ce..fd354e8f6832 100644 --- a/rock.config.mjs +++ b/rock.config.mjs @@ -22,7 +22,7 @@ export default { android: platformAndroid({sourceDir: isHybrid ? './Mobile-Expensify/Android' : './android'}), }, fingerprint: { - extraSources: ['android/gradle.properties', 'ios/Podfile', 'scripts/artifacts-utils/compute-patches-hash.sh', 'patches', ...(isHybrid ? ['Mobile-Expensify/patches'] : [])], + extraSources: ['android/gradle.properties', 'ios/Podfile', 'scripts/compute-patches-hash.sh', 'patches', ...(isHybrid ? ['Mobile-Expensify/patches'] : [])], env: ['USE_WEB_PROXY', 'PUSHER_DEV_SUFFIX', 'SECURE_NGROK_URL', 'NGROK_URL', 'USE_NGROK', 'FORCE_NATIVE_BUILD'], ignorePaths: ['Mobile-Expensify/Android/assets/app/shared/bundle.js'], }, diff --git a/scripts/artifacts-utils/android/ExpensiUtils.gradle b/scripts/artifacts-utils/android/ExpensiUtils.gradle deleted file mode 100644 index b76b617d16d2..000000000000 --- a/scripts/artifacts-utils/android/ExpensiUtils.gradle +++ /dev/null @@ -1,47 +0,0 @@ -import groovy.json.JsonSlurper -import java.util.concurrent.TimeUnit - -ext.ExpensiLog = new Object() { - private final String BLUE = "" - private final String YELLOW = "" - private final String RED = "" - private final String RESET = "" - - private String prefix = "" - void setPrefix(String newPrefix) { this.prefix = newPrefix } - - private String format(String color, String message) { - return "${color}[${prefix}] ${message}${this.RESET}" - } - - void lifecycle(String message) { logger.lifecycle(format(this.BLUE, message)) } - void warn(String message) { logger.warn(format(this.YELLOW, message)) } - void error(String message) { logger.error(format(this.RED, message)) } -} - -ext.ExpensiUtils = new Object() { - Map runCommand(List command, long timeoutSeconds = 10, File workingDir = null) { - def process = workingDir ? command.execute(null, workingDir) : command.execute() - process.waitFor(timeoutSeconds, TimeUnit.SECONDS) - return [output: process.in.text?.trim(), exitCode: process.exitValue()] - } - - // Returns [buildFromSource, version, githubUsername, githubToken]; any failure -> build from source. - Map resolveArtifacts(String packageName, String newDotRootDir, boolean isHybrid) { - def cmd = [ - "bun", "${newDotRootDir}/scripts/artifacts-utils/resolve-artifacts.ts", - "--platform=android", "--package=${packageName}", "--hybrid=${isHybrid}", "--new-dot-root=${newDotRootDir}" - ] - try { - def result = runCommand(cmd, 120, new File(newDotRootDir)) - if (result.exitCode != 0) { - ExpensiLog.error("Artifacts resolver failed (exit ${result.exitCode}). Building react-native from source.") - return [buildFromSource: true, version: null] - } - return new JsonSlurper().parseText(result.output) - } catch (Exception e) { - ExpensiLog.error("Artifacts resolver error: ${e}. Building react-native from source.") - return [buildFromSource: true, version: null] - } - } -} diff --git a/scripts/artifacts-utils/android/PatchedArtifactsSettings.gradle b/scripts/artifacts-utils/android/PatchedArtifactsSettings.gradle deleted file mode 100644 index 5654e57838d6..000000000000 --- a/scripts/artifacts-utils/android/PatchedArtifactsSettings.gradle +++ /dev/null @@ -1,42 +0,0 @@ -apply from: "ExpensiUtils.gradle" - -class PatchedArtifactsConfig { - String version = null - String packageName = null - String githubUsername = null - String githubToken = null - Boolean buildFromSource = true -} - -settings.getExtensions().create('patchedArtifacts', PatchedArtifactsConfig) - -def getNewDotRootDir() { - return "${rootDir}/${hasProperty('newDotRoot') ? getProperty('newDotRoot') : "/.."}" -} - -settings.extensions.configure(PatchedArtifactsConfig) { config -> - ExpensiLog.setPrefix("PatchedArtifacts") - config.buildFromSource = getProperty('patchedArtifacts.forceBuildFromSource') == 'true' - if (config.buildFromSource) { - ExpensiLog.lifecycle("Forcing build from source.") - return - } - - config.packageName = getProperty('patchedArtifacts.packageName') - - def isHybrid = config.packageName == 'react-hybrid' - def resolution = ExpensiUtils.resolveArtifacts(config.packageName, getNewDotRootDir().toString(), isHybrid) - config.version = resolution.version - config.buildFromSource = resolution.buildFromSource - - if (config.buildFromSource || config.version == null) { - config.buildFromSource = true - ExpensiLog.error("No matching artifacts version found for ${config.packageName}. Building react-native from source.") - return - } - - // The resolver returns the credentials the Maven download needs alongside the version. - config.githubUsername = resolution.githubUsername - config.githubToken = resolution.githubToken - ExpensiLog.lifecycle("Using patched react-native artifacts: ${config.packageName}:${config.version}") -} diff --git a/scripts/artifacts-utils/ios/patched_ios_artifacts.rb b/scripts/artifacts-utils/ios/patched_ios_artifacts.rb deleted file mode 100644 index fec1448b7eb0..000000000000 --- a/scripts/artifacts-utils/ios/patched_ios_artifacts.rb +++ /dev/null @@ -1,73 +0,0 @@ -# Consumer for Expensify's patched React Native iOS prebuilt artifacts. -# Reopens ReactNativeCoreUtils (rncore.rb) to point RNCore resolution and -# download at our private GitHub Packages Maven repo (matched by patches hash). -# Must be required after react_native_pods.rb, which defines ReactNativeCoreUtils. - -require 'json' - -module PatchedIOSArtifacts - # scripts/artifacts-utils/ios/ -> repo root is three levels up. - NEW_DOT_ROOT = File.expand_path('../../..', __dir__) - GITHUB_PACKAGES_BASE = 'https://maven.pkg.github.com/Expensify/App' - - def self.setup - is_hybrid = ENV['IS_HYBRID_APP'] == 'true' - package_name = is_hybrid ? 'react-hybrid' : 'react-standalone' - - forced_source = ENV['RCT_USE_PREBUILT_RNCORE'] == '0' - resolution = forced_source ? {'buildFromSource' => true, 'version' => nil} : resolve(package_name, is_hybrid) - - ReactNativeCoreUtils.class_variable_set(:@@patched_version, resolution['version']) - ReactNativeCoreUtils.class_variable_set(:@@patched_package_name, package_name) - ReactNativeCoreUtils.class_variable_set(:@@patched_github_token, resolution['githubToken']) - ReactNativeCoreUtils.class_variable_set(:@@patched_build_from_source, resolution['buildFromSource']) - - ENV['RCT_USE_PREBUILT_RNCORE'] = resolution['buildFromSource'] ? '0' : '1' - end - - def self.resolve(package_name, is_hybrid) - cmd = [ - 'bun', File.join(NEW_DOT_ROOT, 'scripts/artifacts-utils/resolve-artifacts.ts'), - '--platform=ios', "--package=#{package_name}", "--hybrid=#{is_hybrid}", "--new-dot-root=#{NEW_DOT_ROOT}" - ] - # stdout is pure JSON; the resolver logs to stderr. - output = IO.popen(cmd, chdir: NEW_DOT_ROOT, &:read) - raise "resolver exited #{$?.exitstatus}" unless $?.success? - JSON.parse(output) - rescue => e - Pod::UI.warn("[PatchedIOSArtifacts] Resolver failed (#{e.message}); building from source.") if defined?(Pod::UI) - {'buildFromSource' => true, 'version' => nil} - end -end - -class ReactNativeCoreUtils - def self.setup_rncore(react_native_path, react_native_version) - @@react_native_path = react_native_path - # Base RN version (e.g. 0.85.3) — used by RN's install flow as a non-empty guard. The actual - # download URL uses @@patched_version via our stable_tarball_url override, so this stays the plain version. - @@react_native_version = react_native_version - @@build_from_source = @@patched_build_from_source - @@download_dsyms = ENV['RCT_SYMBOLICATE_PREBUILT_FRAMEWORKS'] == '1' - end - - def self.stable_tarball_url(_version, build_type, dsyms = false) - classifier = "reactnative-core-#{dsyms ? 'dSYM-' : ''}#{build_type}" - "#{PatchedIOSArtifacts::GITHUB_PACKAGES_BASE}/com/expensify/#{@@patched_package_name}/react-native-artifacts/#{@@patched_version}/react-native-artifacts-#{@@patched_version}-#{classifier}.tar.gz" - end - - def self.download_rncore_tarball(_react_native_path, tarball_url, version, configuration, dsyms = false) - dir = artifacts_dir - destination = configuration.nil? ? - "#{dir}/reactnative-core-#{version}#{dsyms ? '-dSYM' : ''}.tar.gz" : - "#{dir}/reactnative-core-#{version}#{dsyms ? '-dSYM' : ''}-#{configuration}.tar.gz" - - unless File.exist?(destination) - tmp = "#{dir}/reactnative-core.download" - # curl drops the Authorization header on the cross-host redirect to the object store. - header = @@patched_github_token ? %(-H "Authorization: Bearer #{@@patched_github_token}") : '' - ok = system(%(mkdir -p "#{dir}" && curl --fail --location --proto '=https' #{header} "#{tarball_url}" -o "#{tmp}" && mv "#{tmp}" "#{destination}")) - raise "[PatchedIOSArtifacts] Failed to download #{tarball_url}" unless ok - end - destination - end -end diff --git a/scripts/artifacts-utils/lib/artifactsResolver.ts b/scripts/artifacts-utils/lib/artifactsResolver.ts deleted file mode 100644 index b58ff7330e5f..000000000000 --- a/scripts/artifacts-utils/lib/artifactsResolver.ts +++ /dev/null @@ -1,258 +0,0 @@ -import {isRecord} from '@libs/ObjectUtils'; - -import {getOctokit} from '@actions/github'; -import {execFileSync} from 'child_process'; -import fs from 'fs'; -import path from 'path'; - -/** - * Shared resolver for Expensify's patched React Native prebuilt artifacts. - * Resolves which artifact version to use by matching the local patches hash - * against the `patchesHash` recorded in each candidate's Maven POM. Consumed via - * `resolve-artifacts.ts` by Gradle (Android) and `patched_ios_artifacts.rb` (iOS). - */ - -type Platform = 'ios' | 'android'; - -/** Base credentials — a token authenticates every download. */ -type Credentials = {githubToken: string}; -/** iOS's curl Bearer download needs only the token. */ -type IosCredentials = Credentials; -/** Android's Gradle Maven `credentials {}` block additionally requires the username. */ -type AndroidCredentials = Credentials & {githubUsername: string}; - -type ResolveOptions = { - platform: Platform; - packageName: string; - newDotRoot: string; - isHybrid: boolean; -}; - -/** No prebuilt match (or no credentials): the caller builds react-native from source. Carries no secrets. */ -type SourceBuild = { - buildFromSource: true; - version: null; - packageName: string; - artifactId: string; -}; - -/** A matching prebuilt artifact was found; carries the credentials the native download needs. */ -type Prebuilt = { - buildFromSource: false; - version: string; - packageName: string; - artifactId: string; -} & Creds; - -type IosResult = SourceBuild | Prebuilt; -type AndroidResult = SourceBuild | Prebuilt; -type ResolveResult = IosResult | AndroidResult; - -const GITHUB_REPO = 'Expensify/App'; -const GITHUB_OWNER = 'Expensify'; - -const ARTIFACT_IDS = { - android: 'react-android', - ios: 'react-native-artifacts', -} satisfies Record; - -/** Logs go to stderr; stdout is reserved for the JSON result. */ -function logError(message: string) { - process.stderr.write(`[PatchedArtifacts] ${message}\n`); -} - -/** Credentials as read from the source; fields are validated per platform by the callers. */ -type RawCredentials = {githubToken: string | null; githubUsername: string | null}; - -function isCI(): boolean { - return process.env.CI != null; -} - -/** Reads a non-empty environment variable, or null. */ -function getEnvVar(name: string): string | null { - const value: unknown = process.env[name]; - return typeof value === 'string' && value.length > 0 ? value : null; -} - -/** Runs a `gh` command and returns its trimmed output, or null on failure/empty. */ -function getGh(args: string[]): string | null { - try { - const output = execFileSync('gh', args, {encoding: 'utf8'}).trim(); - return output.length > 0 ? output : null; - } catch { - return null; - } -} - -function hasGithubCLI(): boolean { - try { - execFileSync('which', ['gh'], {stdio: 'ignore'}); - return true; - } catch { - return false; - } -} - -function hasRequiredScopes(): boolean { - try { - const status = execFileSync('gh', ['auth', 'status'], {encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']}).toString(); - return status.includes('read:packages'); - } catch { - return false; - } -} - -/** - * In CI credentials come from the environment (no gh CLI). Locally they come from - * the gh CLI, which must be installed and scoped for read:packages. - */ -function readCredentials(): RawCredentials | null { - if (isCI()) { - return {githubToken: getEnvVar('GITHUB_TOKEN'), githubUsername: getEnvVar('GITHUB_ACTOR')}; - } - if (!hasGithubCLI()) { - logError('No GitHub CLI found.'); - return null; - } - if (!hasRequiredScopes()) { - logError('GitHub token does not have required scope read:packages.'); - return null; - } - return {githubToken: getGh(['auth', 'token']), githubUsername: getGh(['api', 'user', '--jq', '.login'])}; -} - -function getIosCredentials(): IosCredentials | null { - const credentials = readCredentials(); - if (credentials == null || credentials.githubToken == null) { - logError('Missing GitHub token.'); - return null; - } - return {githubToken: credentials.githubToken}; -} - -function getAndroidCredentials(): AndroidCredentials | null { - const credentials = readCredentials(); - if (credentials == null || credentials.githubToken == null || credentials.githubUsername == null) { - logError('Missing GitHub credentials (username and/or token).'); - return null; - } - return {githubToken: credentials.githubToken, githubUsername: credentials.githubUsername}; -} - -function mavenPomUrl(packageName: string, artifactId: string, version: string): string { - return `https://maven.pkg.github.com/${GITHUB_REPO}/com/expensify/${packageName}/${artifactId}/${version}/${artifactId}-${version}.pom`; -} - -function buildAuthHeaders(githubToken: string | null): Record { - return githubToken ? {Authorization: `Bearer ${githubToken}`} : {}; -} - -/** - * GitHub Packages 302-redirects to a signed object-store URL on a different host. - * `fetch` follows the redirect and drops the Authorization header on the - * cross-origin hop, so the token reaches only the initial host, never the object store. - */ -async function fetchTokenSafe(url: string, githubToken: string | null): Promise { - const response = await fetch(url, {headers: buildAuthHeaders(githubToken)}); - if (!response.ok) { - throw new Error(`Request to ${url} failed with status ${response.status}`); - } - return response.text(); -} - -function getReactNativeVersion(newDotRoot: string): string { - const parsed: unknown = JSON.parse(fs.readFileSync(path.join(newDotRoot, 'package.json'), 'utf8')); - const dependencies = isRecord(parsed) ? parsed.dependencies : undefined; - const version = isRecord(dependencies) ? dependencies['react-native'] : undefined; - if (typeof version !== 'string') { - throw new Error('Could not read react-native version from package.json'); - } - return version; -} - -function computePatchesHash(newDotRoot: string, isHybrid: boolean): string { - const script = path.join(newDotRoot, 'scripts/artifacts-utils/compute-patches-hash.sh'); - const args = [script]; - if (isHybrid) { - args.push(path.join(newDotRoot, 'patches'), path.join(newDotRoot, 'Mobile-Expensify/patches')); - } else { - args.push(path.join(newDotRoot, 'patches')); - } - return execFileSync('bash', args, {encoding: 'utf8'}).trim(); -} - -async function getArtifactsCandidates(packageName: string, artifactId: string, rnVersion: string, githubToken: string): Promise { - const octokit = getOctokit(githubToken); - /* eslint-disable @typescript-eslint/naming-convention -- GitHub REST API params are snake_case */ - const versions = await octokit.paginate(octokit.rest.packages.getAllPackageVersionsForPackageOwnedByOrg, { - package_type: 'maven', - org: GITHUB_OWNER, - package_name: `com.expensify.${packageName}.${artifactId}`, - per_page: 100, - }); /* eslint-enable @typescript-eslint/naming-convention */ - return versions.map((version) => version.name).filter((name) => name.startsWith(rnVersion)); -} - -async function getRemotePatchesHash(packageName: string, artifactId: string, version: string, githubToken: string): Promise { - const pom = await fetchTokenSafe(mavenPomUrl(packageName, artifactId, version), githubToken); - return pom.match(/([^<]+)<\/patchesHash>/)?.[1]?.trim() ?? null; -} - -async function findMatchingArtifactsVersion(options: ResolveOptions, artifactId: string, githubToken: string): Promise { - const {packageName, newDotRoot, isHybrid} = options; - try { - const localPatchesHash = computePatchesHash(newDotRoot, isHybrid); - const rnVersion = getReactNativeVersion(newDotRoot); - const candidates = await getArtifactsCandidates(packageName, artifactId, rnVersion, githubToken); - for (const candidate of candidates) { - const remoteHash = await getRemotePatchesHash(packageName, artifactId, candidate, githubToken); - if (remoteHash != null && remoteHash === localPatchesHash) { - return candidate; - } - } - return null; - } catch (error) { - logError(`Failed to find matching artifacts version for ${packageName}. Reason: ${String(error)}`); - return null; - } -} - -async function resolveWithCredentials( - options: ResolveOptions, - artifactId: string, - credentials: Creds | null, - sourceBuild: SourceBuild, -): Promise> { - if (!credentials) { - return sourceBuild; - } - const version = await findMatchingArtifactsVersion(options, artifactId, credentials.githubToken); - if (version == null) { - logError(`No matching artifacts version found for ${options.packageName}. Building react-native from source.`); - return sourceBuild; - } - logError(`Using patched react-native artifacts: ${options.packageName}:${version}`); - return {buildFromSource: false, version, packageName: options.packageName, artifactId, ...credentials}; -} - -/** - * Resolves whether a matching prebuilt artifact exists for the current patches. - * Returns a `SourceBuild` (no secrets) when no match is found or credentials are - * unavailable, otherwise a `Prebuilt` carrying the credentials the native - * download needs — token only for iOS, username + token for Android. - */ -function resolveArtifacts(options: ResolveOptions & {platform: 'ios'}): Promise; -function resolveArtifacts(options: ResolveOptions & {platform: 'android'}): Promise; -function resolveArtifacts(options: ResolveOptions): Promise { - const artifactId = ARTIFACT_IDS[options.platform]; - const sourceBuild: SourceBuild = {buildFromSource: true, version: null, packageName: options.packageName, artifactId}; - - if (options.platform === 'ios') { - return resolveWithCredentials(options, artifactId, getIosCredentials(), sourceBuild); - } - return resolveWithCredentials(options, artifactId, getAndroidCredentials(), sourceBuild); -} - -export default resolveArtifacts; -export {ARTIFACT_IDS}; -export type {Platform, ResolveOptions, ResolveResult, IosResult, AndroidResult}; diff --git a/scripts/artifacts-utils/resolve-artifacts.ts b/scripts/artifacts-utils/resolve-artifacts.ts deleted file mode 100644 index 5ae804c87dc7..000000000000 --- a/scripts/artifacts-utils/resolve-artifacts.ts +++ /dev/null @@ -1,31 +0,0 @@ -import parseCommandLineArguments from '../utils/parseCommandLineArguments'; -import resolveArtifacts from './lib/artifactsResolver'; - -/** - * CLI wrapper around the shared artifacts resolver for the native build systems - * (Gradle and `patched_ios_artifacts.rb`), which cannot import the TS module. - * - * Usage: - * bun scripts/artifacts-utils/resolve-artifacts.ts \ - * --platform=ios --package=react-hybrid --hybrid=true --new-dot-root=. - * - * Prints the result as JSON to stdout (logs go to stderr) and always exits 0. - */ -const args = parseCommandLineArguments(); -const platform = args.platform; -const packageName = args.package ?? ''; - -if (platform !== 'ios' && platform !== 'android') { - process.stderr.write(`[PatchedArtifacts] Invalid or missing --platform "${platform ?? ''}" (expected "ios" or "android"); building from source.\n`); - process.stdout.write(JSON.stringify({buildFromSource: true, version: null, packageName, artifactId: ''})); - process.exit(0); -} - -const options = {packageName, newDotRoot: args['new-dot-root'] ?? '.', isHybrid: args.hybrid === 'true'}; -const resolution = platform === 'ios' ? resolveArtifacts({...options, platform: 'ios'}) : resolveArtifacts({...options, platform: 'android'}); - -resolution - .then((result) => process.stdout.write(JSON.stringify(result))) - .catch(() => { - process.stdout.write(JSON.stringify({buildFromSource: true, version: null, packageName, artifactId: ''})); - }); diff --git a/scripts/artifacts-utils/compute-patches-hash.sh b/scripts/compute-patches-hash.sh similarity index 64% rename from scripts/artifacts-utils/compute-patches-hash.sh rename to scripts/compute-patches-hash.sh index b75f1b85487e..c8dd71b70d9e 100755 --- a/scripts/artifacts-utils/compute-patches-hash.sh +++ b/scripts/compute-patches-hash.sh @@ -2,15 +2,17 @@ SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)" readonly SCRIPT_DIR -source "$SCRIPT_DIR/../shellUtils.sh" +source "$SCRIPT_DIR/shellUtils.sh" -PATCH_DIRS=("$@") - -if [ ${#PATCH_DIRS[@]} -eq 0 ]; then - error "At least one patch directory argument is required" +if [ $# -eq 0 ]; then + error "Please provide at least one path as an argument" exit 1 fi +PATCH_DIRS=("$@") +readonly PATCH_DIRS + +# Find all patches, compute their hash, put filename before hash, sort, compute hash of hashes PATCHES_HASH=$(find "${PATCH_DIRS[@]}" -type f \( -name "react-native+*.patch" -o -name "@react-native+*.patch" \) -exec sha256sum {} \; | awk '{split($2, pathParts, "/"); print pathParts[length(pathParts)], $1 }' | sort | sha256sum | awk '{print $1}') echo "$PATCHES_HASH" diff --git a/scripts/pod-install.sh b/scripts/pod-install.sh index 5d529098dad1..b0739b28d4a4 100755 --- a/scripts/pod-install.sh +++ b/scripts/pod-install.sh @@ -54,7 +54,7 @@ if [[ "$IS_HYBRID_APP_REPO" == "true" && "$NEW_DOT_FLAG" == "false" ]]; then # Navigate to the OldDot repository, and run bundle install and pod install cd Mobile-Expensify/ios bundle install - bundle exec pod install + RCT_USE_RN_DEP=0 RCT_USE_PREBUILT_RNCORE=0 bundle exec pod install exit 0 fi @@ -110,7 +110,7 @@ if [ -d "$CACHED_PODSPEC_DIR" ]; then fi cd ios || cleanupAndExit 1 -bundle exec pod install +RCT_USE_RN_DEP=0 RCT_USE_PREBUILT_RNCORE=0 bundle exec pod install # Go back to where we started cleanupAndExit 0 diff --git a/scripts/run-build.sh b/scripts/run-build.sh index f4f97d4a92c2..bd81a64e7657 100755 --- a/scripts/run-build.sh +++ b/scripts/run-build.sh @@ -65,13 +65,13 @@ fi # Check if the argument is one of the desired values case "$BUILD" in --ios) - npx rock run:ios --configuration $IOS_MODE --scheme "$SCHEME" --verbose --dev-server "${ROCK_FLAGS[@]}" + npx rock run:ios --configuration $IOS_MODE --scheme "$SCHEME" --dev-server "${ROCK_FLAGS[@]}" ;; --ipad) - npx rock run:ios --simulator "iPad Pro (12.9-inch) (6th generation)" --configuration $IOS_MODE --scheme "$SCHEME" --verbose --dev-server "${ROCK_FLAGS[@]}" + npx rock run:ios --simulator "iPad Pro (12.9-inch) (6th generation)" --configuration $IOS_MODE --scheme "$SCHEME" --dev-server "${ROCK_FLAGS[@]}" ;; --ipad-sm) - npx rock run:ios --simulator "iPad Pro (11-inch) (4th generation)" --configuration $IOS_MODE --scheme "$SCHEME" --verbose --dev-server "${ROCK_FLAGS[@]}" + npx rock run:ios --simulator "iPad Pro (11-inch) (4th generation)" --configuration $IOS_MODE --scheme "$SCHEME" --dev-server "${ROCK_FLAGS[@]}" ;; --android) # Check if this is an Expensify developer with WARP (only they need cert import) diff --git a/tests/unit/ArtifactsResolverTest.ts b/tests/unit/ArtifactsResolverTest.ts deleted file mode 100644 index a160d8cdb143..000000000000 --- a/tests/unit/ArtifactsResolverTest.ts +++ /dev/null @@ -1,143 +0,0 @@ -import resolveArtifacts, {ARTIFACT_IDS} from '@scripts/artifacts-utils/lib/artifactsResolver'; - -/** - * @jest-environment node - */ -import {getOctokit} from '@actions/github'; -import {execFileSync} from 'child_process'; -import fs from 'fs'; - -jest.mock('child_process'); -jest.mock('@actions/github'); - -const mockExecFileSync = jest.mocked(execFileSync); -const mockGetOctokit = jest.mocked(getOctokit); -const mockPaginate = jest.fn(); - -const NEW_DOT_ROOT = '/repo'; -const LOCAL_HASH = 'abc123hash'; - -/** A minimal fetch Response stub — only the members the resolver reads. */ -function fakeFetchResponse(body: string) { - return {ok: true, status: 200, text: () => Promise.resolve(body)}; -} - -/** Replaces global fetch with a queue of POM responses (one per candidate lookup). */ -function mockFetchBodies(bodies: string[]) { - let call = 0; - global.fetch = jest.fn().mockImplementation(() => Promise.resolve(fakeFetchResponse(bodies.at(call++) ?? ''))); -} - -/** Makes the Octokit package-versions API return the given version names. */ -function mockVersions(names: string[]) { - mockPaginate.mockResolvedValue(names.map((name) => ({name}))); - // Faking a minimal Octokit surface in a unit test. - // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion - mockGetOctokit.mockReturnValue({ - paginate: mockPaginate, - rest: {packages: {getAllPackageVersionsForPackageOwnedByOrg: jest.fn()}}, - } as unknown as ReturnType); -} - -/** Mocks the gh CLI calls needed for credentials + the local patches hash. */ -function mockResolveExec() { - mockExecFileSync.mockImplementation((cmd: string, args?: readonly string[]) => { - if (cmd === 'bash') { - return LOCAL_HASH; - } - if (cmd === 'gh' && args?.includes('status')) { - return 'Token scopes: read:packages'; - } - if (cmd === 'gh' && args?.includes('user')) { - // `gh api user --jq .login` returns the bare login string. - return 'me'; - } - if (cmd === 'gh' && args?.includes('token')) { - return 'tok'; - } - return ''; - }); - jest.spyOn(fs, 'readFileSync').mockReturnValue('{"dependencies":{"react-native":"0.85.3"}}'); -} - -describe('artifactsResolver', () => { - const ORIGINAL_CI = typeof process.env.CI === 'string' ? process.env.CI : undefined; - - beforeEach(() => { - jest.clearAllMocks(); - // Force the local (gh) credential path deterministically, regardless of the runner. - delete process.env.CI; - }); - - afterEach(() => { - jest.restoreAllMocks(); - if (ORIGINAL_CI === undefined) { - delete process.env.CI; - } else { - process.env.CI = ORIGINAL_CI; - } - }); - - describe('ARTIFACT_IDS', () => { - it('uses the correct Maven artifactId per platform', () => { - expect(ARTIFACT_IDS.android).toBe('react-android'); - expect(ARTIFACT_IDS.ios).toBe('react-native-artifacts'); - }); - }); - - describe('resolveArtifacts', () => { - it('falls back to source build when the GitHub CLI is unavailable', async () => { - mockExecFileSync.mockImplementation((cmd: string) => { - if (cmd === 'which') { - throw new Error('not found'); - } - return ''; - }); - - const result = await resolveArtifacts({platform: 'ios', packageName: 'react-hybrid', newDotRoot: NEW_DOT_ROOT, isHybrid: true}); - - expect(result).toStrictEqual({buildFromSource: true, version: null, packageName: 'react-hybrid', artifactId: 'react-native-artifacts'}); - }); - - it('resolves a matching version and does not build from source', async () => { - mockResolveExec(); - mockVersions(['0.85.3-nomatch', '0.85.3-match']); - mockFetchBodies(['differentHash', `${LOCAL_HASH}`]); - - const result = await resolveArtifacts({platform: 'ios', packageName: 'react-hybrid', newDotRoot: NEW_DOT_ROOT, isHybrid: true}); - - expect(result.buildFromSource).toBe(false); - expect(result.version).toBe('0.85.3-match'); - if (!result.buildFromSource) { - expect(result.githubToken).toBe('tok'); - // iOS carries no username — its result type doesn't even include the field. - expect('githubUsername' in result).toBe(false); - } - }); - - it('returns the username alongside the token for a matching Android artifact', async () => { - mockResolveExec(); - mockVersions(['0.85.3-match']); - mockFetchBodies([`${LOCAL_HASH}`]); - - const result = await resolveArtifacts({platform: 'android', packageName: 'react-standalone', newDotRoot: NEW_DOT_ROOT, isHybrid: false}); - - expect(result.buildFromSource).toBe(false); - if (!result.buildFromSource) { - expect(result.githubToken).toBe('tok'); - expect(result.githubUsername).toBe('me'); - } - }); - - it('falls back to source build when no candidate matches the local patches hash', async () => { - mockResolveExec(); - mockVersions(['0.85.3-other']); - mockFetchBodies(['nomatch']); - - const result = await resolveArtifacts({platform: 'android', packageName: 'react-standalone', newDotRoot: NEW_DOT_ROOT, isHybrid: false}); - - expect(result.buildFromSource).toBe(true); - expect(result.version).toBeNull(); - }); - }); -}); From d547a069b1d6e9d397c64f99272ec7654fcfe3c6 Mon Sep 17 00:00:00 2001 From: mateuuszzzzz Date: Thu, 16 Jul 2026 14:10:43 +0200 Subject: [PATCH 17/18] Add client-side code that consumes iOS artifacts and extract common logic to typescript utilities --- .../publishReactNativeAndroidArtifacts.yml | 4 +- .../publishReactNativeiOSArtifacts.yml | 4 +- .../workflows/verifyReactNativePatches.yml | 8 +- android/build.gradle | 2 +- android/settings.gradle | 2 +- gradleUtils/ExpensiLog.gradle | 26 -- gradleUtils/PatchedArtifactsSettings.gradle | 176 ------------ rock.config.mjs | 2 +- .../android/ExpensiUtils.gradle | 47 ++++ .../android/PatchedArtifactsSettings.gradle | 42 +++ .../compute-patches-hash.sh | 12 +- .../ios/patched_ios_artifacts.rb | 73 +++++ .../artifacts-utils/lib/artifactsResolver.ts | 258 ++++++++++++++++++ scripts/artifacts-utils/resolve-artifacts.ts | 31 +++ scripts/pod-install.sh | 4 +- scripts/run-build.sh | 6 +- tests/unit/ArtifactsResolverTest.ts | 143 ++++++++++ 17 files changed, 615 insertions(+), 225 deletions(-) delete mode 100644 gradleUtils/ExpensiLog.gradle delete mode 100644 gradleUtils/PatchedArtifactsSettings.gradle create mode 100644 scripts/artifacts-utils/android/ExpensiUtils.gradle create mode 100644 scripts/artifacts-utils/android/PatchedArtifactsSettings.gradle rename scripts/{ => artifacts-utils}/compute-patches-hash.sh (64%) create mode 100644 scripts/artifacts-utils/ios/patched_ios_artifacts.rb create mode 100644 scripts/artifacts-utils/lib/artifactsResolver.ts create mode 100644 scripts/artifacts-utils/resolve-artifacts.ts create mode 100644 tests/unit/ArtifactsResolverTest.ts diff --git a/.github/workflows/publishReactNativeAndroidArtifacts.yml b/.github/workflows/publishReactNativeAndroidArtifacts.yml index 460499f76787..705214cf86e1 100644 --- a/.github/workflows/publishReactNativeAndroidArtifacts.yml +++ b/.github/workflows/publishReactNativeAndroidArtifacts.yml @@ -59,9 +59,9 @@ jobs: id: computePatchesHash run: | if [[ '${{ matrix.is_hybrid }}' == 'true' ]]; then - echo "PATCHES_HASH=$(./scripts/compute-patches-hash.sh patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" + echo "PATCHES_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" else - echo "PATCHES_HASH=$(./scripts/compute-patches-hash.sh patches)" >> "$GITHUB_OUTPUT" + echo "PATCHES_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh patches)" >> "$GITHUB_OUTPUT" fi - name: Setup Node diff --git a/.github/workflows/publishReactNativeiOSArtifacts.yml b/.github/workflows/publishReactNativeiOSArtifacts.yml index b4347a53e5d0..3d3987303df0 100644 --- a/.github/workflows/publishReactNativeiOSArtifacts.yml +++ b/.github/workflows/publishReactNativeiOSArtifacts.yml @@ -209,9 +209,9 @@ jobs: id: computePatchesHash run: | if [[ '${{ matrix.is_hybrid }}' == 'true' ]]; then - echo "PATCHES_HASH=$(./scripts/compute-patches-hash.sh patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" + echo "PATCHES_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" else - echo "PATCHES_HASH=$(./scripts/compute-patches-hash.sh patches)" >> "$GITHUB_OUTPUT" + echo "PATCHES_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh patches)" >> "$GITHUB_OUTPUT" fi - name: Determine new patched RN version diff --git a/.github/workflows/verifyReactNativePatches.yml b/.github/workflows/verifyReactNativePatches.yml index 4485f8135ab0..b6d02dfc54db 100644 --- a/.github/workflows/verifyReactNativePatches.yml +++ b/.github/workflows/verifyReactNativePatches.yml @@ -53,8 +53,8 @@ jobs: if: ${{ inputs.ref_after != '' }} id: getOldPatchesHash run: | - echo "HYBRID_APP_HASH=$(./scripts/compute-patches-hash.sh patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" - echo "STANDALONE_APP_HASH=$(./scripts/compute-patches-hash.sh patches)" >> "$GITHUB_OUTPUT" + echo "HYBRID_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" + echo "STANDALONE_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh patches)" >> "$GITHUB_OUTPUT" - name: Get previous react-native version if: ${{ inputs.ref_after != '' }} @@ -71,8 +71,8 @@ jobs: - name: Get new patches hash id: getNewPatchesHash run: | - echo "HYBRID_APP_HASH=$(./scripts/compute-patches-hash.sh patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" - echo "STANDALONE_APP_HASH=$(./scripts/compute-patches-hash.sh patches)" >> "$GITHUB_OUTPUT" + echo "HYBRID_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh patches Mobile-Expensify/patches)" >> "$GITHUB_OUTPUT" + echo "STANDALONE_APP_HASH=$(./scripts/artifacts-utils/compute-patches-hash.sh patches)" >> "$GITHUB_OUTPUT" - name: Get new react-native version if: ${{ inputs.ref_after != '' }} diff --git a/android/build.gradle b/android/build.gradle index 6a970ae008aa..7ab81991995f 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -16,7 +16,7 @@ buildscript { // "mapbox" indicates the usage of the Mapbox SDK. RNMapboxMapsImpl = "mapbox" reactNativeIncludedBuild = gradle.getIncludedBuilds().find { it.name == 'react-native' } - // This is our custom extension that we defined in gradleUtils/PatchedArtifactsSettings.gradle + // This is our custom extension that we defined in scripts/artifacts-utils/android/PatchedArtifactsSettings.gradle // It enables us to use custom artifacts of patched react-native patchedArtifactsConfig = project.gradle.settings.extensions.findByName('patchedArtifacts') } diff --git a/android/settings.gradle b/android/settings.gradle index b41080ec1d7a..e1e1b28c975c 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -6,7 +6,7 @@ plugins { id("com.facebook.react.settings") id("expo-autolinking-settings") } -apply from: "${rootDir}/../gradleUtils/PatchedArtifactsSettings.gradle" +apply from: "${rootDir}/../scripts/artifacts-utils/android/PatchedArtifactsSettings.gradle" extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand(['npx', 'rock', 'config', '-p', 'android']) } rootProject.name = 'NewExpensify' diff --git a/gradleUtils/ExpensiLog.gradle b/gradleUtils/ExpensiLog.gradle deleted file mode 100644 index fcf15c98dc1a..000000000000 --- a/gradleUtils/ExpensiLog.gradle +++ /dev/null @@ -1,26 +0,0 @@ -ext.ExpensiLog = new Object() { - private final String BLUE = "\u001B[34m" - private final String YELLOW = "\u001B[33m" - private final String RED = "\u001B[31m" - private final String RESET = "\u001B[0m" - - private String prefix = "" - void setPrefix(String newPrefix) { this.prefix = newPrefix } - void clearPrefix() { this.prefix = "" } - - private String format(String color, String message) { - return "${color}[${prefix}] ${message}${this.RESET}" - } - - void lifecycle(String message) { - logger.lifecycle(format(this.BLUE, message)) - } - - void warn(String message) { - logger.warn(format(this.YELLOW, message)) - } - - void error(String message) { - logger.error(format(this.RED, message)) - } -} diff --git a/gradleUtils/PatchedArtifactsSettings.gradle b/gradleUtils/PatchedArtifactsSettings.gradle deleted file mode 100644 index 066a70ef982a..000000000000 --- a/gradleUtils/PatchedArtifactsSettings.gradle +++ /dev/null @@ -1,176 +0,0 @@ -import groovy.xml.XmlSlurper -import groovy.json.JsonSlurper -import java.util.concurrent.TimeUnit - -apply from: "ExpensiLog.gradle" - -class PatchedArtifactsConfig { - String version = null - String packageName = null - String githubUsername = null - String githubToken = null - Boolean buildFromSource = true -} - -def patchedArtifacts = settings.getExtensions().create('patchedArtifacts', PatchedArtifactsConfig) - -def warnIfNotConfigured(reason) { - ExpensiLog.warn("$reason") - ExpensiLog.warn("For setup instructions, refer to: https://github.com/Expensify/App/blob/main/contributingGuides/SETUP_ANDROID.md#enabling-prebuilt-react-native-artifacts") -} - -def getNewDotRootDir() { - return "${rootDir}/${hasProperty('newDotRoot') ? getProperty('newDotRoot') : "/.."}" -} - -def runCommand(command) { - def process = command.execute() - process.waitFor(10, TimeUnit.SECONDS) - return [output: process.in.text?.trim(), exitCode: process.exitValue()] -} - -def hasGithubCLI() { - return runCommand(["which", "gh"]).exitCode == 0 -} - -def isCIEnvironment() { - return System.getenv("CI") != null -} - -def hasRequiredScopes() { - def scopes = runCommand(["gh", "auth", "status"]).output - return ['read:packages', 'write:packages'].any { scope -> scopes.contains(scope) } -} - -def getCIGithubActor() { - return System.getenv("GITHUB_ACTOR") -} - -def getCIGithubToken() { - return System.getenv("GITHUB_TOKEN") -} - -def hasRequiredCIEnvs() { - return getCIGithubActor() != null && getCIGithubToken() != null -} - -def getGithubUsername() { - if (isCIEnvironment()) { - return getCIGithubActor() - } - return new JsonSlurper().parseText(runCommand(["gh", "api", "user"]).output).login -} - -def getGithubToken() { - if (isCIEnvironment()) { - return getCIGithubToken() - } - return runCommand(["gh", "auth", "token"]).output -} - -def validateCredentialsContext() { - if (!hasGithubCLI()) { - return "No Github CLI found." - } - - if (isCIEnvironment() && !hasRequiredCIEnvs()) { - return "Missing required CI environment variables GITHUB_ACTOR and/or GITHUB_TOKEN." - } - - if (!isCIEnvironment() && !hasRequiredScopes()) { - return "Github token does not have required scope read:packages." - } - - return null -} - -def getCredentials() { - try { - def validationErrorMessage = validateCredentialsContext() - if (validationErrorMessage != null) { - warnIfNotConfigured(validationErrorMessage) - return null - } - - return [ - githubUsername: getGithubUsername(), - githubToken: getGithubToken(), - ] - } catch (Exception e) { - warnIfNotConfigured("Failed to get Github credentials. This might be due to an expired token or not being logged in.") - return null - } -} - -def getReactNativeVersion() { - def packageJsonPath = "${getNewDotRootDir()}/package.json" - def packageJson = file(packageJsonPath) - return new JsonSlurper().parse(packageJson).dependencies.'react-native' -} - -def getArtifactsCandidates(packageName) { - def reactNativeVersion = getReactNativeVersion() - def patchedVersions = runCommand(["gh", "api", "/orgs/Expensify/packages/maven/com.expensify.${packageName}.react-android/versions", "--jq", ".[].name"]).output - return patchedVersions.split("\n").findAll { it.startsWith(reactNativeVersion) } -} - -def getPomFile(version, packageName, githubToken) { - return new XmlSlurper().parse( - new URL("https://maven.pkg.github.com/Expensify/App/com/expensify/${packageName}/react-android/${version}/react-android-${version}.pom").openConnection().with { - setRequestProperty("Authorization", "Bearer ${githubToken}") - getInputStream() - } - ) -} - -def getLocalPatchesHash() { - def newDotRootDir = getNewDotRootDir() - def computePatchesScript = "${getNewDotRootDir()}/scripts/compute-patches-hash.sh" - def arguments = hasProperty('newDotRoot') ? ["${newDotRootDir}/patches", "${newDotRootDir}/Mobile-Expensify/patches"] : ["${newDotRootDir}/patches"] - def localPatchesHash = runCommand([computePatchesScript] + arguments).output.trim() - - return localPatchesHash -} - -def findMatchingArtifactsVersion(packageName, githubToken) { - try { - def localPatchesHash = getLocalPatchesHash() - def candidates = getArtifactsCandidates(packageName) - for (candidate in candidates) { - def patchesHashFromPomFile = getPomFile(candidate, packageName, githubToken).properties.patchesHash.text().trim() - if (patchesHashFromPomFile == localPatchesHash) { - return candidate - } - } - } catch (Exception e) { - ExpensiLog.error("Failed to find matching artifacts version for ${packageName}. Reason: $e") - return null - } -} - -settings.extensions.configure(PatchedArtifactsConfig) { config -> - ExpensiLog.setPrefix("PatchedArtifacts") - config.buildFromSource = getProperty('patchedArtifacts.forceBuildFromSource') == 'true' - if(config.buildFromSource) { - ExpensiLog.lifecycle("Forcing build from source.") - return - } - - def credentials = getCredentials() - config.packageName = getProperty('patchedArtifacts.packageName') - - if(credentials != null) { - config.githubUsername = credentials.githubUsername - config.githubToken = credentials.githubToken - config.version = findMatchingArtifactsVersion(config.packageName, config.githubToken) - } - - if(config.version == null || config.packageName == null || config.githubUsername == null || config.githubToken == null) { - config.buildFromSource = true - ExpensiLog.error("No matching artifacts version found for ${config.packageName}. Building react-native from source.") - } - else { - config.buildFromSource = false - ExpensiLog.lifecycle("Using patched react-native artifacts: ${config.packageName}:${config.version}") - } -} diff --git a/rock.config.mjs b/rock.config.mjs index fd354e8f6832..533c0a3203ce 100644 --- a/rock.config.mjs +++ b/rock.config.mjs @@ -22,7 +22,7 @@ export default { android: platformAndroid({sourceDir: isHybrid ? './Mobile-Expensify/Android' : './android'}), }, fingerprint: { - extraSources: ['android/gradle.properties', 'ios/Podfile', 'scripts/compute-patches-hash.sh', 'patches', ...(isHybrid ? ['Mobile-Expensify/patches'] : [])], + extraSources: ['android/gradle.properties', 'ios/Podfile', 'scripts/artifacts-utils/compute-patches-hash.sh', 'patches', ...(isHybrid ? ['Mobile-Expensify/patches'] : [])], env: ['USE_WEB_PROXY', 'PUSHER_DEV_SUFFIX', 'SECURE_NGROK_URL', 'NGROK_URL', 'USE_NGROK', 'FORCE_NATIVE_BUILD'], ignorePaths: ['Mobile-Expensify/Android/assets/app/shared/bundle.js'], }, diff --git a/scripts/artifacts-utils/android/ExpensiUtils.gradle b/scripts/artifacts-utils/android/ExpensiUtils.gradle new file mode 100644 index 000000000000..b76b617d16d2 --- /dev/null +++ b/scripts/artifacts-utils/android/ExpensiUtils.gradle @@ -0,0 +1,47 @@ +import groovy.json.JsonSlurper +import java.util.concurrent.TimeUnit + +ext.ExpensiLog = new Object() { + private final String BLUE = "" + private final String YELLOW = "" + private final String RED = "" + private final String RESET = "" + + private String prefix = "" + void setPrefix(String newPrefix) { this.prefix = newPrefix } + + private String format(String color, String message) { + return "${color}[${prefix}] ${message}${this.RESET}" + } + + void lifecycle(String message) { logger.lifecycle(format(this.BLUE, message)) } + void warn(String message) { logger.warn(format(this.YELLOW, message)) } + void error(String message) { logger.error(format(this.RED, message)) } +} + +ext.ExpensiUtils = new Object() { + Map runCommand(List command, long timeoutSeconds = 10, File workingDir = null) { + def process = workingDir ? command.execute(null, workingDir) : command.execute() + process.waitFor(timeoutSeconds, TimeUnit.SECONDS) + return [output: process.in.text?.trim(), exitCode: process.exitValue()] + } + + // Returns [buildFromSource, version, githubUsername, githubToken]; any failure -> build from source. + Map resolveArtifacts(String packageName, String newDotRootDir, boolean isHybrid) { + def cmd = [ + "bun", "${newDotRootDir}/scripts/artifacts-utils/resolve-artifacts.ts", + "--platform=android", "--package=${packageName}", "--hybrid=${isHybrid}", "--new-dot-root=${newDotRootDir}" + ] + try { + def result = runCommand(cmd, 120, new File(newDotRootDir)) + if (result.exitCode != 0) { + ExpensiLog.error("Artifacts resolver failed (exit ${result.exitCode}). Building react-native from source.") + return [buildFromSource: true, version: null] + } + return new JsonSlurper().parseText(result.output) + } catch (Exception e) { + ExpensiLog.error("Artifacts resolver error: ${e}. Building react-native from source.") + return [buildFromSource: true, version: null] + } + } +} diff --git a/scripts/artifacts-utils/android/PatchedArtifactsSettings.gradle b/scripts/artifacts-utils/android/PatchedArtifactsSettings.gradle new file mode 100644 index 000000000000..5654e57838d6 --- /dev/null +++ b/scripts/artifacts-utils/android/PatchedArtifactsSettings.gradle @@ -0,0 +1,42 @@ +apply from: "ExpensiUtils.gradle" + +class PatchedArtifactsConfig { + String version = null + String packageName = null + String githubUsername = null + String githubToken = null + Boolean buildFromSource = true +} + +settings.getExtensions().create('patchedArtifacts', PatchedArtifactsConfig) + +def getNewDotRootDir() { + return "${rootDir}/${hasProperty('newDotRoot') ? getProperty('newDotRoot') : "/.."}" +} + +settings.extensions.configure(PatchedArtifactsConfig) { config -> + ExpensiLog.setPrefix("PatchedArtifacts") + config.buildFromSource = getProperty('patchedArtifacts.forceBuildFromSource') == 'true' + if (config.buildFromSource) { + ExpensiLog.lifecycle("Forcing build from source.") + return + } + + config.packageName = getProperty('patchedArtifacts.packageName') + + def isHybrid = config.packageName == 'react-hybrid' + def resolution = ExpensiUtils.resolveArtifacts(config.packageName, getNewDotRootDir().toString(), isHybrid) + config.version = resolution.version + config.buildFromSource = resolution.buildFromSource + + if (config.buildFromSource || config.version == null) { + config.buildFromSource = true + ExpensiLog.error("No matching artifacts version found for ${config.packageName}. Building react-native from source.") + return + } + + // The resolver returns the credentials the Maven download needs alongside the version. + config.githubUsername = resolution.githubUsername + config.githubToken = resolution.githubToken + ExpensiLog.lifecycle("Using patched react-native artifacts: ${config.packageName}:${config.version}") +} diff --git a/scripts/compute-patches-hash.sh b/scripts/artifacts-utils/compute-patches-hash.sh similarity index 64% rename from scripts/compute-patches-hash.sh rename to scripts/artifacts-utils/compute-patches-hash.sh index c8dd71b70d9e..b75f1b85487e 100755 --- a/scripts/compute-patches-hash.sh +++ b/scripts/artifacts-utils/compute-patches-hash.sh @@ -2,17 +2,15 @@ SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)" readonly SCRIPT_DIR -source "$SCRIPT_DIR/shellUtils.sh" +source "$SCRIPT_DIR/../shellUtils.sh" -if [ $# -eq 0 ]; then - error "Please provide at least one path as an argument" +PATCH_DIRS=("$@") + +if [ ${#PATCH_DIRS[@]} -eq 0 ]; then + error "At least one patch directory argument is required" exit 1 fi -PATCH_DIRS=("$@") -readonly PATCH_DIRS - -# Find all patches, compute their hash, put filename before hash, sort, compute hash of hashes PATCHES_HASH=$(find "${PATCH_DIRS[@]}" -type f \( -name "react-native+*.patch" -o -name "@react-native+*.patch" \) -exec sha256sum {} \; | awk '{split($2, pathParts, "/"); print pathParts[length(pathParts)], $1 }' | sort | sha256sum | awk '{print $1}') echo "$PATCHES_HASH" diff --git a/scripts/artifacts-utils/ios/patched_ios_artifacts.rb b/scripts/artifacts-utils/ios/patched_ios_artifacts.rb new file mode 100644 index 000000000000..fec1448b7eb0 --- /dev/null +++ b/scripts/artifacts-utils/ios/patched_ios_artifacts.rb @@ -0,0 +1,73 @@ +# Consumer for Expensify's patched React Native iOS prebuilt artifacts. +# Reopens ReactNativeCoreUtils (rncore.rb) to point RNCore resolution and +# download at our private GitHub Packages Maven repo (matched by patches hash). +# Must be required after react_native_pods.rb, which defines ReactNativeCoreUtils. + +require 'json' + +module PatchedIOSArtifacts + # scripts/artifacts-utils/ios/ -> repo root is three levels up. + NEW_DOT_ROOT = File.expand_path('../../..', __dir__) + GITHUB_PACKAGES_BASE = 'https://maven.pkg.github.com/Expensify/App' + + def self.setup + is_hybrid = ENV['IS_HYBRID_APP'] == 'true' + package_name = is_hybrid ? 'react-hybrid' : 'react-standalone' + + forced_source = ENV['RCT_USE_PREBUILT_RNCORE'] == '0' + resolution = forced_source ? {'buildFromSource' => true, 'version' => nil} : resolve(package_name, is_hybrid) + + ReactNativeCoreUtils.class_variable_set(:@@patched_version, resolution['version']) + ReactNativeCoreUtils.class_variable_set(:@@patched_package_name, package_name) + ReactNativeCoreUtils.class_variable_set(:@@patched_github_token, resolution['githubToken']) + ReactNativeCoreUtils.class_variable_set(:@@patched_build_from_source, resolution['buildFromSource']) + + ENV['RCT_USE_PREBUILT_RNCORE'] = resolution['buildFromSource'] ? '0' : '1' + end + + def self.resolve(package_name, is_hybrid) + cmd = [ + 'bun', File.join(NEW_DOT_ROOT, 'scripts/artifacts-utils/resolve-artifacts.ts'), + '--platform=ios', "--package=#{package_name}", "--hybrid=#{is_hybrid}", "--new-dot-root=#{NEW_DOT_ROOT}" + ] + # stdout is pure JSON; the resolver logs to stderr. + output = IO.popen(cmd, chdir: NEW_DOT_ROOT, &:read) + raise "resolver exited #{$?.exitstatus}" unless $?.success? + JSON.parse(output) + rescue => e + Pod::UI.warn("[PatchedIOSArtifacts] Resolver failed (#{e.message}); building from source.") if defined?(Pod::UI) + {'buildFromSource' => true, 'version' => nil} + end +end + +class ReactNativeCoreUtils + def self.setup_rncore(react_native_path, react_native_version) + @@react_native_path = react_native_path + # Base RN version (e.g. 0.85.3) — used by RN's install flow as a non-empty guard. The actual + # download URL uses @@patched_version via our stable_tarball_url override, so this stays the plain version. + @@react_native_version = react_native_version + @@build_from_source = @@patched_build_from_source + @@download_dsyms = ENV['RCT_SYMBOLICATE_PREBUILT_FRAMEWORKS'] == '1' + end + + def self.stable_tarball_url(_version, build_type, dsyms = false) + classifier = "reactnative-core-#{dsyms ? 'dSYM-' : ''}#{build_type}" + "#{PatchedIOSArtifacts::GITHUB_PACKAGES_BASE}/com/expensify/#{@@patched_package_name}/react-native-artifacts/#{@@patched_version}/react-native-artifacts-#{@@patched_version}-#{classifier}.tar.gz" + end + + def self.download_rncore_tarball(_react_native_path, tarball_url, version, configuration, dsyms = false) + dir = artifacts_dir + destination = configuration.nil? ? + "#{dir}/reactnative-core-#{version}#{dsyms ? '-dSYM' : ''}.tar.gz" : + "#{dir}/reactnative-core-#{version}#{dsyms ? '-dSYM' : ''}-#{configuration}.tar.gz" + + unless File.exist?(destination) + tmp = "#{dir}/reactnative-core.download" + # curl drops the Authorization header on the cross-host redirect to the object store. + header = @@patched_github_token ? %(-H "Authorization: Bearer #{@@patched_github_token}") : '' + ok = system(%(mkdir -p "#{dir}" && curl --fail --location --proto '=https' #{header} "#{tarball_url}" -o "#{tmp}" && mv "#{tmp}" "#{destination}")) + raise "[PatchedIOSArtifacts] Failed to download #{tarball_url}" unless ok + end + destination + end +end diff --git a/scripts/artifacts-utils/lib/artifactsResolver.ts b/scripts/artifacts-utils/lib/artifactsResolver.ts new file mode 100644 index 000000000000..b58ff7330e5f --- /dev/null +++ b/scripts/artifacts-utils/lib/artifactsResolver.ts @@ -0,0 +1,258 @@ +import {isRecord} from '@libs/ObjectUtils'; + +import {getOctokit} from '@actions/github'; +import {execFileSync} from 'child_process'; +import fs from 'fs'; +import path from 'path'; + +/** + * Shared resolver for Expensify's patched React Native prebuilt artifacts. + * Resolves which artifact version to use by matching the local patches hash + * against the `patchesHash` recorded in each candidate's Maven POM. Consumed via + * `resolve-artifacts.ts` by Gradle (Android) and `patched_ios_artifacts.rb` (iOS). + */ + +type Platform = 'ios' | 'android'; + +/** Base credentials — a token authenticates every download. */ +type Credentials = {githubToken: string}; +/** iOS's curl Bearer download needs only the token. */ +type IosCredentials = Credentials; +/** Android's Gradle Maven `credentials {}` block additionally requires the username. */ +type AndroidCredentials = Credentials & {githubUsername: string}; + +type ResolveOptions = { + platform: Platform; + packageName: string; + newDotRoot: string; + isHybrid: boolean; +}; + +/** No prebuilt match (or no credentials): the caller builds react-native from source. Carries no secrets. */ +type SourceBuild = { + buildFromSource: true; + version: null; + packageName: string; + artifactId: string; +}; + +/** A matching prebuilt artifact was found; carries the credentials the native download needs. */ +type Prebuilt = { + buildFromSource: false; + version: string; + packageName: string; + artifactId: string; +} & Creds; + +type IosResult = SourceBuild | Prebuilt; +type AndroidResult = SourceBuild | Prebuilt; +type ResolveResult = IosResult | AndroidResult; + +const GITHUB_REPO = 'Expensify/App'; +const GITHUB_OWNER = 'Expensify'; + +const ARTIFACT_IDS = { + android: 'react-android', + ios: 'react-native-artifacts', +} satisfies Record; + +/** Logs go to stderr; stdout is reserved for the JSON result. */ +function logError(message: string) { + process.stderr.write(`[PatchedArtifacts] ${message}\n`); +} + +/** Credentials as read from the source; fields are validated per platform by the callers. */ +type RawCredentials = {githubToken: string | null; githubUsername: string | null}; + +function isCI(): boolean { + return process.env.CI != null; +} + +/** Reads a non-empty environment variable, or null. */ +function getEnvVar(name: string): string | null { + const value: unknown = process.env[name]; + return typeof value === 'string' && value.length > 0 ? value : null; +} + +/** Runs a `gh` command and returns its trimmed output, or null on failure/empty. */ +function getGh(args: string[]): string | null { + try { + const output = execFileSync('gh', args, {encoding: 'utf8'}).trim(); + return output.length > 0 ? output : null; + } catch { + return null; + } +} + +function hasGithubCLI(): boolean { + try { + execFileSync('which', ['gh'], {stdio: 'ignore'}); + return true; + } catch { + return false; + } +} + +function hasRequiredScopes(): boolean { + try { + const status = execFileSync('gh', ['auth', 'status'], {encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe']}).toString(); + return status.includes('read:packages'); + } catch { + return false; + } +} + +/** + * In CI credentials come from the environment (no gh CLI). Locally they come from + * the gh CLI, which must be installed and scoped for read:packages. + */ +function readCredentials(): RawCredentials | null { + if (isCI()) { + return {githubToken: getEnvVar('GITHUB_TOKEN'), githubUsername: getEnvVar('GITHUB_ACTOR')}; + } + if (!hasGithubCLI()) { + logError('No GitHub CLI found.'); + return null; + } + if (!hasRequiredScopes()) { + logError('GitHub token does not have required scope read:packages.'); + return null; + } + return {githubToken: getGh(['auth', 'token']), githubUsername: getGh(['api', 'user', '--jq', '.login'])}; +} + +function getIosCredentials(): IosCredentials | null { + const credentials = readCredentials(); + if (credentials == null || credentials.githubToken == null) { + logError('Missing GitHub token.'); + return null; + } + return {githubToken: credentials.githubToken}; +} + +function getAndroidCredentials(): AndroidCredentials | null { + const credentials = readCredentials(); + if (credentials == null || credentials.githubToken == null || credentials.githubUsername == null) { + logError('Missing GitHub credentials (username and/or token).'); + return null; + } + return {githubToken: credentials.githubToken, githubUsername: credentials.githubUsername}; +} + +function mavenPomUrl(packageName: string, artifactId: string, version: string): string { + return `https://maven.pkg.github.com/${GITHUB_REPO}/com/expensify/${packageName}/${artifactId}/${version}/${artifactId}-${version}.pom`; +} + +function buildAuthHeaders(githubToken: string | null): Record { + return githubToken ? {Authorization: `Bearer ${githubToken}`} : {}; +} + +/** + * GitHub Packages 302-redirects to a signed object-store URL on a different host. + * `fetch` follows the redirect and drops the Authorization header on the + * cross-origin hop, so the token reaches only the initial host, never the object store. + */ +async function fetchTokenSafe(url: string, githubToken: string | null): Promise { + const response = await fetch(url, {headers: buildAuthHeaders(githubToken)}); + if (!response.ok) { + throw new Error(`Request to ${url} failed with status ${response.status}`); + } + return response.text(); +} + +function getReactNativeVersion(newDotRoot: string): string { + const parsed: unknown = JSON.parse(fs.readFileSync(path.join(newDotRoot, 'package.json'), 'utf8')); + const dependencies = isRecord(parsed) ? parsed.dependencies : undefined; + const version = isRecord(dependencies) ? dependencies['react-native'] : undefined; + if (typeof version !== 'string') { + throw new Error('Could not read react-native version from package.json'); + } + return version; +} + +function computePatchesHash(newDotRoot: string, isHybrid: boolean): string { + const script = path.join(newDotRoot, 'scripts/artifacts-utils/compute-patches-hash.sh'); + const args = [script]; + if (isHybrid) { + args.push(path.join(newDotRoot, 'patches'), path.join(newDotRoot, 'Mobile-Expensify/patches')); + } else { + args.push(path.join(newDotRoot, 'patches')); + } + return execFileSync('bash', args, {encoding: 'utf8'}).trim(); +} + +async function getArtifactsCandidates(packageName: string, artifactId: string, rnVersion: string, githubToken: string): Promise { + const octokit = getOctokit(githubToken); + /* eslint-disable @typescript-eslint/naming-convention -- GitHub REST API params are snake_case */ + const versions = await octokit.paginate(octokit.rest.packages.getAllPackageVersionsForPackageOwnedByOrg, { + package_type: 'maven', + org: GITHUB_OWNER, + package_name: `com.expensify.${packageName}.${artifactId}`, + per_page: 100, + }); /* eslint-enable @typescript-eslint/naming-convention */ + return versions.map((version) => version.name).filter((name) => name.startsWith(rnVersion)); +} + +async function getRemotePatchesHash(packageName: string, artifactId: string, version: string, githubToken: string): Promise { + const pom = await fetchTokenSafe(mavenPomUrl(packageName, artifactId, version), githubToken); + return pom.match(/([^<]+)<\/patchesHash>/)?.[1]?.trim() ?? null; +} + +async function findMatchingArtifactsVersion(options: ResolveOptions, artifactId: string, githubToken: string): Promise { + const {packageName, newDotRoot, isHybrid} = options; + try { + const localPatchesHash = computePatchesHash(newDotRoot, isHybrid); + const rnVersion = getReactNativeVersion(newDotRoot); + const candidates = await getArtifactsCandidates(packageName, artifactId, rnVersion, githubToken); + for (const candidate of candidates) { + const remoteHash = await getRemotePatchesHash(packageName, artifactId, candidate, githubToken); + if (remoteHash != null && remoteHash === localPatchesHash) { + return candidate; + } + } + return null; + } catch (error) { + logError(`Failed to find matching artifacts version for ${packageName}. Reason: ${String(error)}`); + return null; + } +} + +async function resolveWithCredentials( + options: ResolveOptions, + artifactId: string, + credentials: Creds | null, + sourceBuild: SourceBuild, +): Promise> { + if (!credentials) { + return sourceBuild; + } + const version = await findMatchingArtifactsVersion(options, artifactId, credentials.githubToken); + if (version == null) { + logError(`No matching artifacts version found for ${options.packageName}. Building react-native from source.`); + return sourceBuild; + } + logError(`Using patched react-native artifacts: ${options.packageName}:${version}`); + return {buildFromSource: false, version, packageName: options.packageName, artifactId, ...credentials}; +} + +/** + * Resolves whether a matching prebuilt artifact exists for the current patches. + * Returns a `SourceBuild` (no secrets) when no match is found or credentials are + * unavailable, otherwise a `Prebuilt` carrying the credentials the native + * download needs — token only for iOS, username + token for Android. + */ +function resolveArtifacts(options: ResolveOptions & {platform: 'ios'}): Promise; +function resolveArtifacts(options: ResolveOptions & {platform: 'android'}): Promise; +function resolveArtifacts(options: ResolveOptions): Promise { + const artifactId = ARTIFACT_IDS[options.platform]; + const sourceBuild: SourceBuild = {buildFromSource: true, version: null, packageName: options.packageName, artifactId}; + + if (options.platform === 'ios') { + return resolveWithCredentials(options, artifactId, getIosCredentials(), sourceBuild); + } + return resolveWithCredentials(options, artifactId, getAndroidCredentials(), sourceBuild); +} + +export default resolveArtifacts; +export {ARTIFACT_IDS}; +export type {Platform, ResolveOptions, ResolveResult, IosResult, AndroidResult}; diff --git a/scripts/artifacts-utils/resolve-artifacts.ts b/scripts/artifacts-utils/resolve-artifacts.ts new file mode 100644 index 000000000000..5ae804c87dc7 --- /dev/null +++ b/scripts/artifacts-utils/resolve-artifacts.ts @@ -0,0 +1,31 @@ +import parseCommandLineArguments from '../utils/parseCommandLineArguments'; +import resolveArtifacts from './lib/artifactsResolver'; + +/** + * CLI wrapper around the shared artifacts resolver for the native build systems + * (Gradle and `patched_ios_artifacts.rb`), which cannot import the TS module. + * + * Usage: + * bun scripts/artifacts-utils/resolve-artifacts.ts \ + * --platform=ios --package=react-hybrid --hybrid=true --new-dot-root=. + * + * Prints the result as JSON to stdout (logs go to stderr) and always exits 0. + */ +const args = parseCommandLineArguments(); +const platform = args.platform; +const packageName = args.package ?? ''; + +if (platform !== 'ios' && platform !== 'android') { + process.stderr.write(`[PatchedArtifacts] Invalid or missing --platform "${platform ?? ''}" (expected "ios" or "android"); building from source.\n`); + process.stdout.write(JSON.stringify({buildFromSource: true, version: null, packageName, artifactId: ''})); + process.exit(0); +} + +const options = {packageName, newDotRoot: args['new-dot-root'] ?? '.', isHybrid: args.hybrid === 'true'}; +const resolution = platform === 'ios' ? resolveArtifacts({...options, platform: 'ios'}) : resolveArtifacts({...options, platform: 'android'}); + +resolution + .then((result) => process.stdout.write(JSON.stringify(result))) + .catch(() => { + process.stdout.write(JSON.stringify({buildFromSource: true, version: null, packageName, artifactId: ''})); + }); diff --git a/scripts/pod-install.sh b/scripts/pod-install.sh index b0739b28d4a4..5d529098dad1 100755 --- a/scripts/pod-install.sh +++ b/scripts/pod-install.sh @@ -54,7 +54,7 @@ if [[ "$IS_HYBRID_APP_REPO" == "true" && "$NEW_DOT_FLAG" == "false" ]]; then # Navigate to the OldDot repository, and run bundle install and pod install cd Mobile-Expensify/ios bundle install - RCT_USE_RN_DEP=0 RCT_USE_PREBUILT_RNCORE=0 bundle exec pod install + bundle exec pod install exit 0 fi @@ -110,7 +110,7 @@ if [ -d "$CACHED_PODSPEC_DIR" ]; then fi cd ios || cleanupAndExit 1 -RCT_USE_RN_DEP=0 RCT_USE_PREBUILT_RNCORE=0 bundle exec pod install +bundle exec pod install # Go back to where we started cleanupAndExit 0 diff --git a/scripts/run-build.sh b/scripts/run-build.sh index bd81a64e7657..f4f97d4a92c2 100755 --- a/scripts/run-build.sh +++ b/scripts/run-build.sh @@ -65,13 +65,13 @@ fi # Check if the argument is one of the desired values case "$BUILD" in --ios) - npx rock run:ios --configuration $IOS_MODE --scheme "$SCHEME" --dev-server "${ROCK_FLAGS[@]}" + npx rock run:ios --configuration $IOS_MODE --scheme "$SCHEME" --verbose --dev-server "${ROCK_FLAGS[@]}" ;; --ipad) - npx rock run:ios --simulator "iPad Pro (12.9-inch) (6th generation)" --configuration $IOS_MODE --scheme "$SCHEME" --dev-server "${ROCK_FLAGS[@]}" + npx rock run:ios --simulator "iPad Pro (12.9-inch) (6th generation)" --configuration $IOS_MODE --scheme "$SCHEME" --verbose --dev-server "${ROCK_FLAGS[@]}" ;; --ipad-sm) - npx rock run:ios --simulator "iPad Pro (11-inch) (4th generation)" --configuration $IOS_MODE --scheme "$SCHEME" --dev-server "${ROCK_FLAGS[@]}" + npx rock run:ios --simulator "iPad Pro (11-inch) (4th generation)" --configuration $IOS_MODE --scheme "$SCHEME" --verbose --dev-server "${ROCK_FLAGS[@]}" ;; --android) # Check if this is an Expensify developer with WARP (only they need cert import) diff --git a/tests/unit/ArtifactsResolverTest.ts b/tests/unit/ArtifactsResolverTest.ts new file mode 100644 index 000000000000..a160d8cdb143 --- /dev/null +++ b/tests/unit/ArtifactsResolverTest.ts @@ -0,0 +1,143 @@ +import resolveArtifacts, {ARTIFACT_IDS} from '@scripts/artifacts-utils/lib/artifactsResolver'; + +/** + * @jest-environment node + */ +import {getOctokit} from '@actions/github'; +import {execFileSync} from 'child_process'; +import fs from 'fs'; + +jest.mock('child_process'); +jest.mock('@actions/github'); + +const mockExecFileSync = jest.mocked(execFileSync); +const mockGetOctokit = jest.mocked(getOctokit); +const mockPaginate = jest.fn(); + +const NEW_DOT_ROOT = '/repo'; +const LOCAL_HASH = 'abc123hash'; + +/** A minimal fetch Response stub — only the members the resolver reads. */ +function fakeFetchResponse(body: string) { + return {ok: true, status: 200, text: () => Promise.resolve(body)}; +} + +/** Replaces global fetch with a queue of POM responses (one per candidate lookup). */ +function mockFetchBodies(bodies: string[]) { + let call = 0; + global.fetch = jest.fn().mockImplementation(() => Promise.resolve(fakeFetchResponse(bodies.at(call++) ?? ''))); +} + +/** Makes the Octokit package-versions API return the given version names. */ +function mockVersions(names: string[]) { + mockPaginate.mockResolvedValue(names.map((name) => ({name}))); + // Faking a minimal Octokit surface in a unit test. + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + mockGetOctokit.mockReturnValue({ + paginate: mockPaginate, + rest: {packages: {getAllPackageVersionsForPackageOwnedByOrg: jest.fn()}}, + } as unknown as ReturnType); +} + +/** Mocks the gh CLI calls needed for credentials + the local patches hash. */ +function mockResolveExec() { + mockExecFileSync.mockImplementation((cmd: string, args?: readonly string[]) => { + if (cmd === 'bash') { + return LOCAL_HASH; + } + if (cmd === 'gh' && args?.includes('status')) { + return 'Token scopes: read:packages'; + } + if (cmd === 'gh' && args?.includes('user')) { + // `gh api user --jq .login` returns the bare login string. + return 'me'; + } + if (cmd === 'gh' && args?.includes('token')) { + return 'tok'; + } + return ''; + }); + jest.spyOn(fs, 'readFileSync').mockReturnValue('{"dependencies":{"react-native":"0.85.3"}}'); +} + +describe('artifactsResolver', () => { + const ORIGINAL_CI = typeof process.env.CI === 'string' ? process.env.CI : undefined; + + beforeEach(() => { + jest.clearAllMocks(); + // Force the local (gh) credential path deterministically, regardless of the runner. + delete process.env.CI; + }); + + afterEach(() => { + jest.restoreAllMocks(); + if (ORIGINAL_CI === undefined) { + delete process.env.CI; + } else { + process.env.CI = ORIGINAL_CI; + } + }); + + describe('ARTIFACT_IDS', () => { + it('uses the correct Maven artifactId per platform', () => { + expect(ARTIFACT_IDS.android).toBe('react-android'); + expect(ARTIFACT_IDS.ios).toBe('react-native-artifacts'); + }); + }); + + describe('resolveArtifacts', () => { + it('falls back to source build when the GitHub CLI is unavailable', async () => { + mockExecFileSync.mockImplementation((cmd: string) => { + if (cmd === 'which') { + throw new Error('not found'); + } + return ''; + }); + + const result = await resolveArtifacts({platform: 'ios', packageName: 'react-hybrid', newDotRoot: NEW_DOT_ROOT, isHybrid: true}); + + expect(result).toStrictEqual({buildFromSource: true, version: null, packageName: 'react-hybrid', artifactId: 'react-native-artifacts'}); + }); + + it('resolves a matching version and does not build from source', async () => { + mockResolveExec(); + mockVersions(['0.85.3-nomatch', '0.85.3-match']); + mockFetchBodies(['differentHash', `${LOCAL_HASH}`]); + + const result = await resolveArtifacts({platform: 'ios', packageName: 'react-hybrid', newDotRoot: NEW_DOT_ROOT, isHybrid: true}); + + expect(result.buildFromSource).toBe(false); + expect(result.version).toBe('0.85.3-match'); + if (!result.buildFromSource) { + expect(result.githubToken).toBe('tok'); + // iOS carries no username — its result type doesn't even include the field. + expect('githubUsername' in result).toBe(false); + } + }); + + it('returns the username alongside the token for a matching Android artifact', async () => { + mockResolveExec(); + mockVersions(['0.85.3-match']); + mockFetchBodies([`${LOCAL_HASH}`]); + + const result = await resolveArtifacts({platform: 'android', packageName: 'react-standalone', newDotRoot: NEW_DOT_ROOT, isHybrid: false}); + + expect(result.buildFromSource).toBe(false); + if (!result.buildFromSource) { + expect(result.githubToken).toBe('tok'); + expect(result.githubUsername).toBe('me'); + } + }); + + it('falls back to source build when no candidate matches the local patches hash', async () => { + mockResolveExec(); + mockVersions(['0.85.3-other']); + mockFetchBodies(['nomatch']); + + const result = await resolveArtifacts({platform: 'android', packageName: 'react-standalone', newDotRoot: NEW_DOT_ROOT, isHybrid: false}); + + expect(result.buildFromSource).toBe(true); + expect(result.version).toBeNull(); + }); + }); +}); From 119997dac4f2270434c825fa1bb659dde49226ca Mon Sep 17 00:00:00 2001 From: mateuuszzzzz Date: Thu, 16 Jul 2026 14:19:37 +0200 Subject: [PATCH 18/18] Add popen to cspell --- cspell.json | 1 + 1 file changed, 1 insertion(+) diff --git a/cspell.json b/cspell.json index 1b8055f4a77a..5cef97716ce3 100644 --- a/cspell.json +++ b/cspell.json @@ -827,6 +827,7 @@ "pnrs", "podspec", "podspecs", + "popen", "popen3", "positionMillis", "preauthorization",