diff --git a/.buildkite/release-builds.yml b/.buildkite/release-builds.yml index 034d0e4dea72..b6d0d255cc02 100644 --- a/.buildkite/release-builds.yml +++ b/.buildkite/release-builds.yml @@ -11,10 +11,18 @@ steps: command: ".buildkite/commands/release-build-wordpress.sh $BETA_RELEASE" plugins: [$CI_TOOLKIT_PLUGIN] notify: - - slack: "#build-and-ship" + - slack: "#build-and-ship" + retry: + manual: + # If those jobs fail, one should always prefer re-triggering a new build from ReleaseV2 rather than retrying the individual job from Buildkite + allowed: false - label: ":jetpack: :testflight: Jetpack Release Build (App Store Connect)" command: ".buildkite/commands/release-build-jetpack.sh" plugins: [$CI_TOOLKIT_PLUGIN] notify: - - slack: "#build-and-ship" + - slack: "#build-and-ship" + retry: + manual: + # If those jobs fail, one should always prefer re-triggering a new build from ReleaseV2 rather than retrying the individual job from Buildkite + allowed: false diff --git a/.buildkite/release-pipelines/code-freeze.yml b/.buildkite/release-pipelines/code-freeze.yml index eff779f6a518..3a52c3961afa 100644 --- a/.buildkite/release-pipelines/code-freeze.yml +++ b/.buildkite/release-pipelines/code-freeze.yml @@ -18,3 +18,7 @@ steps: echo '--- :shipit: Run code freeze' bundle exec fastlane code_freeze skip_confirm:true + retry: + manual: + # If those jobs fail, one should always prefer re-triggering a new build from ReleaseV2 rather than retrying the individual job from Buildkite + allowed: false diff --git a/.buildkite/release-pipelines/complete-code-freeze.yml b/.buildkite/release-pipelines/complete-code-freeze.yml index 73e704792dff..c53c5e45839d 100644 --- a/.buildkite/release-pipelines/complete-code-freeze.yml +++ b/.buildkite/release-pipelines/complete-code-freeze.yml @@ -9,9 +9,17 @@ steps: - label: Complete Code Freeze key: complete_code_freeze plugins: [$CI_TOOLKIT_PLUGIN] - command: ".buildkite/commands/complete-code-freeze.sh $RELEASE_VERSION" + command: .buildkite/commands/complete-code-freeze.sh $RELEASE_VERSION + retry: + manual: + # If those jobs fail, one should always prefer re-triggering a new build from ReleaseV2 rather than retrying the individual job from Buildkite + allowed: false - label: Log Outdated Pods depends_on: complete_code_freeze plugins: [$CI_TOOLKIT_PLUGIN] - command: ".buildkite/commands/log-outdated-pods.sh $RELEASE_VERSION" + command: .buildkite/commands/log-outdated-pods.sh $RELEASE_VERSION + retry: + manual: + # If those jobs fail, one should always prefer re-triggering a new build from ReleaseV2 rather than retrying the individual job from Buildkite + allowed: false diff --git a/.buildkite/release-pipelines/finalize-hotfix.yml b/.buildkite/release-pipelines/finalize-hotfix.yml index ab38db40adf1..9e022101d71f 100644 --- a/.buildkite/release-pipelines/finalize-hotfix.yml +++ b/.buildkite/release-pipelines/finalize-hotfix.yml @@ -7,4 +7,8 @@ steps: queue: mac env: IMAGE_ID: $IMAGE_ID - command: ".buildkite/commands/finalize-hotfix.sh $VERSION" + command: .buildkite/commands/finalize-hotfix.sh $VERSION + retry: + manual: + # If those jobs fail, one should always prefer re-triggering a new build from ReleaseV2 rather than retrying the individual job from Buildkite + allowed: false diff --git a/.buildkite/release-pipelines/finalize-release.yml b/.buildkite/release-pipelines/finalize-release.yml index 39ec2f5f9a61..72165fe57bed 100644 --- a/.buildkite/release-pipelines/finalize-release.yml +++ b/.buildkite/release-pipelines/finalize-release.yml @@ -7,4 +7,8 @@ steps: queue: mac env: IMAGE_ID: $IMAGE_ID - command: ".buildkite/commands/finalize-release.sh $RELEASE_VERSION" + command: .buildkite/commands/finalize-release.sh $RELEASE_VERSION + retry: + manual: + # If those jobs fail, one should always prefer re-triggering a new build from ReleaseV2 rather than retrying the individual job from Buildkite + allowed: false diff --git a/.buildkite/release-pipelines/new-beta-release.yml b/.buildkite/release-pipelines/new-beta-release.yml index d40a7e644e54..b816570c64f1 100644 --- a/.buildkite/release-pipelines/new-beta-release.yml +++ b/.buildkite/release-pipelines/new-beta-release.yml @@ -19,3 +19,7 @@ steps: echo '--- :shipit: Deploy new beta' bundle exec fastlane new_beta_release skip_confirm:true + retry: + manual: + # If those jobs fail, one should always prefer re-triggering a new build from ReleaseV2 rather than retrying the individual job from Buildkite + allowed: false diff --git a/.buildkite/release-pipelines/new-hotfix.yml b/.buildkite/release-pipelines/new-hotfix.yml index dbcf2366126b..8369461c3fb0 100644 --- a/.buildkite/release-pipelines/new-hotfix.yml +++ b/.buildkite/release-pipelines/new-hotfix.yml @@ -19,3 +19,7 @@ steps: echo '--- :shipit: Start new hotfix' bundle exec fastlane new_hotfix_release skip_confirm:true version:"$VERSION" + retry: + manual: + # If those jobs fail, one should always prefer re-triggering a new build from ReleaseV2 rather than retrying the individual job from Buildkite + allowed: false diff --git a/.buildkite/release-pipelines/publish-release.yml b/.buildkite/release-pipelines/publish-release.yml new file mode 100644 index 000000000000..ddc73f278fa4 --- /dev/null +++ b/.buildkite/release-pipelines/publish-release.yml @@ -0,0 +1,31 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json +--- + +agents: + queue: mac + +env: + IMAGE_ID: $IMAGE_ID + +steps: + - label: Publish Release + plugins: [$CI_TOOLKIT_PLUGIN] + command: | + echo '--- :git: Configure Git for release management' + .buildkite/commands/configure-git-for-release-management.sh + + echo '--- :git: Checkout release branch' + .buildkite/commands/checkout-release-branch.sh $RELEASE_VERSION + + echo '--- :ruby: Setup Ruby tools' + install_gems + + echo '--- :closed_lock_with_key: Access secrets' + bundle exec fastlane run configure_apply + + echo '--- :package: Publish Release' + bundle exec fastlane publish_release skip_confirm:true + retry: + manual: + # If those jobs fail, one should always prefer re-triggering a new build from ReleaseV2 rather than retrying the individual job from Buildkite + allowed: false diff --git a/.buildkite/release-pipelines/update-app-store-strings.yml b/.buildkite/release-pipelines/update-app-store-strings.yml index ab01946a4669..96529145f8cf 100644 --- a/.buildkite/release-pipelines/update-app-store-strings.yml +++ b/.buildkite/release-pipelines/update-app-store-strings.yml @@ -18,3 +18,7 @@ steps: echo '--- :shipit: Update relaese notes and other App Store metadata' bundle exec fastlane update_appstore_strings skip_confirm:true + retry: + manual: + # If those jobs fail, one should always prefer re-triggering a new build from ReleaseV2 rather than retrying the individual job from Buildkite + allowed: false diff --git a/Gemfile b/Gemfile index 3ff95462ebef..3556736476d7 100644 --- a/Gemfile +++ b/Gemfile @@ -3,7 +3,7 @@ source 'https://rubygems.org' gem 'cocoapods', '~> 1.14' -gem 'danger-dangermattic', '~> 1.0' +gem 'danger-dangermattic', '~> 1.1' gem 'dotenv' # 2.221.0 includes a fix for an ASC-interfacing bug # @@ -15,7 +15,7 @@ gem 'fastlane-plugin-sentry' # This comment avoids typing to switch to a development version for testing. # # gem 'fastlane-plugin-wpmreleasetoolkit', git: 'https://github.com/wordpress-mobile/release-toolkit', ref: '' -gem 'fastlane-plugin-wpmreleasetoolkit', '~> 11.0' +gem 'fastlane-plugin-wpmreleasetoolkit', '~> 12.0' gem 'rake' gem 'rubocop', '~> 1.60' gem 'rubocop-rake', '~> 0.6' diff --git a/Gemfile.lock b/Gemfile.lock index 443ed025f6f4..3c946df805c6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,16 +5,17 @@ GEM base64 nkf rexml - activesupport (7.1.3.4) + activesupport (7.2.1) base64 bigdecimal - concurrent-ruby (~> 1.0, >= 1.0.2) + concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) + logger (>= 1.4.2) minitest (>= 5.1) - mutex_m - tzinfo (~> 2.0) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) algoliasearch (1.27.5) @@ -24,17 +25,17 @@ GEM ast (2.4.2) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.962.0) - aws-sdk-core (3.201.3) + aws-partitions (1.975.0) + aws-sdk-core (3.205.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.8) + aws-sigv4 (~> 1.9) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.88.0) - aws-sdk-core (~> 3, >= 3.201.0) + aws-sdk-kms (1.91.0) + aws-sdk-core (~> 3, >= 3.205.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.157.0) - aws-sdk-core (~> 3, >= 3.201.0) + aws-sdk-s3 (1.162.0) + aws-sdk-core (~> 3, >= 3.205.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.9.1) @@ -91,7 +92,7 @@ GEM colored2 (3.1.2) commander (4.6.0) highline (~> 2.0.0) - concurrent-ruby (1.3.3) + concurrent-ruby (1.3.4) connection_pool (2.4.1) cork (0.3.0) colored2 (~> 3.1) @@ -204,7 +205,7 @@ GEM fastlane-plugin-appcenter (2.1.2) fastlane-plugin-sentry (1.24.0) os (~> 1.1, >= 1.1.4) - fastlane-plugin-wpmreleasetoolkit (11.1.0) + fastlane-plugin-wpmreleasetoolkit (12.1.0) activesupport (>= 6.1.7.1) buildkit (~> 1.5) chroma (= 0.2.0) @@ -213,7 +214,7 @@ GEM git (~> 1.3) google-cloud-storage (~> 1.31) java-properties (~> 0.3.0) - nokogiri (~> 1.11, < 1.17) + nokogiri (~> 1.11) octokit (~> 6.1) parallel (~> 1.14) plist (~> 3.1) @@ -265,29 +266,29 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.6) + http-cookie (1.0.7) domain_name (~> 0.5) httpclient (2.8.3) - i18n (1.14.5) + i18n (1.14.6) concurrent-ruby (~> 1.0) java-properties (0.3.0) jmespath (1.6.2) json (2.7.2) - jwt (2.8.2) + jwt (2.9.0) base64 kramdown (2.4.0) rexml kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) language_server-protocol (3.17.0.3) + logger (1.6.1) mini_magick (4.13.2) mini_mime (1.1.5) mini_portile2 (2.8.7) - minitest (5.24.1) + minitest (5.25.1) molinillo (0.8.0) multi_json (1.15.0) multipart-post (2.4.1) - mutex_m (0.2.0) nanaimo (0.3.0) nap (1.1.0) naturally (2.2.1) @@ -303,7 +304,7 @@ GEM options (2.3.2) optparse (0.5.0) os (1.1.4) - parallel (1.25.1) + parallel (1.26.3) parser (3.3.4.1) ast (~> 2.4.1) racc @@ -325,8 +326,7 @@ GEM trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.3.6) - strscan + rexml (3.3.7) rmagick (5.3.0) pkg-config (~> 1.4) rouge (2.0.7) @@ -352,6 +352,7 @@ GEM sawyer (0.9.2) addressable (>= 2.3.5) faraday (>= 0.17.3, < 3) + securerandom (0.3.1) security (0.1.5) signet (0.19.0) addressable (~> 2.8) @@ -361,7 +362,6 @@ GEM simctl (1.6.10) CFPropertyList naturally - strscan (3.1.0) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) @@ -375,7 +375,7 @@ GEM tzinfo (2.0.6) concurrent-ruby (~> 1.0) uber (0.1.0) - unicode-display_width (2.5.0) + unicode-display_width (2.6.0) word_wrap (1.0.0) xcodeproj (1.25.0) CFPropertyList (>= 2.3.3, < 4.0) @@ -394,12 +394,12 @@ PLATFORMS DEPENDENCIES cocoapods (~> 1.14) - danger-dangermattic (~> 1.0) + danger-dangermattic (~> 1.1) dotenv fastlane (~> 2.221) fastlane-plugin-appcenter (~> 2.1) fastlane-plugin-sentry - fastlane-plugin-wpmreleasetoolkit (~> 11.0) + fastlane-plugin-wpmreleasetoolkit (~> 12.0) rake rmagick (~> 5.3.0) rubocop (~> 1.60) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index d7d2b8a8f4e6..23cdbf3f9acd 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -314,8 +314,3 @@ end def editorial_branch_name(version: release_version_current) "release_notes/#{version}" end - -def ensure_git_branch_is_release_branch - # Verify that the current branch is a release branch. Notice that `ensure_git_branch` expects a RegEx parameter - ensure_git_branch(branch: '^release/') -end diff --git a/fastlane/lanes/build.rb b/fastlane/lanes/build.rb index f11ae30f87a2..e79816f0d97b 100644 --- a/fastlane/lanes/build.rb +++ b/fastlane/lanes/build.rb @@ -209,7 +209,7 @@ release_version = release_version_current version = options[:beta_release] ? build_code : release_version - release_url = create_release( + release_url = create_github_release( repository: GITHUB_REPO, version: version, release_notes_file_path: WORDPRESS_RELEASE_NOTES_PATH, diff --git a/fastlane/lanes/localization.rb b/fastlane/lanes/localization.rb index 085f853ffe10..bd76edc28f8a 100644 --- a/fastlane/lanes/localization.rb +++ b/fastlane/lanes/localization.rb @@ -243,11 +243,7 @@ def generate_strings_file(gutenberg_path:, derived_data_path:) push_to_git_remote(tags: false) - pr_url = create_release_management_pull_request( - release_version: release_version, - base_branch: compute_release_branch_name(options: options, version: release_version), - title: "Merge editorialized release notes in #{release_version}" - ) + pr_url = create_backmerge_pr message = <<~MESSAGE Release notes and metadata localization sources successfully generated. Next, review and merge the [integration PR](#{pr_url}). diff --git a/fastlane/lanes/release.rb b/fastlane/lanes/release.rb index 0f2245b43bc8..1620d7bda674 100644 --- a/fastlane/lanes/release.rb +++ b/fastlane/lanes/release.rb @@ -13,16 +13,19 @@ # desc 'Executes the initial steps needed during code freeze' lane :code_freeze do |options| - # Verify that there's nothing in progress in the working copy ensure_git_status_clean # Check out the up-to-date default branch, the designated starting point for the code freeze Fastlane::Helper::GitHelper.checkout_and_pull(DEFAULT_BRANCH) + # Checks if internal dependencies are on a stable version + check_pods_references + # Make sure that Gutenberg is configured as expected for a successful code freeze gutenberg_dep_check release_branch_name = compute_release_branch_name(options: options, version: release_version_next) + ensure_branch_does_not_exist!(release_branch_name) # The `release_version_next` is used as the `new internal release version` value because the external and internal # release versions are always the same. @@ -104,30 +107,44 @@ push_to_git_remote(tags: false) - attempts = 0 + # Protect release/* branch + copy_branch_protection( + repository: GITHUB_REPO, + from_branch: DEFAULT_BRANCH, + to_branch: release_branch_name + ) + begin - attempts += 1 - copy_branch_protection( + # Move PRs to next milestone + moved_prs = update_assigned_milestone( repository: GITHUB_REPO, - from_branch: DEFAULT_BRANCH, - to_branch: release_branch_name, - github_token: get_required_env('GITHUB_TOKEN') + from_milestone: new_version, + to_milestone: release_version_next, + comment: "Version `#{new_version}` has now entered code-freeze, so the milestone of this PR has been updated to `#{release_version_next}`." + ) + + # Add ❄️ marker to milestone title to indicate we entered code-freeze + set_milestone_frozen_marker( + repository: GITHUB_REPO, + milestone: new_version ) rescue StandardError => e - if attempts < 2 - sleep_time = 5 - UI.message("Failed to set branch protection on GitHub. Retrying in #{sleep_time} seconds in case it was because the API hadn't noticed the new branch yet.") - sleep(sleep_time) - retry - else - UI.error("Failed to set branch protection on GitHub after #{attempts} attempts") - raise e - end + moved_prs = [] + + report_milestone_error(error_title: "Error freezing milestone `#{new_version}`: #{e.message}") end - set_milestone_frozen_marker(repository: GITHUB_REPO, milestone: new_version) + UI.message("Moved the following PRs to milestone #{release_version_next}: #{moved_prs.join(', ')}") - check_pods_references + # Annotate the build with the moved PRs + moved_prs_info = if moved_prs.empty? + "👍 No open PRs were targeting `#{new_version}` at the time of code-freeze" + else + "#{moved_prs.count} PRs targeting `#{new_version}` were still open and thus moved to `#{release_version_next}`:\n" \ + + moved_prs.map { |pr_num| "[##{pr_num}](https://github.com/#{GITHUB_REPO}/pull/#{pr_num})" }.join(', ') + end + + buildkite_annotate(style: moved_prs.empty? ? 'success' : 'warning', context: 'start-code-freeze', message: moved_prs_info) if is_ci print_release_notes_reminder @@ -152,9 +169,7 @@ # desc 'Completes the final steps for the code freeze' lane :complete_code_freeze do |skip_confirm: false| - ensure_git_branch_is_release_branch - - # Verify that there's nothing in progress in the working copy + ensure_git_branch_is_release_branch! ensure_git_status_clean version = release_version_current @@ -174,12 +189,7 @@ trigger_beta_build - pr_url = create_release_management_pull_request( - release_version: version, - base_branch: DEFAULT_BRANCH, - title: "Merge #{version} code freeze" - ) - + pr_url = create_backmerge_pr message = <<~MESSAGE Code freeze completed successfully. Next, review and merge the [integration PR](#{pr_url}). MESSAGE @@ -203,7 +213,7 @@ UI.user_error!("Release branch for version #{release_version} doesn't exist.") end - ensure_git_branch_is_release_branch # This check is mostly redundant + ensure_git_branch_is_release_branch! # This check is mostly redundant # The `release_version_next` is used as the `new internal release version` value because the external and internal # release versions are always the same. @@ -236,16 +246,7 @@ trigger_beta_build - # Create an intermediate branch to avoid conflicts when integrating the changes - Fastlane::Helper::GitHelper.create_branch("new_beta/#{release_version}") - push_to_git_remote(tags: false) - - pr_url = create_release_management_pull_request( - release_version: release_version, - base_branch: DEFAULT_BRANCH, - title: "Merge changes from #{build_code_current}" - ) - + pr_url = create_backmerge_pr message = <<~MESSAGE Beta deployment was successful. Next, review and merge the [integration PR](#{pr_url}). MESSAGE @@ -261,7 +262,7 @@ UI.user_error!("Release branch for version #{release_version} doesn't exist.") end - ensure_git_branch_is_release_branch # This check is mostly redundant + ensure_git_branch_is_release_branch! # This check is mostly redundant git_pull @@ -354,19 +355,30 @@ # @option [Boolean] skip_confirm (default: false) If true, avoids any interactive prompt # desc 'Performs the final checks and triggers a release build for the hotfix in the current branch' - lane :finalize_hotfix_release do |options| - ensure_git_branch_is_release_branch - - # Verify that there's nothing in progress in the working copy + lane :finalize_hotfix_release do |skip_confirm: false| + ensure_git_branch_is_release_branch! ensure_git_status_clean - # Pull the latest hotfix release branch changes - git_pull + hotfix_version = release_version_current - UI.important("Triggering hotfix build for version: #{release_version_current}") - UI.user_error!('Aborted by user request') unless options[:skip_confirm] || UI.confirm('Do you want to continue?') + UI.important("Triggering hotfix build for version: #{hotfix_version}") + unless skip_confirm || UI.confirm('Do you want to continue?') + UI.user_error!("Terminating as requested. Don't forget to run the remainder of this automation manually.") + end + + trigger_release_build(branch_to_build: release_branch_name(version: hotfix_version)) - trigger_release_build(branch_to_build: "release/#{release_version_current}") + create_backmerge_pr + + # Close hotfix milestone + begin + close_milestone( + repository: GITHUB_REPO, + milestone: hotfix_version + ) + rescue StandardError => e + report_milestone_error(error_title: "Error closing milestone `#{hotfix_version}`: #{e.message}") + end end # Finalizes a release at the end of a sprint to submit to the App Store @@ -379,53 +391,88 @@ # @option [Boolean] skip_confirm (default: false) If true, avoids any interactive prompt # desc 'Trigger the final release build on CI' - lane :finalize_release do |options| + lane :finalize_release do |skip_confirm: false| UI.user_error!('To finalize a hotfix, please use the finalize_hotfix_release lane instead') if ios_current_branch_is_hotfix - ensure_git_branch_is_release_branch - - # Verify that there's nothing in progress in the working copy + ensure_git_branch_is_release_branch! ensure_git_status_clean - skip_user_confirmation = options[:skip_confirm] - UI.important("Finalizing release: #{release_version_current}") - UI.user_error!('Aborted by user request') unless skip_user_confirmation || UI.confirm('Do you want to continue?') - - git_pull + UI.user_error!('Aborted by user request') unless skip_confirm || UI.confirm('Do you want to continue?') - check_all_translations(interactive: skip_user_confirmation == false) + check_all_translations(interactive: skip_confirm == false) download_localized_strings_and_metadata(options) - lint_localizations(allow_retry: skip_user_confirmation == false) + lint_localizations(allow_retry: skip_confirm == false) bump_build_codes - unless skip_user_confirmation || UI.confirm('Ready to push changes to remote and trigger the release build?') - UI.message("Terminating as requested. Don't forget to run the remainder of this automation manually.") - next + unless skip_confirm || UI.confirm('Ready to push changes to remote and trigger the release build?') + UI.user_error!("Terminating as requested. Don't forget to run the remainder of this automation manually.") end push_to_git_remote(tags: false) version = release_version_current - remove_branch_protection(repository: GITHUB_REPO, branch: release_branch_name) - set_milestone_frozen_marker(repository: GITHUB_REPO, milestone: version, freeze: false) - create_new_milestone(repository: GITHUB_REPO) - close_milestone(repository: GITHUB_REPO, milestone: version) trigger_release_build - pr_url = create_release_management_pull_request( - release_version: release_version_next, - base_branch: DEFAULT_BRANCH, - title: "Merge #{version} release finalization" - ) - + pr_url = create_backmerge_pr message = <<~MESSAGE Release successfully finalized. Next, review and merge the [integration PR](#{pr_url}). MESSAGE buildkite_annotate(context: 'finalization-completed', style: 'success', message: message) if is_ci + + # Close milestone + begin + set_milestone_frozen_marker(repository: GITHUB_REPO, milestone: version, freeze: false) + close_milestone(repository: GITHUB_REPO, milestone: version) + rescue StandardError => e + report_milestone_error(error_title: "Error closing milestone `#{version}`: #{e.message}") + end + end + + # This lane publishes a release on GitHub and creates a PR to backmerge the current release branch into the next release/ branch + # + # @param [Boolean] skip_confirm (default: false) If set, will skip the confirmation prompt before running the rest of the lane + # + # @example Running the lane + # bundle exec fastlane publish_release skip_confirm:true + # + lane :publish_release do |skip_confirm: false| + ensure_git_status_clean + ensure_git_branch_is_release_branch! + + version_number = release_version_current + + current_branch = release_branch_name(version: version_number) + next_release_branch = release_branch_name(version: release_version_next) + + UI.important <<~PROMPT + Publish the #{version_number} release. This will: + - Publish the existing draft `#{version_number}` release on GitHub + - Which will also have GitHub create the associated git tag, pointing to the tip of the branch + - If the release branch for the next version `#{next_release_branch}` already exists, backmerge `#{current_branch}` into it + - If needed, backmerge `#{current_branch}` back into `#{DEFAULT_BRANCH}` + - Delete the `#{current_branch}` branch + PROMPT + unless skip_confirm || UI.confirm('Do you want to continue?') + UI.user_error!("Terminating as requested. Don't forget to run the remainder of this automation manually.") + end + + UI.important "Publishing release #{version_number} on GitHub" + + publish_github_release( + repository: GITHUB_REPO, + name: version_number + ) + + create_backmerge_pr + + # At this point, an intermediate branch has been created by creating a backmerge PR to a hotfix or the next version release branch. + # This allows us to safely delete the `release/*` branch. + # Note that if a hotfix or new release branches haven't been created, the backmerge PR won't be created as well. + delete_remote_git_branch!(current_branch) end # Triggers a beta build on CI @@ -457,7 +504,7 @@ # @param [Boolean] beta Indicate if we should build a beta or regular release # def trigger_buildkite_release_build(branch:, beta:) - buildkite_trigger_build( + build_url = buildkite_trigger_build( buildkite_organization: BUILDKITE_ORGANIZATION, buildkite_pipeline: BUILDKITE_PIPELINE, branch: branch, @@ -465,6 +512,11 @@ def trigger_buildkite_release_build(branch:, beta:) pipeline_file: 'release-builds.yml', message: beta ? 'Beta Builds' : 'Release Builds' ) + + return unless is_ci + + message = "This build triggered a build on `#{branch}`:\n\n- #{build_url}" + buildkite_annotate(style: 'info', context: 'trigger-release-build', message: message) end # Checks that the Gutenberg pod is reference by a tag and not a commit @@ -562,37 +614,69 @@ def commit_version_and_build_files ) end -def create_release_management_pull_request(release_version:, base_branch:, title:) - token = ENV.fetch('GITHUB_TOKEN', nil) - - UI.user_error!('Please export a GitHub API token in the environment as GITHUB_TOKEN') if token.nil? +def create_backmerge_pr + version = release_version_current - pr_url = create_pull_request( - api_token: token, - repo: 'wordpress-mobile/WordPress-iOS', - title: title, - head: Fastlane::Helper::GitHelper.current_git_branch, - base: base_branch, - labels: 'Releases' - ) - - # Next, set the milestone for the PR - # - # The create_pull_request action has a 'milestone' parameter, but it expects the milestone id. - # We don't know the id of the milestone, but we can use a different action to set it. - # - # PR URLs are in the format github.com/org/repo/pull/id - pr_number = File.basename(pr_url) - update_assigned_milestone( + pr_url = create_release_backmerge_pull_request( repository: GITHUB_REPO, - numbers: [pr_number], - to_milestone: release_version + source_branch: release_branch_name(version: version), + labels: ['Releases'], + milestone_title: release_version_next ) +rescue StandardError => e + error_message = <<-MESSAGE + Error creating backmerge pull request: + + #{e.message} + + If this is not the first time you are running the release task, the backmerge PR for the version `#{version}` might have already been previously created. + Please close any previous backmerge PR for `#{version}`, delete the previous merge branch, then run the release task again. + MESSAGE + + buildkite_annotate(style: 'error', context: 'error-creating-backmerge', message: error_message) if is_ci + + UI.user_error!(error_message) - # Return the PR URL pr_url end +def ensure_git_branch_is_release_branch! + # Verify that the current branch is a release branch. Notice that `ensure_git_branch` expects a RegEx parameter + ensure_git_branch(branch: '^release/') +end + +def ensure_branch_does_not_exist!(branch_name) + return unless Fastlane::Helper::GitHelper.branch_exists_on_remote?(branch_name: branch_name) + + error_message = "The branch `#{branch_name}` already exists. Please check first if there is an existing Pull Request that needs to be merged or closed first, " \ + 'or delete the branch to then run again the release task.' + + buildkite_annotate(style: 'error', context: 'error-checking-branch', message: error_message) if is_ci + + UI.user_error!(error_message) +end + +# Delete a branch remotely, after having removed any GitHub branch protection +# +def delete_remote_git_branch!(branch_name) + remove_branch_protection(repository: GITHUB_REPO, branch: branch_name) + + Git.open(Dir.pwd).push('origin', branch_name, delete: true) +end + +def report_milestone_error(error_title:) + error_message = <<-MESSAGE + #{error_title} + + - If this is not the first time you are running the release task (e.g. retrying because it failed on first attempt), the milestone might have already been closed and this error is expected. + - Otherwise if this is the first you are running the release task for this version, please investigate the error. + MESSAGE + + UI.error(error_message) + + buildkite_annotate(style: 'warning', context: 'error-with-milestone', message: error_message) if is_ci +end + def check_pods_references result = ios_check_beta_deps(lockfile: File.join(PROJECT_ROOT_FOLDER, 'Podfile.lock'))