Skip to content

Commit 5ca7866

Browse files
iangmaiaclaude
andcommitted
Switch update_apps_cdn_build_metadata to the dedicated a8c-cdn/builds endpoint
Standard WP REST API writes are now blocked for CDN builds; the new /wpcom/v2/sites/{site_id}/a8c-cdn/builds/{post_id} endpoint accepts string-based values, removing the need for visibility term ID lookups. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent afaf20d commit 5ca7866

5 files changed

Lines changed: 44 additions & 115 deletions

File tree

.bundle/config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ BUNDLE_PATH: "vendor/bundle"
33
BUNDLE_FORCE_RUBY_PLATFORM: "true"
44
BUNDLE_SPECIFIC_PLATFORM: "false"
55
BUNDLE_JOBS: "16"
6+
BUNDLE_BUILD__NOKOGIRI: "--with-cflags=-Wno-default-const-init-field-unsafe"

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-
- 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. Accepts an array of `post_ids` and performs the visibility term lookup only once. This enables a two-phase release flow: upload builds as Internal first, then flip to External at publish time. [#701]
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

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

Lines changed: 10 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,11 @@ def self.run(params)
1212
post_ids = params[:post_ids]
1313
UI.message("Updating Apps CDN build metadata for #{post_ids.size} post(s): #{post_ids.join(', ')}...")
1414

15-
# Build the base JSON body for the WP REST API v2
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'`)
1617
body = {}
17-
body['status'] = params[:post_status] if params[:post_status]
18-
19-
if params[:visibility]
20-
term_id = lookup_visibility_term_id(site_id: params[:site_id], api_token: params[:api_token], visibility: params[:visibility])
21-
body['visibility'] = [term_id]
22-
end
18+
body['post_status'] = params[:post_status] if params[:post_status]
19+
body['visibility'] = params[:visibility].to_s.capitalize if params[:visibility]
2320

2421
UI.user_error!('No metadata to update. Provide at least one of: visibility, post_status') if body.empty?
2522

@@ -31,16 +28,17 @@ def self.run(params)
3128
results
3229
end
3330

34-
# Update a single CDN build post with the given body via the WP REST API v2.
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.
3533
#
3634
# @param site_id [String] the WordPress.com site ID
3735
# @param api_token [String] the WordPress.com API bearer token
38-
# @param post_id [Integer] the ID of the post to update
36+
# @param post_id [Integer] the ID of the build post to update
3937
# @param body [Hash] the JSON body to send in the POST request
4038
# @return [Integer] the ID of the updated post
4139
# @raise [FastlaneCore::Interface::FastlaneError] if the API returns a non-success response
4240
def self.update_single_post(site_id:, api_token:, post_id:, body:)
43-
uri = Helper::AppsCdnHelper.wp_v2_url(site_id: site_id, path: "a8c_cdn_build/#{post_id}")
41+
uri = Helper::AppsCdnHelper.wpcom_v2_url(site_id: site_id, path: "a8c-cdn/builds/#{post_id}")
4442

4543
request = Net::HTTP::Post.new(uri.request_uri)
4644
request.body = JSON.generate(body)
@@ -69,37 +67,6 @@ def self.update_single_post(site_id:, api_token:, post_id:, body:)
6967
end
7068
end
7169

72-
# Look up the taxonomy term ID for a visibility value (e.g. :internal -> 1316).
73-
#
74-
# @param site_id [String] the WordPress.com site ID
75-
# @param api_token [String] the WordPress.com API bearer token
76-
# @param visibility [Symbol] the visibility to look up (:internal or :external)
77-
# @return [Integer] the taxonomy term ID for the given visibility
78-
# @raise [FastlaneCore::Interface::FastlaneError] if no term is found or the API returns a non-success response
79-
def self.lookup_visibility_term_id(site_id:, api_token:, visibility:)
80-
slug = visibility.to_s.downcase
81-
uri = Helper::AppsCdnHelper.wp_v2_url(site_id: site_id, path: "visibility?slug=#{slug}")
82-
83-
request = Net::HTTP::Get.new(uri.request_uri)
84-
request['Accept'] = 'application/json'
85-
request['Authorization'] = "Bearer #{api_token}"
86-
87-
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
88-
http.open_timeout = 10
89-
http.read_timeout = 30
90-
http.request(request)
91-
end
92-
93-
case response
94-
when Net::HTTPSuccess
95-
terms = JSON.parse(response.body)
96-
UI.user_error!("No visibility term found for '#{slug}'") if terms.empty?
97-
terms.first['id']
98-
else
99-
UI.user_error!("Failed to look up visibility term '#{slug}': #{response.code} #{response.message}")
100-
end
101-
end
102-
10370
def self.description
10471
'Updates metadata of one or more existing builds on the Apps CDN'
10572
end
@@ -115,8 +82,8 @@ def self.return_value
11582
def self.details
11683
<<~DETAILS
11784
Updates metadata (such as post status or visibility) for one or more existing build posts on a WordPress blog
118-
that has the Apps CDN plugin enabled, using the WordPress.com REST API (WP v2).
119-
When updating visibility for multiple posts, the visibility term ID is looked up only once.
85+
that has the Apps CDN plugin enabled, using the dedicated `/wpcom/v2/sites/{site_id}/a8c-cdn/builds/{post_id}`
86+
endpoint. Standard WP REST API writes are blocked for builds, so this endpoint is the only way to update them.
12087
See PCYsg-15tP-p2 internal a8c documentation for details about the Apps CDN plugin.
12188
DETAILS
12289
end

lib/fastlane/plugin/wpmreleasetoolkit/helper/apps_cdn_helper.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ def self.rest_v1_1_url(site_id:, path:)
2222
URI.parse("#{API_BASE_URL}/rest/v1.1/sites/#{site_id}/#{path}")
2323
end
2424

25-
# Builds a WordPress.com WP REST API v2 URI scoped to a site.
25+
# Builds a WordPress.com REST API wpcom/v2 URI scoped to a site.
2626
#
2727
# @param site_id [String] the WordPress.com site ID
28-
# @param path [String] the API path relative to the site (e.g. 'a8c_cdn_build/123')
28+
# @param path [String] the API path relative to the site (e.g. 'a8c-cdn/builds/123')
2929
# @return [URI] the parsed full API URI
30-
def self.wp_v2_url(site_id:, path:)
31-
URI.parse("#{API_BASE_URL}/wp/v2/sites/#{site_id}/#{path}")
30+
def self.wpcom_v2_url(site_id:, path:)
31+
URI.parse("#{API_BASE_URL}/wpcom/v2/sites/#{site_id}/#{path}")
3232
end
3333

3434
# Returns a proc that validates a visibility parameter value against {VALID_VISIBILITIES}.

spec/update_apps_cdn_build_metadata_spec.rb

Lines changed: 28 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,17 @@
77
let(:test_site_id) { '12345678' }
88
let(:test_post_id) { 98_765 }
99
let(:second_post_id) { 54_321 }
10-
let(:api_url) { "https://public-api.wordpress.com/wp/v2/sites/#{test_site_id}/a8c_cdn_build/#{test_post_id}" }
11-
let(:second_api_url) { "https://public-api.wordpress.com/wp/v2/sites/#{test_site_id}/a8c_cdn_build/#{second_post_id}" }
12-
let(:visibility_term_url) { "https://public-api.wordpress.com/wp/v2/sites/#{test_site_id}/visibility" }
10+
let(:api_url) { "https://public-api.wordpress.com/wpcom/v2/sites/#{test_site_id}/a8c-cdn/builds/#{test_post_id}" }
11+
let(:second_api_url) { "https://public-api.wordpress.com/wpcom/v2/sites/#{test_site_id}/a8c-cdn/builds/#{second_post_id}" }
1312
let(:test_api_token) { 'test_api_token' }
1413

15-
let(:external_term_id) { 21_293 }
16-
let(:internal_term_id) { 1_316 }
17-
1814
let(:stub_success_response) do
1915
{
2016
id: test_post_id,
21-
title: { rendered: 'WordPress.com Studio 1.7.5' },
22-
status: 'publish',
23-
visibility: [external_term_id],
24-
class_list: ['visibility-external']
17+
post_status: 'publish',
18+
release_notes: 'Release 1.7.5',
19+
visibility: 'External',
20+
product: 'WordPress.com Studio'
2521
}.to_json
2622
end
2723

@@ -31,14 +27,6 @@
3127

3228
describe 'updating visibility' do
3329
it 'successfully updates the visibility to external' do
34-
stub_request(:get, visibility_term_url)
35-
.with(query: { 'slug' => 'external' })
36-
.to_return(
37-
status: 200,
38-
body: [{ 'id' => external_term_id, 'name' => 'External', 'slug' => 'external' }].to_json,
39-
headers: { 'Content-Type' => 'application/json' }
40-
)
41-
4230
stub_request(:post, api_url)
4331
.to_return(
4432
status: 200,
@@ -62,25 +50,17 @@
6250
expect(req.headers['Authorization']).to eq("Bearer #{test_api_token}")
6351
expect(req.headers['Content-Type']).to eq('application/json')
6452
body = JSON.parse(req.body)
65-
expect(body['visibility']).to eq([external_term_id])
53+
expect(body['visibility']).to eq('External')
6654
true
6755
end
6856
)
6957
end
7058

7159
it 'successfully updates the visibility to internal' do
72-
stub_request(:get, visibility_term_url)
73-
.with(query: { 'slug' => 'internal' })
74-
.to_return(
75-
status: 200,
76-
body: [{ 'id' => internal_term_id, 'name' => 'Internal', 'slug' => 'internal' }].to_json,
77-
headers: { 'Content-Type' => 'application/json' }
78-
)
79-
8060
internal_response = {
8161
id: test_post_id,
82-
visibility: [internal_term_id],
83-
class_list: ['visibility-internal']
62+
post_status: 'publish',
63+
visibility: 'Internal'
8464
}.to_json
8565

8666
stub_request(:post, api_url)
@@ -102,23 +82,15 @@
10282
expect(WebMock).to(
10383
have_requested(:post, api_url).with do |req|
10484
body = JSON.parse(req.body)
105-
expect(body['visibility']).to eq([internal_term_id])
85+
expect(body['visibility']).to eq('Internal')
10686
true
10787
end
10888
)
10989
end
11090
end
11191

11292
describe 'batch updating multiple posts' do
113-
it 'updates all posts with a single visibility term lookup' do
114-
stub_request(:get, visibility_term_url)
115-
.with(query: { 'slug' => 'external' })
116-
.to_return(
117-
status: 200,
118-
body: [{ 'id' => external_term_id, 'name' => 'External', 'slug' => 'external' }].to_json,
119-
headers: { 'Content-Type' => 'application/json' }
120-
)
121-
93+
it 'updates each post with a single request' do
12294
stub_request(:post, api_url)
12395
.to_return(
12496
status: 200,
@@ -143,8 +115,6 @@
143115
expect(results.size).to eq(2)
144116
expect(results).to eq([test_post_id, second_post_id])
145117

146-
# Visibility term lookup should have been called only once
147-
expect(WebMock).to have_requested(:get, visibility_term_url).with(query: { 'slug' => 'external' }).once
148118
expect(WebMock).to have_requested(:post, api_url).once
149119
expect(WebMock).to have_requested(:post, second_api_url).once
150120
end
@@ -155,7 +125,7 @@
155125
stub_request(:post, api_url)
156126
.to_return(
157127
status: 200,
158-
body: { id: test_post_id, status: 'draft' }.to_json,
128+
body: { id: test_post_id, post_status: 'draft' }.to_json,
159129
headers: { 'Content-Type' => 'application/json' }
160130
)
161131

@@ -171,7 +141,7 @@
171141
expect(WebMock).to(
172142
have_requested(:post, api_url).with do |req|
173143
body = JSON.parse(req.body)
174-
expect(body['status']).to eq('draft')
144+
expect(body['post_status']).to eq('draft')
175145
true
176146
end
177147
)
@@ -180,14 +150,6 @@
180150

181151
describe 'updating multiple fields' do
182152
it 'successfully updates both visibility and post_status' do
183-
stub_request(:get, visibility_term_url)
184-
.with(query: { 'slug' => 'external' })
185-
.to_return(
186-
status: 200,
187-
body: [{ 'id' => external_term_id, 'name' => 'External', 'slug' => 'external' }].to_json,
188-
headers: { 'Content-Type' => 'application/json' }
189-
)
190-
191153
stub_request(:post, api_url)
192154
.to_return(
193155
status: 200,
@@ -208,8 +170,8 @@
208170
expect(WebMock).to(
209171
have_requested(:post, api_url).with do |req|
210172
body = JSON.parse(req.body)
211-
expect(body['visibility']).to eq([external_term_id])
212-
expect(body['status']).to eq('publish')
173+
expect(body['visibility']).to eq('External')
174+
expect(body['post_status']).to eq('publish')
213175
true
214176
end
215177
)
@@ -221,7 +183,7 @@
221183
stub_request(:post, api_url)
222184
.to_return(
223185
status: 403,
224-
body: { code: 'rest_forbidden', message: 'You are not authorized.' }.to_json,
186+
body: { code: 'rest_forbidden', message: 'You do not have permission to update builds.' }.to_json,
225187
headers: { 'Content-Type' => 'application/json' }
226188
)
227189

@@ -235,41 +197,40 @@
235197
end.to raise_error(FastlaneCore::Interface::FastlaneError, "Update of Apps CDN build metadata failed for post #{test_post_id}")
236198
end
237199

238-
it 'handles server errors properly' do
200+
it 'handles a non-existent build post properly' do
239201
stub_request(:post, api_url)
240202
.to_return(
241-
status: 500,
242-
body: 'Internal Server Error',
243-
headers: { 'Content-Type' => 'text/plain' }
203+
status: 404,
204+
body: { code: 'rest_build_not_found', message: 'Build not found.' }.to_json,
205+
headers: { 'Content-Type' => 'application/json' }
244206
)
245207

246208
expect do
247209
run_described_fastlane_action(
248210
site_id: test_site_id,
249211
api_token: test_api_token,
250212
post_ids: [test_post_id],
251-
post_status: 'publish'
213+
visibility: :external
252214
)
253215
end.to raise_error(FastlaneCore::Interface::FastlaneError, "Update of Apps CDN build metadata failed for post #{test_post_id}")
254216
end
255217

256-
it 'handles visibility term lookup failure' do
257-
stub_request(:get, visibility_term_url)
258-
.with(query: { 'slug' => 'external' })
218+
it 'handles server errors properly' do
219+
stub_request(:post, api_url)
259220
.to_return(
260-
status: 200,
261-
body: [].to_json,
262-
headers: { 'Content-Type' => 'application/json' }
221+
status: 500,
222+
body: 'Internal Server Error',
223+
headers: { 'Content-Type' => 'text/plain' }
263224
)
264225

265226
expect do
266227
run_described_fastlane_action(
267228
site_id: test_site_id,
268229
api_token: test_api_token,
269230
post_ids: [test_post_id],
270-
visibility: :external
231+
post_status: 'publish'
271232
)
272-
end.to raise_error(FastlaneCore::Interface::FastlaneError, "No visibility term found for 'external'")
233+
end.to raise_error(FastlaneCore::Interface::FastlaneError, "Update of Apps CDN build metadata failed for post #{test_post_id}")
273234
end
274235
end
275236

0 commit comments

Comments
 (0)