fastlane-plugin-wpmreleasetoolkit 14.6.0 → 14.8.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 +4 -4
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/find_or_create_pull_request_action.rb +125 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/openai_ask_action.rb +116 -24
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/update_apps_cdn_build_metadata.rb +162 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/upload_build_to_apps_cdn.rb +4 -13
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/apps_cdn_helper.rb +55 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/configure_helper.rb +2 -2
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb +17 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_linter_helper.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/version.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/versioning/formatters/continuous_build_code_formatter.rb +117 -0
- metadata +8 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 614ca9504d0baa99cc411f83e7bb94f179260a0e8154d738aecfed47a4de7ce5
|
|
4
|
+
data.tar.gz: 7edfec9cb051abe68182905d8ee0620c73a0da73c02e829e7dcb69f1593ea49e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 32d6b0a90838debf53ba8fc6c5fbdf729a5159a12c0b69ffa8be1709cca3be571d28e1471d2736bb911c97f67d6720b2aec2320bbcd221362a873b14d597ecf0
|
|
7
|
+
data.tar.gz: d9170171309d3b0f11cf102757bd4bdcdc380ef207f7b06d17e2f03e4aef17094c77bf84e1eba17de43cf2e29f833b9a8de4277dab853118a26ba6fa23e391d2
|
data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/find_or_create_pull_request_action.rb
ADDED
|
@@ -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
|
|
@@ -8,8 +8,10 @@ module Fastlane
|
|
|
8
8
|
module Actions
|
|
9
9
|
class OpenaiAskAction < Action
|
|
10
10
|
OPENAI_API_ENDPOINT = URI('https://api.openai.com/v1/chat/completions').freeze
|
|
11
|
+
# Preserve the previous `max_tokens` ceiling while using the current API field.
|
|
12
|
+
DEFAULT_MAX_COMPLETION_TOKENS = 2048
|
|
11
13
|
DEFAULT_MAX_TOOL_ITERATIONS = 5
|
|
12
|
-
DEFAULT_MODEL = 'gpt-
|
|
14
|
+
DEFAULT_MODEL = 'gpt-4.1'
|
|
13
15
|
|
|
14
16
|
PREDEFINED_PROMPTS = {
|
|
15
17
|
release_notes: <<~PROMPT
|
|
@@ -39,12 +41,16 @@ module Fastlane
|
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
# Backwards-compatible single-shot path when no tools are provided.
|
|
42
|
-
if tools.nil?
|
|
44
|
+
if tools.nil?
|
|
43
45
|
body = request_body(prompt: prompt, question: question, model: model)
|
|
44
46
|
response = Net::HTTP.post(OPENAI_API_ENDPOINT, body, headers)
|
|
45
47
|
return parse_text_response(response)
|
|
46
48
|
end
|
|
47
49
|
|
|
50
|
+
validate_tools_array!(tools)
|
|
51
|
+
validate_max_tool_iterations!(max_tool_iterations)
|
|
52
|
+
validate_tools!(tools)
|
|
53
|
+
|
|
48
54
|
run_with_tools(
|
|
49
55
|
prompt: prompt,
|
|
50
56
|
question: question,
|
|
@@ -62,7 +68,9 @@ module Fastlane
|
|
|
62
68
|
format_message(role: 'user', text: question),
|
|
63
69
|
].compact
|
|
64
70
|
|
|
65
|
-
|
|
71
|
+
tool_iterations = 0
|
|
72
|
+
|
|
73
|
+
loop do
|
|
66
74
|
body = request_body_with_messages(messages: messages, tools: tools, model: model)
|
|
67
75
|
response = Net::HTTP.post(OPENAI_API_ENDPOINT, body, headers)
|
|
68
76
|
assistant_message = parse_assistant_message(response)
|
|
@@ -71,26 +79,31 @@ module Fastlane
|
|
|
71
79
|
# No tool calls — model produced a final answer.
|
|
72
80
|
return assistant_message['content'] if tool_calls.nil? || tool_calls.empty?
|
|
73
81
|
|
|
82
|
+
if tool_iterations >= max_tool_iterations
|
|
83
|
+
UI.user_error!(
|
|
84
|
+
"OpenAI tool-use loop did not produce a final answer after #{max_tool_iterations} tool iterations. " \
|
|
85
|
+
'Refusing to execute additional tool calls. Increase `max_tool_iterations` or check that your prompt instructs the model to stop calling tools.'
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
74
89
|
# Append the assistant's tool-call message verbatim, then run each handler
|
|
75
90
|
# and append the corresponding `role: tool` results.
|
|
76
91
|
messages << assistant_message
|
|
77
92
|
tool_calls.each do |tool_call|
|
|
78
93
|
messages << execute_tool_call(tool_call, tool_handlers)
|
|
79
94
|
end
|
|
80
|
-
end
|
|
81
95
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
'Increase `max_tool_iterations` or check that your prompt instructs the model to stop calling tools.'
|
|
85
|
-
)
|
|
96
|
+
tool_iterations += 1
|
|
97
|
+
end
|
|
86
98
|
end
|
|
87
99
|
|
|
88
100
|
def self.request_body(prompt:, question:, model: DEFAULT_MODEL)
|
|
89
101
|
{
|
|
90
102
|
model: model,
|
|
103
|
+
store: false,
|
|
91
104
|
response_format: { type: 'text' },
|
|
92
105
|
temperature: 1,
|
|
93
|
-
|
|
106
|
+
max_completion_tokens: DEFAULT_MAX_COMPLETION_TOKENS,
|
|
94
107
|
top_p: 1,
|
|
95
108
|
messages: [
|
|
96
109
|
format_message(role: 'system', text: prompt),
|
|
@@ -102,9 +115,10 @@ module Fastlane
|
|
|
102
115
|
def self.request_body_with_messages(messages:, tools:, model: DEFAULT_MODEL)
|
|
103
116
|
{
|
|
104
117
|
model: model,
|
|
118
|
+
store: false,
|
|
105
119
|
response_format: { type: 'text' },
|
|
106
120
|
temperature: 1,
|
|
107
|
-
|
|
121
|
+
max_completion_tokens: DEFAULT_MAX_COMPLETION_TOKENS,
|
|
108
122
|
top_p: 1,
|
|
109
123
|
messages: messages,
|
|
110
124
|
tools: tools
|
|
@@ -140,8 +154,49 @@ module Fastlane
|
|
|
140
154
|
end
|
|
141
155
|
end
|
|
142
156
|
|
|
157
|
+
def self.validate_max_tool_iterations!(max_tool_iterations)
|
|
158
|
+
UI.user_error!("Parameter `max_tool_iterations` must be an Integer (got #{max_tool_iterations.class})") unless max_tool_iterations.is_a?(Integer)
|
|
159
|
+
UI.user_error!("Parameter `max_tool_iterations` must be >= 1 (got #{max_tool_iterations})") if max_tool_iterations < 1
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def self.validate_tools_array!(tools)
|
|
163
|
+
UI.user_error!('Parameter `tools` must be a non-empty Array when provided') unless tools.is_a?(Array) && !tools.empty?
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def self.validate_tools!(tools)
|
|
167
|
+
invalid_tools = tools.each_with_index.filter_map do |tool, index|
|
|
168
|
+
type = tool_type(tool)
|
|
169
|
+
next "tools[#{index}] type #{type.nil? ? '<missing>' : type.inspect}" unless type == 'function'
|
|
170
|
+
|
|
171
|
+
function = tool[:function] || tool['function']
|
|
172
|
+
name = function[:name] || function['name'] if function.is_a?(Hash)
|
|
173
|
+
next if valid_tool_name?(name)
|
|
174
|
+
|
|
175
|
+
"tools[#{index}] missing function.name"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
return if invalid_tools.empty?
|
|
179
|
+
|
|
180
|
+
UI.user_error!(
|
|
181
|
+
'Parameter `tools` only supports OpenAI function tools with a non-empty `function.name`. ' \
|
|
182
|
+
"Invalid tool definitions: #{invalid_tools.join(', ')}"
|
|
183
|
+
)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def self.tool_type(tool)
|
|
187
|
+
return nil unless tool.is_a?(Hash)
|
|
188
|
+
|
|
189
|
+
(tool[:type] || tool['type'])&.to_s
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def self.valid_tool_name?(name)
|
|
193
|
+
(name.is_a?(String) || name.is_a?(Symbol)) && !name.to_s.empty?
|
|
194
|
+
end
|
|
195
|
+
|
|
143
196
|
def self.execute_tool_call(tool_call, tool_handlers)
|
|
144
|
-
|
|
197
|
+
return unsupported_tool_call_result(tool_call) unless function_tool_call?(tool_call)
|
|
198
|
+
|
|
199
|
+
name = tool_call.dig('function', 'name').to_s
|
|
145
200
|
raw_args = tool_call.dig('function', 'arguments') || '{}'
|
|
146
201
|
|
|
147
202
|
result =
|
|
@@ -151,8 +206,8 @@ module Fastlane
|
|
|
151
206
|
rescue JSON::ParserError
|
|
152
207
|
# Short-circuit: the handler never sees malformed args. Tell the model the
|
|
153
208
|
# tool-call payload was invalid so it can retry with valid JSON, and log the
|
|
154
|
-
#
|
|
155
|
-
UI.error("Invalid JSON arguments for tool '#{name}'. Raw payload
|
|
209
|
+
# local failure without recording raw arguments that might contain secrets.
|
|
210
|
+
UI.error("Invalid JSON arguments for tool '#{name}' in tool call '#{tool_call['id']}'. Raw payload omitted because it may contain secrets.")
|
|
156
211
|
{ error: "Invalid JSON arguments for tool '#{name}' — payload could not be parsed. Retry with valid JSON." }
|
|
157
212
|
end
|
|
158
213
|
|
|
@@ -163,6 +218,40 @@ module Fastlane
|
|
|
163
218
|
}
|
|
164
219
|
end
|
|
165
220
|
|
|
221
|
+
def self.function_tool_call?(tool_call)
|
|
222
|
+
return false unless tool_call['type'] == 'function'
|
|
223
|
+
return false unless tool_call['function'].is_a?(Hash)
|
|
224
|
+
|
|
225
|
+
name = tool_call.dig('function', 'name')
|
|
226
|
+
valid_tool_name?(name)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def self.unsupported_tool_call_result(tool_call)
|
|
230
|
+
type = tool_call['type'] || '<missing>'
|
|
231
|
+
error =
|
|
232
|
+
if type == 'function'
|
|
233
|
+
'Function tool call is missing a non-empty function.name.'
|
|
234
|
+
else
|
|
235
|
+
"Unsupported tool call type '#{type}'. Only function tool calls are supported."
|
|
236
|
+
end
|
|
237
|
+
log_message =
|
|
238
|
+
if type == 'function'
|
|
239
|
+
"Invalid OpenAI function tool call '#{tool_call['id']}': missing a non-empty function.name."
|
|
240
|
+
else
|
|
241
|
+
"Unsupported OpenAI tool call type '#{type}' in tool call '#{tool_call['id']}'. Only function tool calls are supported."
|
|
242
|
+
end
|
|
243
|
+
UI.error(log_message)
|
|
244
|
+
|
|
245
|
+
{
|
|
246
|
+
role: 'tool',
|
|
247
|
+
tool_call_id: tool_call['id'],
|
|
248
|
+
content: serialize_tool_result(
|
|
249
|
+
name: type,
|
|
250
|
+
result: { error: error }
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
end
|
|
254
|
+
|
|
166
255
|
# Serializes a tool result to a JSON string. Handlers are contracted to return
|
|
167
256
|
# JSON-serializable values, but a buggy handler might return something like a
|
|
168
257
|
# `Pathname`, `Proc`, or a custom object whose `to_json` raises. Failing the
|
|
@@ -175,7 +264,7 @@ module Fastlane
|
|
|
175
264
|
def self.serialize_tool_result(name:, result:)
|
|
176
265
|
JSON.generate(result)
|
|
177
266
|
rescue StandardError => e
|
|
178
|
-
UI.error("Could not serialize tool result for '#{name}': #{e.class}
|
|
267
|
+
UI.error("Could not serialize tool result for '#{name}': #{e.class}. Result class: #{result.class}. Error message omitted because it may contain secrets.")
|
|
179
268
|
JSON.generate({ error: "Tool result for '#{name}' could not be serialized to JSON. Returned class: #{result.class}." })
|
|
180
269
|
end
|
|
181
270
|
|
|
@@ -185,9 +274,9 @@ module Fastlane
|
|
|
185
274
|
#
|
|
186
275
|
# - Missing or non-callable handler: structured `{ error: ... }` so the model can recover.
|
|
187
276
|
# - Handler raised: structured `{ error:, exception: }` carrying only the exception class
|
|
188
|
-
# so the model can see the failure category and adjust. The
|
|
189
|
-
# are
|
|
190
|
-
#
|
|
277
|
+
# so the model can see the failure category and adjust. The exception message and
|
|
278
|
+
# backtrace are intentionally omitted from local logs and from the model response
|
|
279
|
+
# because tool results and CI logs can expose release secrets
|
|
191
280
|
# (tokens, file contents, internal API responses). The loop keeps going rather than
|
|
192
281
|
# aborting the lane mid-conversation — the model is the better judge of whether the
|
|
193
282
|
# failure is recoverable than a global `rescue` here.
|
|
@@ -198,7 +287,7 @@ module Fastlane
|
|
|
198
287
|
begin
|
|
199
288
|
handler.call(args)
|
|
200
289
|
rescue StandardError => e
|
|
201
|
-
UI.error("Handler for tool '#{name}' raised #{e.class}
|
|
290
|
+
UI.error("Handler for tool '#{name}' raised #{e.class}. Error message and backtrace omitted because they may contain secrets.")
|
|
202
291
|
{ error: "Handler for tool '#{name}' raised an exception", exception: e.class.name }
|
|
203
292
|
end
|
|
204
293
|
end
|
|
@@ -228,7 +317,8 @@ module Fastlane
|
|
|
228
317
|
When `tools` and `tool_handlers` are provided, the action runs a tool-use (function-calling) loop:
|
|
229
318
|
on each turn, if the model calls one or more tools, the corresponding handler is invoked locally
|
|
230
319
|
and its return value is sent back to the model as a `role: tool` message. The loop ends when the
|
|
231
|
-
model returns a plain text response, or
|
|
320
|
+
model returns a plain text response, or before executing tool calls beyond `max_tool_iterations`.
|
|
321
|
+
The model gets one final API turn to answer after the last permitted local tool execution round.
|
|
232
322
|
DETAILS
|
|
233
323
|
end
|
|
234
324
|
|
|
@@ -306,19 +396,20 @@ module Fastlane
|
|
|
306
396
|
sensitive: true,
|
|
307
397
|
type: String),
|
|
308
398
|
FastlaneCore::ConfigItem.new(key: :model,
|
|
309
|
-
description: 'The OpenAI model to send the request to (e.g. `gpt-
|
|
399
|
+
description: 'The OpenAI model to send the request to (e.g. `gpt-4.1`, `gpt-4.1-mini`, `gpt-4o`). ' \
|
|
310
400
|
"Defaults to `#{DEFAULT_MODEL}`",
|
|
311
401
|
optional: true,
|
|
312
402
|
default_value: DEFAULT_MODEL,
|
|
313
403
|
type: String),
|
|
314
404
|
FastlaneCore::ConfigItem.new(key: :tools,
|
|
315
|
-
description: 'Optional array of
|
|
405
|
+
description: 'Optional array of OpenAI function tool definitions. Each definition must have a non-empty `function.name`. ' \
|
|
316
406
|
'When provided, the action runs a tool-use loop',
|
|
317
407
|
optional: true,
|
|
318
408
|
default_value: nil,
|
|
319
409
|
type: Array,
|
|
320
410
|
verify_block: proc do |value|
|
|
321
|
-
|
|
411
|
+
validate_tools_array!(value)
|
|
412
|
+
validate_tools!(value)
|
|
322
413
|
end),
|
|
323
414
|
FastlaneCore::ConfigItem.new(key: :tool_handlers,
|
|
324
415
|
description: 'Hash of tool name to a callable (e.g. a Proc) invoked when the model calls that tool. ' \
|
|
@@ -332,13 +423,14 @@ module Fastlane
|
|
|
332
423
|
UI.user_error!("Parameter `tool_handlers` values must respond to :call. Non-callable handlers: #{non_callable.keys}") if non_callable.any?
|
|
333
424
|
end),
|
|
334
425
|
FastlaneCore::ConfigItem.new(key: :max_tool_iterations,
|
|
335
|
-
description: 'Maximum number of tool
|
|
426
|
+
description: 'Maximum number of local tool execution rounds before the action fails. ' \
|
|
427
|
+
'The model can receive one final API turn to answer after the last permitted tool result. ' \
|
|
336
428
|
'Only used when `tools` are provided',
|
|
337
429
|
optional: true,
|
|
338
430
|
default_value: DEFAULT_MAX_TOOL_ITERATIONS,
|
|
339
431
|
type: Integer,
|
|
340
432
|
verify_block: proc do |value|
|
|
341
|
-
|
|
433
|
+
validate_max_tool_iterations!(value)
|
|
342
434
|
end),
|
|
343
435
|
]
|
|
344
436
|
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fastlane/action'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'json'
|
|
6
|
+
require_relative '../../helper/apps_cdn_helper'
|
|
7
|
+
|
|
8
|
+
module Fastlane
|
|
9
|
+
module Actions
|
|
10
|
+
class UpdateAppsCdnBuildMetadataAction < Action
|
|
11
|
+
def self.run(params)
|
|
12
|
+
post_ids = params[:post_ids]
|
|
13
|
+
UI.message("Updating Apps CDN build metadata for #{post_ids.size} post(s): #{post_ids.join(', ')}...")
|
|
14
|
+
|
|
15
|
+
# Build the JSON body for the dedicated Apps CDN builds endpoint, which accepts
|
|
16
|
+
# the same string-based format as the upload flow (e.g. `visibility: 'Internal'`)
|
|
17
|
+
body = {}
|
|
18
|
+
body['post_status'] = params[:post_status] if params[:post_status]
|
|
19
|
+
body['visibility'] = params[:visibility].to_s.capitalize if params[:visibility]
|
|
20
|
+
|
|
21
|
+
UI.user_error!('No metadata to update. Provide at least one of: visibility, post_status') if body.empty?
|
|
22
|
+
|
|
23
|
+
results = post_ids.map do |post_id|
|
|
24
|
+
update_single_post(site_id: params[:site_id], api_token: params[:api_token], post_id: post_id, body: body)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
UI.success("Successfully updated Apps CDN build metadata for #{results.size} post(s)")
|
|
28
|
+
results
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Update a single CDN build post with the given body via the dedicated
|
|
32
|
+
# `/wpcom/v2/sites/{site_id}/a8c-cdn/builds/{post_id}` endpoint.
|
|
33
|
+
#
|
|
34
|
+
# @param site_id [String] the WordPress.com site ID
|
|
35
|
+
# @param api_token [String] the WordPress.com API bearer token
|
|
36
|
+
# @param post_id [Integer] the ID of the build post to update
|
|
37
|
+
# @param body [Hash] the JSON body to send in the POST request
|
|
38
|
+
# @return [Integer] the ID of the updated post
|
|
39
|
+
# @raise [FastlaneCore::Interface::FastlaneError] if the API returns a non-success response
|
|
40
|
+
def self.update_single_post(site_id:, api_token:, post_id:, body:)
|
|
41
|
+
uri = Helper::AppsCdnHelper.wpcom_v2_url(site_id: site_id, path: "a8c-cdn/builds/#{post_id}")
|
|
42
|
+
|
|
43
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
|
44
|
+
request.body = JSON.generate(body)
|
|
45
|
+
request['Content-Type'] = 'application/json'
|
|
46
|
+
request['Accept'] = 'application/json'
|
|
47
|
+
request['Authorization'] = "Bearer #{api_token}"
|
|
48
|
+
|
|
49
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https', open_timeout: 10, read_timeout: 30) do |http|
|
|
50
|
+
http.request(request)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
case response
|
|
54
|
+
when Net::HTTPSuccess
|
|
55
|
+
result = JSON.parse(response.body)
|
|
56
|
+
updated_id = result['id']
|
|
57
|
+
|
|
58
|
+
UI.message(" Updated post #{updated_id}")
|
|
59
|
+
|
|
60
|
+
updated_id
|
|
61
|
+
else
|
|
62
|
+
UI.error("Failed to update Apps CDN build metadata for post #{post_id}: #{response.code} #{response.message}")
|
|
63
|
+
UI.error(response.body)
|
|
64
|
+
UI.user_error!("Update of Apps CDN build metadata failed for post #{post_id}")
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.description
|
|
69
|
+
'Updates metadata of one or more existing builds on the Apps CDN'
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.authors
|
|
73
|
+
['Automattic']
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.return_value
|
|
77
|
+
'Returns an Array of post IDs (Integer) that were successfully updated. On error, raises a FastlaneError.'
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.details
|
|
81
|
+
<<~DETAILS
|
|
82
|
+
Updates metadata (such as post status or visibility) for one or more existing build posts on a WordPress blog
|
|
83
|
+
that has the Apps CDN plugin enabled, using the dedicated `/wpcom/v2/sites/{site_id}/a8c-cdn/builds/{post_id}`
|
|
84
|
+
endpoint. Standard WP REST API writes are blocked for builds, so this endpoint is the only way to update them.
|
|
85
|
+
See PCYsg-15tP-p2 internal a8c documentation for details about the Apps CDN plugin.
|
|
86
|
+
DETAILS
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def self.available_options
|
|
90
|
+
[
|
|
91
|
+
FastlaneCore::ConfigItem.new(
|
|
92
|
+
key: :site_id,
|
|
93
|
+
env_name: 'APPS_CDN_SITE_ID',
|
|
94
|
+
description: 'The WordPress.com CDN site ID where the build was uploaded',
|
|
95
|
+
optional: false,
|
|
96
|
+
type: String,
|
|
97
|
+
verify_block: proc do |value|
|
|
98
|
+
UI.user_error!('Site ID cannot be empty') if value.to_s.empty?
|
|
99
|
+
end
|
|
100
|
+
),
|
|
101
|
+
FastlaneCore::ConfigItem.new(
|
|
102
|
+
key: :post_ids,
|
|
103
|
+
description: 'The IDs of the build posts to update',
|
|
104
|
+
optional: false,
|
|
105
|
+
type: Array,
|
|
106
|
+
verify_block: proc do |value|
|
|
107
|
+
UI.user_error!('Post IDs must be a non-empty array') unless value.is_a?(Array) && !value.empty?
|
|
108
|
+
value.each do |id|
|
|
109
|
+
UI.user_error!("Each post ID must be a positive integer, got: #{id.inspect}") unless id.is_a?(Integer) && id.positive?
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
),
|
|
113
|
+
FastlaneCore::ConfigItem.new(
|
|
114
|
+
key: :api_token,
|
|
115
|
+
env_name: 'WPCOM_API_TOKEN',
|
|
116
|
+
description: 'The WordPress.com API token for authentication',
|
|
117
|
+
optional: false,
|
|
118
|
+
type: String,
|
|
119
|
+
verify_block: proc do |value|
|
|
120
|
+
UI.user_error!('API token cannot be empty') if value.to_s.empty?
|
|
121
|
+
end
|
|
122
|
+
),
|
|
123
|
+
FastlaneCore::ConfigItem.new(
|
|
124
|
+
key: :visibility,
|
|
125
|
+
description: 'The new visibility for the build (:internal or :external)',
|
|
126
|
+
optional: true,
|
|
127
|
+
type: Symbol,
|
|
128
|
+
verify_block: Helper::AppsCdnHelper.verify_visibility_param
|
|
129
|
+
),
|
|
130
|
+
FastlaneCore::ConfigItem.new(
|
|
131
|
+
key: :post_status,
|
|
132
|
+
description: "The new post status ('publish' or 'draft')",
|
|
133
|
+
optional: true,
|
|
134
|
+
type: String,
|
|
135
|
+
verify_block: Helper::AppsCdnHelper.verify_post_status_param
|
|
136
|
+
),
|
|
137
|
+
]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def self.is_supported?(platform)
|
|
141
|
+
true
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def self.example_code
|
|
145
|
+
[
|
|
146
|
+
'update_apps_cdn_build_metadata(
|
|
147
|
+
site_id: "12345678",
|
|
148
|
+
api_token: ENV["WPCOM_API_TOKEN"],
|
|
149
|
+
post_ids: [98765],
|
|
150
|
+
post_status: "publish"
|
|
151
|
+
)',
|
|
152
|
+
'update_apps_cdn_build_metadata(
|
|
153
|
+
site_id: "12345678",
|
|
154
|
+
api_token: ENV["WPCOM_API_TOKEN"],
|
|
155
|
+
post_ids: [12345, 67890, 11111],
|
|
156
|
+
visibility: :external
|
|
157
|
+
)',
|
|
158
|
+
]
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -4,6 +4,7 @@ require 'fastlane/action'
|
|
|
4
4
|
require 'net/http'
|
|
5
5
|
require 'uri'
|
|
6
6
|
require 'json'
|
|
7
|
+
require_relative '../../helper/apps_cdn_helper'
|
|
7
8
|
|
|
8
9
|
module Fastlane
|
|
9
10
|
module Actions
|
|
@@ -17,8 +18,6 @@ module Fastlane
|
|
|
17
18
|
class UploadBuildToAppsCdnAction < Action
|
|
18
19
|
# See https://github.a8c.com/Automattic/wpcom/blob/trunk/wp-content/lib/a8c/cdn/src/enums/enum-resource-type.php
|
|
19
20
|
RESOURCE_TYPE = 'Build'
|
|
20
|
-
# These are from the WordPress.com API, not the Apps CDN plugin
|
|
21
|
-
VALID_POST_STATUS = %w[publish draft].freeze
|
|
22
21
|
# See https://github.a8c.com/Automattic/wpcom/blob/trunk/wp-content/lib/a8c/cdn/src/enums/enum-build-type.php
|
|
23
22
|
VALID_BUILD_TYPES = %w[
|
|
24
23
|
Alpha
|
|
@@ -48,17 +47,13 @@ module Fastlane
|
|
|
48
47
|
'Full Install',
|
|
49
48
|
'Update',
|
|
50
49
|
].freeze
|
|
51
|
-
# See https://github.a8c.com/Automattic/wpcom/blob/trunk/wp-content/lib/a8c/cdn/src/enums/enum-visibility.php
|
|
52
|
-
VALID_VISIBILITIES = %i[internal external].freeze
|
|
53
|
-
|
|
54
50
|
def self.run(params)
|
|
55
51
|
UI.message('Uploading build to Apps CDN...')
|
|
56
52
|
|
|
57
53
|
file_path = params[:file_path]
|
|
58
54
|
UI.user_error!("File not found at path '#{file_path}'") unless File.exist?(file_path)
|
|
59
55
|
|
|
60
|
-
|
|
61
|
-
uri = URI.parse(api_endpoint)
|
|
56
|
+
uri = Helper::AppsCdnHelper.rest_v1_1_url(site_id: params[:site_id], path: 'media/new')
|
|
62
57
|
|
|
63
58
|
# Create the request body and headers
|
|
64
59
|
parameters = {
|
|
@@ -261,9 +256,7 @@ module Fastlane
|
|
|
261
256
|
description: 'The visibility of the build (:internal or :external)',
|
|
262
257
|
optional: false,
|
|
263
258
|
type: Symbol,
|
|
264
|
-
verify_block:
|
|
265
|
-
UI.user_error!("Visibility must be one of: #{VALID_VISIBILITIES.map { "`:#{_1}`" }.join(', ')}") unless VALID_VISIBILITIES.include?(value.to_s.downcase.to_sym)
|
|
266
|
-
end
|
|
259
|
+
verify_block: Helper::AppsCdnHelper.verify_visibility_param
|
|
267
260
|
),
|
|
268
261
|
FastlaneCore::ConfigItem.new(
|
|
269
262
|
key: :post_status,
|
|
@@ -271,9 +264,7 @@ module Fastlane
|
|
|
271
264
|
optional: true,
|
|
272
265
|
default_value: 'publish',
|
|
273
266
|
type: String,
|
|
274
|
-
verify_block:
|
|
275
|
-
UI.user_error!("Post status must be one of: #{VALID_POST_STATUS.join(', ')}") unless VALID_POST_STATUS.include?(value)
|
|
276
|
-
end
|
|
267
|
+
verify_block: Helper::AppsCdnHelper.verify_post_status_param
|
|
277
268
|
),
|
|
278
269
|
FastlaneCore::ConfigItem.new(
|
|
279
270
|
key: :version,
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
5
|
+
module Fastlane
|
|
6
|
+
module Helper
|
|
7
|
+
module AppsCdnHelper
|
|
8
|
+
API_BASE_URL = 'https://public-api.wordpress.com'
|
|
9
|
+
|
|
10
|
+
# See https://github.a8c.com/Automattic/wpcom/blob/trunk/wp-content/lib/a8c/cdn/src/enums/enum-visibility.php
|
|
11
|
+
VALID_VISIBILITIES = %i[internal external].freeze
|
|
12
|
+
|
|
13
|
+
# These are from the WordPress.com API, not the Apps CDN plugin
|
|
14
|
+
VALID_POST_STATUS = %w[publish draft].freeze
|
|
15
|
+
|
|
16
|
+
# Builds a WordPress.com REST API v1.1 URI scoped to a site.
|
|
17
|
+
#
|
|
18
|
+
# @param site_id [String] the WordPress.com site ID
|
|
19
|
+
# @param path [String] the API path relative to the site (e.g. 'media/new')
|
|
20
|
+
# @return [URI] the parsed full API URI
|
|
21
|
+
def self.rest_v1_1_url(site_id:, path:)
|
|
22
|
+
URI.parse("#{API_BASE_URL}/rest/v1.1/sites/#{site_id}/#{path}")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Builds a WordPress.com REST API wpcom/v2 URI scoped to a site.
|
|
26
|
+
#
|
|
27
|
+
# @param site_id [String] the WordPress.com site ID
|
|
28
|
+
# @param path [String] the API path relative to the site (e.g. 'a8c-cdn/builds/123')
|
|
29
|
+
# @return [URI] the parsed full API URI
|
|
30
|
+
def self.wpcom_v2_url(site_id:, path:)
|
|
31
|
+
URI.parse("#{API_BASE_URL}/wpcom/v2/sites/#{site_id}/#{path}")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Returns a proc that validates a visibility parameter value against {VALID_VISIBILITIES}.
|
|
35
|
+
# Intended for use as a `verify_block` in Fastlane ConfigItem definitions.
|
|
36
|
+
#
|
|
37
|
+
# @return [Proc] a proc that raises FastlaneError if the value is invalid
|
|
38
|
+
def self.verify_visibility_param
|
|
39
|
+
proc do |value|
|
|
40
|
+
UI.user_error!("Visibility must be one of: #{VALID_VISIBILITIES.map { "`:#{_1}`" }.join(', ')}") unless VALID_VISIBILITIES.include?(value.to_s.downcase.to_sym)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Returns a proc that validates a post status parameter value against {VALID_POST_STATUS}.
|
|
45
|
+
# Intended for use as a `verify_block` in Fastlane ConfigItem definitions.
|
|
46
|
+
#
|
|
47
|
+
# @return [Proc] a proc that raises FastlaneError if the value is invalid
|
|
48
|
+
def self.verify_post_status_param
|
|
49
|
+
proc do |value|
|
|
50
|
+
UI.user_error!("Post status must be one of: #{VALID_POST_STATUS.join(', ')}") unless VALID_POST_STATUS.include?(value)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -151,7 +151,7 @@ module Fastlane
|
|
|
151
151
|
end
|
|
152
152
|
|
|
153
153
|
### A helper function to extract the distance from the provided string.
|
|
154
|
-
### (ie – this function will
|
|
154
|
+
### (ie – this function will receive "behind 2" or "ahead 6" and return 2 or 6, respectively.
|
|
155
155
|
def self.parse_distance(match)
|
|
156
156
|
distance = match.to_s.scan(/\d+/).first
|
|
157
157
|
|
|
@@ -265,7 +265,7 @@ module Fastlane
|
|
|
265
265
|
end
|
|
266
266
|
|
|
267
267
|
## Updates the project encryption key defined in ~/.mobile-secrets/keys.json
|
|
268
|
-
## The updated file is
|
|
268
|
+
## The updated file is committed and pushed to the repo
|
|
269
269
|
def self.update_project_encryption_key
|
|
270
270
|
# Update keys.json with the new key
|
|
271
271
|
keys_json = mobile_secrets_keys_json
|
|
@@ -304,6 +304,23 @@ module Fastlane
|
|
|
304
304
|
reuse_identifier
|
|
305
305
|
end
|
|
306
306
|
|
|
307
|
+
# Find an existing Pull Request matching the given head (and optionally base) branch.
|
|
308
|
+
#
|
|
309
|
+
# @param [String] repository The repository name, including the organization (e.g. `wordpress-mobile/wordpress-ios`)
|
|
310
|
+
# @param [String] head The head branch to look for. May be given as `branch` or as the fully-qualified `owner:branch`;
|
|
311
|
+
# when unqualified, it is automatically prefixed with the repository's owner.
|
|
312
|
+
# @param [String?] base The base branch the PR should target. If nil, PRs targeting any base are considered.
|
|
313
|
+
# @param [String] state The PR state to match (`open`, `closed`, or `all`). Defaults to `open`.
|
|
314
|
+
# @return [Sawyer::Resource, nil] The first matching Pull Request, or nil if none matches.
|
|
315
|
+
#
|
|
316
|
+
def find_pull_request(repository:, head:, base: nil, state: 'open')
|
|
317
|
+
qualified_head = head.include?(':') ? head : "#{repository.split('/').first}:#{head}"
|
|
318
|
+
options = { state: state, head: qualified_head }
|
|
319
|
+
options[:base] = base unless base.nil?
|
|
320
|
+
|
|
321
|
+
client.pull_requests(repository, options).first
|
|
322
|
+
end
|
|
323
|
+
|
|
307
324
|
# Update a milestone for a repository
|
|
308
325
|
#
|
|
309
326
|
# @param [String] repository The repository name (including the organization)
|
|
@@ -14,7 +14,7 @@ module Fastlane
|
|
|
14
14
|
attr_reader :install_path, :version
|
|
15
15
|
|
|
16
16
|
# @param [String] install_path The path to install SwiftGen to. Usually something like "$PROJECT_DIR/vendor/swiftgen/#{SWIFTGEN_VERSION}".
|
|
17
|
-
# It's recommended to provide an absolute path here rather than a relative one, to ensure it's not
|
|
17
|
+
# It's recommended to provide an absolute path here rather than a relative one, to ensure it's not dependent on where the action is run from.
|
|
18
18
|
# @param [String] version The version of SwiftGen to use. This will be used both:
|
|
19
19
|
# - to check if the current version located in `install_path`, if it already exists, is the expected one
|
|
20
20
|
# - to know which version to download if there is not one installed in `install_path` yet
|
data/lib/fastlane/plugin/wpmreleasetoolkit/versioning/formatters/continuous_build_code_formatter.rb
ADDED
|
@@ -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.
|
|
4
|
+
version: 14.8.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-
|
|
11
|
+
date: 2026-06-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: buildkit
|
|
@@ -72,14 +72,14 @@ dependencies:
|
|
|
72
72
|
requirements:
|
|
73
73
|
- - "~>"
|
|
74
74
|
- !ruby/object:Gem::Version
|
|
75
|
-
version: '2.
|
|
75
|
+
version: '2.235'
|
|
76
76
|
type: :runtime
|
|
77
77
|
prerelease: false
|
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
79
|
requirements:
|
|
80
80
|
- - "~>"
|
|
81
81
|
- !ruby/object:Gem::Version
|
|
82
|
-
version: '2.
|
|
82
|
+
version: '2.235'
|
|
83
83
|
- !ruby/object:Gem::Dependency
|
|
84
84
|
name: gettext
|
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -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
|
|
@@ -451,6 +452,7 @@ files:
|
|
|
451
452
|
- lib/fastlane/plugin/wpmreleasetoolkit/actions/common/remove_branch_protection_action.rb
|
|
452
453
|
- lib/fastlane/plugin/wpmreleasetoolkit/actions/common/set_branch_protection_action.rb
|
|
453
454
|
- lib/fastlane/plugin/wpmreleasetoolkit/actions/common/set_milestone_frozen_marker_action.rb
|
|
455
|
+
- lib/fastlane/plugin/wpmreleasetoolkit/actions/common/update_apps_cdn_build_metadata.rb
|
|
454
456
|
- lib/fastlane/plugin/wpmreleasetoolkit/actions/common/update_assigned_milestone_action.rb
|
|
455
457
|
- lib/fastlane/plugin/wpmreleasetoolkit/actions/common/upload_build_to_apps_cdn.rb
|
|
456
458
|
- lib/fastlane/plugin/wpmreleasetoolkit/actions/common/upload_to_s3.rb
|
|
@@ -479,6 +481,7 @@ files:
|
|
|
479
481
|
- lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_tools_path_helper.rb
|
|
480
482
|
- lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_version_helper.rb
|
|
481
483
|
- lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb
|
|
484
|
+
- lib/fastlane/plugin/wpmreleasetoolkit/helper/apps_cdn_helper.rb
|
|
482
485
|
- lib/fastlane/plugin/wpmreleasetoolkit/helper/buildkite_aware_log_groups.rb
|
|
483
486
|
- lib/fastlane/plugin/wpmreleasetoolkit/helper/ci_helper.rb
|
|
484
487
|
- lib/fastlane/plugin/wpmreleasetoolkit/helper/configure_helper.rb
|
|
@@ -517,6 +520,7 @@ files:
|
|
|
517
520
|
- lib/fastlane/plugin/wpmreleasetoolkit/versioning/files/android_version_file.rb
|
|
518
521
|
- lib/fastlane/plugin/wpmreleasetoolkit/versioning/files/ios_version_file.rb
|
|
519
522
|
- lib/fastlane/plugin/wpmreleasetoolkit/versioning/formatters/abstract_version_formatter.rb
|
|
523
|
+
- lib/fastlane/plugin/wpmreleasetoolkit/versioning/formatters/continuous_build_code_formatter.rb
|
|
520
524
|
- lib/fastlane/plugin/wpmreleasetoolkit/versioning/formatters/derived_build_code_formatter.rb
|
|
521
525
|
- lib/fastlane/plugin/wpmreleasetoolkit/versioning/formatters/four_part_build_code_formatter.rb
|
|
522
526
|
- lib/fastlane/plugin/wpmreleasetoolkit/versioning/formatters/four_part_version_formatter.rb
|