Repository: wordpress-mobile/release-toolkit
Pull request #641: Add `api_url` parameter to `create_release_backmerge_pull_request`
Author: login: iangmaia
Labels: name: enhancement
description: New feature or request
Created: 2025-03-17T20:15:13Z | Merged: 2025-03-21T14:48:44Z

## What does it do?

Currently, the action `create_release_backmerge_pull_request` does not take an `api_url` to forward to Fastlane's `create_pull_request` action. `api_url` is required when dealing with GitHub Enterprise.

This PR adds the `api_url` parameter.

## Checklist before requesting a review

- [x] Run `bundle exec rubocop` to test for code style violations and recommendations.
- [x] Add Unit Tests (aka `specs/*_spec.rb`) if applicable.
- [x] Run `bundle exec rspec` to run the whole test suite and ensure all your tests pass.
- [x] Make sure you added an entry in [the `CHANGELOG.md` file](https://github.com/wordpress-mobile/release-toolkit/blob/trunk/CHANGELOG.md#trunk) to describe your changes under the appropriate existing `###` subsection of the existing `## Trunk` section.
- [x] If applicable, add an entry in [the `MIGRATION.md` file](https://github.com/wordpress-mobile/release-toolkit/blob/trunk/MIGRATION.md) to describe how the changes will affect the migration from the previous major version and what the clients will need to change and consider.



Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9b8dcb475..0a81f16aa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,9 +11,11 @@ _None_
 ### New Features
 
 - Make fastlane log actions as collapsible groups in Buildkite. [#638]
+- Add `api_url` parameter to `create_release_backmerge_pull_request` to support GitHub Enterprise. Also, updated the code so the intermediate branch tracks its remote branch after pushing. [#641]
 
 ### Bug Fixes
 
+- Fix issue in `create_release_backmerge_pull_request` where calling another fastlane action from the `intermediate_branch_created_callback` proc now behaves correctly. [#641]
 - Fix an issue where `prototype_build_details_comment` added some unexpected metadata rows in the comment table if we didn't use `firebase_app_distribution` (e.g. for Prototype Builds uploaded to S3 and not using FAD) [#642]
 
 ### Internal Changes
diff --git a/Gemfile.lock b/Gemfile.lock
index b37b4e618..69d2e094a 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -26,8 +26,9 @@ GEM
       base64
       nkf
       rexml
-    activesupport (7.2.1.2)
+    activesupport (7.2.2.1)
       base64
+      benchmark (>= 0.3)
       bigdecimal
       concurrent-ruby (~> 1.0, >= 1.3.1)
       connection_pool (>= 2.2.5)
@@ -45,38 +46,40 @@ GEM
     artifactory (3.0.17)
     ast (2.4.2)
     atomos (0.1.3)
-    aws-eventstream (1.3.0)
-    aws-partitions (1.997.0)
-    aws-sdk-core (3.211.0)
+    aws-eventstream (1.3.2)
+    aws-partitions (1.1068.0)
+    aws-sdk-core (3.220.1)
       aws-eventstream (~> 1, >= 1.3.0)
       aws-partitions (~> 1, >= 1.992.0)
       aws-sigv4 (~> 1.9)
+      base64
       jmespath (~> 1, >= 1.6.1)
-    aws-sdk-kms (1.95.0)
-      aws-sdk-core (~> 3, >= 3.210.0)
+    aws-sdk-kms (1.99.0)
+      aws-sdk-core (~> 3, >= 3.216.0)
       aws-sigv4 (~> 1.5)
-    aws-sdk-s3 (1.169.0)
-      aws-sdk-core (~> 3, >= 3.210.0)
+    aws-sdk-s3 (1.182.0)
+      aws-sdk-core (~> 3, >= 3.216.0)
       aws-sdk-kms (~> 1)
       aws-sigv4 (~> 1.5)
-    aws-sigv4 (1.10.1)
+    aws-sigv4 (1.11.0)
       aws-eventstream (~> 1, >= 1.0.2)
     babosa (1.0.4)
     base64 (0.2.0)
-    bigdecimal (3.1.8)
+    benchmark (0.4.0)
+    bigdecimal (3.1.9)
     buildkit (1.6.1)
       sawyer (>= 0.6)
-    buildkite-test_collector (2.7.0)
+    buildkite-test_collector (2.9.0)
     chroma (0.2.0)
     claide (1.1.0)
     claide-plugins (0.9.2)
       cork
       nap
       open4 (~> 1.3)
-    cocoapods (1.16.0)
+    cocoapods (1.16.2)
       addressable (~> 2.8)
       claide (>= 1.0.2, < 2.0)
-      cocoapods-core (= 1.16.0)
+      cocoapods-core (= 1.16.2)
       cocoapods-deintegrate (>= 1.0.3, < 2.0)
       cocoapods-downloader (>= 2.1, < 3.0)
       cocoapods-plugins (>= 1.0.0, < 2.0)
@@ -90,8 +93,8 @@ GEM
       molinillo (~> 0.8.0)
       nap (~> 1.0)
       ruby-macho (>= 2.3.0, < 3.0)
-      xcodeproj (>= 1.26.0, < 2.0)
-    cocoapods-core (1.16.0)
+      xcodeproj (>= 1.27.0, < 2.0)
+    cocoapods-core (1.16.2)
       activesupport (>= 5.0, < 8)
       addressable (~> 2.8)
       algoliasearch (~> 1.0)
@@ -117,8 +120,8 @@ GEM
     colored2 (3.1.2)
     commander (4.6.0)
       highline (~> 2.0.0)
-    concurrent-ruby (1.3.4)
-    connection_pool (2.4.1)
+    concurrent-ruby (1.3.5)
+    connection_pool (2.5.0)
     cork (0.3.0)
       colored2 (~> 3.1)
     crack (1.0.0)
@@ -138,7 +141,7 @@ GEM
       octokit (>= 4.0)
       pstore (~> 0.1)
       terminal-table (>= 1, < 4)
-    danger-dangermattic (1.1.2)
+    danger-dangermattic (1.2.2)
       danger (~> 9.4)
       danger-plugin-api (~> 1.0)
       danger-rubocop (~> 0.13)
@@ -149,9 +152,9 @@ GEM
       danger
       rubocop (~> 1.0)
     declarative (0.0.20)
-    diff-lcs (1.5.1)
+    diff-lcs (1.6.0)
     diffy (3.4.3)
-    digest-crc (0.6.5)
+    digest-crc (0.7.0)
       rake (>= 12.0.0, < 14.0.0)
     docile (1.4.1)
     domain_name (0.6.20240107)
@@ -183,8 +186,8 @@ GEM
     faraday-http-cache (2.5.1)
       faraday (>= 0.8)
     faraday-httpclient (1.0.1)
-    faraday-multipart (1.0.4)
-      multipart-post (~> 2)
+    faraday-multipart (1.1.0)
+      multipart-post (~> 2.0)
     faraday-net_http (1.0.2)
     faraday-net_http_persistent (1.2.0)
     faraday-patron (1.0.0)
@@ -192,8 +195,8 @@ GEM
     faraday-retry (1.0.3)
     faraday_middleware (1.2.1)
       faraday (~> 1.0)
-    fastimage (2.3.1)
-    fastlane (2.225.0)
+    fastimage (2.4.0)
+    fastlane (2.227.0)
       CFPropertyList (>= 2.3, < 4.0.0)
       addressable (>= 2.8, < 3.0.0)
       artifactory (~> 3.0)
@@ -233,11 +236,11 @@ GEM
       tty-spinner (>= 0.8.0, < 1.0.0)
       word_wrap (~> 1.0.0)
       xcodeproj (>= 1.13.0, < 2.0.0)
-      xcpretty (~> 0.3.0)
+      xcpretty (~> 0.4.0)
       xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
     fastlane-sirp (1.0.0)
       sysrandom (~> 1.0)
-    ffi (1.17.0)
+    ffi (1.17.1)
     fourflusher (2.3.1)
     fuzzy_match (2.0.4)
     gh_inspector (1.1.3)
@@ -260,12 +263,12 @@ GEM
       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.7.1)
+    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.4.0)
+    google-cloud-errors (1.5.0)
     google-cloud-storage (1.47.0)
       addressable (~> 2.8)
       digest-crc (~> 0.4)
@@ -280,38 +283,41 @@ GEM
       multi_json (~> 1.11)
       os (>= 0.9, < 2.0)
       signet (>= 0.16, < 2.a)
-    hashdiff (1.1.1)
+    hashdiff (1.1.2)
     highline (2.0.3)
-    http-cookie (1.0.7)
+    http-cookie (1.0.8)
       domain_name (~> 0.5)
-    httpclient (2.8.3)
-    i18n (1.14.6)
+    httpclient (2.9.0)
+      mutex_m
+    i18n (1.14.7)
       concurrent-ruby (~> 1.0)
     java-properties (0.3.0)
     jmespath (1.6.2)
-    json (2.7.4)
-    jwt (2.9.3)
+    json (2.10.2)
+    jwt (2.10.1)
       base64
-    kramdown (2.4.0)
-      rexml
+    kramdown (2.5.1)
+      rexml (>= 3.3.9)
     kramdown-parser-gfm (1.1.0)
       kramdown (~> 2.0)
-    language_server-protocol (3.17.0.3)
-    logger (1.6.1)
+    language_server-protocol (3.17.0.4)
+    lint_roller (1.1.0)
+    logger (1.6.6)
     method_source (0.9.2)
     mini_magick (4.13.2)
     mini_mime (1.1.5)
-    mini_portile2 (2.8.7)
-    minitest (5.25.1)
+    mini_portile2 (2.8.8)
+    minitest (5.25.5)
     molinillo (0.8.0)
     multi_json (1.15.0)
     multipart-post (2.4.1)
+    mutex_m (0.3.0)
     nanaimo (0.4.0)
     nap (1.1.0)
     naturally (2.2.1)
     netrc (0.11.0)
     nkf (0.2.0)
-    nokogiri (1.16.7)
+    nokogiri (1.18.4)
       mini_portile2 (~> 2.8.2)
       racc (~> 1.4)
     observer (0.1.2)
@@ -320,44 +326,44 @@ GEM
       sawyer (~> 0.9)
     open4 (1.3.4)
     options (2.3.2)
-    optparse (0.5.0)
+    optparse (0.6.0)
     os (1.1.4)
     parallel (1.26.3)
-    parser (3.3.5.0)
+    parser (3.3.7.1)
       ast (~> 2.4.1)
       racc
-    pkg-config (1.5.7)
-    plist (3.7.1)
+    pkg-config (1.6.0)
+    plist (3.7.2)
     progress_bar (1.3.4)
       highline (>= 1.6)
       options (~> 2.3.0)
     pry (0.12.2)
       coderay (~> 1.1.0)
       method_source (~> 0.9.0)
-    pstore (0.1.3)
+    pstore (0.2.0)
     public_suffix (4.0.7)
     racc (1.8.1)
     rainbow (3.1.1)
     rake (13.2.1)
-    rake-compiler (1.2.8)
+    rake-compiler (1.2.9)
       rake
-    rchardet (1.8.0)
-    regexp_parser (2.9.2)
+    rchardet (1.9.0)
+    regexp_parser (2.10.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.3.9)
+    rexml (3.4.1)
     rmagick (5.5.0)
       observer (~> 0.1)
       pkg-config (~> 1.4)
-    rouge (2.0.7)
+    rouge (3.28.0)
     rspec (3.13.0)
       rspec-core (~> 3.13.0)
       rspec-expectations (~> 3.13.0)
       rspec-mocks (~> 3.13.0)
-    rspec-core (3.13.2)
+    rspec-core (3.13.3)
       rspec-support (~> 3.13.0)
     rspec-expectations (3.13.3)
       diff-lcs (>= 1.2.0, < 2.0)
@@ -365,31 +371,32 @@ GEM
     rspec-mocks (3.13.2)
       diff-lcs (>= 1.2.0, < 2.0)
       rspec-support (~> 3.13.0)
-    rspec-support (3.13.1)
+    rspec-support (3.13.2)
     rspec_junit_formatter (0.4.1)
       rspec-core (>= 2, < 4, != 2.12.0)
-    rubocop (1.67.0)
+    rubocop (1.74.0)
       json (~> 2.3)
-      language_server-protocol (>= 3.17.0)
+      language_server-protocol (~> 3.17.0.2)
+      lint_roller (~> 1.1.0)
       parallel (~> 1.10)
       parser (>= 3.3.0.2)
       rainbow (>= 2.2.2, < 4.0)
-      regexp_parser (>= 2.4, < 3.0)
-      rubocop-ast (>= 1.32.2, < 2.0)
+      regexp_parser (>= 2.9.3, < 3.0)
+      rubocop-ast (>= 1.38.0, < 2.0)
       ruby-progressbar (~> 1.7)
-      unicode-display_width (>= 2.4.0, < 3.0)
-    rubocop-ast (1.32.3)
+      unicode-display_width (>= 2.4.0, < 4.0)
+    rubocop-ast (1.39.0)
       parser (>= 3.3.1.0)
     rubocop-rspec (3.0.0)
       rubocop (~> 1.40)
     ruby-macho (2.5.1)
     ruby-progressbar (1.13.0)
     ruby2_keywords (0.0.5)
-    rubyzip (2.3.2)
+    rubyzip (2.4.1)
     sawyer (0.9.2)
       addressable (>= 2.3.5)
       faraday (>= 0.17.3, < 3)
-    securerandom (0.3.1)
+    securerandom (0.4.1)
     security (0.1.5)
     signet (0.19.0)
       addressable (~> 2.8)
@@ -419,20 +426,20 @@ GEM
       concurrent-ruby (~> 1.0)
     uber (0.1.0)
     unicode-display_width (2.6.0)
-    webmock (3.24.0)
+    webmock (3.25.1)
       addressable (>= 2.8.0)
       crack (>= 0.3.2)
       hashdiff (>= 0.4.0, < 2.0.0)
     word_wrap (1.0.0)
-    xcodeproj (1.26.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.3.0)
-      rouge (~> 2.0.7)
+    xcpretty (0.4.0)
+      rouge (~> 3.28.0)
     xcpretty-travis-formatter (1.0.1)
       xcpretty (~> 0.2, >= 0.0.7)
     yard (0.9.37)
@@ -459,4 +466,4 @@ DEPENDENCIES
   yard
 
 BUNDLED WITH
-   2.4.20
+   2.6.3
diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_release_backmerge_pull_request_action.rb b/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_release_backmerge_pull_request_action.rb
index 13d6081f0..2ab583330 100644
--- a/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_release_backmerge_pull_request_action.rb
+++ b/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_release_backmerge_pull_request_action.rb
@@ -9,6 +9,7 @@ class CreateReleaseBackmergePullRequestAction < Action
       DEFAULT_BRANCH = 'trunk'
 
       def self.run(params)
+        api_url = params[:api_url]
         token = params[:github_token]
         repository = params[:repository]
         source_branch = params[:source_branch]
@@ -41,6 +42,7 @@ def self.run(params)
           Fastlane::Helper::GitHelper.checkout_and_pull(source_branch)
 
           create_backmerge_pr(
+            api_url: api_url,
             token: token,
             repository: repository,
             title: "Merge #{source_branch} into #{target_branch}",
@@ -76,6 +78,7 @@ def self.determine_target_branches(source_release_version:, default_branch:)
 
       # Creates a backmerge pull request using the `create_pull_request` Fastlane Action.
       #
+      # @param api_url [String] the GitHub API URL to use for creating the pull request
       # @param token [String] the GitHub token for authentication.
       # @param repository [String] the repository where the pull request will be created.
       # @param title [String] the title of the pull request.
@@ -90,7 +93,7 @@ def self.determine_target_branches(source_release_version:, default_branch:)
       #
       # @return [String] The URL of the created Pull Request, or `nil` if no PR was created.
       #
-      def self.create_backmerge_pr(token:, repository:, title:, head_branch:, base_branch:, labels:, milestone:, reviewers:, team_reviewers:, intermediate_branch_created_callback:)
+      def self.create_backmerge_pr(api_url:, token:, repository:, title:, head_branch:, base_branch:, labels:, milestone:, reviewers:, team_reviewers:, intermediate_branch_created_callback:) # rubocop:disable Metrics/ParameterLists
         # Do an early pre-check to see if the PR would be valid, but only if no callback (as a callback might add new commits on intermediate branch)
         if intermediate_branch_created_callback.nil? && !can_merge?(head_branch, into: base_branch)
           UI.error("Nothing to merge from #{head_branch} into #{base_branch}. Skipping PR creation.")
@@ -108,7 +111,9 @@ def self.create_backmerge_pr(token:, repository:, title:, head_branch:, base_bra
 
         # Call the callback if one was provided to allow the use to add commits on the intermediate branch (e.g. solve conflicts)
         unless intermediate_branch_created_callback.nil?
-          intermediate_branch_created_callback.call(base_branch, intermediate_branch)
+          Dir.chdir(FastlaneCore::FastlaneFolder.path) do
+            intermediate_branch_created_callback.call(base_branch, intermediate_branch)
+          end
           # Make sure the callback block didn't switch branches
           other_action.ensure_git_branch(branch: "^#{intermediate_branch}$")
 
@@ -120,7 +125,7 @@ def self.create_backmerge_pr(token:, repository:, title:, head_branch:, base_bra
           end
         end
 
-        other_action.push_to_git_remote(tags: false)
+        other_action.push_to_git_remote(tags: false, remote_branch: intermediate_branch, set_upstream: true)
 
         pr_body = <<~BODY
           Merging `#{head_branch}` into `#{base_branch}`.
@@ -136,6 +141,7 @@ def self.create_backmerge_pr(token:, repository:, title:, head_branch:, base_bra
         BODY
 
         other_action.create_pull_request(
+          api_url: api_url,
           api_token: token,
           repo: repository,
           title: title,
@@ -191,50 +197,63 @@ def self.details
       end
 
       def self.available_options
+        # Parameters we want to forward from Fastlane's create_pull_request action
+        forwarded_param_keys = %i[
+          api_url
+          labels
+          assignees
+          reviewers
+          team_reviewers
+        ].freeze
+
+        forwarded_params = Fastlane::Actions::CreatePullRequestAction.available_options.select do |opt|
+          forwarded_param_keys.include?(opt.key)
+        end
+
         [
-          FastlaneCore::ConfigItem.new(key: :repository,
-                                       env_name: 'GHHELPER_REPOSITORY',
-                                       description: 'The remote path of the GH repository on which we work',
-                                       optional: false,
-                                       type: String),
-          FastlaneCore::ConfigItem.new(key: :source_branch,
-                                       description: 'The source branch to create a backmerge PR from, in the format `release/x.y.z`',
-                                       optional: false,
-                                       type: String),
-          FastlaneCore::ConfigItem.new(key: :default_branch,
-                                       description: 'The default branch to target if no newer release branches exist',
-                                       optional: true,
-                                       default_value: DEFAULT_BRANCH,
-                                       type: String),
-          FastlaneCore::ConfigItem.new(key: :target_branches,
-                                       description: 'Array of target branches for the backmerge. If empty, the action will determine target branches by finding all `release/x.y.z` branches with a `x.y.z` version greater than the version in source branch\'s name. If none are found, it will target `default_branch`', # rubocop:disable Layout/LineLength
-                                       optional: true,
-                                       default_value: [],
-                                       type: Array),
-          FastlaneCore::ConfigItem.new(key: :labels,
-                                       description: 'The labels that should be assigned to the backmerge PRs',
-                                       optional: true,
-                                       default_value: [],
-                                       type: Array),
-          FastlaneCore::ConfigItem.new(key: :milestone_title,
-                                       description: 'The title of the milestone to assign to the created PRs',
-                                       optional: true,
-                                       type: String),
-          FastlaneCore::ConfigItem.new(key: :reviewers,
-                                       description: 'An array of GitHub users that will be assigned to the pull request',
-                                       optional: true,
-                                       type: Array),
-          FastlaneCore::ConfigItem.new(key: :team_reviewers,
-                                       description: 'An array of GitHub team slugs that will be assigned to the pull request',
-                                       optional: true,
-                                       type: Array),
-          FastlaneCore::ConfigItem.new(key: :intermediate_branch_created_callback,
-                                       description: 'Callback to allow for the caller to perform operations on the intermediate branch (e.g. pushing new commits to pre-solve conflicts) before creating the PR. ' \
-                                        + 'The callback receives two parameters: the base (target) branch for the PR and the intermediate branch name that has been created.' \
-                                        + 'Note that if you use the callback to add new commits to the intermediate branch, you are responsible for git-pushing them too',
-                                       optional: true,
-                                       type: Proc),
-          Fastlane::Helper::GithubHelper.github_token_config_item,
+          *forwarded_params,
+          Fastlane::Helper::GithubHelper.github_token_config_item, # we forward `github_token` to `api_token` in the `create_pull_request` action
+          FastlaneCore::ConfigItem.new(
+            key: :repository,
+            env_name: 'GHHELPER_REPOSITORY',
+            description: 'The remote path of the GH repository on which we work',
+            optional: false,
+            type: String
+          ),
+          FastlaneCore::ConfigItem.new(
+            key: :source_branch,
+            description: 'The source branch to create a backmerge PR from, in the format `release/x.y.z`',
+            optional: false,
+            type: String
+          ),
+          FastlaneCore::ConfigItem.new(
+            key: :default_branch,
+            description: 'The default branch to target if no newer release branches exist',
+            optional: true,
+            default_value: DEFAULT_BRANCH,
+            type: String
+          ),
+          FastlaneCore::ConfigItem.new(
+            key: :target_branches,
+            description: 'Array of target branches for the backmerge. If empty, the action will determine target branches by finding all `release/x.y.z` branches with a `x.y.z` version greater than the version in source branch\'s name. If none are found, it will target `default_branch`',
+            optional: true,
+            default_value: [],
+            type: Array
+          ),
+          FastlaneCore::ConfigItem.new(
+            key: :milestone_title,
+            description: 'The title of the milestone to assign to the created PRs',
+            optional: true,
+            type: String
+          ),
+          FastlaneCore::ConfigItem.new(
+            key: :intermediate_branch_created_callback,
+            description: 'Callback to allow for the caller to perform operations on the intermediate branch (e.g. pushing new commits to pre-solve conflicts) before creating the PR. ' \
+             + 'The callback receives two parameters: the base (target) branch for the PR and the intermediate branch name that has been created.' \
+             + 'Note that if you use the callback to add new commits to the intermediate branch, you are responsible for git-pushing them too',
+            optional: true,
+            type: Proc
+          ),
         ]
       end
 
diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_localize_helper.rb b/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_localize_helper.rb
index b493c1123..2a4a8d414 100644
--- a/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_localize_helper.rb
+++ b/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_localize_helper.rb
@@ -342,7 +342,10 @@ def self.merge_xml_documents(all_xmls)
         #
         def self.post_process_xml!(translated_xml, locale_code:, original_xml:)
           copy_orig_attributes = lambda do |node, xpath|
-            orig_attributes = original_xml.xpath(xpath)&.first&.attribute_nodes&.to_h do |attr|
+            found_node = original_xml.xpath(xpath)&.first
+            return unless found_node
+
+            orig_attributes = found_node.attribute_nodes&.to_h do |attr|
               [[attr.namespace&.prefix, attr.name].compact.join(':'), attr.value]
             end
             orig_attributes&.each { |k, v| node[k] = v unless k == 'name' }
diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_adc_app_sizes_helper.rb b/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_adc_app_sizes_helper.rb
index c4ac186e8..40503eb9d 100644
--- a/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_adc_app_sizes_helper.rb
+++ b/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_adc_app_sizes_helper.rb
@@ -54,7 +54,7 @@ def self.format_csv(app_sizes, devices: nil)
           csv = "Version\t#{devices.join("\t")}\n"
           app_sizes.each do |details|
             build_number = details['cfBundleVersion']
-            sizes = details['sizesInBytes'].select { |name, _| devices.include?(name) }
+            sizes = details['sizesInBytes'].slice(*devices)
             csv += "#{build_number}\t" + devices.map { |d| sz(sizes[d]['compressed']) }.join("\t") + "\n"
           end
           csv
@@ -64,7 +64,7 @@ def self.format_markdown(app_sizes, devices: nil)
           devices = DEFAULT_DEVICES if devices.nil? || devices.empty?
           app_sizes.map do |details|
             build_number = details['cfBundleVersion']
-            sizes = details['sizesInBytes'].select { |name, _| devices.include?(name) }
+            sizes = details['sizesInBytes'].slice(*devices)
             col_size = devices.map(&:length).max
             table = "| #{build_number.ljust(col_size)} | Download | Install  |\n"
             table += "|:#{'-' * col_size}-|---------:|---------:|\n"
diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_strings_file_validation_helper.rb b/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_strings_file_validation_helper.rb
index 097623dee..9857f6c12 100644
--- a/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_strings_file_validation_helper.rb
+++ b/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_strings_file_validation_helper.rb
@@ -65,10 +65,10 @@ class StringsFileValidationHelper
         # Inspects the given `.strings` file for duplicated keys, returning them if any.
         #
         # @param [String] file The path to the file to inspect.
-        # @return [Hash<String, Array<Int>] Hash with the dublipcated keys.
+        # @return [Hash<String, Array<Int>] Hash with the duplicated keys.
         #         Each element has the duplicated key (from the `.strings`) as key and an array of line numbers where the key occurs as value.
         def self.find_duplicated_keys(file:)
-          keys_with_lines = Hash.new([])
+          keys_with_lines = Hash.new { |h, k| h[k] = [] }
 
           state = State.new(context: :root, buffer: StringIO.new, in_escaped_ctx: false, found_key: nil)
 
diff --git a/spec/create_release_backmerge_pull_request_spec.rb b/spec/create_release_backmerge_pull_request_spec.rb
index 8e6c72c70..378d7c672 100644
--- a/spec/create_release_backmerge_pull_request_spec.rb
+++ b/spec/create_release_backmerge_pull_request_spec.rb
@@ -41,7 +41,7 @@ def stub_git_release_branches(branches)
     end
   end
 
-  def stub_expected_pull_requests(expected_backmerge_branches:, source_branch:, labels: [], milestone_number: nil, reviewers: nil, team_reviewers: nil, branch_exists_on_remote: false, nothing_to_merge_between: [])
+  def stub_expected_pull_requests(expected_backmerge_branches:, source_branch:, labels: nil, milestone_number: nil, reviewers: nil, team_reviewers: nil, branch_exists_on_remote: false, nothing_to_merge_between: [], api_url: 'https://api.github.com')
     expected_backmerge_branches.map do |target_branch|
       expected_intermediate_branch = "merge/#{source_branch.gsub('/', '-')}-into-#{target_branch.gsub('/', '-')}"
 
@@ -58,7 +58,11 @@ def stub_expected_pull_requests(expected_backmerge_branches:, source_branch:, la
 
       expect(Fastlane::Helper::GitHelper).to receive(:checkout_and_pull).with(source_branch)
       expect(Fastlane::Helper::GitHelper).to receive(:create_branch).with(expected_intermediate_branch)
-      expect(other_action_mock).to receive(:push_to_git_remote).with(tags: false)
+      expect(other_action_mock).to receive(:push_to_git_remote).with(
+        tags: false,
+        remote_branch: expected_intermediate_branch,
+        set_upstream: true
+      )
 
       allow(other_action_mock).to receive(:create_pull_request).with(
         api_token: test_token,
@@ -70,7 +74,8 @@ def stub_expected_pull_requests(expected_backmerge_branches:, source_branch:, la
         labels: labels,
         milestone: milestone_number,
         reviewers: reviewers,
-        team_reviewers: team_reviewers
+        team_reviewers: team_reviewers,
+        api_url: api_url
       ).and_return(mock_pr_url(target_branch))
 
       expected_intermediate_branch
@@ -85,6 +90,8 @@ def stub_expected_pull_requests(expected_backmerge_branches:, source_branch:, la
     allow(Fastlane::Helper::GitHelper).to receive(:delete_remote_branch_if_exists!)
     allow(Git).to receive(:open).and_return(git_client)
     allow(Fastlane::Action).to receive(:other_action).and_return(other_action_mock)
+    # Stub git rev-parse command used by CreatePullRequestAction when getting default branch value
+    allow(Fastlane::Actions).to receive(:sh).with('git rev-parse --abbrev-ref HEAD', { log: false }).and_return('main')
   end
 
   context 'when `target_branches` is provided' do
@@ -237,7 +244,7 @@ def stub_expected_pull_requests(expected_backmerge_branches:, source_branch:, la
   context 'when providing labels' do
     [
       %w[java ruby perl],
-      [],
+      nil,
     ].each do |labels|
       it "creates a backmerge PR setting the labels: #{labels}" do
         stub_git_release_branches([])
@@ -294,6 +301,31 @@ def stub_expected_pull_requests(expected_backmerge_branches:, source_branch:, la
     end
   end
 
+  context 'when providing an api_url' do
+    it 'creates a backmerge PR using the provided api_url' do
+      stub_git_release_branches([])
+
+      source_branch = 'release/30.6'
+      api_url = 'https://github.company.com/api/v3'
+
+      stub_expected_pull_requests(
+        expected_backmerge_branches: [default_branch],
+        source_branch: source_branch,
+        api_url: api_url
+      )
+
+      result = run_described_fastlane_action(
+        github_token: test_token,
+        repository: test_repo,
+        source_branch: source_branch,
+        default_branch: default_branch,
+        api_url: api_url
+      )
+
+      expect(result).to eq([mock_pr_url(default_branch)])
+    end
+  end
+
   context 'when there is nothing to merge' do
     context 'when no callback is provided' do
       it 'detects it should not create a pull request before even creating the intermediate branch' do


Reviews:
user: login: AliSoftware
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T20:36:10Z
user: login: iangmaia
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T13:02:47Z
user: login: AliSoftware
body: Tested via https://github.tumblr.net/Tumblr/ios/pull/29539/files, after:

 - Creating a dummy `release/100.0` branch
 - Making its `Gemfile` point to this branch of the `release-toolkit`,
 - removing the `ENV[…]` workaround we had for the API URL, and adding the`api_url:` parameter to the action call instead
 - Tweaking the code called in the `intermediate_branch_created_callback` block to force a dummy change (so we have something to commit) then calling the `git_commit` action from it
 - Finally, run `bundle exec fastlane backmerge_release_branch`

And I was able to confirm that:

 - The `api_url` parameter was properly forwarded and allowed to create the PR on our private GH:E instance
 - The `merge/release-100.0-into-develop` intermediate branch was set up to track the remote branch after it being pushed to the remote
 - The call to `git_commit` action worked, without erroring with "not a git repo" as it was before.

🎉 

---

⚠️  The only think remaining before merging is to update the `CHANGELOG.md` with the extra stuff that this PR fixes (in addition to the item you initially created the PR for).
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T21:14:05Z

Review comments:
diff_hunk: @@ -192,6 +196,10 @@ def self.details
 
       def self.available_options
         [
+          FastlaneCore::ConfigItem.new(key: :api_url,
+                                       description: 'The GitHub API URL to use for creating the pull request. Primarily used when working with GitHub Enterprise instances',
+                                       optional: true,
+                                       type: String),
path: lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_release_backmerge_pull_request_action.rb
user: login: AliSoftware
body: I wonder if it'd make more sense to reference the `ConfigItem` from the original `create_pull_request` action, filtering on the parameters we want to just forward, instead of duplicating them (and tiehr `optional`. `description`, `type`,…) ourselves?

i.e.

```ruby
FORWARDED_PARAM_KEYS = %i[api_token api_bearer repo labels api_url assignees reviewers team_reviewers].freeze

def self.available_options
  forwarded_params = Fastlane::Actions::CreatePullRequestAction.available_options.select do |opt|
    FORWARDED_PARAM_KEYS.include?(opt.key)
  end
  [
    *forwarded_params,
    # Then here only add the params that are specific to our `create_release_backmerge_pull_request` action
    # that are not directly forwarded to `create_pull_request`
    FastlaneCore::ConfigItem.new(key: :source_branch,
       …
```
And then when we call the `create_pull_request` in the implementation of our action, we can inject the forwarded params by constructing a hash of `fwd_params = FORWARDED_PARAMS_KEYS.to_h { |k| [k, params[k] }` then inject it in `create_pull_request(title: …, body: …, **fwd_params)`

wdyt?
created_at: 2025-03-17T20:36:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -192,6 +196,10 @@ def self.details
 
       def self.available_options
         [
+          FastlaneCore::ConfigItem.new(key: :api_url,
+                                       description: 'The GitHub API URL to use for creating the pull request. Primarily used when working with GitHub Enterprise instances',
+                                       optional: true,
+                                       type: String),
path: lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_release_backmerge_pull_request_action.rb
user: login: iangmaia
body: I liked the idea and implemented it... but then the tests started to fail mysteriously due to an additional call to `git rev-parse --abbrev-ref HEAD`, and some default values also failed (i.e. `nil` vs. `[]`) 😓 so this is something to consider when implementing this pattern. Eventually I got it working, though! Updated on 21adece2db7ca62f362c9f8acb56da7d12383a1e.
created_at: 2025-03-18T13:02:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,6 +11,7 @@ _None_
 ### New Features
 
 - Make fastlane log actions as collapsible groups in Buildkite. [#638]
+- Add `api_url` parameter to `create_release_backmerge_pull_request` to support GitHub Enterprise. [#641]
 
 ### Bug Fixes
path: CHANGELOG.md
user: login: AliSoftware
body: You should also add an entry under `### Bug Fixes` to mention that the calling another fastlane action from the `intermediate_branch_created_callback` proc now behaves correctly.
created_at: 2025-03-20T21:11:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,6 +11,7 @@ _None_
 ### New Features
 
 - Make fastlane log actions as collapsible groups in Buildkite. [#638]
+- Add `api_url` parameter to `create_release_backmerge_pull_request` to support GitHub Enterprise. [#641]
path: CHANGELOG.md
user: login: AliSoftware
body: Could be worth mentioning (probably in the same list item) that the intermediate branch that action creates is also now tracking the remote branch?
created_at: 2025-03-20T21:12:33Z
author_association: CONTRIBUTOR

PR comments:
user: login: AliSoftware
created_at: 2025-03-18T12:47:06Z
body: 💡 While we're fixing this action, it'd be nice if, when we push the intermediate branch on line L123/R126 (`other_action.push_to_git_remote(tags: false)`), we could also ask to set the tracking branch (`-u`) during push.

_This would especially be useful not only to note if new commits have been pushed on remote but not on local etc… but also so that the local working copy can detect when the remote branch has been deleted (i.e. the PR has been merged) when the `git remote -vv` marks it as `[gone]`, and so that `git branch -d` can see that the branch was merged and can thus be safely deleted locally too._
author_association: CONTRIBUTOR
user: login: iangmaia
created_at: 2025-03-18T13:21:40Z
body: > 💡 While we're fixing this action, it'd be nice if, when we push the intermediate branch on line L123/R126 (`other_action.push_to_git_remote(tags: false)`), we could also ask to set the tracking branch (`-u`) during push.
> 
> _This would especially be useful not only to note if new commits have been pushed on remote but not on local etc… but also so that the local working copy can detect when the remote branch has been deleted (i.e. the PR has been merged) when the `git remote -vv` marks it as `[gone]`, and so that `git branch -d` can see that the branch was merged and can thus be safely deleted locally too._

Good idea 👍 Updated on 4795969fa58c459a976cf72d5c72401cccfc9bc6.
author_association: CONTRIBUTOR
user: login: AliSoftware
created_at: 2025-03-18T14:13:47Z
body: @iangmaia Another fix we need to do (and test, probably using some live example not just specs) is wrapping the call to the `intermediate_branch_created_callback.call` callback within a `Dir.chdir(FastlaneCore::FastlaneFolder.path) do … end` (at least I think that'd be the fix?) to workaround the fact that if we try to call another action from this `proc` defined in the caller's `Fastfile`, it wouldn't try to go up a directory and fail running the action. See Tumblr/ios#29516 & Tumblr/android#24360.

Another solution is for the caller to use `other_action.git_commit` instead of `git_commit` when declaring their `proc`, but that seems less natural to expect to realize that the `proc` would be called from within the implementation of `create_release_backmerge_pull_request` and thus from another action and that the caller would have to use `other_action` even if the code of the `proc` is within their `Fastfile`…
author_association: CONTRIBUTOR
user: login: iangmaia
created_at: 2025-03-18T16:50:07Z
body: > @iangmaia Another fix we need to do (and test, probably using some live example not just specs) is wrapping the call to the `intermediate_branch_created_callback.call` callback within a `Dir.chdir(FastlaneCore::FastlaneFolder.path) do … end` (at least I think that'd be the fix?) to workaround the fact that if we try to call another action from this `proc` defined in the caller's `Fastfile`, it wouldn't try to go up a directory and fail running the action. See Tumblr/ios#29516 & Tumblr/android#24360.
> 
> Another solution is for the caller to use `other_action.git_commit` instead of `git_commit` when declaring their `proc`, but that seems less natural to expect to realize that the `proc` would be called from within the implementation of `create_release_backmerge_pull_request` and thus from another action and that the caller would have to use `other_action` even if the code of the `proc` is within their `Fastfile`…

Isn't `FastlaneCore::FastlaneFolder.path` going to be something like `./fastlane/` (based on [this](https://github.com/fastlane/fastlane/blob/master/fastlane_core/lib/fastlane_core/fastlane_folder.rb))? 🤔 
author_association: CONTRIBUTOR
user: login: AliSoftware
created_at: 2025-03-18T18:19:51Z
body: > Isn't `FastlaneCore::FastlaneFolder.path` going to be something like `./fastlane/` (based on [this](https://github.com/fastlane/fastlane/blob/master/fastlane_core/lib/fastlane_core/fastlane_folder.rb))? 🤔

Yes, which is the path in which fastlane is expecting your `pwd` to be when you run code written from the `Fastfile`, while when you run code written from an action it expects to run from the root of your repo (this is for legacy reasons mostly)

See [this section in the doc which explains it all](https://docs.fastlane.tools/advanced/fastlane/#directory-behavior)

To my rough understanding, this is also [why `other_action` is passing `FastlaneCore::FastlaneFolder.path`](https://github.com/fastlane/fastlane/blob/master/fastlane/lib/fastlane/other_action.rb#L14-L26), to workaround this implementation detail. 
 - Indeed, when you call an action from the Fastile, fastlane expects the `pwd` to be inside `./fastlane` at the the you call the code… but also due to those legacy reasons, the _inner implementation_ of fastlane actions are implemented in a way that assumes their code runs from the root of the repo (due to legacy very early implementations from before the `fastlane/` folder was introduced), and that's why the runner [does a `cd ..` at the time it hands over the baton from the `Fastfile` code to the action code](https://github.com/fastlane/fastlane/blob/master/fastlane/lib/fastlane/runner.rb#L223), to reconcile those discrepancies…
 - This is also why, when you want to call one action from another action's implementation, [like we do here for example](https://github.com/wordpress-mobile/release-toolkit/blob/trunk/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_release_backmerge_pull_request_action.rb#L123), you use `other_action` so that it behaves as is that action was called from the `fastfile/` folder (as when it's called directly from the `Fastfile`) and not from the root folder (where the `pwd` would be during the execution of the parent action calling that child action)

Is it convoluted implementation and logic? Yes, definitievly… but that's what you get when there's lots of legacy and you don't want to break millions of codebases by changing that suddenly 😅 
author_association: CONTRIBUTOR
user: login: iangmaia
created_at: 2025-03-18T18:50:10Z
body: > * Indeed, when you call an action from the Fastile, fastlane expects the `pwd` to be inside `./fastlane` at the the you call the code… but also due to those legacy reasons, the _inner implementation_ of fastlane actions are implemented in a way that assumes their code runs from the root of the repo (due to legacy very early implementations from before the `fastlane/` folder was introduced), and that's why the runner [does a `cd ..` at the time it hands over the baton from the `Fastfile` code to the action code](https://github.com/fastlane/fastlane/blob/master/fastlane/lib/fastlane/runner.rb#L223), to reconcile those discrepancies…
> * This is also why, when you want to call one action from another action's implementation, [like we do here for example](https://github.com/wordpress-mobile/release-toolkit/blob/trunk/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_release_backmerge_pull_request_action.rb#L123), you use `other_action` so that it behaves as is that action was called from the `fastfile/` folder (as when it's called directly from the `Fastfile`) and not from the root folder (where the `pwd` would be during the execution of the parent action calling that child action)
 
Ahh, TIL 😅  Thanks a lot for the details, it all makes sense now 👍 this is the kind of implementation detail I never got into before with Fastlane (or only end up seeing add-hoc when this kind of issue arises).

> that's what you get when there's lots of legacy and you don't want to break millions of codebases by changing that suddenly 😅

💯 


---

I've just pushed daff9062e3a0a2d56649bbb06e4328badcdd876b.
author_association: CONTRIBUTOR