fastlane-plugin-wpmreleasetoolkit 14.7.0 → 14.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c3a14d01dbfc81f832b5d1b4c009e16c2b78ff148b3c25c592a1bc32c5e8d442
4
- data.tar.gz: 8fbb44705e3d5799cb21857647c83f8321b9494548dc0342eb093781f707538d
3
+ metadata.gz: f4682e90d456806e69e70758d89fb49a05470d2ccddf680d967a077b744a2a5c
4
+ data.tar.gz: a0b958abad01adf90ba744473469674caf4142ef4df833248b1442cfe6f65a4e
5
5
  SHA512:
6
- metadata.gz: 73ee821d6f4d161a1479bedd639f7427c6022cd17551dce93d8d87cabf47d95513a9595f8aeb02523b60ada7d52141fb82bbbbbbb9538b0b9aaa2dd9d7e681ef
7
- data.tar.gz: 4dee3516eb30a8bbbc97ef67568f97d541452a44dd56a16080606bbdbe790fe816890108ca97a843ee8af511fb307d39dd19f7999cb5429d2d0ee4a630c44afd
6
+ metadata.gz: 1d5d9c8f61b57fd12b4cdcf746c66211cec731cc5c17cb4b7be65d9d18fc54a3ddc94ddfbb0e07828d100df042ad60b97eb58d9e14002e109fe569874f67dd8b
7
+ data.tar.gz: c04ad671d17c8afc2ebe0825db13a3c40cd1326b92d5269c365a3ae0cc2fe86cd76663e58c02de3c13283833c9069154c087ee0799f7fff3e07126cac79c2af4
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fastlane/action'
4
+ require_relative '../../helper/github_helper'
5
+
6
+ module Fastlane
7
+ module Actions
8
+ class FindOrCreatePullRequestAction < Action
9
+ def self.run(params)
10
+ github_helper = Fastlane::Helper::GithubHelper.new(github_token: params[:github_token])
11
+
12
+ existing_pr = github_helper.find_pull_request(
13
+ repository: params[:repository],
14
+ head: params[:head],
15
+ base: params[:base]
16
+ )
17
+
18
+ unless existing_pr.nil?
19
+ UI.message("An open Pull Request already exists for `#{params[:head]}`: #{existing_pr.html_url}")
20
+ return existing_pr.html_url
21
+ end
22
+
23
+ other_action.create_pull_request(
24
+ api_url: params[:api_url],
25
+ api_token: params[:github_token],
26
+ repo: params[:repository],
27
+ title: params[:title],
28
+ body: params[:body],
29
+ draft: params[:draft],
30
+ head: params[:head],
31
+ base: params[:base],
32
+ labels: params[:labels],
33
+ assignees: params[:assignees],
34
+ reviewers: params[:reviewers],
35
+ team_reviewers: params[:team_reviewers],
36
+ milestone: params[:milestone]
37
+ )
38
+ end
39
+
40
+ def self.description
41
+ 'Returns the URL of the open Pull Request for a head branch, creating one if none exists yet'
42
+ end
43
+
44
+ def self.details
45
+ <<~DETAILS
46
+ Looks for an open Pull Request whose head is the given branch and which targets the given base,
47
+ and returns its URL if found. Otherwise, creates a new Pull Request and returns its URL.
48
+
49
+ This is useful for "rolling" automations (e.g. a daily translations or dependency-update job) that
50
+ force-push the same head branch on every run: GitHub automatically refreshes the diff of the existing
51
+ PR, so this action only needs to open a PR the first time.
52
+ DETAILS
53
+ end
54
+
55
+ def self.authors
56
+ ['Automattic']
57
+ end
58
+
59
+ def self.return_type
60
+ :string
61
+ end
62
+
63
+ def self.return_value
64
+ 'The URL of the existing or newly-created Pull Request'
65
+ end
66
+
67
+ def self.available_options
68
+ # Parameters we forward as-is from Fastlane's `create_pull_request` action
69
+ forwarded_param_keys = %i[
70
+ api_url
71
+ draft
72
+ labels
73
+ assignees
74
+ reviewers
75
+ team_reviewers
76
+ milestone
77
+ ].freeze
78
+
79
+ forwarded_params = Fastlane::Actions::CreatePullRequestAction.available_options.select do |opt|
80
+ forwarded_param_keys.include?(opt.key)
81
+ end
82
+
83
+ [
84
+ *forwarded_params,
85
+ Fastlane::Helper::GithubHelper.github_token_config_item, # forwarded to `api_token` in the `create_pull_request` action
86
+ FastlaneCore::ConfigItem.new(
87
+ key: :repository,
88
+ env_name: 'GHHELPER_REPOSITORY',
89
+ description: 'The remote path of the GH repository on which we work, e.g. `wordpress-mobile/wordpress-ios`',
90
+ optional: false,
91
+ type: String
92
+ ),
93
+ FastlaneCore::ConfigItem.new(
94
+ key: :title,
95
+ description: 'The title of the Pull Request to create if none exists yet',
96
+ optional: false,
97
+ type: String
98
+ ),
99
+ FastlaneCore::ConfigItem.new(
100
+ key: :body,
101
+ description: 'The body of the Pull Request to create if none exists yet',
102
+ optional: true,
103
+ type: String
104
+ ),
105
+ FastlaneCore::ConfigItem.new(
106
+ key: :head,
107
+ description: 'The head branch of the Pull Request (the branch with the changes)',
108
+ optional: false,
109
+ type: String
110
+ ),
111
+ FastlaneCore::ConfigItem.new(
112
+ key: :base,
113
+ description: 'The base branch the Pull Request targets (e.g. `trunk`)',
114
+ optional: false,
115
+ type: String
116
+ ),
117
+ ]
118
+ end
119
+
120
+ def self.is_supported?(platform)
121
+ true
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fastlane/action'
4
+ require_relative '../../helper/github_helper'
5
+
6
+ module Fastlane
7
+ module Actions
8
+ class UploadGithubReleaseAssetsAction < Action
9
+ def self.run(params)
10
+ repository = params[:repository]
11
+ version = params[:version]
12
+ assets = params[:release_assets]
13
+ replace_existing = params[:replace_existing]
14
+
15
+ UI.message("Uploading #{assets.count} GitHub Release asset(s) to #{repository} #{version}.")
16
+
17
+ github_helper = Fastlane::Helper::GithubHelper.new(github_token: params[:github_token])
18
+ url = github_helper.upload_release_assets(
19
+ repository: repository,
20
+ version: version,
21
+ assets: assets,
22
+ replace_existing: replace_existing
23
+ )
24
+
25
+ UI.success("Successfully uploaded GitHub Release assets. You can see the release at '#{url}'")
26
+ url
27
+ end
28
+
29
+ def self.description
30
+ 'Uploads assets to an existing GitHub Release'
31
+ end
32
+
33
+ def self.authors
34
+ ['Automattic']
35
+ end
36
+
37
+ def self.return_value
38
+ 'The URL of the GitHub Release'
39
+ end
40
+
41
+ def self.details
42
+ 'Uploads assets to an existing GitHub Release. By default, existing release assets with matching filenames are replaced; when replace_existing is false, matching assets cause the action to fail.'
43
+ end
44
+
45
+ def self.available_options
46
+ [
47
+ FastlaneCore::ConfigItem.new(key: :repository,
48
+ description: 'The slug (`<org>/<repo>`) of the GitHub repository containing the release',
49
+ optional: false,
50
+ type: String,
51
+ verify_block: proc do |value|
52
+ UI.user_error!('Repository cannot be empty') if value.to_s.empty?
53
+ end),
54
+ FastlaneCore::ConfigItem.new(key: :version,
55
+ description: 'The version of the release. Used as the git tag name',
56
+ optional: false,
57
+ type: String,
58
+ verify_block: proc do |value|
59
+ UI.user_error!('Version cannot be empty') if value.to_s.empty?
60
+ end),
61
+ FastlaneCore::ConfigItem.new(key: :release_assets,
62
+ description: 'Assets to upload',
63
+ type: Array,
64
+ optional: false,
65
+ verify_block: proc do |value|
66
+ UI.user_error!('You must provide at least one release asset') if value.nil? || value.empty?
67
+ value.each do |asset|
68
+ UI.user_error!('release_assets must contain file paths') unless asset.is_a?(String) && !asset.empty?
69
+ end
70
+ end),
71
+ FastlaneCore::ConfigItem.new(key: :replace_existing,
72
+ description: 'True to delete existing release assets with matching filenames before uploading. False to fail if a matching asset exists',
73
+ optional: true,
74
+ default_value: true,
75
+ type: Boolean),
76
+ Fastlane::Helper::GithubHelper.github_token_config_item,
77
+ ]
78
+ end
79
+
80
+ def self.is_supported?(platform)
81
+ true
82
+ end
83
+ end
84
+ end
85
+ end
@@ -192,6 +192,58 @@ module Fastlane
192
192
  release[:html_url]
193
193
  end
194
194
 
195
+ # Returns the GitHub release matching a given tag/version, including draft releases.
196
+ #
197
+ # @param [String] repository The repository to fetch the GitHub release from. Typically a repo slug (<org>/<repo>).
198
+ # @param [String] version The release version/tag to fetch.
199
+ # @return [Sawyer::Resource] The matching GitHub Release.
200
+ # @raise [Fastlane::UI::Error] UI.user_error! if the release does not exist.
201
+ #
202
+ def get_release(repository:, version:)
203
+ release = client.releases(repository).find { |candidate| candidate.tag_name == version }
204
+ return release unless release.nil?
205
+
206
+ UI.user_error!("Could not find GitHub Release for tag #{version} in #{repository}")
207
+ end
208
+
209
+ # Uploads assets to an existing GitHub release, optionally replacing matching filenames.
210
+ #
211
+ # @param [String] repository The repository to upload the GitHub release assets to. Typically a repo slug (<org>/<repo>).
212
+ # @param [String] version The release version/tag to upload assets to.
213
+ # @param [Array<String>] assets List of local file paths to attach as release assets.
214
+ # @param [TrueClass|FalseClass] replace_existing Delete existing same-filename assets before uploading. When false, fail if a matching asset exists.
215
+ # @return [String] URL of the corresponding GitHub Release.
216
+ # @raise [Fastlane::UI::Error] UI.user_error! if the release or any local asset file does not exist.
217
+ #
218
+ def upload_release_assets(repository:, version:, assets:, replace_existing: true)
219
+ asset_paths = validate_release_assets!(assets)
220
+ release = get_release(repository: repository, version: version)
221
+ existing_assets = client.release_assets(release.url)
222
+
223
+ asset_paths.each do |file_path|
224
+ file_name = File.basename(file_path)
225
+ matching_assets = existing_assets.select { |asset| asset.name == file_name }
226
+
227
+ unless matching_assets.empty?
228
+ if replace_existing
229
+ matching_assets.each do |asset|
230
+ UI.message("Deleting existing GitHub Release asset #{asset.name}")
231
+ client.delete_release_asset(asset.url)
232
+ end
233
+ existing_assets -= matching_assets
234
+ else
235
+ UI.user_error!("GitHub Release #{version} already has an asset named #{file_name}. Set replace_existing: true to replace it.")
236
+ end
237
+ end
238
+
239
+ UI.message("Uploading #{file_path} to GitHub Release #{version}")
240
+ uploaded_asset = client.upload_asset(release.url, file_path, content_type: 'application/octet-stream')
241
+ existing_assets << uploaded_asset unless uploaded_asset.nil?
242
+ end
243
+
244
+ release.html_url
245
+ end
246
+
195
247
  # Use the GitHub API to generate release notes based on the list of PRs between current tag and previous tag.
196
248
  # @note This API uses the `.github/release.yml` config file to classify the PRs by category in the generated list according to PR labels.
197
249
  #
@@ -304,6 +356,23 @@ module Fastlane
304
356
  reuse_identifier
305
357
  end
306
358
 
359
+ # Find an existing Pull Request matching the given head (and optionally base) branch.
360
+ #
361
+ # @param [String] repository The repository name, including the organization (e.g. `wordpress-mobile/wordpress-ios`)
362
+ # @param [String] head The head branch to look for. May be given as `branch` or as the fully-qualified `owner:branch`;
363
+ # when unqualified, it is automatically prefixed with the repository's owner.
364
+ # @param [String?] base The base branch the PR should target. If nil, PRs targeting any base are considered.
365
+ # @param [String] state The PR state to match (`open`, `closed`, or `all`). Defaults to `open`.
366
+ # @return [Sawyer::Resource, nil] The first matching Pull Request, or nil if none matches.
367
+ #
368
+ def find_pull_request(repository:, head:, base: nil, state: 'open')
369
+ qualified_head = head.include?(':') ? head : "#{repository.split('/').first}:#{head}"
370
+ options = { state: state, head: qualified_head }
371
+ options[:base] = base unless base.nil?
372
+
373
+ client.pull_requests(repository, options).first
374
+ end
375
+
307
376
  # Update a milestone for a repository
308
377
  #
309
378
  # @param [String] repository The repository name (including the organization)
@@ -351,6 +420,26 @@ module Fastlane
351
420
  client.protect_branch(repository, branch, options)
352
421
  end
353
422
 
423
+ def validate_release_assets!(assets)
424
+ asset_paths = Array(assets)
425
+ UI.user_error!('You must provide at least one release asset') if asset_paths.empty?
426
+
427
+ asset_paths.each do |file_path|
428
+ UI.user_error!('release_assets must contain file paths') unless file_path.is_a?(String) && !file_path.empty?
429
+ end
430
+
431
+ file_names = asset_paths.map { |file_path| File.basename(file_path) }
432
+ UI.user_error!('release_assets must not contain duplicate filenames') if file_names.uniq.length != file_names.length
433
+
434
+ asset_paths.each do |file_path|
435
+ UI.user_error!("Can't find file #{file_path}!") unless File.file?(file_path)
436
+ end
437
+
438
+ asset_paths
439
+ end
440
+
441
+ private :validate_release_assets!
442
+
354
443
  # Convert a response from the `/branch-protection` API endpoint into a Hash
355
444
  # suitable to be returned and/or reused to pass to a subsequent `/branch-protection` API request
356
445
  # @param [Sawyer::Resource] response The API response returned by `#get_branch_protection` or `#set_branch_protection`
@@ -3,6 +3,6 @@
3
3
  module Fastlane
4
4
  module Wpmreleasetoolkit
5
5
  NAME = 'fastlane-plugin-wpmreleasetoolkit'
6
- VERSION = '14.7.0'
6
+ VERSION = '14.9.0'
7
7
  end
8
8
  end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fastlane
4
+ module Wpmreleasetoolkit
5
+ module Versioning
6
+ # Google Play Store's maximum allowed versionCode.
7
+ MAX_PLAY_STORE_VERSION_CODE = 2_100_000_000
8
+
9
+ # The `ContinuousBuildCodeFormatter` derives an Android Play Store `versionCode` for a
10
+ # "continuous trunk" release model, where the low-order term is a high-cardinality,
11
+ # monotonically increasing build number (e.g. a Buildkite build number).
12
+ #
13
+ # The build code is computed as:
14
+ #
15
+ # versionCode = (major * 10 + minor) * 10^build_digits + build_number
16
+ #
17
+ # It takes `major`, `minor`, and `build_number` as explicit arguments rather than an
18
+ # `AppVersion`, because the inputs come from different sources and an `AppVersion` does not
19
+ # model them: the marketing `major`/`minor` come from the parsed version, while `build_number`
20
+ # is an independent CI counter (e.g. `BUILDKITE_BUILD_NUMBER`). Notably, `AppVersion#build_number`
21
+ # means something else in this domain (the RC/beta iteration counter, e.g. `-rc-1`), so taking an
22
+ # `AppVersion` here would invite reading the wrong field. There is also no `patch`: in a
23
+ # continuous-trunk model the build number strictly orders every build and subsumes patch's
24
+ # ordering role (hotfixes get a new build number, not a patch digit in the code).
25
+ #
26
+ # Because the build number is globally monotonic and the version prefix only ever increases,
27
+ # the resulting code is always strictly increasing — even if the build number eventually
28
+ # exceeds `10^build_digits` (which only costs human-readability, not ordering). The only hard
29
+ # correctness constraint is staying at or below the Play Store's max versionCode.
30
+ #
31
+ # Unlike `DerivedBuildCodeFormatter` (fixed-width string concatenation capped at 8 total digits
32
+ # and 3 digits per component, i.e. build <= 999), this formatter can hold a large build number.
33
+ # The two formatters target different release models; this one does not replace the other.
34
+ class ContinuousBuildCodeFormatter
35
+ # @param [Integer] build_digits Number of digits reserved for the build number, which sets the
36
+ # multiplier applied to the `major * 10 + minor` prefix (multiplier = 10^build_digits).
37
+ # Must be a positive integer. Defaults to 6 (multiplier = 1_000_000).
38
+ #
39
+ def initialize(build_digits: 6)
40
+ validate_build_digits!(build_digits)
41
+ @build_digits = build_digits
42
+ end
43
+
44
+ # Derive the build code (Android `versionCode`).
45
+ #
46
+ # @param [Integer] major The major (marketing) version number.
47
+ # @param [Integer] minor The minor (marketing) version number. Must be 9 or lower.
48
+ # @param [Integer] build_number A high-cardinality, monotonically increasing build number
49
+ # (e.g. a Buildkite build number). This is a CI counter, not `AppVersion#build_number`.
50
+ #
51
+ # @return [Integer] The derived `versionCode`.
52
+ #
53
+ def build_code(major:, minor:, build_number:)
54
+ # Validate up front so bad input (e.g. strings from env vars or file reads) raises a
55
+ # user-friendly error rather than an opaque `TypeError` from the arithmetic below.
56
+ validate_component!('major', major)
57
+ validate_component!('minor', minor)
58
+ validate_component!('build_number', build_number)
59
+
60
+ # `major * 10 + minor` is only unambiguous while minor is a single digit.
61
+ if minor > 9
62
+ UI.user_error!("Minor version (#{minor}) must be 9 or lower to derive an unambiguous build code with `#{self.class.name}`")
63
+ end
64
+
65
+ prefix = (major * 10) + minor
66
+ code = (prefix * (10**@build_digits)) + build_number
67
+
68
+ # Sanity check: Play Store versionCodes must be positive integers.
69
+ if code <= 0
70
+ UI.user_error!("Derived build code (#{code}) must be a positive integer")
71
+ end
72
+
73
+ if code > MAX_PLAY_STORE_VERSION_CODE
74
+ UI.user_error!("Derived build code (#{code}) exceeds the maximum allowed Play Store versionCode (#{MAX_PLAY_STORE_VERSION_CODE})")
75
+ end
76
+
77
+ code
78
+ end
79
+
80
+ private
81
+
82
+ # Validates that a version component is a non-negative integer.
83
+ #
84
+ # @param [String] name The component name, used in the error message
85
+ # @param [Integer] value The value to validate
86
+ #
87
+ # @raise [StandardError] If the value is not a non-negative integer
88
+ #
89
+ def validate_component!(name, value)
90
+ unless value.is_a?(Integer)
91
+ UI.user_error!("`#{name}` must be an integer, got: #{value.class}")
92
+ end
93
+
94
+ return unless value.negative?
95
+
96
+ UI.user_error!("`#{name}` must be a non-negative integer, got: #{value}")
97
+ end
98
+
99
+ # Validates that `build_digits` is a positive integer.
100
+ #
101
+ # @param [Integer] build_digits The build digit count to validate
102
+ #
103
+ # @raise [StandardError] If the value is not a positive integer
104
+ #
105
+ def validate_build_digits!(build_digits)
106
+ unless build_digits.is_a?(Integer)
107
+ UI.user_error!("`build_digits` must be an integer, got: #{build_digits.class}")
108
+ end
109
+
110
+ return if build_digits.positive?
111
+
112
+ UI.user_error!("`build_digits` must be a positive integer, got: #{build_digits}")
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastlane-plugin-wpmreleasetoolkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 14.7.0
4
+ version: 14.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Automattic
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-12 00:00:00.000000000 Z
11
+ date: 2026-06-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: buildkit
@@ -439,6 +439,7 @@ files:
439
439
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_new_milestone_action.rb
440
440
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_release_backmerge_pull_request_action.rb
441
441
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/common/extract_release_notes_for_version_action.rb
442
+ - lib/fastlane/plugin/wpmreleasetoolkit/actions/common/find_or_create_pull_request_action.rb
442
443
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/common/find_previous_tag.rb
443
444
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/common/firebase_login.rb
444
445
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/common/get_prs_between_tags.rb
@@ -454,6 +455,7 @@ files:
454
455
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/common/update_apps_cdn_build_metadata.rb
455
456
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/common/update_assigned_milestone_action.rb
456
457
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/common/upload_build_to_apps_cdn.rb
458
+ - lib/fastlane/plugin/wpmreleasetoolkit/actions/common/upload_github_release_assets_action.rb
457
459
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/common/upload_to_s3.rb
458
460
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_add_files_to_copy_action.rb
459
461
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_apply_action.rb
@@ -519,6 +521,7 @@ files:
519
521
  - lib/fastlane/plugin/wpmreleasetoolkit/versioning/files/android_version_file.rb
520
522
  - lib/fastlane/plugin/wpmreleasetoolkit/versioning/files/ios_version_file.rb
521
523
  - lib/fastlane/plugin/wpmreleasetoolkit/versioning/formatters/abstract_version_formatter.rb
524
+ - lib/fastlane/plugin/wpmreleasetoolkit/versioning/formatters/continuous_build_code_formatter.rb
522
525
  - lib/fastlane/plugin/wpmreleasetoolkit/versioning/formatters/derived_build_code_formatter.rb
523
526
  - lib/fastlane/plugin/wpmreleasetoolkit/versioning/formatters/four_part_build_code_formatter.rb
524
527
  - lib/fastlane/plugin/wpmreleasetoolkit/versioning/formatters/four_part_version_formatter.rb