From 7869fa010420732b68f75421fdf71abc19aeca14 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Mon, 22 Jun 2026 15:08:29 +0200 Subject: [PATCH 1/9] AINFRA-2526: Support synchronized iOS and tvOS releases --- .buildkite/commands/release-build.sh | 5 ++- .buildkite/commands/release-upload.sh | 32 +++++++++++++------ .buildkite/release-builds.yml | 4 +-- .../Config/PocketCastsTV.base.xcconfig | 3 -- fastlane/Fastfile | 9 ++++-- podcasts.xcodeproj/project.pbxproj | 4 --- 6 files changed, 34 insertions(+), 23 deletions(-) diff --git a/.buildkite/commands/release-build.sh b/.buildkite/commands/release-build.sh index 8753e19fc3..fc6a47a217 100755 --- a/.buildkite/commands/release-build.sh +++ b/.buildkite/commands/release-build.sh @@ -9,5 +9,8 @@ RELEASE_VERSION="${1:?RELEASE_VERSION parameter missing}" echo "--- :closed_lock_with_key: Installing Secrets" bundle exec fastlane run configure_apply -echo "--- :hammer_and_wrench: Building" +echo "--- :hammer_and_wrench: Building iOS" bundle exec fastlane build_app_store_connect + +echo "--- :hammer_and_wrench: Building tvOS" +bundle exec fastlane build_app_store_connect_tvos diff --git a/.buildkite/commands/release-upload.sh b/.buildkite/commands/release-upload.sh index 7a663e2d9a..7dce7ef29a 100755 --- a/.buildkite/commands/release-upload.sh +++ b/.buildkite/commands/release-upload.sh @@ -20,19 +20,31 @@ install_gems echo "--- :closed_lock_with_key: Installing Secrets" bundle exec fastlane run configure_apply -echo "--- :testflight: Uploading to TestFlight" +echo "--- :testflight: Uploading iOS to TestFlight" bundle exec fastlane upload_app_store_connect_build_to_testflight -echo "--- :arrow_up: Uploading dSYM to Sentry" -set +e -bundle exec fastlane symbols_upload -SENTRY_UPLOAD_STATUS=$? -set -e +echo "--- :testflight: Uploading tvOS to TestFlight" +bundle exec fastlane upload_app_store_connect_build_to_testflight_tvos -if [[ $SENTRY_UPLOAD_STATUS -ne 0 ]]; then - echo "^^^ +++ Failed to upload dSYM to Sentry! Make sure to download dSYM from the build step artifacts and upload manually." - buildkite-agent annotate --style error --context sentry-failure 'Failed to upload dSYM to Sentry! Make sure to download dSYM from the build step artifacts and upload manually.' -fi +upload_symbols() { + local platform="$1" + local dsym_path="$2" + local annotation_context="$3" + + echo "--- :arrow_up: Uploading $platform dSYM to Sentry" + set +e + bundle exec fastlane symbols_upload "dsym_path:$dsym_path" + local sentry_upload_status=$? + set -e + + if [[ $sentry_upload_status -ne 0 ]]; then + echo "^^^ +++ Failed to upload $platform dSYM to Sentry! Make sure to download dSYM from the build step artifacts and upload manually." + buildkite-agent annotate --style error --context "$annotation_context" "Failed to upload $platform dSYM to Sentry! Make sure to download dSYM from the build step artifacts and upload manually." + fi +} + +upload_symbols "iOS" "$ARTIFACTS_DIR/pocket-casts.app.dSYM.zip" "sentry-failure-ios" +upload_symbols "tvOS" "$ARTIFACTS_DIR/pocket-casts-tvos.app.dSYM.zip" "sentry-failure-tvos" echo "--- :github: Creating GitHub Release" bundle exec fastlane create_release_on_github beta_release:"$BETA_RELEASE" diff --git a/.buildkite/release-builds.yml b/.buildkite/release-builds.yml index 46d170f010..a28dc49859 100644 --- a/.buildkite/release-builds.yml +++ b/.buildkite/release-builds.yml @@ -12,7 +12,7 @@ env: steps: - - label: "🛠 Release Build (App Store Connect)" + - label: "🛠 Release Builds (iOS + tvOS App Store Connect)" key: testflight_build command: .buildkite/commands/release-build.sh "${RELEASE_VERSION}" priority: 1 @@ -23,7 +23,7 @@ steps: notify: - slack: "#build-and-ship" - - label: ":testflight: Release Upload (TestFlight, Sentry, GitHub)" + - label: ":testflight: Release Uploads (iOS + tvOS TestFlight, Sentry, GitHub)" depends_on: testflight_build command: .buildkite/commands/release-upload.sh "${RELEASE_VERSION}" "${BETA_RELEASE}" priority: 1 diff --git a/Pocket Casts TV App/Config/PocketCastsTV.base.xcconfig b/Pocket Casts TV App/Config/PocketCastsTV.base.xcconfig index 95b0139f43..8842227ae6 100644 --- a/Pocket Casts TV App/Config/PocketCastsTV.base.xcconfig +++ b/Pocket Casts TV App/Config/PocketCastsTV.base.xcconfig @@ -1,8 +1,5 @@ #include "../../config/PocketCasts.base.xcconfig" -// tvOS tracks its own marketing version — still early-days, so we don't piggyback on the iOS `VERSION_SHORT` release automation. -MARKETING_VERSION = 0.1 - SDKROOT = appletvos TVOS_DEPLOYMENT_TARGET = 26.0 TARGETED_DEVICE_FAMILY = 3 diff --git a/fastlane/Fastfile b/fastlane/Fastfile index d28eaa7692..207f06ab3b 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -838,14 +838,17 @@ platform :ios do # # @param beta_release [Boolean] If true, the GitHub Release is for a beta and will be marked as prerelease and published immediately. # If false, the GitHub Release is for a final build and will not be marked as prerelease but will be created as Draft (to be manually published only once approved by Apple) - # @param archive_zip_path [String] Path to the `.xcarchive.zip` file to attach as an asset to the created GitHub Release + # @param archive_zip_path [String, nil] Optional path to a single `.xcarchive.zip` file to attach as an asset to the created GitHub Release. + # @param archive_zip_paths [Array, nil] Optional paths to `.xcarchive.zip` files to attach as assets to the created GitHub Release. # - lane :create_release_on_github do |beta_release: true, archive_zip_path: APP_STORE_CONNECT_XCARCHIVE_ZIP_PATH| + lane :create_release_on_github do |beta_release: true, archive_zip_path: nil, archive_zip_paths: nil| require_env_vars!('GITHUB_TOKEN', 'SLACK_WEBHOOK') app_version = release_version_current build_version = build_code_current version = beta_release ? build_version : app_version + upload_assets = Array(archive_zip_paths || [archive_zip_path].compact) + upload_assets = [APP_STORE_CONNECT_XCARCHIVE_ZIP_PATH, APP_STORE_CONNECT_TVOS_XCARCHIVE_ZIP_PATH] if upload_assets.empty? changelog = extract_release_notes_for_version( version: release_version_current, @@ -861,7 +864,7 @@ platform :ios do tag_name: version, description: changelog, commitish: Git.open(PROJECT_ROOT_FOLDER).log.first.sha, - upload_assets: [archive_zip_path.to_s], + upload_assets: upload_assets.map(&:to_s), is_draft: !beta_release, is_prerelease: beta_release ) diff --git a/podcasts.xcodeproj/project.pbxproj b/podcasts.xcodeproj/project.pbxproj index 850f5449a5..f03d89996b 100644 --- a/podcasts.xcodeproj/project.pbxproj +++ b/podcasts.xcodeproj/project.pbxproj @@ -9961,7 +9961,6 @@ "@executable_path/Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 0.1; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; SDKROOT = appletvos; @@ -10014,7 +10013,6 @@ "@executable_path/Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 0.1; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = appletvos; @@ -10067,7 +10065,6 @@ "@executable_path/Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 0.1; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = appletvos; @@ -10120,7 +10117,6 @@ "@executable_path/Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 0.1; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = appletvos; From a2d01650efbb22afe27a6d30ddbd8cb3943428bc Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Wed, 24 Jun 2026 13:58:48 +0200 Subject: [PATCH 2/9] Address tvOS release automation review feedback --- .buildkite/commands/build-tvos-app-store.sh | 3 +- fastlane/Fastfile | 60 ++++++++++++++------- 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/.buildkite/commands/build-tvos-app-store.sh b/.buildkite/commands/build-tvos-app-store.sh index 3860cab0f4..6cf9b3ab1b 100755 --- a/.buildkite/commands/build-tvos-app-store.sh +++ b/.buildkite/commands/build-tvos-app-store.sh @@ -12,5 +12,6 @@ echo "--- :closed_lock_with_key: Installing Secrets" bundle exec fastlane run configure_apply echo "--- :hammer_and_wrench: Building" -# The `0.` prefix keeps CI-driven builds lexicographically below any real release build code. +# The `0.` prefix keeps CI-driven builds below release build codes as builds may be +# rejected after a final release build already exists for the same version. bundle exec fastlane build_app_store_connect_tvos "build_number:0.$BUILDKITE_BUILD_NUMBER" diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 207f06ab3b..c17f2e912c 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -420,12 +420,15 @@ platform :ios do create_backmerge_pr end - # Finalizes a release at the end of a sprint to submit to the App Store + # Finalizes release bookkeeping and triggers CI to upload final TestFlight builds. # - # - Updates store metadata + # This lane does not submit builds to App Store Review. Submission remains a + # manual App Store Connect/Releases v2 step. + # + # - Refreshes and lints localized strings and App Store Connect metadata # - Bumps final version number # - Removes branch protection and close milestone - # - Triggers the final release on CI + # - Triggers final iOS and tvOS release builds/uploads on CI # # @param skip_confirm [Boolean] If true, avoids any interactive prompt (default: false) # @@ -1135,22 +1138,18 @@ platform :ios do # @param with_screenshots [Boolean] If true, will also upload the latest screenshot files to ASC (default: false) # lane :update_metadata_on_app_store_connect do |with_screenshots: false| - # Skip screenshots by default. The naming is "with" to make it clear that - # callers need to opt-in to adding screenshots. The naming of the deliver - # (upload_to_app_store) parameter, on the other hand, uses the skip verb. - skip_screenshots = with_screenshots == false + upload_metadata_to_app_store_connect(platform: 'ios', with_screenshots: with_screenshots) + end - upload_to_app_store( - app_identifier: APP_BUNDLE_IDENTIFIER_APP_STORE, - app_version: release_version_current, - skip_binary_upload: true, - screenshots_path: File.join(FASTLANE_FOLDER, 'screenshots'), - skip_screenshots: skip_screenshots, - overwrite_screenshots: true, # won't have effect if `skip_screenshots` is true - phased_release: true, - precheck_include_in_app_purchases: false, - api_key: app_store_connect_api_key - ) + # Upload the localized tvOS metadata (from `fastlane/metadata/`) to App Store Connect. + # + # The tvOS app currently reuses iOS release notes and metadata. Screenshots stay opt-in; + # callers should only use `with_screenshots:true` once tvOS screenshots are present. + # + # @param with_screenshots [Boolean] If true, will also upload the latest screenshot files to ASC (default: false) + # + lane :update_metadata_on_app_store_connect_tvos do |with_screenshots: false| + upload_metadata_to_app_store_connect(platform: 'appletvos', with_screenshots: with_screenshots) end # Generates a HTML containing the libraries acknowledgments. @@ -1316,6 +1315,31 @@ platform :ios do end end + # Upload localized App Store Connect metadata for a specific App Store platform. + # + # @param platform [String] Fastlane deliver platform, such as `ios` or `appletvos` + # @param with_screenshots [Boolean] If true, will also upload the latest screenshot files to ASC + # + def upload_metadata_to_app_store_connect(platform:, with_screenshots:) + # Skip screenshots by default. The naming is "with" to make it clear that + # callers need to opt-in to adding screenshots. The naming of the deliver + # (upload_to_app_store) parameter, on the other hand, uses the skip verb. + skip_screenshots = with_screenshots == false + + upload_to_app_store( + app_identifier: APP_BUNDLE_IDENTIFIER_APP_STORE, + app_version: release_version_current, + platform: platform, + skip_binary_upload: true, + screenshots_path: File.join(FASTLANE_FOLDER, 'screenshots'), + skip_screenshots: skip_screenshots, + overwrite_screenshots: true, # won't have effect if `skip_screenshots` is true + phased_release: true, + precheck_include_in_app_purchases: false, + api_key: app_store_connect_api_key + ) + end + # Update the milestone of still-open PRs # # - Update the milestone of all still-open PRs that were assigned the milestone of the version being code-frozen, From 1d7a7f6d1e6b80cf5e664f1fd5a607a1bad81ce3 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Thu, 25 Jun 2026 18:23:38 +0200 Subject: [PATCH 3/9] Separate iOS and tvOS release builds --- .buildkite/commands/release-build.sh | 20 +++- .buildkite/commands/release-upload.sh | 53 +++++++-- .buildkite/release-builds.yml | 12 +- fastlane/Fastfile | 165 +++++++++++++++++++------- 4 files changed, 185 insertions(+), 65 deletions(-) diff --git a/.buildkite/commands/release-build.sh b/.buildkite/commands/release-build.sh index fc6a47a217..64514aa550 100755 --- a/.buildkite/commands/release-build.sh +++ b/.buildkite/commands/release-build.sh @@ -2,6 +2,7 @@ # Ensure we get the latest commit of the `release/*` branch, especially to get last version bump commit before building the release RELEASE_VERSION="${1:?RELEASE_VERSION parameter missing}" +RELEASE_PLATFORM="${2:-${RELEASE_PLATFORM:-ios}}" "$(dirname "${BASH_SOURCE[0]}")/checkout-release-branch.sh" "$RELEASE_VERSION" "$(dirname "${BASH_SOURCE[0]}")/shared_setup.sh" @@ -9,8 +10,17 @@ RELEASE_VERSION="${1:?RELEASE_VERSION parameter missing}" echo "--- :closed_lock_with_key: Installing Secrets" bundle exec fastlane run configure_apply -echo "--- :hammer_and_wrench: Building iOS" -bundle exec fastlane build_app_store_connect - -echo "--- :hammer_and_wrench: Building tvOS" -bundle exec fastlane build_app_store_connect_tvos +case "$RELEASE_PLATFORM" in + ios) + echo "--- :hammer_and_wrench: Building iOS" + bundle exec fastlane build_app_store_connect + ;; + tvos) + echo "--- :hammer_and_wrench: Building tvOS" + bundle exec fastlane build_app_store_connect_tvos + ;; + *) + echo "Unsupported RELEASE_PLATFORM: $RELEASE_PLATFORM. Expected 'ios' or 'tvos'." >&2 + exit 1 + ;; +esac diff --git a/.buildkite/commands/release-upload.sh b/.buildkite/commands/release-upload.sh index 7dce7ef29a..ab4ad5ca18 100755 --- a/.buildkite/commands/release-upload.sh +++ b/.buildkite/commands/release-upload.sh @@ -5,14 +5,40 @@ RELEASE_VERSION="${1:?RELEASE_VERSION parameter missing}" "$(dirname "${BASH_SOURCE[0]}")/checkout-release-branch.sh" "$RELEASE_VERSION" BETA_RELEASE=${2:-true} # use second call param, default to true for safety +RELEASE_PLATFORM="${3:-${RELEASE_PLATFORM:-ios}}" +NOTIFY_SLACK="${4:-${NOTIFY_SLACK:-true}}" +CREATE_GITHUB_RELEASE="${5:-${CREATE_GITHUB_RELEASE:-true}}" -echo "Running $0 with BETA_RELEASE = $BETA_RELEASE..." +case "$RELEASE_PLATFORM" in + ios) + PLATFORM_NAME="iOS" + IPA_PATH="artifacts/pocket-casts.ipa" + DSYM_PATH="artifacts/pocket-casts.app.dSYM.zip" + ARCHIVE_ZIP_PATH="artifacts/pocket-casts.xcarchive.zip" + SENTRY_ANNOTATION_CONTEXT="sentry-failure-ios" + TESTFLIGHT_LANE="upload_app_store_connect_build_to_testflight" + ;; + tvos) + PLATFORM_NAME="tvOS" + IPA_PATH="artifacts/pocket-casts-tvos.ipa" + DSYM_PATH="artifacts/pocket-casts-tvos.app.dSYM.zip" + ARCHIVE_ZIP_PATH="artifacts/pocket-casts-tvos.xcarchive.zip" + SENTRY_ANNOTATION_CONTEXT="sentry-failure-tvos" + TESTFLIGHT_LANE="upload_app_store_connect_build_to_testflight_tvos" + ;; + *) + echo "Unsupported RELEASE_PLATFORM: $RELEASE_PLATFORM. Expected 'ios' or 'tvos'." >&2 + exit 1 + ;; +esac + +echo "Running $0 with BETA_RELEASE = $BETA_RELEASE, RELEASE_PLATFORM = $RELEASE_PLATFORM, NOTIFY_SLACK = $NOTIFY_SLACK, CREATE_GITHUB_RELEASE = $CREATE_GITHUB_RELEASE..." echo "--- :arrow_down: Downloading Artifacts" -ARTIFACTS_DIR='artifacts' # Defined in Fastlane, see ARTIFACTS_FOLDER -STEP=testflight_build -buildkite-agent artifact download "$ARTIFACTS_DIR/*.ipa" . --step $STEP -buildkite-agent artifact download "$ARTIFACTS_DIR/*.zip" . --step $STEP +STEP=release_build +buildkite-agent artifact download "$IPA_PATH" . --step "$STEP" +buildkite-agent artifact download "$DSYM_PATH" . --step "$STEP" +buildkite-agent artifact download "$ARCHIVE_ZIP_PATH" . --step "$STEP" echo "--- :rubygems: Setting up Gems" install_gems @@ -20,11 +46,13 @@ install_gems echo "--- :closed_lock_with_key: Installing Secrets" bundle exec fastlane run configure_apply -echo "--- :testflight: Uploading iOS to TestFlight" -bundle exec fastlane upload_app_store_connect_build_to_testflight +if [[ "$CREATE_GITHUB_RELEASE" != "true" ]]; then + echo "--- :github: Verifying GitHub Release exists" + bundle exec fastlane ensure_github_release_exists beta_release:"$BETA_RELEASE" +fi -echo "--- :testflight: Uploading tvOS to TestFlight" -bundle exec fastlane upload_app_store_connect_build_to_testflight_tvos +echo "--- :testflight: Uploading $PLATFORM_NAME to TestFlight" +bundle exec fastlane "$TESTFLIGHT_LANE" ipa_path:"$IPA_PATH" upload_symbols() { local platform="$1" @@ -43,8 +71,7 @@ upload_symbols() { fi } -upload_symbols "iOS" "$ARTIFACTS_DIR/pocket-casts.app.dSYM.zip" "sentry-failure-ios" -upload_symbols "tvOS" "$ARTIFACTS_DIR/pocket-casts-tvos.app.dSYM.zip" "sentry-failure-tvos" +upload_symbols "$PLATFORM_NAME" "$DSYM_PATH" "$SENTRY_ANNOTATION_CONTEXT" -echo "--- :github: Creating GitHub Release" -bundle exec fastlane create_release_on_github beta_release:"$BETA_RELEASE" +echo "--- :github: Updating GitHub Release" +bundle exec fastlane create_release_on_github beta_release:"$BETA_RELEASE" archive_zip_path:"$ARCHIVE_ZIP_PATH" notify_slack:"$NOTIFY_SLACK" create_release:"$CREATE_GITHUB_RELEASE" diff --git a/.buildkite/release-builds.yml b/.buildkite/release-builds.yml index a28dc49859..77e9175594 100644 --- a/.buildkite/release-builds.yml +++ b/.buildkite/release-builds.yml @@ -12,9 +12,9 @@ env: steps: - - label: "🛠 Release Builds (iOS + tvOS App Store Connect)" - key: testflight_build - command: .buildkite/commands/release-build.sh "${RELEASE_VERSION}" + - label: "🛠 Release Build (${RELEASE_PLATFORM})" + key: release_build + command: .buildkite/commands/release-build.sh "${RELEASE_VERSION}" "${RELEASE_PLATFORM}" priority: 1 plugins: [$CI_TOOLKIT] artifact_paths: @@ -23,9 +23,9 @@ steps: notify: - slack: "#build-and-ship" - - label: ":testflight: Release Uploads (iOS + tvOS TestFlight, Sentry, GitHub)" - depends_on: testflight_build - command: .buildkite/commands/release-upload.sh "${RELEASE_VERSION}" "${BETA_RELEASE}" + - label: ":testflight: Release Upload (${RELEASE_PLATFORM})" + depends_on: release_build + command: .buildkite/commands/release-upload.sh "${RELEASE_VERSION}" "${BETA_RELEASE}" "${RELEASE_PLATFORM}" "${NOTIFY_SLACK}" "${CREATE_GITHUB_RELEASE}" priority: 1 plugins: [$CI_TOOLKIT] notify: diff --git a/fastlane/Fastfile b/fastlane/Fastfile index c17f2e912c..91a91339d9 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -289,7 +289,6 @@ platform :ios do # - Cuts a new release branch, Bumps the version # - Extracts the Release Notes # - Generates `.strings` files from code then merges the other, manually-maintained `.strings` files with it - # - Triggers the build of the first beta on CI # - Open backmerge PR # - Enables the GitHub branch protection for the new branch # - Move all still-opened PRs targeting current milestone to the next one and Freezes the GitHub milestone @@ -356,7 +355,6 @@ platform :ios do push_to_git_remote(set_upstream: true, tags: false) - trigger_beta_build(branch_to_build: release_branch_name) create_backmerge_pr # Copy the branch protection settings from the default branch to the new release branch @@ -368,7 +366,7 @@ platform :ios do update_milestone end - # Creates a new beta by bumping the app version appropriately then triggering a beta build on CI + # Creates a new beta by bumping the app version appropriately. # # @param base_version [String] If set, bases the beta on the specified version and `release/` branch # instead of using the current app version. Useful e.g. for triggering betas on hotfixes. @@ -416,19 +414,18 @@ platform :ios do push_to_git_remote(tags: false) - trigger_beta_build(branch_to_build: release_branch_name) create_backmerge_pr end - # Finalizes release bookkeeping and triggers CI to upload final TestFlight builds. + # Finalizes release bookkeeping before separate CI jobs upload the final TestFlight builds. # # This lane does not submit builds to App Store Review. Submission remains a - # manual App Store Connect/Releases v2 step. + # manual App Store Connect/Releases v2 step. It also does not build or upload + # app binaries; Releases V2 starts those per-platform tasks separately. # # - Refreshes and lints localized strings and App Store Connect metadata # - Bumps final version number # - Removes branch protection and close milestone - # - Triggers final iOS and tvOS release builds/uploads on CI # # @param skip_confirm [Boolean] If true, avoids any interactive prompt (default: false) # @@ -462,9 +459,6 @@ platform :ios do set_milestone_frozen_marker(repository: GITHUB_REPO, milestone: release_version_current, freeze: false) close_milestone(repository: GITHUB_REPO, milestone: release_version_current) - # Start the build - - trigger_release_build(branch_to_build: release_branch_name) create_backmerge_pr end @@ -582,7 +576,7 @@ platform :ios do push_to_git_remote(set_upstream: true, tags: false) end - # Finalizes a hotfix, by triggering a release build on CI + # Finalizes a hotfix before separate CI jobs upload the final TestFlight builds. # lane :finalize_hotfix_release do |skip_confirm: false| # Verify that the current branch is a release branch. Notice that `ensure_git_branch` expects a RegEx parameter @@ -591,10 +585,9 @@ platform :ios do # Verify that there's nothing in progress in the working copy ensure_git_status_clean - UI.important("Triggering hotfix build for version: #{release_version_current}") + UI.important("Finalizing hotfix for version: #{release_version_current}") UI.user_error!('Aborted by user request') unless skip_confirm || UI.confirm('Do you want to continue?') push_to_git_remote(tags: false) - trigger_release_build(branch_to_build: "release/#{release_version_current}") create_backmerge_pr begin @@ -606,18 +599,30 @@ platform :ios do # Triggers a beta build on CI # + # Kept for manual/ad-hoc triggering. Releases V2 starts release builds directly + # through `release-builds.yml` with platform-specific environment variables. + # # @param branch_to_build [String] The name of the branch we want the CI to build, e.g. `release/19.3` + # @param platform [String] The platform build to trigger, either `ios` or `tvos` + # @param notify_slack [Boolean, nil] If true, sends the GitHub release Slack notification. Defaults to true for iOS, false for tvOS. + # @param create_release [Boolean, nil] If true, creates the GitHub Release if missing. Defaults to true for iOS, false for tvOS. # - lane :trigger_beta_build do |branch_to_build:| - trigger_buildkite_release_build(branch: branch_to_build, beta: true) + lane :trigger_beta_build do |branch_to_build:, platform: 'ios', notify_slack: nil, create_release: nil| + trigger_buildkite_release_build(branch: branch_to_build, beta: true, platform: platform, notify_slack: notify_slack, create_release: create_release) end # Triggers a stable release build on CI # + # Kept for manual/ad-hoc triggering. Releases V2 starts release builds directly + # through `release-builds.yml` with platform-specific environment variables. + # # @param branch_to_build [String] The name of the branch we want the CI to build, e.g. `release/19.3` + # @param platform [String] The platform build to trigger, either `ios` or `tvos` + # @param notify_slack [Boolean, nil] If true, sends the GitHub release Slack notification. Defaults to true for iOS, false for tvOS. + # @param create_release [Boolean, nil] If true, creates the GitHub Release if missing. Defaults to true for iOS, false for tvOS. # - lane :trigger_release_build do |branch_to_build:| - trigger_buildkite_release_build(branch: branch_to_build, beta: false) + lane :trigger_release_build do |branch_to_build:, platform: 'ios', notify_slack: nil, create_release: nil| + trigger_buildkite_release_build(branch: branch_to_build, beta: false, platform: platform, notify_slack: notify_slack, create_release: create_release) end # Builds the app binary for distribution to App Store Connect @@ -843,13 +848,20 @@ platform :ios do # If false, the GitHub Release is for a final build and will not be marked as prerelease but will be created as Draft (to be manually published only once approved by Apple) # @param archive_zip_path [String, nil] Optional path to a single `.xcarchive.zip` file to attach as an asset to the created GitHub Release. # @param archive_zip_paths [Array, nil] Optional paths to `.xcarchive.zip` files to attach as assets to the created GitHub Release. + # @param notify_slack [Boolean] If true, sends the release notification to Slack. + # @param create_release [Boolean] If true, creates the GitHub Release when it does not already exist. # - lane :create_release_on_github do |beta_release: true, archive_zip_path: nil, archive_zip_paths: nil| - require_env_vars!('GITHUB_TOKEN', 'SLACK_WEBHOOK') + lane :create_release_on_github do |beta_release: true, archive_zip_path: nil, archive_zip_paths: nil, notify_slack: true, create_release: true| + beta_release = beta_release.to_s == 'true' unless [true, false].include?(beta_release) + notify_slack = notify_slack.to_s == 'true' unless [true, false].include?(notify_slack) + create_release = create_release.to_s == 'true' unless [true, false].include?(create_release) + + require_env_vars!('GITHUB_TOKEN') + require_env_vars!('SLACK_WEBHOOK') if notify_slack app_version = release_version_current build_version = build_code_current - version = beta_release ? build_version : app_version + version = github_release_version(beta_release: beta_release) upload_assets = Array(archive_zip_paths || [archive_zip_path].compact) upload_assets = [APP_STORE_CONNECT_XCARCHIVE_ZIP_PATH, APP_STORE_CONNECT_TVOS_XCARCHIVE_ZIP_PATH] if upload_assets.empty? @@ -859,26 +871,57 @@ platform :ios do ) changelog = '_`CHANGELOG.md` was empty for this version._' if changelog.empty? - UI.message("Creating #{version} release on GitHub...") - set_github_release( - repository_name: GITHUB_REPO, - api_token: get_required_env!('GITHUB_TOKEN'), - name: version, - tag_name: version, - description: changelog, - commitish: Git.open(PROJECT_ROOT_FOLDER).log.first.sha, - upload_assets: upload_assets.map(&:to_s), - is_draft: !beta_release, - is_prerelease: beta_release - ) + api_token = get_required_env!('GITHUB_TOKEN') + release = get_github_release(url: GITHUB_REPO, version: version, api_token: api_token) + + if release.nil? && create_release + UI.message("Creating #{version} release on GitHub...") + release = set_github_release( + repository_name: GITHUB_REPO, + api_token: api_token, + name: version, + tag_name: version, + description: changelog, + commitish: Git.open(PROJECT_ROOT_FOLDER).log.first.sha, + is_draft: !beta_release, + is_prerelease: beta_release + ) + end - UI.message('Sending message to Slack...') - slack( - pretext: slack_message(version: app_version, build_number: build_version, is_beta: beta_release), - default_payloads: [], - slack_url: get_required_env!('SLACK_WEBHOOK'), - fail_on_error: false - ) + release ||= get_github_release(url: GITHUB_REPO, version: version, api_token: api_token) + UI.user_error!("GitHub release #{version} does not exist yet. Run the iOS release upload first, then retry this upload step.") unless release && release['upload_url'] + + upload_github_release_assets(release: release, assets: upload_assets, api_token: api_token) + + if notify_slack + UI.message('Sending message to Slack...') + slack( + pretext: slack_message(version: app_version, build_number: build_version, is_beta: beta_release), + default_payloads: [], + slack_url: get_required_env!('SLACK_WEBHOOK'), + fail_on_error: false + ) + else + UI.message('Skipping Slack notification.') + end + end + + # Verifies that the GitHub Release for the current build already exists. + # + # Used by platform upload jobs that should only attach an asset to an + # existing release, so they fail before non-idempotent upload steps if run + # before the release-owning platform job. + # + # @param beta_release [Boolean] If true, checks the beta release named after the build code. If false, checks the final release named after the app version. + # + lane :ensure_github_release_exists do |beta_release: true| + beta_release = beta_release.to_s == 'true' unless [true, false].include?(beta_release) + + require_env_vars!('GITHUB_TOKEN') + + version = github_release_version(beta_release: beta_release) + release = get_github_release(url: GITHUB_REPO, version: version, api_token: get_required_env!('GITHUB_TOKEN')) + UI.user_error!("GitHub release #{version} does not exist yet. Run the iOS release upload first, then retry this upload step.") unless release && release['upload_url'] end ##################################################################################### @@ -1288,18 +1331,58 @@ platform :ios do "release/#{release_version_current}" end + def github_release_version(beta_release:) + beta_release ? build_code_current : release_version_current + end + + def upload_github_release_assets(release:, assets:, api_token:) + assets.each do |asset| + asset_path = asset.to_s + asset_name = File.basename(asset_path) + existing_assets = Array(release['assets']).select { |release_asset| release_asset['name'] == asset_name } + + existing_assets.each do |existing_asset| + UI.message("Deleting existing GitHub release asset #{asset_name}...") + Fastlane::Actions::GithubApiAction.run( + server_url: 'https://api.github.com', + api_token: api_token, + http_method: 'DELETE', + path: "repos/#{GITHUB_REPO}/releases/assets/#{existing_asset['id']}", + error_handlers: { + '*' => proc do |result| + UI.user_error!("GitHub responded with #{result[:status]}:#{result[:body]}") + end + } + ) + end + + Fastlane::Actions::SetGithubReleaseAction.upload(asset_path, release['upload_url'], api_token, nil) + end + end + # Kicks off a Buildkite build using the `release-builds.yml` pipeline # # @param branch [String] The git branch on which to trigger the CI build on # @param beta [Boolean] If true, build a beta; if false, build a final release + # @param platform [String] The platform build to trigger, either `ios` or `tvos` + # @param notify_slack [Boolean, nil] If true, sends the GitHub release Slack notification. Defaults to true for iOS, false for tvOS. + # @param create_release [Boolean, nil] If true, creates the GitHub Release if missing. Defaults to true for iOS, false for tvOS. # @return [String] The URL of the build that has been triggered # - def trigger_buildkite_release_build(branch:, beta:) + def trigger_buildkite_release_build(branch:, beta:, platform: 'ios', notify_slack: nil, create_release: nil) + platform = platform.to_s.downcase + UI.user_error!("Unsupported release platform '#{platform}'. Expected 'ios' or 'tvos'.") unless %w[ios tvos].include?(platform) + notify_slack = platform == 'ios' if notify_slack.nil? + create_release = platform == 'ios' if create_release.nil? + pipeline_args = { pipeline_file: 'release-builds.yml', environment: { RELEASE_VERSION: release_version_current, - BETA_RELEASE: beta + BETA_RELEASE: beta, + RELEASE_PLATFORM: platform, + NOTIFY_SLACK: notify_slack, + CREATE_GITHUB_RELEASE: create_release } } if is_ci? From 16e309574309fb8d9b682298fdc7e8685a735b10 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Mon, 29 Jun 2026 23:27:26 +0200 Subject: [PATCH 4/9] Handle missing GitHub release archive assets --- fastlane/Fastfile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 91a91339d9..11bc212f9c 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -864,6 +864,11 @@ platform :ios do version = github_release_version(beta_release: beta_release) upload_assets = Array(archive_zip_paths || [archive_zip_path].compact) upload_assets = [APP_STORE_CONNECT_XCARCHIVE_ZIP_PATH, APP_STORE_CONNECT_TVOS_XCARCHIVE_ZIP_PATH] if upload_assets.empty? + missing_assets = upload_assets.reject { |path| File.exist?(path) } + upload_assets -= missing_assets + + UI.user_error!("No GitHub release archive assets found. Expected at least one of: #{missing_assets.join(', ')}") if upload_assets.empty? + UI.important("Skipping missing GitHub release archive assets: #{missing_assets.join(', ')}") if missing_assets.any? changelog = extract_release_notes_for_version( version: release_version_current, From 5dfd717971cf90c63b8932e284521d0009dec2a9 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Mon, 29 Jun 2026 23:27:45 +0200 Subject: [PATCH 5/9] Clarify release trigger defaults --- fastlane/Fastfile | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 11bc212f9c..a223a7b5e5 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -604,8 +604,8 @@ platform :ios do # # @param branch_to_build [String] The name of the branch we want the CI to build, e.g. `release/19.3` # @param platform [String] The platform build to trigger, either `ios` or `tvos` - # @param notify_slack [Boolean, nil] If true, sends the GitHub release Slack notification. Defaults to true for iOS, false for tvOS. - # @param create_release [Boolean, nil] If true, creates the GitHub Release if missing. Defaults to true for iOS, false for tvOS. + # @param notify_slack [Boolean, nil] Optional Slack notification override. nil keeps the platform default: true for iOS, false for tvOS. + # @param create_release [Boolean, nil] Optional GitHub Release creation override. nil keeps the platform default: true for iOS, false for tvOS. # lane :trigger_beta_build do |branch_to_build:, platform: 'ios', notify_slack: nil, create_release: nil| trigger_buildkite_release_build(branch: branch_to_build, beta: true, platform: platform, notify_slack: notify_slack, create_release: create_release) @@ -618,8 +618,8 @@ platform :ios do # # @param branch_to_build [String] The name of the branch we want the CI to build, e.g. `release/19.3` # @param platform [String] The platform build to trigger, either `ios` or `tvos` - # @param notify_slack [Boolean, nil] If true, sends the GitHub release Slack notification. Defaults to true for iOS, false for tvOS. - # @param create_release [Boolean, nil] If true, creates the GitHub Release if missing. Defaults to true for iOS, false for tvOS. + # @param notify_slack [Boolean, nil] Optional Slack notification override. nil keeps the platform default: true for iOS, false for tvOS. + # @param create_release [Boolean, nil] Optional GitHub Release creation override. nil keeps the platform default: true for iOS, false for tvOS. # lane :trigger_release_build do |branch_to_build:, platform: 'ios', notify_slack: nil, create_release: nil| trigger_buildkite_release_build(branch: branch_to_build, beta: false, platform: platform, notify_slack: notify_slack, create_release: create_release) @@ -1370,8 +1370,8 @@ platform :ios do # @param branch [String] The git branch on which to trigger the CI build on # @param beta [Boolean] If true, build a beta; if false, build a final release # @param platform [String] The platform build to trigger, either `ios` or `tvos` - # @param notify_slack [Boolean, nil] If true, sends the GitHub release Slack notification. Defaults to true for iOS, false for tvOS. - # @param create_release [Boolean, nil] If true, creates the GitHub Release if missing. Defaults to true for iOS, false for tvOS. + # @param notify_slack [Boolean, nil] Optional Slack notification override. nil keeps the platform default: true for iOS, false for tvOS. + # @param create_release [Boolean, nil] Optional GitHub Release creation override. nil keeps the platform default: true for iOS, false for tvOS. # @return [String] The URL of the build that has been triggered # def trigger_buildkite_release_build(branch:, beta:, platform: 'ios', notify_slack: nil, create_release: nil) From 650b7ef09f597820dbfebd8755e214de210beb0f Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Mon, 29 Jun 2026 23:38:33 +0200 Subject: [PATCH 6/9] Use release-toolkit for GitHub release assets --- Gemfile | 5 ++++- Gemfile.lock | 44 +++++++++++++++++++++++++------------------- fastlane/Fastfile | 33 +++++++-------------------------- 3 files changed, 36 insertions(+), 46 deletions(-) diff --git a/Gemfile b/Gemfile index 6ffe25cc44..6c2fa923fd 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,10 @@ gem 'danger-dangermattic', '~> 1.3' gem 'fastlane', '~> 2.236' gem 'fastlane-plugin-firebase_app_distribution', '~> 1.0' gem 'fastlane-plugin-sentry', '~> 2.5' -gem 'fastlane-plugin-wpmreleasetoolkit', '~> 14.7' +# TODO: Switch back to a RubyGems version once release-toolkit PR #743 is released. +gem 'fastlane-plugin-wpmreleasetoolkit', + git: 'https://github.com/wordpress-mobile/release-toolkit.git', + ref: 'd1d2011d0b4bc6bdb8f9c355a74be56369aa08a3' # To avoid errors like: # # SSL_connect returned=1 errno=0 peeraddr=3.5.132.155:443 state=error: certificate verify failed (unable to get certificate CRL) diff --git a/Gemfile.lock b/Gemfile.lock index b7079953d3..7674df6d05 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,27 @@ +GIT + remote: https://github.com/wordpress-mobile/release-toolkit.git + revision: d1d2011d0b4bc6bdb8f9c355a74be56369aa08a3 + ref: d1d2011d0b4bc6bdb8f9c355a74be56369aa08a3 + specs: + fastlane-plugin-wpmreleasetoolkit (14.8.0) + buildkit (~> 1.5) + chroma (= 0.2.0) + diffy (~> 3.3) + dotenv (~> 2.8) + fastlane (~> 2.235) + gettext (~> 3.5) + git (~> 1.3) + google-cloud-storage (~> 1.31) + java-properties (~> 0.3.0) + nokogiri (~> 1.19, >= 1.19.3) + octokit (~> 6.1) + parallel (~> 1.14) + plist (~> 3.1) + progress_bar (~> 1.3) + rake (>= 12.3, < 14.0) + rake-compiler (~> 1.0) + xcodeproj (~> 1.22) + GEM remote: https://rubygems.org/ specs: @@ -169,24 +193,6 @@ GEM google-apis-firebaseappdistribution_v1alpha (>= 0.12.0) fastlane-plugin-sentry (2.5.8) os (~> 1.1, >= 1.1.4) - fastlane-plugin-wpmreleasetoolkit (14.7.0) - buildkit (~> 1.5) - chroma (= 0.2.0) - diffy (~> 3.3) - dotenv (~> 2.8) - fastlane (~> 2.235) - gettext (~> 3.5) - git (~> 1.3) - google-cloud-storage (~> 1.31) - java-properties (~> 0.3.0) - nokogiri (~> 1.19, >= 1.19.3) - octokit (~> 6.1) - parallel (~> 1.14) - plist (~> 3.1) - progress_bar (~> 1.3) - rake (>= 12.3, < 14.0) - rake-compiler (~> 1.0) - xcodeproj (~> 1.22) fastlane-sirp (1.1.0) fiddle (1.1.8) forwardable (1.4.0) @@ -380,7 +386,7 @@ DEPENDENCIES fastlane (~> 2.236) fastlane-plugin-firebase_app_distribution (~> 1.0) fastlane-plugin-sentry (~> 2.5) - fastlane-plugin-wpmreleasetoolkit (~> 14.7) + fastlane-plugin-wpmreleasetoolkit! openssl (~> 4.0) rake (>= 12.0, < 14.0) rubocop (~> 1.88) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index a223a7b5e5..934f06ea26 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -896,7 +896,13 @@ platform :ios do release ||= get_github_release(url: GITHUB_REPO, version: version, api_token: api_token) UI.user_error!("GitHub release #{version} does not exist yet. Run the iOS release upload first, then retry this upload step.") unless release && release['upload_url'] - upload_github_release_assets(release: release, assets: upload_assets, api_token: api_token) + upload_github_release_assets( + repository: GITHUB_REPO, + version: version, + release_assets: upload_assets, + replace_existing: true, + github_token: api_token + ) if notify_slack UI.message('Sending message to Slack...') @@ -1340,31 +1346,6 @@ platform :ios do beta_release ? build_code_current : release_version_current end - def upload_github_release_assets(release:, assets:, api_token:) - assets.each do |asset| - asset_path = asset.to_s - asset_name = File.basename(asset_path) - existing_assets = Array(release['assets']).select { |release_asset| release_asset['name'] == asset_name } - - existing_assets.each do |existing_asset| - UI.message("Deleting existing GitHub release asset #{asset_name}...") - Fastlane::Actions::GithubApiAction.run( - server_url: 'https://api.github.com', - api_token: api_token, - http_method: 'DELETE', - path: "repos/#{GITHUB_REPO}/releases/assets/#{existing_asset['id']}", - error_handlers: { - '*' => proc do |result| - UI.user_error!("GitHub responded with #{result[:status]}:#{result[:body]}") - end - } - ) - end - - Fastlane::Actions::SetGithubReleaseAction.upload(asset_path, release['upload_url'], api_token, nil) - end - end - # Kicks off a Buildkite build using the `release-builds.yml` pipeline # # @param branch [String] The git branch on which to trigger the CI build on From a5c1cf182aae57cb237eb61bf4116c63e96b1283 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Tue, 30 Jun 2026 00:20:16 +0200 Subject: [PATCH 7/9] Fix split release upload retry flow --- .buildkite/commands/build-tvos-app-store.sh | 4 +- .buildkite/commands/release-build.sh | 8 ++- .buildkite/commands/release-upload.sh | 50 +++++++++++---- Gemfile | 2 +- Gemfile.lock | 4 +- fastlane/Fastfile | 69 +++++++++------------ 6 files changed, 80 insertions(+), 57 deletions(-) diff --git a/.buildkite/commands/build-tvos-app-store.sh b/.buildkite/commands/build-tvos-app-store.sh index 6cf9b3ab1b..5b4cd817fd 100755 --- a/.buildkite/commands/build-tvos-app-store.sh +++ b/.buildkite/commands/build-tvos-app-store.sh @@ -12,6 +12,6 @@ echo "--- :closed_lock_with_key: Installing Secrets" bundle exec fastlane run configure_apply echo "--- :hammer_and_wrench: Building" -# The `0.` prefix keeps CI-driven builds below release build codes as builds may be -# rejected after a final release build already exists for the same version. +# CI-driven tvOS uploads use a low 0.x build number. App Store Connect may +# reject these after a release build already exists for the same version. bundle exec fastlane build_app_store_connect_tvos "build_number:0.$BUILDKITE_BUILD_NUMBER" diff --git a/.buildkite/commands/release-build.sh b/.buildkite/commands/release-build.sh index 64514aa550..b13e3d131c 100755 --- a/.buildkite/commands/release-build.sh +++ b/.buildkite/commands/release-build.sh @@ -2,7 +2,13 @@ # Ensure we get the latest commit of the `release/*` branch, especially to get last version bump commit before building the release RELEASE_VERSION="${1:?RELEASE_VERSION parameter missing}" -RELEASE_PLATFORM="${2:-${RELEASE_PLATFORM:-ios}}" +RELEASE_PLATFORM="${2:-${RELEASE_PLATFORM:-}}" + +if [[ -z "$RELEASE_PLATFORM" ]]; then + echo "RELEASE_PLATFORM parameter missing. Expected 'ios' or 'tvos'." >&2 + exit 1 +fi + "$(dirname "${BASH_SOURCE[0]}")/checkout-release-branch.sh" "$RELEASE_VERSION" "$(dirname "${BASH_SOURCE[0]}")/shared_setup.sh" diff --git a/.buildkite/commands/release-upload.sh b/.buildkite/commands/release-upload.sh index ab4ad5ca18..ff955852f3 100755 --- a/.buildkite/commands/release-upload.sh +++ b/.buildkite/commands/release-upload.sh @@ -2,12 +2,16 @@ # Ensure we get the latest commit of the `release/*` branch, especially to get last version bump commit before publishing the GitHub Release and creating the git tag RELEASE_VERSION="${1:?RELEASE_VERSION parameter missing}" -"$(dirname "${BASH_SOURCE[0]}")/checkout-release-branch.sh" "$RELEASE_VERSION" BETA_RELEASE=${2:-true} # use second call param, default to true for safety -RELEASE_PLATFORM="${3:-${RELEASE_PLATFORM:-ios}}" -NOTIFY_SLACK="${4:-${NOTIFY_SLACK:-true}}" -CREATE_GITHUB_RELEASE="${5:-${CREATE_GITHUB_RELEASE:-true}}" +RELEASE_PLATFORM="${3:-${RELEASE_PLATFORM:-}}" +NOTIFY_SLACK="${4:-${NOTIFY_SLACK:-}}" +CREATE_GITHUB_RELEASE="${5:-${CREATE_GITHUB_RELEASE:-}}" + +if [[ -z "$RELEASE_PLATFORM" ]]; then + echo "RELEASE_PLATFORM parameter missing. Expected 'ios' or 'tvos'." >&2 + exit 1 +fi case "$RELEASE_PLATFORM" in ios) @@ -32,8 +36,26 @@ case "$RELEASE_PLATFORM" in ;; esac +if [[ -z "$NOTIFY_SLACK" ]]; then + if [[ "$RELEASE_PLATFORM" == "ios" ]]; then + NOTIFY_SLACK="true" + else + NOTIFY_SLACK="false" + fi +fi + +if [[ -z "$CREATE_GITHUB_RELEASE" ]]; then + if [[ "$RELEASE_PLATFORM" == "ios" ]]; then + CREATE_GITHUB_RELEASE="true" + else + CREATE_GITHUB_RELEASE="false" + fi +fi + echo "Running $0 with BETA_RELEASE = $BETA_RELEASE, RELEASE_PLATFORM = $RELEASE_PLATFORM, NOTIFY_SLACK = $NOTIFY_SLACK, CREATE_GITHUB_RELEASE = $CREATE_GITHUB_RELEASE..." +"$(dirname "${BASH_SOURCE[0]}")/checkout-release-branch.sh" "$RELEASE_VERSION" + echo "--- :arrow_down: Downloading Artifacts" STEP=release_build buildkite-agent artifact download "$IPA_PATH" . --step "$STEP" @@ -46,13 +68,8 @@ install_gems echo "--- :closed_lock_with_key: Installing Secrets" bundle exec fastlane run configure_apply -if [[ "$CREATE_GITHUB_RELEASE" != "true" ]]; then - echo "--- :github: Verifying GitHub Release exists" - bundle exec fastlane ensure_github_release_exists beta_release:"$BETA_RELEASE" -fi - -echo "--- :testflight: Uploading $PLATFORM_NAME to TestFlight" -bundle exec fastlane "$TESTFLIGHT_LANE" ipa_path:"$IPA_PATH" +echo "--- :github: Updating GitHub Release" +bundle exec fastlane create_release_on_github beta_release:"$BETA_RELEASE" archive_zip_path:"$ARCHIVE_ZIP_PATH" notify_slack:false create_release:"$CREATE_GITHUB_RELEASE" upload_symbols() { local platform="$1" @@ -73,5 +90,12 @@ upload_symbols() { upload_symbols "$PLATFORM_NAME" "$DSYM_PATH" "$SENTRY_ANNOTATION_CONTEXT" -echo "--- :github: Updating GitHub Release" -bundle exec fastlane create_release_on_github beta_release:"$BETA_RELEASE" archive_zip_path:"$ARCHIVE_ZIP_PATH" notify_slack:"$NOTIFY_SLACK" create_release:"$CREATE_GITHUB_RELEASE" +echo "--- :testflight: Uploading $PLATFORM_NAME to TestFlight" +bundle exec fastlane "$TESTFLIGHT_LANE" ipa_path:"$IPA_PATH" + +if [[ "$NOTIFY_SLACK" == "true" ]]; then + echo "--- :slack: Notifying Slack" + bundle exec fastlane notify_release_on_slack beta_release:"$BETA_RELEASE" +else + echo "--- :slack: Skipping Slack notification" +fi diff --git a/Gemfile b/Gemfile index 6c2fa923fd..84b0854418 100644 --- a/Gemfile +++ b/Gemfile @@ -10,7 +10,7 @@ gem 'fastlane-plugin-sentry', '~> 2.5' # TODO: Switch back to a RubyGems version once release-toolkit PR #743 is released. gem 'fastlane-plugin-wpmreleasetoolkit', git: 'https://github.com/wordpress-mobile/release-toolkit.git', - ref: 'd1d2011d0b4bc6bdb8f9c355a74be56369aa08a3' + ref: '448e95925e9cd1ec5be6794eb2232b6f9a0c1ec4' # To avoid errors like: # # SSL_connect returned=1 errno=0 peeraddr=3.5.132.155:443 state=error: certificate verify failed (unable to get certificate CRL) diff --git a/Gemfile.lock b/Gemfile.lock index 7674df6d05..fe5d239082 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ GIT remote: https://github.com/wordpress-mobile/release-toolkit.git - revision: d1d2011d0b4bc6bdb8f9c355a74be56369aa08a3 - ref: d1d2011d0b4bc6bdb8f9c355a74be56369aa08a3 + revision: 448e95925e9cd1ec5be6794eb2232b6f9a0c1ec4 + ref: 448e95925e9cd1ec5be6794eb2232b6f9a0c1ec4 specs: fastlane-plugin-wpmreleasetoolkit (14.8.0) buildkit (~> 1.5) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 934f06ea26..d06429e9b3 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -846,29 +846,19 @@ platform :ios do # # @param beta_release [Boolean] If true, the GitHub Release is for a beta and will be marked as prerelease and published immediately. # If false, the GitHub Release is for a final build and will not be marked as prerelease but will be created as Draft (to be manually published only once approved by Apple) - # @param archive_zip_path [String, nil] Optional path to a single `.xcarchive.zip` file to attach as an asset to the created GitHub Release. - # @param archive_zip_paths [Array, nil] Optional paths to `.xcarchive.zip` files to attach as assets to the created GitHub Release. + # @param archive_zip_path [String] Path to the `.xcarchive.zip` file to attach as an asset to the created GitHub Release. # @param notify_slack [Boolean] If true, sends the release notification to Slack. # @param create_release [Boolean] If true, creates the GitHub Release when it does not already exist. # - lane :create_release_on_github do |beta_release: true, archive_zip_path: nil, archive_zip_paths: nil, notify_slack: true, create_release: true| - beta_release = beta_release.to_s == 'true' unless [true, false].include?(beta_release) - notify_slack = notify_slack.to_s == 'true' unless [true, false].include?(notify_slack) - create_release = create_release.to_s == 'true' unless [true, false].include?(create_release) + lane :create_release_on_github do |beta_release: true, archive_zip_path: APP_STORE_CONNECT_XCARCHIVE_ZIP_PATH, notify_slack: true, create_release: true| + beta_release = boolean_option(beta_release, default: true) + notify_slack = boolean_option(notify_slack, default: true) + create_release = boolean_option(create_release, default: true) require_env_vars!('GITHUB_TOKEN') - require_env_vars!('SLACK_WEBHOOK') if notify_slack - app_version = release_version_current - build_version = build_code_current version = github_release_version(beta_release: beta_release) - upload_assets = Array(archive_zip_paths || [archive_zip_path].compact) - upload_assets = [APP_STORE_CONNECT_XCARCHIVE_ZIP_PATH, APP_STORE_CONNECT_TVOS_XCARCHIVE_ZIP_PATH] if upload_assets.empty? - missing_assets = upload_assets.reject { |path| File.exist?(path) } - upload_assets -= missing_assets - - UI.user_error!("No GitHub release archive assets found. Expected at least one of: #{missing_assets.join(', ')}") if upload_assets.empty? - UI.important("Skipping missing GitHub release archive assets: #{missing_assets.join(', ')}") if missing_assets.any? + UI.user_error!("Can't find file #{archive_zip_path}!") unless File.file?(archive_zip_path.to_s) changelog = extract_release_notes_for_version( version: release_version_current, @@ -891,48 +881,44 @@ platform :ios do is_draft: !beta_release, is_prerelease: beta_release ) + release ||= get_github_release(url: GITHUB_REPO, version: version, api_token: api_token) end - release ||= get_github_release(url: GITHUB_REPO, version: version, api_token: api_token) UI.user_error!("GitHub release #{version} does not exist yet. Run the iOS release upload first, then retry this upload step.") unless release && release['upload_url'] upload_github_release_assets( repository: GITHUB_REPO, version: version, - release_assets: upload_assets, + release_assets: [archive_zip_path], replace_existing: true, github_token: api_token ) if notify_slack - UI.message('Sending message to Slack...') - slack( - pretext: slack_message(version: app_version, build_number: build_version, is_beta: beta_release), - default_payloads: [], - slack_url: get_required_env!('SLACK_WEBHOOK'), - fail_on_error: false - ) + notify_release_on_slack(beta_release: beta_release) else UI.message('Skipping Slack notification.') end end - # Verifies that the GitHub Release for the current build already exists. + # Sends the release notification to Slack. # - # Used by platform upload jobs that should only attach an asset to an - # existing release, so they fail before non-idempotent upload steps if run - # before the release-owning platform job. + # Typically called after the build has been uploaded to TestFlight. # - # @param beta_release [Boolean] If true, checks the beta release named after the build code. If false, checks the final release named after the app version. + # @param beta_release [Boolean] If true, sends a beta notification. If false, sends a final/hotfix notification. # - lane :ensure_github_release_exists do |beta_release: true| - beta_release = beta_release.to_s == 'true' unless [true, false].include?(beta_release) + lane :notify_release_on_slack do |beta_release: true| + beta_release = boolean_option(beta_release, default: true) - require_env_vars!('GITHUB_TOKEN') + require_env_vars!('SLACK_WEBHOOK') - version = github_release_version(beta_release: beta_release) - release = get_github_release(url: GITHUB_REPO, version: version, api_token: get_required_env!('GITHUB_TOKEN')) - UI.user_error!("GitHub release #{version} does not exist yet. Run the iOS release upload first, then retry this upload step.") unless release && release['upload_url'] + UI.message('Sending message to Slack...') + slack( + pretext: slack_message(version: release_version_current, build_number: build_code_current, is_beta: beta_release), + default_payloads: [], + slack_url: get_required_env!('SLACK_WEBHOOK'), + fail_on_error: false + ) end ##################################################################################### @@ -1346,6 +1332,13 @@ platform :ios do beta_release ? build_code_current : release_version_current end + def boolean_option(value, default:) + return value if [true, false].include?(value) + return default if value.nil? + + value.to_s == 'true' + end + # Kicks off a Buildkite build using the `release-builds.yml` pipeline # # @param branch [String] The git branch on which to trigger the CI build on @@ -1358,8 +1351,8 @@ platform :ios do def trigger_buildkite_release_build(branch:, beta:, platform: 'ios', notify_slack: nil, create_release: nil) platform = platform.to_s.downcase UI.user_error!("Unsupported release platform '#{platform}'. Expected 'ios' or 'tvos'.") unless %w[ios tvos].include?(platform) - notify_slack = platform == 'ios' if notify_slack.nil? - create_release = platform == 'ios' if create_release.nil? + notify_slack = boolean_option(notify_slack, default: platform == 'ios') + create_release = boolean_option(create_release, default: platform == 'ios') pipeline_args = { pipeline_file: 'release-builds.yml', From 492186c88afd05e8bc828d7899740be4096c3c8f Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Tue, 30 Jun 2026 19:44:48 +1000 Subject: [PATCH 8/9] Use stable version of `release-toolkit`, 14.9.0 --- Gemfile | 4 +--- Gemfile.lock | 44 +++++++++++++++++++------------------------- 2 files changed, 20 insertions(+), 28 deletions(-) diff --git a/Gemfile b/Gemfile index 84b0854418..74e5945b19 100644 --- a/Gemfile +++ b/Gemfile @@ -8,9 +8,7 @@ gem 'fastlane', '~> 2.236' gem 'fastlane-plugin-firebase_app_distribution', '~> 1.0' gem 'fastlane-plugin-sentry', '~> 2.5' # TODO: Switch back to a RubyGems version once release-toolkit PR #743 is released. -gem 'fastlane-plugin-wpmreleasetoolkit', - git: 'https://github.com/wordpress-mobile/release-toolkit.git', - ref: '448e95925e9cd1ec5be6794eb2232b6f9a0c1ec4' +gem 'fastlane-plugin-wpmreleasetoolkit', '~> 14.9.0' # To avoid errors like: # # SSL_connect returned=1 errno=0 peeraddr=3.5.132.155:443 state=error: certificate verify failed (unable to get certificate CRL) diff --git a/Gemfile.lock b/Gemfile.lock index fe5d239082..0cf3e9c809 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,27 +1,3 @@ -GIT - remote: https://github.com/wordpress-mobile/release-toolkit.git - revision: 448e95925e9cd1ec5be6794eb2232b6f9a0c1ec4 - ref: 448e95925e9cd1ec5be6794eb2232b6f9a0c1ec4 - specs: - fastlane-plugin-wpmreleasetoolkit (14.8.0) - buildkit (~> 1.5) - chroma (= 0.2.0) - diffy (~> 3.3) - dotenv (~> 2.8) - fastlane (~> 2.235) - gettext (~> 3.5) - git (~> 1.3) - google-cloud-storage (~> 1.31) - java-properties (~> 0.3.0) - nokogiri (~> 1.19, >= 1.19.3) - octokit (~> 6.1) - parallel (~> 1.14) - plist (~> 3.1) - progress_bar (~> 1.3) - rake (>= 12.3, < 14.0) - rake-compiler (~> 1.0) - xcodeproj (~> 1.22) - GEM remote: https://rubygems.org/ specs: @@ -193,6 +169,24 @@ GEM google-apis-firebaseappdistribution_v1alpha (>= 0.12.0) fastlane-plugin-sentry (2.5.8) os (~> 1.1, >= 1.1.4) + fastlane-plugin-wpmreleasetoolkit (14.9.0) + buildkit (~> 1.5) + chroma (= 0.2.0) + diffy (~> 3.3) + dotenv (~> 2.8) + fastlane (~> 2.235) + gettext (~> 3.5) + git (~> 1.3) + google-cloud-storage (~> 1.31) + java-properties (~> 0.3.0) + nokogiri (~> 1.19, >= 1.19.3) + octokit (~> 6.1) + parallel (~> 1.14) + plist (~> 3.1) + progress_bar (~> 1.3) + rake (>= 12.3, < 14.0) + rake-compiler (~> 1.0) + xcodeproj (~> 1.22) fastlane-sirp (1.1.0) fiddle (1.1.8) forwardable (1.4.0) @@ -386,7 +380,7 @@ DEPENDENCIES fastlane (~> 2.236) fastlane-plugin-firebase_app_distribution (~> 1.0) fastlane-plugin-sentry (~> 2.5) - fastlane-plugin-wpmreleasetoolkit! + fastlane-plugin-wpmreleasetoolkit (~> 14.9.0) openssl (~> 4.0) rake (>= 12.0, < 14.0) rubocop (~> 1.88) From 7f70c9eb7f9a28e3e8c360dbc87bafc01e8541be Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Tue, 30 Jun 2026 19:54:40 +1000 Subject: [PATCH 9/9] Remove leftover TODO comment --- Gemfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Gemfile b/Gemfile index 74e5945b19..8767cef15e 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,6 @@ gem 'danger-dangermattic', '~> 1.3' gem 'fastlane', '~> 2.236' gem 'fastlane-plugin-firebase_app_distribution', '~> 1.0' gem 'fastlane-plugin-sentry', '~> 2.5' -# TODO: Switch back to a RubyGems version once release-toolkit PR #743 is released. gem 'fastlane-plugin-wpmreleasetoolkit', '~> 14.9.0' # To avoid errors like: #