Skip to content

Commit 05b48b5

Browse files
authored
Add update_apps_cdn_build_metadata action (#701)
2 parents 1c34595 + fe3967a commit 05b48b5

5 files changed

Lines changed: 547 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ _None_
1010

1111
### New Features
1212

13-
_None_
13+
- Added new `update_apps_cdn_build_metadata` action to update metadata (e.g. visibility) of one or more existing builds on the Apps CDN without re-uploading the files, via the dedicated `/wpcom/v2/sites/{site_id}/a8c-cdn/builds/{post_id}` endpoint. Accepts an array of `post_ids`. This enables a two-phase release flow: upload builds as Internal first, then flip to External at publish time. [#701]
1414

1515
### Bug Fixes
1616

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

lib/fastlane/plugin/wpmreleasetoolkit/actions/common/upload_build_to_apps_cdn.rb

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
require 'net/http'
55
require 'uri'
66
require 'json'
7+
require_relative '../../helper/apps_cdn_helper'
78

89
module Fastlane
910
module Actions
@@ -17,8 +18,6 @@ module SharedValues
1718
class UploadBuildToAppsCdnAction < Action
1819
# See https://github.a8c.com/Automattic/wpcom/blob/trunk/wp-content/lib/a8c/cdn/src/enums/enum-resource-type.php
1920
RESOURCE_TYPE = 'Build'
20-
# These are from the WordPress.com API, not the Apps CDN plugin
21-
VALID_POST_STATUS = %w[publish draft].freeze
2221
# See https://github.a8c.com/Automattic/wpcom/blob/trunk/wp-content/lib/a8c/cdn/src/enums/enum-build-type.php
2322
VALID_BUILD_TYPES = %w[
2423
Alpha
@@ -48,17 +47,13 @@ class UploadBuildToAppsCdnAction < Action
4847
'Full Install',
4948
'Update',
5049
].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-
5450
def self.run(params)
5551
UI.message('Uploading build to Apps CDN...')
5652

5753
file_path = params[:file_path]
5854
UI.user_error!("File not found at path '#{file_path}'") unless File.exist?(file_path)
5955

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')
6257

6358
# Create the request body and headers
6459
parameters = {
@@ -261,19 +256,15 @@ def self.available_options
261256
description: 'The visibility of the build (:internal or :external)',
262257
optional: false,
263258
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
267260
),
268261
FastlaneCore::ConfigItem.new(
269262
key: :post_status,
270263
description: 'The post status (defaults to \'publish\')',
271264
optional: true,
272265
default_value: 'publish',
273266
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
277268
),
278269
FastlaneCore::ConfigItem.new(
279270
key: :version,
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

0 commit comments

Comments
 (0)