Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ _None_

### New Features

_None_
- Added new `update_apps_cdn_build_metadata` action to update metadata (e.g. visibility) of an existing build on the Apps CDN without re-uploading the file. This enables a two-phase release flow: upload builds as Internal first, then flip to External at publish time. [#701]

### Bug Fixes

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
# frozen_string_literal: true

require 'fastlane/action'
require 'net/http'
require 'uri'
require 'json'

module Fastlane
module Actions
class UpdateAppsCdnBuildMetadataAction < Action
VALID_VISIBILITIES = %i[internal external].freeze
VALID_POST_STATUS = %w[publish draft].freeze
Comment thread
AliSoftware marked this conversation as resolved.
Outdated

def self.run(params)
UI.message("Updating Apps CDN build metadata for post #{params[:post_id]}...")

# Build the JSON body for the WP REST API v2
body = {}
body['status'] = params[:post_status] if params[:post_status]

if params[:visibility]
term_id = lookup_visibility_term_id(site_id: params[:site_id], api_token: params[:api_token], visibility: params[:visibility])
body['visibility'] = [term_id]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably more of a question for Hannah but it feels surprising to me that the API would technically allow an array if values for this metadata instead of a single one 🤔

Like, what would happen at the API level if we called the API with {"visibility":[1345, 1367]} body (where, say, 1345 is internal and 1367 is external, for example) 😅

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just double checked using the API. It indeed accepts multiple term IDs and assigns both:

POST /wp/v2/sites/{site_id}/a8c_cdn_build/737
{"visibility": [1316, 21293]}

→ 200 OK
{"id": 737, "visibility": [21293, 1316], ...}

So a post would end up being both "internal" and "external" at the same time 😅 it just treats visibility as a taxonomy that supports multiple terms.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting! I wonder why we didn't have to pass an array but just a string in the API call that creates the build in upload_build_to_apps_cdn then 🤔 🤷

Also, this doesn't match the table in the FieldGuide documentation (interal ref: PCYsg-15tP-p2#How-to-upload-a-build-to-a-CDN-blog), so I guess one of the 2 should probably be updated.
I'd vote for only updating the AppsCDN plugin for accepting only one visibility (both in the API call and on the visibility metadata stored for a post taxonomy), but I'm not sure if that's technically feasible/easy (vs if this is an inherent technical limitation of how taxonomies work in WordPress? cc @hannahtinkler).

Anyway, I think ultimately it's not really critical (given the way we use the API and this visibility taxonomy we always set only one value in practice in all our call sites); as long as we're sure that calling update_build_to_apps_cdn with a one-item array [term_id] on a post that already had a different value set for visibility indeed replaces the visibility of that post with a single visibility value of term_id—as opposed to adding it to the existing ones.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I didn’t realise how annoying uploading via that endpoint would be with having to use the internal IDs, pass arrays etc. I looked at customising the update functionality for this post type, but it got complex to the point it seemed better to add a dedicated upload endpoint - you can find the docs for that here: PCYsg-15tP-p2 🙂

I also blocked edits via the standard REST post endpoints so the validation can be enforced - this should all be available in prod now! FYI, I also added a test site so we don't need to clean up testing calls in the prod CDN sites - you can find the details for that in AINFRA-2171.

end

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

api_endpoint = "https://public-api.wordpress.com/wp/v2/sites/#{params[:site_id]}/a8c_cdn_build/#{params[:post_id]}"
uri = URI.parse(api_endpoint)

# Create and send the HTTP request
request = Net::HTTP::Post.new(uri.request_uri)
request.body = JSON.generate(body)
request['Content-Type'] = 'application/json'
request['Accept'] = 'application/json'
request['Authorization'] = "Bearer #{params[:api_token]}"

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
Comment thread
iangmaia marked this conversation as resolved.
http.open_timeout = 10
http.read_timeout = 30
http.request(request)
end

# Handle the response
case response
when Net::HTTPSuccess
result = JSON.parse(response.body)
post_id = result['id']

UI.success("Successfully updated Apps CDN build metadata for post #{post_id}")

{ post_id: post_id }
else
UI.error("Failed to update Apps CDN build metadata: #{response.code} #{response.message}")
UI.error(response.body)
UI.user_error!('Update of Apps CDN build metadata failed')
end
end

# Look up the taxonomy term ID for a visibility value (e.g. :internal -> 1316)
def self.lookup_visibility_term_id(site_id:, api_token:, visibility:)
slug = visibility.to_s.downcase
api_endpoint = "https://public-api.wordpress.com/wp/v2/sites/#{site_id}/visibility?slug=#{slug}"
Comment thread
AliSoftware marked this conversation as resolved.
Outdated
uri = URI.parse(api_endpoint)

request = Net::HTTP::Get.new(uri.request_uri)
request['Accept'] = 'application/json'
request['Authorization'] = "Bearer #{api_token}"

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
http.open_timeout = 10
http.read_timeout = 30
http.request(request)
end
Comment thread
AliSoftware marked this conversation as resolved.

case response
when Net::HTTPSuccess
terms = JSON.parse(response.body)
UI.user_error!("No visibility term found for '#{slug}'") if terms.empty?
terms.first['id']
else
UI.user_error!("Failed to look up visibility term '#{slug}': #{response.code} #{response.message}")
end
end

def self.description
'Updates metadata of an existing build on the Apps CDN'
end

def self.authors
['Automattic']
end

def self.return_value
'Returns a Hash containing { post_id: }. On error, raises a FastlaneError.'
end

def self.details
<<~DETAILS
Updates metadata (such as post status or visibility) for an existing build post on a WordPress blog
that has the Apps CDN plugin enabled, using the WordPress.com REST API (WP v2).
See PCYsg-15tP-p2 internal a8c documentation for details about the Apps CDN plugin.
DETAILS
end

def self.available_options
[
FastlaneCore::ConfigItem.new(
key: :site_id,
env_name: 'APPS_CDN_SITE_ID',
description: 'The WordPress.com CDN site ID where the build was uploaded',
optional: false,
type: String,
verify_block: proc do |value|
UI.user_error!('Site ID cannot be empty') if value.to_s.empty?
end
),
FastlaneCore::ConfigItem.new(
key: :post_id,
description: 'The ID of the build post to update',
optional: false,
type: Integer,
verify_block: proc do |value|
UI.user_error!('Post ID must be a positive integer') unless value.is_a?(Integer) && value.positive?
end
),
FastlaneCore::ConfigItem.new(
key: :api_token,
env_name: 'WPCOM_API_TOKEN',
description: 'The WordPress.com API token for authentication',
optional: false,
type: String,
verify_block: proc do |value|
UI.user_error!('API token cannot be empty') if value.to_s.empty?
end
),
FastlaneCore::ConfigItem.new(
key: :visibility,
description: 'The new visibility for the build (:internal or :external)',
optional: true,
type: Symbol,
verify_block: proc do |value|
UI.user_error!("Visibility must be one of: #{VALID_VISIBILITIES.map { "`:#{_1}`" }.join(', ')}") unless VALID_VISIBILITIES.include?(value.to_s.downcase.to_sym)
end
),
FastlaneCore::ConfigItem.new(
key: :post_status,
description: "The new post status ('publish' or 'draft')",
optional: true,
type: String,
verify_block: proc do |value|
UI.user_error!("Post status must be one of: #{VALID_POST_STATUS.join(', ')}") unless VALID_POST_STATUS.include?(value)
end
),
Comment thread
AliSoftware marked this conversation as resolved.
]
end

def self.is_supported?(platform)
true
end

def self.example_code
[
'update_apps_cdn_build_metadata(
site_id: "12345678",
api_token: ENV["WPCOM_API_TOKEN"],
post_id: 98765,
post_status: "publish"
)',
'update_apps_cdn_build_metadata(
site_id: "12345678",
api_token: ENV["WPCOM_API_TOKEN"],
post_id: 98765,
visibility: :internal,
post_status: "draft"
)',
]
end
end
end
end
Loading