From a0bcf104398eff803bc4e4e89ecfedeecdf59e3e Mon Sep 17 00:00:00 2001 From: Joao Dordio Date: Thu, 25 Jun 2026 12:03:04 +0100 Subject: [PATCH 1/3] fix: fix ios SDK release --- .github/workflows/ios-sdk-release.yml | 21 ++++++++++++++ fastlane/Fastfile | 41 +++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ios-sdk-release.yml b/.github/workflows/ios-sdk-release.yml index caac353d1..c4a4b0cdd 100644 --- a/.github/workflows/ios-sdk-release.yml +++ b/.github/workflows/ios-sdk-release.yml @@ -58,5 +58,26 @@ jobs: - name: push pods to trunk run: bundle exec fastlane ios pod_trunk_push + - name: verify cocoapods publication + run: | + set -e + for pod in Iterable-iOS-SDK Iterable-iOS-AppExtensions; do + echo "Checking trunk for $pod $VERSION..." + status=$(curl -s -o /dev/null -w "%{http_code}" "https://trunk.cocoapods.org/api/v1/pods/$pod/versions/$VERSION") + if [ "$status" != "200" ]; then + echo "::error::$pod $VERSION is not available on CocoaPods trunk (HTTP $status). The pod_trunk_push step likely failed or COCOAPODS_TRUNK_TOKEN is invalid." + exit 1 + fi + echo "$pod $VERSION confirmed on trunk." + done + - name: slack notification run: bundle exec fastlane ios slack_message version:$VERSION changelog_section:$CHANGELOG_SECTION slack_webhook:$SLACK_WEBHOOK + + - name: slack failure notification + if: failure() + run: | + run_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}" + payload=$(printf '{"text":":alert: iOS SDK release %s failed (attempt %s). Run: %s"}' \ + "$VERSION" "${{ github.run_attempt }}" "$run_url") + curl -sS -X POST -H 'Content-type: application/json' --data "$payload" "$SLACK_WEBHOOK" diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 7dd5a1039..15a08199d 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -82,6 +82,13 @@ platform :ios do lane :create_git_tag do |options| version = options[:version] + # Skip if the tag is already on the remote (re-runs of a partially-failed release) + sh("git fetch origin --tags --quiet") + if git_tag_exists(tag: version, remote: true) + UI.important("Tag #{version} already exists on remote, skipping add/push.") + next + end + add_git_tag(tag: "#{version}") push_git_tags end @@ -100,6 +107,18 @@ platform :ios do section_identifier: "[#{changelog_section}]", ) + # Skip if a release for this tag already exists (re-runs of a partially-failed release) + existing_release = begin + get_github_release(url: "Iterable/iterable-swift-sdk", version: version, api_token: github_token) + rescue StandardError + nil + end + + if existing_release + UI.important("GitHub release #{version} already exists, skipping.") + next + end + github_release = set_github_release( repository_name: "Iterable/iterable-swift-sdk", api_token: github_token, @@ -114,8 +133,26 @@ platform :ios do desc "push pod trunk" lane :pod_trunk_push do - pod_push(path: "Iterable-iOS-AppExtensions.podspec", allow_warnings: true) - pod_push(path: "Iterable-iOS-SDK.podspec", allow_warnings: true) + push_pod_if_missing("Iterable-iOS-AppExtensions.podspec") + push_pod_if_missing("Iterable-iOS-SDK.podspec") + end + + # Pushes a podspec to CocoaPods trunk only if that exact version is not already published. + # Makes the release workflow safe to re-run after a partial failure. + def push_pod_if_missing(podspec) + require 'net/http' + require 'uri' + + pod_name = File.basename(podspec, ".podspec") + spec_version = read_podspec(path: podspec)["version"] + url = URI("https://trunk.cocoapods.org/api/v1/pods/#{pod_name}/versions/#{spec_version}") + response = Net::HTTP.get_response(url) + + if response.code == "200" + UI.important("#{pod_name} #{spec_version} already published to CocoaPods trunk, skipping.") + else + pod_push(path: podspec, allow_warnings: true) + end end desc "slack message" From 2b68883ef651ed7eaba96a4f2f377341d0f2ec6e Mon Sep 17 00:00:00 2001 From: Joao Dordio Date: Thu, 25 Jun 2026 12:28:21 +0100 Subject: [PATCH 2/3] chore(release): drop Gemfile.lock, install gems via bundle install --- .github/workflows/ios-sdk-release.yml | 7 +- .gitignore | 1 + Gemfile.lock | 308 -------------------------- 3 files changed, 5 insertions(+), 311 deletions(-) delete mode 100644 Gemfile.lock diff --git a/.github/workflows/ios-sdk-release.yml b/.github/workflows/ios-sdk-release.yml index c4a4b0cdd..620a93d6c 100644 --- a/.github/workflows/ios-sdk-release.yml +++ b/.github/workflows/ios-sdk-release.yml @@ -38,9 +38,10 @@ jobs: - uses: ruby/setup-ruby@v1 with: ruby-version: "3.2" - bundler-cache: true - - name: Install Cocoapods - run: gem install cocoapods + bundler-cache: false + + - name: Install gems + run: bundle install - name: clean cocaopods cache and lint run: bundle exec fastlane ios clean_and_lint diff --git a/.gitignore b/.gitignore index 95a10044a..338de8948 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ tests/business-critical-integration/screenshots .claude *~ +Gemfile.lock Podfile.lock Pods/ diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 3558528df..000000000 --- a/Gemfile.lock +++ /dev/null @@ -1,308 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - CFPropertyList (3.0.7) - base64 - nkf - rexml - activesupport (7.1.2) - base64 - bigdecimal - concurrent-ruby (~> 1.0, >= 1.0.2) - connection_pool (>= 2.2.5) - drb - i18n (>= 1.6, < 2) - minitest (>= 5.1) - mutex_m - tzinfo (~> 2.0) - addressable (2.8.7) - public_suffix (>= 2.0.2, < 7.0) - algoliasearch (1.27.5) - httpclient (~> 2.8, >= 2.8.3) - json (>= 1.5.1) - artifactory (3.0.17) - atomos (0.1.3) - aws-eventstream (1.4.0) - aws-partitions (1.1154.0) - aws-sdk-core (3.232.0) - aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.992.0) - aws-sigv4 (~> 1.9) - base64 - bigdecimal - jmespath (~> 1, >= 1.6.1) - logger - aws-sdk-kms (1.112.0) - aws-sdk-core (~> 3, >= 3.231.0) - aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.198.0) - aws-sdk-core (~> 3, >= 3.231.0) - aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.5) - aws-sigv4 (1.12.1) - aws-eventstream (~> 1, >= 1.0.2) - babosa (1.0.4) - base64 (0.2.0) - bigdecimal (3.1.4) - claide (1.1.0) - cocoapods (1.14.2) - addressable (~> 2.8) - claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.14.2) - cocoapods-deintegrate (>= 1.0.3, < 2.0) - cocoapods-downloader (>= 2.0) - cocoapods-plugins (>= 1.0.0, < 2.0) - cocoapods-search (>= 1.0.0, < 2.0) - cocoapods-trunk (>= 1.6.0, < 2.0) - cocoapods-try (>= 1.1.0, < 2.0) - colored2 (~> 3.1) - escape (~> 0.0.4) - fourflusher (>= 2.3.0, < 3.0) - gh_inspector (~> 1.0) - molinillo (~> 0.8.0) - nap (~> 1.0) - ruby-macho (>= 2.3.0, < 3.0) - xcodeproj (>= 1.23.0, < 2.0) - cocoapods-core (1.14.2) - activesupport (>= 5.0, < 8) - addressable (~> 2.8) - algoliasearch (~> 1.0) - concurrent-ruby (~> 1.1) - fuzzy_match (~> 2.0.4) - nap (~> 1.0) - netrc (~> 0.11) - public_suffix (~> 4.0) - typhoeus (~> 1.0) - cocoapods-deintegrate (1.0.5) - cocoapods-downloader (2.0) - cocoapods-plugins (1.0.0) - nap - cocoapods-search (1.0.1) - cocoapods-trunk (1.6.0) - nap (>= 0.8, < 2.0) - netrc (~> 0.11) - cocoapods-try (1.2.0) - colored (1.2) - colored2 (3.1.2) - commander (4.6.0) - highline (~> 2.0.0) - concurrent-ruby (1.2.2) - connection_pool (2.4.1) - declarative (0.0.20) - digest-crc (0.7.0) - rake (>= 12.0.0, < 14.0.0) - domain_name (0.6.20240107) - dotenv (2.8.1) - drb (2.2.0) - ruby2_keywords - emoji_regex (3.2.3) - escape (0.0.4) - ethon (0.16.0) - ffi (>= 1.15.0) - excon (0.112.0) - faraday (1.10.4) - faraday-em_http (~> 1.0) - faraday-em_synchrony (~> 1.0) - faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0) - faraday-multipart (~> 1.0) - faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.0) - faraday-patron (~> 1.0) - faraday-rack (~> 1.0) - faraday-retry (~> 1.0) - ruby2_keywords (>= 0.0.4) - faraday-cookie_jar (0.0.7) - faraday (>= 0.8.0) - http-cookie (~> 1.0.0) - faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.1) - faraday-excon (1.1.0) - faraday-httpclient (1.0.1) - faraday-multipart (1.1.1) - multipart-post (~> 2.0) - faraday-net_http (1.0.2) - faraday-net_http_persistent (1.2.0) - faraday-patron (1.0.0) - faraday-rack (1.0.0) - faraday-retry (1.0.3) - faraday_middleware (1.2.1) - faraday (~> 1.0) - fastimage (2.4.0) - fastlane (2.228.0) - CFPropertyList (>= 2.3, < 4.0.0) - addressable (>= 2.8, < 3.0.0) - artifactory (~> 3.0) - aws-sdk-s3 (~> 1.0) - babosa (>= 1.0.3, < 2.0.0) - bundler (>= 1.12.0, < 3.0.0) - colored (~> 1.2) - commander (~> 4.6) - dotenv (>= 2.1.1, < 3.0.0) - emoji_regex (>= 0.1, < 4.0) - excon (>= 0.71.0, < 1.0.0) - faraday (~> 1.0) - faraday-cookie_jar (~> 0.0.6) - faraday_middleware (~> 1.0) - fastimage (>= 2.1.0, < 3.0.0) - fastlane-sirp (>= 1.0.0) - gh_inspector (>= 1.1.2, < 2.0.0) - google-apis-androidpublisher_v3 (~> 0.3) - google-apis-playcustomapp_v1 (~> 0.1) - google-cloud-env (>= 1.6.0, < 2.0.0) - google-cloud-storage (~> 1.31) - highline (~> 2.0) - http-cookie (~> 1.0.5) - json (< 3.0.0) - jwt (>= 2.1.0, < 3) - mini_magick (>= 4.9.4, < 5.0.0) - multipart-post (>= 2.0.0, < 3.0.0) - naturally (~> 2.2) - optparse (>= 0.1.1, < 1.0.0) - plist (>= 3.1.0, < 4.0.0) - rubyzip (>= 2.0.0, < 3.0.0) - security (= 0.1.5) - simctl (~> 1.6.3) - terminal-notifier (>= 2.0.0, < 3.0.0) - terminal-table (~> 3) - tty-screen (>= 0.6.3, < 1.0.0) - tty-spinner (>= 0.8.0, < 1.0.0) - word_wrap (~> 1.0.0) - xcodeproj (>= 1.13.0, < 2.0.0) - xcpretty (~> 0.4.1) - xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) - fastlane-plugin-changelog (0.16.0) - fastlane-plugin-create_xcframework (1.1.2) - fastlane-sirp (1.0.0) - sysrandom (~> 1.0) - ffi (1.16.3) - fourflusher (2.3.1) - fuzzy_match (2.0.4) - gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.54.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-core (0.11.3) - addressable (~> 2.5, >= 2.5.1) - googleauth (>= 0.16.2, < 2.a) - httpclient (>= 2.8.1, < 3.a) - mini_mime (~> 1.0) - representable (~> 3.0) - retriable (>= 2.0, < 4.a) - rexml - google-apis-iamcredentials_v1 (0.17.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-playcustomapp_v1 (0.13.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-storage_v1 (0.31.0) - google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.8.0) - google-cloud-env (>= 1.0, < 3.a) - google-cloud-errors (~> 1.0) - google-cloud-env (1.6.0) - faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.5.0) - google-cloud-storage (1.47.0) - addressable (~> 2.8) - digest-crc (~> 0.4) - google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.31.0) - google-cloud-core (~> 1.6) - googleauth (>= 0.16.2, < 2.a) - mini_mime (~> 1.0) - googleauth (1.8.1) - faraday (>= 0.17.3, < 3.a) - jwt (>= 1.4, < 3.0) - multi_json (~> 1.11) - os (>= 0.9, < 2.0) - signet (>= 0.16, < 2.a) - highline (2.0.3) - http-cookie (1.0.8) - domain_name (~> 0.5) - httpclient (2.9.0) - mutex_m - i18n (1.14.1) - concurrent-ruby (~> 1.0) - jmespath (1.6.2) - json (2.13.2) - jwt (2.10.2) - base64 - logger (1.7.0) - mini_magick (4.13.2) - mini_mime (1.1.5) - minitest (5.20.0) - molinillo (0.8.0) - multi_json (1.17.0) - multipart-post (2.4.1) - mutex_m (0.2.0) - nanaimo (0.4.0) - nap (1.1.0) - naturally (2.3.0) - netrc (0.11.0) - nkf (0.2.0) - optparse (0.6.0) - os (1.1.4) - plist (3.7.2) - public_suffix (4.0.7) - rake (13.3.0) - representable (3.2.0) - declarative (< 0.1.0) - trailblazer-option (>= 0.1.1, < 0.2.0) - uber (< 0.2.0) - retriable (3.1.2) - rexml (3.4.2) - rouge (3.28.0) - ruby-macho (2.5.1) - ruby2_keywords (0.0.5) - rubyzip (2.4.1) - security (0.1.5) - signet (0.21.0) - addressable (~> 2.8) - faraday (>= 0.17.5, < 3.a) - jwt (>= 1.5, < 4.0) - multi_json (~> 1.10) - simctl (1.6.10) - CFPropertyList - naturally - sysrandom (1.0.5) - terminal-notifier (2.0.0) - terminal-table (3.0.2) - unicode-display_width (>= 1.1.1, < 3) - trailblazer-option (0.1.2) - tty-cursor (0.7.1) - tty-screen (0.8.2) - tty-spinner (0.9.3) - tty-cursor (~> 0.7) - typhoeus (1.4.0) - ethon (>= 0.9.0) - tzinfo (2.0.6) - concurrent-ruby (~> 1.0) - uber (0.1.0) - unicode-display_width (2.6.0) - word_wrap (1.0.0) - xcodeproj (1.27.0) - CFPropertyList (>= 2.3.3, < 4.0) - atomos (~> 0.1.3) - claide (>= 1.0.2, < 2.0) - colored2 (~> 3.1) - nanaimo (~> 0.4.0) - rexml (>= 3.3.6, < 4.0) - xcpretty (0.4.1) - rouge (~> 3.28.0) - xcpretty-travis-formatter (1.0.1) - xcpretty (~> 0.2, >= 0.0.7) - -PLATFORMS - arm64-darwin-21 - arm64-darwin-22 - arm64-darwin-23 - arm64-darwin-24 - x86_64-darwin-20 - -DEPENDENCIES - cocoapods - fastlane - fastlane-plugin-changelog - fastlane-plugin-create_xcframework - -BUNDLED WITH - 2.6.9 From a46e042de44125f679af169659324fb82b5dd54d Mon Sep 17 00:00:00 2001 From: Joao Dordio Date: Thu, 25 Jun 2026 17:38:48 +0100 Subject: [PATCH 3/3] chore(release): retry trunk verification, pin gems to current minor --- .github/workflows/ios-sdk-release.yml | 27 +++++++++++++++++++++++---- Gemfile | 7 +++++-- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ios-sdk-release.yml b/.github/workflows/ios-sdk-release.yml index 620a93d6c..787f0d986 100644 --- a/.github/workflows/ios-sdk-release.yml +++ b/.github/workflows/ios-sdk-release.yml @@ -62,14 +62,33 @@ jobs: - name: verify cocoapods publication run: | set -e + # Retries with linear backoff distinguish "trunk is briefly down" (5xx, 0) + # from "pod isn't published" (404). 404 fails fast, transient errors retry up to ~75s. for pod in Iterable-iOS-SDK Iterable-iOS-AppExtensions; do echo "Checking trunk for $pod $VERSION..." - status=$(curl -s -o /dev/null -w "%{http_code}" "https://trunk.cocoapods.org/api/v1/pods/$pod/versions/$VERSION") - if [ "$status" != "200" ]; then - echo "::error::$pod $VERSION is not available on CocoaPods trunk (HTTP $status). The pod_trunk_push step likely failed or COCOAPODS_TRUNK_TOKEN is invalid." + verified=false + for attempt in 1 2 3 4 5; do + status=$(curl -s -o /dev/null -w "%{http_code}" "https://trunk.cocoapods.org/api/v1/pods/$pod/versions/$VERSION") + case "$status" in + 200) + echo "$pod $VERSION confirmed on trunk." + verified=true + break + ;; + 404) + echo "::error::$pod $VERSION is not available on CocoaPods trunk (HTTP 404). The pod_trunk_push step likely failed or COCOAPODS_TRUNK_TOKEN is invalid." + exit 1 + ;; + *) + echo "Attempt $attempt got HTTP $status, retrying..." + sleep $((attempt * 5)) + ;; + esac + done + if [ "$verified" != "true" ]; then + echo "::error::Could not reach CocoaPods trunk to verify $pod $VERSION after 5 attempts." exit 1 fi - echo "$pod $VERSION confirmed on trunk." done - name: slack notification diff --git a/Gemfile b/Gemfile index 9ee8aa70a..e04873861 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,10 @@ source "https://rubygems.org" -gem "cocoapods" -gem "fastlane" +# Pinned to the current minor to allow patch-level bug fixes while protecting +# release day from a major-version upstream regression. Bump the ceiling +# intentionally as a separate change after testing locally. +gem "cocoapods", "~> 1.16" +gem "fastlane", "~> 2.236" plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') eval_gemfile(plugins_path) if File.exist?(plugins_path)