Skip to content

Commit 1cb6055

Browse files
iangmaiaclaude
andauthored
[Tooling] Upload CDN builds as Internal first, publish as External later (#2724)
## Related issues - Fixes AINFRA-2102 - Depends on wordpress-mobile/release-toolkit#701 — released in toolkit 14.7.0, bundled in this PR - Supersedes #2689 and #2695 ## How AI was used in this PR Claude Code implemented the Fastfile changes based on the approach designed in #2689 and refined through review of #2695, and assisted with rebasing and verification. All generated code was reviewed. ## Proposed Changes Today, production builds become publicly available on the Apps CDN as soon as `finalize_release` uploads them — before the Release Manager has a chance to verify them. This PR closes that gap by giving the team an internal testing window between finalize and publish: - `finalize_release` uploads Production builds with **Internal** visibility: downloadable by Automatticians for smoke-testing, but not served to users (the auto-update endpoint only serves External builds) - `publish_release` flips them to **External** (via the new `update_apps_cdn_build_metadata` toolkit action) right before publishing the GitHub release - Nightlies and Betas keep External visibility — they don't go through `publish_release`, so nothing would ever flip them - The CDN post IDs to flip are carried in the draft GitHub release body, so publish only touches builds from the latest finalization (an earlier draft-status approach, #2695, was abandoned because draft posts aren't easily testable by the RM) ## Testing Instructions - The visibility flips (single + batch) were verified end-to-end against the CDN sandbox site (AINFRA-2171) — see the test plan on wordpress-mobile/release-toolkit#701 - The full finalize → publish flow will be exercised during the next release cycle ## Pre-merge Checklist - [x] Have you checked for TypeScript, React or other console errors? - [x] release-toolkit#701 merged, released (14.7.0), and bundled in this PR 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 600021d commit 1cb6055

2 files changed

Lines changed: 99 additions & 7 deletions

File tree

Gemfile.lock

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,12 @@ GEM
132132
xcodeproj (>= 1.13.0, < 2.0.0)
133133
xcpretty (~> 0.4.1)
134134
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
135-
fastlane-plugin-wpmreleasetoolkit (14.6.0)
135+
fastlane-plugin-wpmreleasetoolkit (14.7.0)
136136
buildkit (~> 1.5)
137137
chroma (= 0.2.0)
138138
diffy (~> 3.3)
139139
dotenv (~> 2.8)
140-
fastlane (~> 2.231)
140+
fastlane (~> 2.235)
141141
gettext (~> 3.5)
142142
git (~> 1.3)
143143
google-cloud-storage (~> 1.31)
@@ -269,7 +269,7 @@ GEM
269269
rake (13.4.2)
270270
rake-compiler (1.3.1)
271271
rake
272-
rchardet (1.10.0)
272+
rchardet (1.10.2)
273273
regexp_parser (2.12.0)
274274
representable (3.2.0)
275275
declarative (< 0.1.0)

fastlane/Fastfile

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ fastlane_require 'zip'
55
fastlane_require 'aws-sdk-cloudfront'
66
fastlane_require 'json'
77
fastlane_require 'net/http'
8+
fastlane_require 'octokit'
89
fastlane_require 'uri'
910

1011
require_relative 'lib/studio_release_git'
@@ -427,7 +428,7 @@ lane :finalize_release do |version:, skip_confirm: false|
427428
- Download latest translations from GlotPress
428429
- Bump version to #{version} (remove beta suffix)
429430
- Trigger a release build for all platforms (macOS, Windows), which will then:
430-
- Upload build artifacts to the Apps CDN
431+
- Upload build artifacts to the Apps CDN (as Internal, visible only to Automatticians)
431432
- Create a draft GitHub release with release notes and download links
432433
- Notify #studio on Slack
433434
PROMPT
@@ -458,16 +459,23 @@ end
458459
# @param skip_confirm [Boolean] Skip interactive confirmation prompts (default: false)
459460
#
460461
lane :publish_release do |version:, skip_confirm: false, github_username: nil|
462+
# Required by update_apps_cdn_build_metadata to flip CDN visibility; check early for a clear failure
463+
get_required_env('WPCOM_API_TOKEN')
464+
461465
release_branch = "release/#{version}"
462466

463467
UI.important <<~PROMPT
464468
Publishing release #{version}. This will:
469+
- Make CDN builds public (change visibility from Internal to External)
465470
- Publish the draft GitHub release for v#{version}
466471
- Create a backmerge PR from `#{release_branch}` into `#{MAIN_BRANCH}`
467472
- Delete the `#{release_branch}` branch after creating the backmerge PR
468473
PROMPT
469474
next unless skip_confirm || UI.confirm('Continue?')
470475

476+
# Update CDN build visibility from Internal to External
477+
make_cdn_builds_public(version: version)
478+
471479
# Publish the draft GitHub release — this creates the release tag (e.g., v1.7.4)
472480
publish_github_release(
473481
repository: GITHUB_REPO,
@@ -611,6 +619,11 @@ def distribute_builds(
611619
appx_version = "#{version[/\d+\.\d+\.\d+/]}.0"
612620
release_notes = release_tag.nil? ? "Development build #{version}-#{build_number}" : "Release #{release_tag}"
613621

622+
# Only Production builds go through the finalize → publish flow where {make_cdn_builds_public}
623+
# flips them to External. Nightlies and Betas have no publish step, so uploading them as
624+
# Internal would leave them invisible to users (the updates endpoint only serves External builds).
625+
visibility = build_type == 'Production' ? :internal : :external
626+
614627
update_builds = {
615628
x64: {
616629
binary_path: File.join(BUILDS_FOLDER, 'Studio-darwin-x64', 'Studio.app.zip'),
@@ -726,15 +739,16 @@ def distribute_builds(
726739
arch: build[:arch],
727740
build_type: build_type,
728741
install_type: build[:install_type],
729-
visibility: 'external',
742+
visibility: visibility,
730743
version: version,
731744
build_number: build_number,
732745
release_notes: release_notes,
733746
sha: build[:sha],
734747
error_on_duplicate: false
735748
)
736-
# Store the download URL for later use
749+
# Store the download URL and post ID for later use
737750
build[:cdn_url] = result[:media_url]
751+
build[:post_id] = result[:post_id]
738752
end
739753

740754
unless DRY_RUN
@@ -799,7 +813,8 @@ def upload_file_to_apps_cdn(site_id:, product:, file_path:, platform:, arch:, bu
799813
UI.message(" error on duplicate: #{error_on_duplicate}")
800814

801815
return {
802-
media_url: media_url
816+
media_url: media_url,
817+
post_id: 0
803818
}
804819
end
805820

@@ -1203,6 +1218,18 @@ def create_draft_github_release(version:, release_tag:, builds:)
12031218
'The latest version is always available on the [WordPress Studio](https://developer.wordpress.com/studio/) site.'
12041219
end
12051220

1221+
# Embed CDN post IDs so publish_release can update their visibility later.
1222+
# Coerce to Integer defensively: the CDN API returns integer post IDs, but the
1223+
# update action requires Integers, so don't rely on the API's serialization.
1224+
cdn_post_ids = builds.values.filter_map { |b| b[:post_id]&.to_i }.select(&:positive?)
1225+
1226+
# Every build must have a post ID: builds missing from the embedded list would
1227+
# silently stay Internal after publish_release and never reach users.
1228+
missing = builds.values.reject { |b| b[:post_id]&.to_i&.positive? }.map { |b| b[:name] }
1229+
UI.user_error!("Missing CDN post IDs for: #{missing.join(', ')}. publish_release needs every build's post ID to flip its visibility to External.") unless missing.empty?
1230+
1231+
body += "\n<!-- CDN_POST_IDS:#{cdn_post_ids.join(',')} -->"
1232+
12061233
release_name = github_release_name(version:)
12071234
release_notes_path = File.join(PROJECT_ROOT_FOLDER, 'fastlane', 'github_release_notes.txt')
12081235
File.write(release_notes_path, body)
@@ -1222,6 +1249,71 @@ def create_draft_github_release(version:, release_tag:, builds:)
12221249
UI.success("Created draft GitHub release '#{release_name}' with download links")
12231250
end
12241251

1252+
# Make CDN builds public by updating their visibility from Internal to External.
1253+
#
1254+
# Reads CDN post IDs embedded in the draft GitHub release body by {create_draft_github_release},
1255+
# then calls {update_apps_cdn_build_metadata} for each to set visibility to External.
1256+
#
1257+
# @param version [String] The version to publish (e.g., '1.7.5')
1258+
#
1259+
def make_cdn_builds_public(version:)
1260+
release_name = github_release_name(version:)
1261+
post_ids = extract_cdn_post_ids_from_draft_release(release_name: release_name)
1262+
1263+
if post_ids.empty?
1264+
UI.user_error! <<~ERROR
1265+
No CDN post IDs found in draft release #{release_name}. Cannot publish without updating CDN visibility.
1266+
To recover manually, an Automattician can find the post IDs by running this from a proxied machine:
1267+
curl -H "Authorization: Bearer $WPCOM_API_TOKEN" \\
1268+
"https://public-api.wordpress.com/wp/v2/sites/#{WPCOM_STUDIO_SITE_ID}/a8c_cdn_build?search=#{version}&per_page=50" \\
1269+
| jq '[.[] | select(.class_list | index("visibility-internal")) | {id, slug}]'
1270+
(the class_list filter selects builds that are still Internal)
1271+
Then edit the draft GitHub release body to add a comment like:
1272+
<!-- CDN_POST_IDS:123,456,789 -->
1273+
and retry the publish_release lane or release tool task.
1274+
ERROR
1275+
end
1276+
1277+
UI.message("Updating CDN build visibility to External for #{post_ids.size} builds...")
1278+
1279+
update_apps_cdn_build_metadata(
1280+
site_id: WPCOM_STUDIO_SITE_ID,
1281+
post_ids: post_ids,
1282+
visibility: :external
1283+
)
1284+
1285+
UI.success('All CDN builds are now public (External visibility)')
1286+
end
1287+
1288+
# Extract CDN post IDs from the draft GitHub release body.
1289+
#
1290+
# The post IDs are embedded as an HTML comment by {create_draft_github_release}:
1291+
# <!-- CDN_POST_IDS:123,456,789 -->
1292+
#
1293+
# @param release_name [String] The release name as created by {create_draft_github_release},
1294+
# i.e. {github_release_name} (e.g., 'Version 1.7.5')
1295+
# @return [Array<Integer>] An array of post IDs, or empty array if not found
1296+
#
1297+
def extract_cdn_post_ids_from_draft_release(release_name:)
1298+
client = Octokit::Client.new(access_token: get_required_env('GITHUB_TOKEN'), auto_paginate: true)
1299+
releases = client.releases(GITHUB_REPO)
1300+
release = releases.find { |r| r.draft && r.name == release_name }
1301+
1302+
unless release
1303+
UI.important("No draft release found with name #{release_name}.")
1304+
return []
1305+
end
1306+
1307+
body = release.body || ''
1308+
match = body.match(/<!-- CDN_POST_IDS:([0-9,]+) -->/)
1309+
unless match
1310+
UI.important("Draft release #{release_name} found but no CDN post IDs embedded in the body.")
1311+
return []
1312+
end
1313+
1314+
match[1].split(',').map(&:to_i)
1315+
end
1316+
12251317
# Trigger a release build in Buildkite for the given version.
12261318
#
12271319
# Uses `buildkite_add_trigger_step` on CI (to create a separate build with proper Git mirroring)

0 commit comments

Comments
 (0)