From 37ca8cc11b3ff7f769fbaf43b18cd6b48475ff9b Mon Sep 17 00:00:00 2001 From: Niamh Coleman <92738247+niamh-coleman@users.noreply.github.com> Date: Mon, 9 Nov 2026 16:10:14 +0000 Subject: [PATCH 1/5] Download the Intercom iOS SDK from its GitHub release The Intercom pod is no longer published to the CocoaPods trunk, so the podspec now downloads Intercom.xcframework for a pinned version from the intercom-ios GitHub release, verifies its SHA-256 and vendors it. The dependency update workflow bumps the pinned version and checksum. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/update-dependencies.yml | 17 ++++-- .gitignore | 1 + README.md | 12 +++-- examples/example/ios/Podfile.lock | 6 +-- examples/with-notifications/ios/Podfile.lock | 6 +-- intercom-react-native.podspec | 11 +++- ios/intercom_ios_sdk.rb | 57 ++++++++++++++++++++ 7 files changed, 91 insertions(+), 19 deletions(-) create mode 100644 ios/intercom_ios_sdk.rb diff --git a/.github/workflows/update-dependencies.yml b/.github/workflows/update-dependencies.yml index 83fa8687..a40ed30b 100644 --- a/.github/workflows/update-dependencies.yml +++ b/.github/workflows/update-dependencies.yml @@ -36,7 +36,7 @@ jobs: id: current-versions run: | # Extract current iOS version from podspec - IOS_VERSION=$(grep -E "s\.dependency.*Intercom" intercom-react-native.podspec | sed -E "s/.*'([^']+)'.*/\1/" | sed 's/~> //') + IOS_VERSION=$(sed -nE 's/^intercom_ios_sdk_version = "([^"]+)"$/\1/p' intercom-react-native.podspec) # Extract current Android version from build.gradle ANDROID_VERSION=$(grep -E "io\.intercom\.android:intercom-sdk:" android/build.gradle | head -1 | sed -E "s/.*:([^']+)'.*/\1/") @@ -262,7 +262,18 @@ jobs: # Update iOS if needed if [ "$IOS_NEEDS_UPDATE" = "true" ]; then echo "📱 Updating iOS SDK: $IOS_OLD_VERSION → $IOS_NEW_VERSION" - sed -i '' "s/s\.dependency \"Intercom\", '~> [^']*'/s.dependency \"Intercom\", '~> $IOS_NEW_VERSION'/" intercom-react-native.podspec + IOS_ZIP="$RUNNER_TEMP/Intercom.xcframework.zip" + curl --fail --silent --show-error --location --retry 3 --output "$IOS_ZIP" \ + "https://github.com/intercom/intercom-ios/releases/download/$IOS_NEW_VERSION/Intercom.xcframework.zip" + IOS_SHA256=$(shasum -a 256 "$IOS_ZIP" | cut -d ' ' -f 1) + IOS_RELEASE_DIGEST=$(curl -s "https://api.github.com/repos/intercom/intercom-ios/releases/tags/$IOS_NEW_VERSION" \ + | jq -r '.assets[] | select(.name == "Intercom.xcframework.zip") | .digest') + if [ "$IOS_RELEASE_DIGEST" != "sha256:$IOS_SHA256" ]; then + echo "❌ Downloaded Intercom.xcframework.zip ($IOS_SHA256) does not match the release digest ($IOS_RELEASE_DIGEST)" + exit 1 + fi + sed -i '' -E "s/^intercom_ios_sdk_version = \"[^\"]*\"/intercom_ios_sdk_version = \"$IOS_NEW_VERSION\"/" intercom-react-native.podspec + sed -i '' -E "s/^intercom_ios_sdk_sha256 = \"[^\"]*\"/intercom_ios_sdk_sha256 = \"$IOS_SHA256\"/" intercom-react-native.podspec CHANGES="$CHANGES\\n- Updated iOS SDK from $IOS_OLD_VERSION to $IOS_NEW_VERSION" PR_TITLE_PARTS="iOS: $IOS_OLD_VERSION → $IOS_NEW_VERSION" @@ -343,7 +354,7 @@ jobs: if [ -f "$dir/Podfile" ]; then echo "📱 Updating pods in $dir" cd "$dir" - bundle exec pod install --repo-update || pod install --repo-update || pod update Intercom || echo "Pod install failed in $dir" + bundle exec pod install --repo-update || pod install --repo-update || echo "Pod install failed in $dir" cd - > /dev/null # Add any updated Podfile.lock files diff --git a/.gitignore b/.gitignore index 4b7725b2..2a0e607b 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,7 @@ examples/*/vendor/ # Cocoapods # examples/*/ios/Pods +intercom-ios-sdk/ # node.js # diff --git a/README.md b/README.md index 0233def2..077c04bc 100644 --- a/README.md +++ b/README.md @@ -738,11 +738,13 @@ The Expo plugin automatically generates a `FirebaseMessagingService` for Android ### Versioning in dependencies -We define the iOS and Android versions used in React Native with optimistic operators -i.e. `~> for iOS` and `+ for Android`, ensuring compatibility while allowing for non-breaking updates: - -iOS (~> in CocoaPods): -We use a version constraint e.g.`~> 18.6.1` which allows updates from `18.6.1 to 18.6.x` (up to but not including 18.7.0). +iOS (exact version): +The Intercom iOS SDK is no longer published to the CocoaPods trunk. During `pod install`, the podspec downloads +`Intercom.xcframework.zip` for the exact iOS SDK version this release was built against from the +[intercom-ios GitHub release](https://github.com/intercom/intercom-ios/releases), verifies its SHA-256 checksum +and links it. You don't need to add the `Intercom` pod to your Podfile (remove it if you added it yourself). +The download is cached in `~/Library/Caches/intercom-react-native`; set `INTERCOM_IOS_SDK_CACHE_DIR` to use +another directory, for example one your CI caches or one you pre-fill for offline builds. Android (+ in Gradle): We use a version constraint e.g. `18.6.+` which allows updates to any patch version within 18.6.x diff --git a/examples/example/ios/Podfile.lock b/examples/example/ios/Podfile.lock index faf993e9..1ef5132a 100644 --- a/examples/example/ios/Podfile.lock +++ b/examples/example/ios/Podfile.lock @@ -8,7 +8,6 @@ PODS: - hermes-engine (0.81.1): - hermes-engine/Pre-built (= 0.81.1) - hermes-engine/Pre-built (0.81.1) - - Intercom (19.9.2) - intercom-react-native (10.9.0): - boost - DoubleConversion @@ -16,7 +15,6 @@ PODS: - fmt - glog - hermes-engine - - Intercom (~> 19.9.2) - RCT-Folly - RCT-Folly/Fabric - RCTRequired @@ -2398,7 +2396,6 @@ DEPENDENCIES: SPEC REPOS: trunk: - - Intercom - SocketRocket EXTERNAL SOURCES: @@ -2560,8 +2557,7 @@ SPEC CHECKSUMS: fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 hermes-engine: 4f8246b1f6d79f625e0d99472d1f3a71da4d28ca - Intercom: c69d3e6909b6ef64b1bfbac5ca746bcdf2f67a1f - intercom-react-native: 2cbdb91ae67222232817745b3fcdeb2379e8ec76 + intercom-react-native: 4972c5f28ed080e890474dd446c8040c4a9e08e8 RCT-Folly: 59ec0ac1f2f39672a0c6e6cecdd39383b764646f RCTDeprecation: c4b9e2fd0ab200e3af72b013ed6113187c607077 RCTRequired: e97dd5dafc1db8094e63bc5031e0371f092ae92a diff --git a/examples/with-notifications/ios/Podfile.lock b/examples/with-notifications/ios/Podfile.lock index 4471bf68..5dc470d7 100644 --- a/examples/with-notifications/ios/Podfile.lock +++ b/examples/with-notifications/ios/Podfile.lock @@ -8,7 +8,6 @@ PODS: - hermes-engine (0.81.1): - hermes-engine/Pre-built (= 0.81.1) - hermes-engine/Pre-built (0.81.1) - - Intercom (19.9.2) - intercom-react-native (10.9.0): - boost - DoubleConversion @@ -16,7 +15,6 @@ PODS: - fmt - glog - hermes-engine - - Intercom (~> 19.9.2) - RCT-Folly - RCT-Folly/Fabric - RCTRequired @@ -2603,7 +2601,6 @@ DEPENDENCIES: SPEC REPOS: trunk: - - Intercom - MMKV - MMKVCore - SocketRocket @@ -2773,8 +2770,7 @@ SPEC CHECKSUMS: fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 hermes-engine: 4f8246b1f6d79f625e0d99472d1f3a71da4d28ca - Intercom: c69d3e6909b6ef64b1bfbac5ca746bcdf2f67a1f - intercom-react-native: 2cbdb91ae67222232817745b3fcdeb2379e8ec76 + intercom-react-native: 4972c5f28ed080e890474dd446c8040c4a9e08e8 MMKV: 7b5df6a8bf785c6705cc490c541b9d8a957c4a64 MMKVCore: 3f40b896e9ab522452df9df3ce983471aa2449ba RCT-Folly: 59ec0ac1f2f39672a0c6e6cecdd39383b764646f diff --git a/intercom-react-native.podspec b/intercom-react-native.podspec index 971b2e49..56d24758 100644 --- a/intercom-react-native.podspec +++ b/intercom-react-native.podspec @@ -1,7 +1,10 @@ require "json" +require File.join(__dir__, "ios", "intercom_ios_sdk") package = JSON.parse(File.read(File.join(__dir__, "package.json"))) +intercom_ios_sdk_version = "19.9.2" +intercom_ios_sdk_sha256 = "6af284508f58de833313b263f3d82eaebc255c4d9a56ad9adf2fc6af4228c0f4" Pod::Spec.new do |s| s.name = "intercom-react-native" @@ -19,7 +22,13 @@ Pod::Spec.new do |s| s.pod_target_xcconfig = { "DEFINES_MODULE" => "YES" } - s.dependency "Intercom", '~> 19.9.2' + s.vendored_frameworks = IntercomIosSdk.install_xcframework( + version: intercom_ios_sdk_version, + sha256: intercom_ios_sdk_sha256, + package_root: __dir__ + ) + s.frameworks = ["Foundation", "UIKit", "Accelerate", "Security", "SystemConfiguration", "MobileCoreServices", "ImageIO", "AudioToolbox", "QuartzCore", "CoreGraphics", "Photos", "WebKit"] + s.libraries = "icucore", "xml2" is_new_arch_enabled = ENV["RCT_NEW_ARCH_ENABLED"] == "1" folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' diff --git a/ios/intercom_ios_sdk.rb b/ios/intercom_ios_sdk.rb new file mode 100644 index 00000000..c045c44c --- /dev/null +++ b/ios/intercom_ios_sdk.rb @@ -0,0 +1,57 @@ +require 'digest' +require 'fileutils' +require 'tmpdir' + +# The Intercom pod is no longer published to the CocoaPods trunk; fetch the GitHub release instead. +module IntercomIosSdk + RELEASE_URL = 'https://github.com/intercom/intercom-ios/releases/download/%s/Intercom.xcframework.zip'.freeze + + module_function + + def install_xcframework(version:, sha256:, package_root:) + relative_path = File.join('intercom-ios-sdk', version, 'Intercom.xcframework') + framework_path = File.join(package_root, relative_path) + unless File.exist?(File.join(framework_path, 'Info.plist')) + extract(cached_zip(version, sha256), File.dirname(framework_path)) + end + relative_path + end + + def cache_dir + ENV['INTERCOM_IOS_SDK_CACHE_DIR'] || File.join(Dir.home, 'Library', 'Caches', 'intercom-react-native') + end + + def cached_zip(version, sha256) + zip_path = File.join(cache_dir, version, 'Intercom.xcframework.zip') + return zip_path if File.exist?(zip_path) && Digest::SHA256.file(zip_path).hexdigest == sha256 + + url = format(RELEASE_URL, version) + download_path = "#{zip_path}.download" + FileUtils.mkdir_p(File.dirname(zip_path)) + Pod::UI.puts "[Intercom] Downloading the Intercom iOS SDK #{version} from #{url}" + unless system('curl', '--fail', '--silent', '--show-error', '--location', '--retry', '3', '--output', download_path, url) + FileUtils.rm_f(download_path) + raise "[Intercom] Could not download #{url}. Check your network, or place the zip at #{zip_path} " \ + '(set INTERCOM_IOS_SDK_CACHE_DIR to use another cache directory).' + end + + actual = Digest::SHA256.file(download_path).hexdigest + unless actual == sha256 + FileUtils.rm_f(download_path) + raise "[Intercom] Checksum mismatch for #{url}: expected #{sha256}, got #{actual}." + end + File.rename(download_path, zip_path) + zip_path + end + + def extract(zip_path, destination) + FileUtils.mkdir_p(destination) + Dir.mktmpdir('extract-', destination) do |tmp| + raise "[Intercom] Could not unzip #{zip_path}." unless system('unzip', '-q', zip_path, '-d', tmp) + + target = File.join(destination, 'Intercom.xcframework') + FileUtils.rm_rf(target) + File.rename(File.join(tmp, 'Intercom.xcframework'), target) + end + end +end From 3c2ae9c644898edc090ed234e19a4c7140495908 Mon Sep 17 00:00:00 2001 From: Niamh Coleman <92738247+niamh-coleman@users.noreply.github.com> Date: Mon, 9 Nov 2026 17:16:18 +0000 Subject: [PATCH 2/5] Run pod install in CI and harden the iOS SDK bump A CircleCI job now installs the example app's pods with an empty SDK cache, so every PR exercises the download, checksum and extract. The update workflow takes the release digest from the release it already fetched and fails if the podspec version and checksum weren't rewritten. Stale extracted SDK versions are removed, and the helper logs without CocoaPods loaded. Co-Authored-By: Claude Opus 5.5 --- .circleci/config.yml | 18 ++++++++++++++++++ .github/workflows/update-dependencies.yml | 10 ++++++++-- ios/intercom_ios_sdk.rb | 11 ++++++++++- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e1dcd081..02f33307 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -98,6 +98,20 @@ jobs: command: | yarn prepare + pod-install: + <<: *macos_config + steps: + - attach_project + - node/install: + node-version: 22.14.0 + - run: + name: Install example pods with an empty Intercom iOS SDK cache + command: | + export INTERCOM_IOS_SDK_CACHE_DIR="$(mktemp -d)" + cd examples/example/ios + pod install + ls "$INTERCOM_IOS_SDK_CACHE_DIR"/*/Intercom.xcframework.zip + workflows: version: 2.1 build-and-test: @@ -120,3 +134,7 @@ workflows: <<: *run_always requires: - install-dependencies + - pod-install: + <<: *run_always + requires: + - install-dependencies diff --git a/.github/workflows/update-dependencies.yml b/.github/workflows/update-dependencies.yml index a40ed30b..c3a81836 100644 --- a/.github/workflows/update-dependencies.yml +++ b/.github/workflows/update-dependencies.yml @@ -56,6 +56,7 @@ jobs: IOS_RELEASE=$(curl -s https://api.github.com/repos/intercom/intercom-ios/releases/latest) IOS_VERSION=$(echo "$IOS_RELEASE" | jq -r '.tag_name // "null"') IOS_CHANGELOG=$(echo "$IOS_RELEASE" | jq -r '.body // "No changelog available"') + IOS_ZIP_DIGEST=$(echo "$IOS_RELEASE" | jq -r '[.assets[]? | select(.name == "Intercom.xcframework.zip") | .digest][0] // ""') # Get Android release ANDROID_RELEASE=$(curl -s https://api.github.com/repos/intercom/intercom-android/releases/latest) @@ -66,6 +67,7 @@ jobs: echo "🤖 Latest Android SDK: $ANDROID_VERSION" echo "ios_version=$IOS_VERSION" >> $GITHUB_OUTPUT + echo "ios_zip_digest=$IOS_ZIP_DIGEST" >> $GITHUB_OUTPUT echo "android_version=$ANDROID_VERSION" >> $GITHUB_OUTPUT echo "ios_changelog<> $GITHUB_OUTPUT echo "$IOS_CHANGELOG" >> $GITHUB_OUTPUT @@ -252,6 +254,7 @@ jobs: IOS_NEEDS_UPDATE: ${{ steps.update-analysis.outputs.ios_needs_update }} IOS_NEW_VERSION: ${{ steps.update-analysis.outputs.ios_new_version }} IOS_OLD_VERSION: ${{ steps.current-versions.outputs.ios_version }} + IOS_RELEASE_DIGEST: ${{ steps.latest-versions.outputs.ios_zip_digest }} ANDROID_NEEDS_UPDATE: ${{ steps.update-analysis.outputs.android_needs_update }} ANDROID_NEW_VERSION: ${{ steps.update-analysis.outputs.android_new_version }} ANDROID_OLD_VERSION: ${{ steps.current-versions.outputs.android_version }} @@ -266,14 +269,17 @@ jobs: curl --fail --silent --show-error --location --retry 3 --output "$IOS_ZIP" \ "https://github.com/intercom/intercom-ios/releases/download/$IOS_NEW_VERSION/Intercom.xcframework.zip" IOS_SHA256=$(shasum -a 256 "$IOS_ZIP" | cut -d ' ' -f 1) - IOS_RELEASE_DIGEST=$(curl -s "https://api.github.com/repos/intercom/intercom-ios/releases/tags/$IOS_NEW_VERSION" \ - | jq -r '.assets[] | select(.name == "Intercom.xcframework.zip") | .digest') if [ "$IOS_RELEASE_DIGEST" != "sha256:$IOS_SHA256" ]; then echo "❌ Downloaded Intercom.xcframework.zip ($IOS_SHA256) does not match the release digest ($IOS_RELEASE_DIGEST)" exit 1 fi sed -i '' -E "s/^intercom_ios_sdk_version = \"[^\"]*\"/intercom_ios_sdk_version = \"$IOS_NEW_VERSION\"/" intercom-react-native.podspec sed -i '' -E "s/^intercom_ios_sdk_sha256 = \"[^\"]*\"/intercom_ios_sdk_sha256 = \"$IOS_SHA256\"/" intercom-react-native.podspec + if ! grep -qxF "intercom_ios_sdk_version = \"$IOS_NEW_VERSION\"" intercom-react-native.podspec \ + || ! grep -qxF "intercom_ios_sdk_sha256 = \"$IOS_SHA256\"" intercom-react-native.podspec; then + echo "❌ Could not update the iOS SDK version and checksum in intercom-react-native.podspec" + exit 1 + fi CHANGES="$CHANGES\\n- Updated iOS SDK from $IOS_OLD_VERSION to $IOS_NEW_VERSION" PR_TITLE_PARTS="iOS: $IOS_OLD_VERSION → $IOS_NEW_VERSION" diff --git a/ios/intercom_ios_sdk.rb b/ios/intercom_ios_sdk.rb index c045c44c..017f2017 100644 --- a/ios/intercom_ios_sdk.rb +++ b/ios/intercom_ios_sdk.rb @@ -13,10 +13,19 @@ def install_xcframework(version:, sha256:, package_root:) framework_path = File.join(package_root, relative_path) unless File.exist?(File.join(framework_path, 'Info.plist')) extract(cached_zip(version, sha256), File.dirname(framework_path)) + remove_other_versions(File.join(package_root, 'intercom-ios-sdk'), version) end relative_path end + def remove_other_versions(sdk_root, version) + (Dir.children(sdk_root) - [version]).each { |entry| FileUtils.rm_rf(File.join(sdk_root, entry)) } + end + + def log(message) + defined?(Pod::UI) ? Pod::UI.puts(message) : puts(message) + end + def cache_dir ENV['INTERCOM_IOS_SDK_CACHE_DIR'] || File.join(Dir.home, 'Library', 'Caches', 'intercom-react-native') end @@ -28,7 +37,7 @@ def cached_zip(version, sha256) url = format(RELEASE_URL, version) download_path = "#{zip_path}.download" FileUtils.mkdir_p(File.dirname(zip_path)) - Pod::UI.puts "[Intercom] Downloading the Intercom iOS SDK #{version} from #{url}" + log "[Intercom] Downloading the Intercom iOS SDK #{version} from #{url}" unless system('curl', '--fail', '--silent', '--show-error', '--location', '--retry', '3', '--output', download_path, url) FileUtils.rm_f(download_path) raise "[Intercom] Could not download #{url}. Check your network, or place the zip at #{zip_path} " \ From 00374d18867d9e26778c02b1b75a63361ebc5482 Mon Sep 17 00:00:00 2001 From: Niamh Coleman <92738247+niamh-coleman@users.noreply.github.com> Date: Mon, 9 Nov 2026 17:22:45 +0000 Subject: [PATCH 3/5] Use Xcode 26.4 for the pod-install job React Native 0.81's pod install refuses Xcode older than 16.1, and the shared macOS config pins 16.0. Co-Authored-By: Claude Opus 5.5 --- .circleci/config.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 02f33307..8634e3d1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -100,6 +100,9 @@ jobs: pod-install: <<: *macos_config + # React Native 0.81's pod install requires Xcode 16.1 or newer. + macos: + xcode: 26.4.0 steps: - attach_project - node/install: From 5c42ef34cc9a08918d273d898f41c689ff857b6d Mon Sep 17 00:00:00 2001 From: Niamh Coleman <92738247+niamh-coleman@users.noreply.github.com> Date: Mon, 9 Nov 2026 21:44:14 +0000 Subject: [PATCH 4/5] Install the pinned Ruby before the CI pod install The Xcode 26 image has no Ruby 3.3.4, so rbenv refused to run pod. Install it from .ruby-version as script/setup does, and run pod through bundler so CI uses the Gemfile's CocoaPods. Co-Authored-By: Claude Opus 5.5 --- .circleci/config.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8634e3d1..1cb09d34 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -107,12 +107,18 @@ jobs: - attach_project - node/install: node-version: 22.14.0 + - run: + # The Xcode 26 image does not ship the Ruby pinned in .ruby-version. + name: Install Ruby and gems + command: | + rbenv install --skip-existing + bundle install - run: name: Install example pods with an empty Intercom iOS SDK cache command: | export INTERCOM_IOS_SDK_CACHE_DIR="$(mktemp -d)" cd examples/example/ios - pod install + bundle exec pod install ls "$INTERCOM_IOS_SDK_CACHE_DIR"/*/Intercom.xcframework.zip workflows: From 933e588ab0dc2009d634205c871721a371aa74a3 Mon Sep 17 00:00:00 2001 From: Niamh Coleman <92738247+niamh-coleman@users.noreply.github.com> Date: Tue, 10 Nov 2026 09:45:43 +0000 Subject: [PATCH 5/5] Only treat SDK bump PRs as existing dependency PRs by title The full-text search also matched PRs whose comments mention those words, so the daily run closed an unrelated open PR. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/update-dependencies.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-dependencies.yml b/.github/workflows/update-dependencies.yml index c3a81836..9fa5529a 100644 --- a/.github/workflows/update-dependencies.yml +++ b/.github/workflows/update-dependencies.yml @@ -152,7 +152,7 @@ jobs: echo " Target: iOS=$TARGET_IOS, Android=$TARGET_ANDROID" # Find existing PRs - EXISTING_PRS=$(gh pr list --state open --search "update Intercom SDK dependencies" --json number,title,headRefName 2>/dev/null || echo "[]") + EXISTING_PRS=$(gh pr list --state open --search "update Intercom SDK dependencies in:title" --json number,title,headRefName 2>/dev/null || echo "[]") if [ "$EXISTING_PRS" = "[]" ] || [ "$(echo "$EXISTING_PRS" | jq 'length')" -eq 0 ]; then echo "✅ No existing PRs found - proceeding with update"