fastlane-plugin-wpmreleasetoolkit 14.6.0 → 14.7.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: 787be90be7c62894c1cbee1a45d28d7815991f7158a654703f31c8db0d541f61
4
- data.tar.gz: 8f6312bdc9f3ecd5f9fbc17a78d22b91483cd7ee7d4e4fa511b9bb367a26c08c
3
+ metadata.gz: c3a14d01dbfc81f832b5d1b4c009e16c2b78ff148b3c25c592a1bc32c5e8d442
4
+ data.tar.gz: 8fbb44705e3d5799cb21857647c83f8321b9494548dc0342eb093781f707538d
5
5
  SHA512:
6
- metadata.gz: f23b5d15c71550aa20af5536f2f6517a780f4cc2a0a286e21316a431e803bcd29e66dd2e93432d1998ebb922ff72b80f0fbeaabcbf5472fb1dbf3dd600900134
7
- data.tar.gz: a69a5537a36109cdb5442c3a0c302ccc0bc8702c6380c6ac68c4c9a7beb95d0777996f53505de54f4142a4fd1f9cd6e1637d726c917a751f43529d9427825fe9
6
+ metadata.gz: 73ee821d6f4d161a1479bedd639f7427c6022cd17551dce93d8d87cabf47d95513a9595f8aeb02523b60ada7d52141fb82bbbbbbb9538b0b9aaa2dd9d7e681ef
7
+ data.tar.gz: 4dee3516eb30a8bbbc97ef67568f97d541452a44dd56a16080606bbdbe790fe816890108ca97a843ee8af511fb307d39dd19f7999cb5429d2d0ee4a630c44afd
@@ -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-4o'
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? || tools.empty?
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
- max_tool_iterations.times do
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
- UI.user_error!(
83
- "OpenAI tool-use loop did not terminate after #{max_tool_iterations} iterations. " \
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
- max_tokens: 2048,
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
- max_tokens: 2048,
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
- name = tool_call.dig('function', 'name')
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
- # raw arguments locally for debugging without forwarding them to the API.
155
- UI.error("Invalid JSON arguments for tool '#{name}'. Raw payload: #{raw_args}")
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}: #{e.message}. Result class: #{result.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 full message and backtrace
189
- # are logged locally via `UI.error` but NOT forwarded to the model, because tool
190
- # results are sent to OpenAI and handler exception messages can contain secrets
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}: #{e.message}\n#{e.backtrace&.first(5)&.join("\n")}")
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 when `max_tool_iterations` is reached.
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-4o`, `gpt-4o-mini`, `gpt-4.1`). ' \
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 tool (function-calling) definitions in OpenAI format. ' \
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
- UI.user_error!('Parameter `tools` must be a non-empty Array when provided') if value.empty?
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-use loop iterations before the action fails. ' \
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
- UI.user_error!("Parameter `max_tool_iterations` must be >= 1 (got #{value})") if value < 1
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
- api_endpoint = "https://public-api.wordpress.com/rest/v1.1/sites/#{params[:site_id]}/media/new"
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: proc do |value|
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: proc do |value|
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 recieve "behind 2" or "ahead 6" and return 2 or 6, respectively.
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 commited and push to the repo
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
@@ -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 dependant on where the action is run from.
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
@@ -3,6 +3,6 @@
3
3
  module Fastlane
4
4
  module Wpmreleasetoolkit
5
5
  NAME = 'fastlane-plugin-wpmreleasetoolkit'
6
- VERSION = '14.6.0'
6
+ VERSION = '14.7.0'
7
7
  end
8
8
  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.6.0
4
+ version: 14.7.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-05-13 00:00:00.000000000 Z
11
+ date: 2026-06-12 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.231'
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.231'
82
+ version: '2.235'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: gettext
85
85
  requirement: !ruby/object:Gem::Requirement
@@ -451,6 +451,7 @@ files:
451
451
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/common/remove_branch_protection_action.rb
452
452
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/common/set_branch_protection_action.rb
453
453
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/common/set_milestone_frozen_marker_action.rb
454
+ - lib/fastlane/plugin/wpmreleasetoolkit/actions/common/update_apps_cdn_build_metadata.rb
454
455
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/common/update_assigned_milestone_action.rb
455
456
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/common/upload_build_to_apps_cdn.rb
456
457
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/common/upload_to_s3.rb
@@ -479,6 +480,7 @@ files:
479
480
  - lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_tools_path_helper.rb
480
481
  - lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_version_helper.rb
481
482
  - lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb
483
+ - lib/fastlane/plugin/wpmreleasetoolkit/helper/apps_cdn_helper.rb
482
484
  - lib/fastlane/plugin/wpmreleasetoolkit/helper/buildkite_aware_log_groups.rb
483
485
  - lib/fastlane/plugin/wpmreleasetoolkit/helper/ci_helper.rb
484
486
  - lib/fastlane/plugin/wpmreleasetoolkit/helper/configure_helper.rb