diff --git a/.buildkite/commands/post-process-binary-for-distribution.sh b/.buildkite/commands/post-process-binary-for-distribution.sh deleted file mode 100755 index 855dc518ff..0000000000 --- a/.buildkite/commands/post-process-binary-for-distribution.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -eu - -# Prepares an existing binary for distribution and generates its corresponding manifest. - -echo "--- :node: Generate Releases Manifest" -node ./scripts/generate-releases-manifest.mjs diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 74a219ea04..780f6f8ad5 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -152,13 +152,10 @@ steps: echo "--- :node: Generating Release Manifest" node ./scripts/prepare-dev-build-version.mjs - node ./scripts/generate-releases-manifest.mjs echo "--- :fastlane: Distributing Dev Builds" install_gems bundle exec fastlane distribute_dev_build - artifact_paths: - - out/releases.json agents: queue: mac # Using concurrency_group to ensure the CI builds from `trunk` & the git tag, which are likely to run at roughly the @@ -256,14 +253,9 @@ steps: .buildkite/commands/install-node-dependencies.sh - echo "--- :node: Generating Release Manifest" - node ./scripts/generate-releases-manifest.mjs - echo "--- :fastlane: Distributing Release Builds" install_gems bundle exec fastlane distribute_release_build - artifact_paths: - - out/releases.json agents: queue: mac # Using concurrency_group to ensure the CI builds from `trunk` & the git tag, which are likely to run at roughly the diff --git a/Gemfile b/Gemfile index 4c3e29c663..52c4a21fc3 100644 --- a/Gemfile +++ b/Gemfile @@ -3,7 +3,7 @@ source 'https://rubygems.org' gem 'fastlane', '~> 2.212' -gem 'fastlane-plugin-wpmreleasetoolkit', '~> 12.0' +gem 'fastlane-plugin-wpmreleasetoolkit', '~> 13.7' gem 'aws-sdk-cloudfront', '~> 1.87' diff --git a/Gemfile.lock b/Gemfile.lock index 7d9256f5e8..b9148a1e70 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,14 +5,14 @@ GEM base64 nkf rexml - activesupport (8.0.2) + activesupport (8.1.1) base64 - benchmark (>= 0.3) bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) + json logger (>= 1.4.2) minitest (>= 5.1) securerandom (>= 0.3) @@ -23,29 +23,31 @@ GEM artifactory (3.0.17) ast (2.4.2) atomos (0.1.3) - aws-eventstream (1.3.0) - aws-partitions (1.970.0) + aws-eventstream (1.4.0) + aws-partitions (1.1179.0) aws-sdk-cloudfront (1.96.0) aws-sdk-core (~> 3, >= 3.201.0) aws-sigv4 (~> 1.5) - aws-sdk-core (3.202.2) + aws-sdk-core (3.236.0) aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.651.0) + aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) + base64 + bigdecimal jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.88.0) - aws-sdk-core (~> 3, >= 3.201.0) + logger + aws-sdk-kms (1.116.0) + aws-sdk-core (~> 3, >= 3.234.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.159.0) - aws-sdk-core (~> 3, >= 3.201.0) + aws-sdk-s3 (1.202.0) + aws-sdk-core (~> 3, >= 3.234.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) - aws-sigv4 (1.9.1) + aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) - base64 (0.2.0) - benchmark (0.4.0) - bigdecimal (3.1.9) + base64 (0.3.0) + bigdecimal (3.3.1) buildkit (1.6.1) sawyer (>= 0.6) chroma (0.2.0) @@ -55,17 +57,17 @@ GEM commander (4.6.0) highline (~> 2.0.0) concurrent-ruby (1.3.5) - connection_pool (2.5.0) + connection_pool (2.5.4) declarative (0.0.20) - diffy (3.4.3) - digest-crc (0.6.5) + diffy (3.4.4) + digest-crc (0.7.0) rake (>= 12.0.0, < 14.0.0) domain_name (0.6.20240107) dotenv (2.8.1) - drb (2.2.1) + drb (2.2.3) emoji_regex (3.2.3) - excon (0.111.0) - faraday (1.10.3) + excon (0.112.0) + faraday (1.10.4) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -81,20 +83,20 @@ GEM faraday (>= 0.8.0) http-cookie (~> 1.0.0) faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.0) + faraday-em_synchrony (1.0.1) faraday-excon (1.1.0) faraday-httpclient (1.0.1) - faraday-multipart (1.0.4) - multipart-post (~> 2) + faraday-multipart (1.1.1) + multipart-post (~> 2.0) faraday-net_http (1.0.2) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) faraday-retry (1.0.3) - faraday_middleware (1.2.0) + faraday_middleware (1.2.1) faraday (~> 1.0) - fastimage (2.3.1) - fastlane (2.222.0) + fastimage (2.4.0) + fastlane (2.228.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -110,6 +112,7 @@ GEM faraday-cookie_jar (~> 0.0.6) faraday_middleware (~> 1.0) fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.0.0) gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) @@ -133,9 +136,9 @@ GEM tty-spinner (>= 0.8.0, < 1.0.0) word_wrap (~> 1.0.0) xcodeproj (>= 1.13.0, < 2.0.0) - xcpretty (~> 0.3.0) + xcpretty (~> 0.4.1) xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) - fastlane-plugin-wpmreleasetoolkit (12.5.0) + fastlane-plugin-wpmreleasetoolkit (13.7.0) activesupport (>= 6.1.7.1) buildkit (~> 1.5) chroma (= 0.2.0) @@ -152,6 +155,8 @@ GEM rake (>= 12.3, < 14.0) rake-compiler (~> 1.0) xcodeproj (~> 1.22) + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) gh_inspector (1.1.3) git (1.19.1) addressable (~> 2.8) @@ -172,12 +177,12 @@ GEM google-apis-core (>= 0.11.0, < 2.a) google-apis-storage_v1 (0.31.0) google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.7.1) + google-cloud-core (1.8.0) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.4.0) + google-cloud-errors (1.5.0) google-cloud-storage (1.47.0) addressable (~> 2.8) digest-crc (~> 0.4) @@ -193,61 +198,63 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.7) + http-cookie (1.0.8) domain_name (~> 0.5) - httpclient (2.8.3) + httpclient (2.9.0) + mutex_m i18n (1.14.7) concurrent-ruby (~> 1.0) java-properties (0.3.0) jmespath (1.6.2) - json (2.7.2) - jwt (2.8.2) + json (2.15.2) + jwt (2.10.2) base64 language_server-protocol (3.17.0.3) - logger (1.6.6) + logger (1.7.0) mini_magick (4.13.2) mini_mime (1.1.5) - minitest (5.25.5) - multi_json (1.15.0) + minitest (5.26.0) + multi_json (1.17.0) multipart-post (2.4.1) - nanaimo (0.3.0) - naturally (2.2.1) + mutex_m (0.3.0) + nanaimo (0.4.0) + naturally (2.3.0) nkf (0.2.0) - nokogiri (1.18.9-aarch64-linux-gnu) + nokogiri (1.18.10-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.18.9-arm64-darwin) + nokogiri (1.18.10-arm64-darwin) racc (~> 1.4) - nokogiri (1.18.9-x86_64-linux-gnu) + nokogiri (1.18.10-x86_64-linux-gnu) racc (~> 1.4) octokit (6.1.1) faraday (>= 1, < 3) sawyer (~> 0.9) options (2.3.2) - optparse (0.5.0) + optparse (0.8.0) os (1.1.4) - parallel (1.26.3) + parallel (1.27.0) parser (3.3.4.2) ast (~> 2.4.1) racc - plist (3.7.1) + plist (3.7.2) progress_bar (1.3.4) highline (>= 1.6) options (~> 2.3.0) - public_suffix (6.0.1) + public_suffix (6.0.2) racc (1.8.1) rainbow (3.1.1) - rake (13.2.1) - rake-compiler (1.2.9) + rake (13.3.1) + rake-compiler (1.3.0) rake - rchardet (1.9.0) + rchardet (1.10.0) regexp_parser (2.9.2) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.4.2) - rouge (2.0.7) + rexml (3.4.4) + rouge (3.28.0) rubocop (1.66.0) json (~> 2.3) language_server-protocol (>= 3.17.0) @@ -262,20 +269,21 @@ GEM parser (>= 3.3.1.0) ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) - rubyzip (2.3.2) - sawyer (0.9.2) + rubyzip (2.4.1) + sawyer (0.9.3) addressable (>= 2.3.5) faraday (>= 0.17.3, < 3) securerandom (0.4.1) security (0.1.5) - signet (0.19.0) + signet (0.21.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) - jwt (>= 1.5, < 3.0) + jwt (>= 1.5, < 4.0) multi_json (~> 1.10) simctl (1.6.10) CFPropertyList naturally + sysrandom (1.0.5) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) @@ -287,18 +295,18 @@ GEM tzinfo (2.0.6) concurrent-ruby (~> 1.0) uber (0.1.0) - unicode-display_width (2.5.0) - uri (1.0.3) + unicode-display_width (2.6.0) + uri (1.1.1) word_wrap (1.0.0) - xcodeproj (1.25.0) + xcodeproj (1.27.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) - nanaimo (~> 0.3.0) - rexml (>= 3.3.2, < 4.0) - xcpretty (0.3.0) - rouge (~> 2.0.7) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) + xcpretty (0.4.1) + rouge (~> 3.28.0) xcpretty-travis-formatter (1.0.1) xcpretty (~> 0.2, >= 0.0.7) @@ -306,12 +314,13 @@ PLATFORMS aarch64-linux arm64-darwin-22 arm64-darwin-23 + arm64-darwin-24 x86_64-linux DEPENDENCIES aws-sdk-cloudfront (~> 1.87) fastlane (~> 2.212) - fastlane-plugin-wpmreleasetoolkit (~> 12.0) + fastlane-plugin-wpmreleasetoolkit (~> 13.7) rubocop (~> 1.42) BUNDLED WITH diff --git a/docs/versioning-and-updates.md b/docs/versioning-and-updates.md index 8364a75aad..653cb79384 100644 --- a/docs/versioning-and-updates.md +++ b/docs/versioning-and-updates.md @@ -42,9 +42,4 @@ latest dev build, then will be updated to the prod build. Otherwise, to the late ## Releases Manifest and CDN -CI uses the `generate-releases-manifest.json` script to genreate a -`releases.json` file which acts as an authoritative source of update info for -the update server. - -When CI has finished building installers it uploads installers _and_ the -releases manifest to the CDN for distribution. +The `releases.json` file serves as an authoritative source of update information for the App to update. It is generated entirely by the Apps CDN endpoint https://appscdn.wordpress.com/builds/wordpress-com-studio/releases.json proxied from https://public-api.wordpress.com/wpcom/v2/studio-app/updates?platform=darwin&arch=arm64&version=1.5.3-dev2 diff --git a/fastlane/Fastfile b/fastlane/Fastfile index f192d82e17..0d7dcdac0e 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -17,6 +17,10 @@ BUILDS_FOLDER = File.join(PROJECT_ROOT_FOLDER, 'out') # Enable dry run mode through environment variable DRY_RUN = ENV['DRY_RUN'] == 'true' +# Make sure the WPCOM API token is set +UI.user_error!("Environment variable WPCOM_API_TOKEN is not set") if ENV['WPCOM_API_TOKEN'].nil? && !DRY_RUN +UI.message("Running in #{DRY_RUN ? 'DRY RUN' : 'NORMAL'} mode") + # Read version from package.json PACKAGE_VERSION = JSON.parse(File.read(File.join(PROJECT_ROOT_FOLDER, 'package.json')))['version'] @@ -24,8 +28,8 @@ APPLE_TEAM_ID = 'PZYM8XX95Q' APPLE_BUNDLE_IDENTIFIER = 'com.automattic.studio' APPLE_API_KEY_PATH = File.join(SECRETS_FOLDER, 'app_store_connect_fastlane_api_key.json') -CDN_URL = 'https://cdn.a8c-ci.services' -CLOUDFRONT_DISTRIBUTION_ID = 'EF4A01YASGPY5' +# Site ID for WordPress.com Studio in the Apps CDN +WPCOM_STUDIO_SITE_ID = '239164481' # Use this instead of getting values from ENV directly # It will throw an error if the requested value is missing @@ -77,11 +81,15 @@ end desc 'Ship release build' lane :distribute_release_build do |_options| - next if DRY_RUN - release_tag = get_required_env('BUILDKITE_TAG') builds = distribute_builds(release_tag:) + if DRY_RUN + UI.message("[DRY RUN] Would notify Slack about #{release_tag}") + UI.message("[DRY RUN] Release assets prepared for #{builds.each_value.map { |build| build[:name] }.join(', ')}") + next + end + slack( username: 'CI Bot', icon_url: 'https://octodex.github.com/images/jenktocat.jpg', @@ -96,22 +104,20 @@ lane :distribute_release_build do |_options| ) end -def aws_upload_to_s3(bucket:, key:, file:, if_exists:, auto_prefix: false, dry_run: DRY_RUN) - if dry_run - UI.message("[DRY RUN] Would upload file: #{file}") - UI.message(" to S3 bucket: #{bucket}") - UI.message(" with key: #{key}") - UI.message(" if_exists: #{if_exists}") - return true +def get_windows_update_release_sha(arch: 'x64') + releases_file_path = File.join(BUILDS_FOLDER, 'make', 'squirrel.windows', arch, 'RELEASES') + + begin + releases_content = File.read(releases_file_path) + rescue => error + UI.user_error!("Couldn't read RELEASES file of Windows build at #{releases_file_path}. Please ensure that the file compute the release SHA1.") end - upload_to_s3( - bucket:, - key:, - file:, - if_exists:, - auto_prefix: - ) + match_data = releases_content.match(/([a-zA-Z\d]{40})\s(.*\.nupkg)\s(\d+)/) + UI.user_error!("Could not parse Windows RELEASES file format") unless match_data + + sha1, filename, size = match_data.captures + sha1 end def distribute_builds( @@ -119,183 +125,207 @@ def distribute_builds( build_number: get_required_env('BUILDKITE_BUILD_NUMBER'), release_tag: nil ) - UI.message("Running in #{DRY_RUN ? 'DRY RUN' : 'NORMAL'} mode") - # If we are distributing a build without a tag, i.e. a development build, we also want to update the latest build reference for distribution. - update_latest = release_tag.nil? - suffix = release_tag.nil? ? "v#{PACKAGE_VERSION}" : release_tag - appx_version = "#{suffix[/\d+\.\d+\.\d+/]}.0" - filename_root = 'studio' - bucket_folder = 'studio' - - builds = { + + build_type = if release_tag.nil? + 'Nightly' + elsif release_tag.downcase.include?('beta') + 'Beta' + else + 'Production' + end + version = release_tag.nil? ? "v#{PACKAGE_VERSION}" : release_tag + appx_version = "#{version[/\d+\.\d+\.\d+/]}.0" + release_notes = release_tag.nil? ? "Development build #{version}-#{build_number}" : "Release #{release_tag}" + + update_builds = { x64: { binary_path: File.join(BUILDS_FOLDER, 'Studio-darwin-x64', 'Studio.app.zip'), - filename_core: 'darwin-x64', - extension: 'app.zip', - name: 'Mac Intel' - }, - x64_dmg: { - binary_path: File.join(BUILDS_FOLDER, 'Studio-darwin-x64.dmg'), - filename_core: 'darwin-x64', - extension: 'dmg', - name: 'Mac Intel (DMG)' + name: 'Mac Intel', + platform: 'Mac - Intel', + arch: 'x64', + install_type: 'Update', + sha: commit_hash }, arm64: { binary_path: File.join(BUILDS_FOLDER, 'Studio-darwin-arm64', 'Studio.app.zip'), - filename_core: 'darwin-arm64', - extension: 'app.zip', - name: 'Mac Apple Silicon' + name: 'Mac Apple Silicon', + platform: 'Mac - Silicon', + arch: 'arm64', + install_type: 'Update', + sha: commit_hash + }, + windows_update: { + binary_path: File.join(BUILDS_FOLDER, 'make', 'squirrel.windows', 'x64', "studio-update.nupkg"), + name: 'Windows Update', + platform: 'Windows - x64', + arch: 'x64', + install_type: 'Update', + sha: get_windows_update_release_sha(arch: 'x64') + }, + windows_arm64_update: { + binary_path: File.join(BUILDS_FOLDER, 'make', 'squirrel.windows', 'arm64', "studio-update.nupkg"), + name: 'Windows Update', + platform: 'Windows - ARM64', + arch: 'arm64', + install_type: 'Update', + sha: get_windows_update_release_sha(arch: 'arm64') + } + } + + full_install_builds = { + x64_dmg: { + binary_path: File.join(BUILDS_FOLDER, 'Studio-darwin-x64.dmg'), + name: 'Mac Intel (DMG)', + platform: 'Mac - Intel', + arch: 'x64', + install_type: 'Full Install' }, arm64_dmg: { binary_path: File.join(BUILDS_FOLDER, 'Studio-darwin-arm64.dmg'), - filename_core: 'darwin-arm64', - extension: 'dmg', - name: 'Mac Apple Silicon (DMG)' + name: 'Mac Apple Silicon (DMG)', + platform: 'Mac - Silicon', + arch: 'arm64', + install_type: 'Full Install', }, - windows_x64: { + windows: { binary_path: File.join(BUILDS_FOLDER, 'make', 'squirrel.windows', 'x64', 'studio-setup.exe'), - filename_core: 'win32-x64', - extension: 'exe', - name: 'Windows x64' - }, - windows_x64_update: { - binary_path: File.join(BUILDS_FOLDER, 'make', 'squirrel.windows', 'x64', "studio-update.nupkg"), - filename_core: 'win32-x64', - suffix: 'full', - extension: 'nupkg', - name: 'Windows x64 Update' + name: 'Windows', + platform: 'Windows - x64', + arch: 'x64', + install_type: 'Full Install' }, - windows_x64_appx: { - binary_path: File.join(BUILDS_FOLDER, 'Studio-appx-x64-signed', "Studio #{appx_version}.appx"), - filename_core: 'win32-x64', - extension: 'appx', - name: 'Windows x64 (Appx)' - }, - windows_x64_appx_unsigned: { - binary_path: File.join(BUILDS_FOLDER, 'Studio-appx-x64-unsigned', "Studio #{appx_version} unsigned.appx"), - filename_core: 'win32-x64', - suffix: 'unsigned', - extension: 'appx', - name: 'Windows x64 (Unsigned Appx)' - }, - windows_arm64: { + windows_arm64: { binary_path: File.join(BUILDS_FOLDER, 'make', 'squirrel.windows', 'arm64', 'studio-setup.exe'), - filename_core: 'win32-arm64', - extension: 'exe', - name: 'Windows ARM64' - }, - windows_arm64_update: { - binary_path: File.join(BUILDS_FOLDER, 'make', 'squirrel.windows', 'arm64', "studio-update.nupkg"), - filename_core: 'win32-arm64', - suffix: 'full', - extension: 'nupkg', - name: 'Windows ARM64 Update' - }, - windows_arm64_appx: { - binary_path: File.join(BUILDS_FOLDER, 'Studio-appx-arm64-signed', "Studio #{appx_version}.appx"), - filename_core: 'win32-arm64', - extension: 'appx', - name: 'Windows ARM64 (Appx)' - }, - windows_arm64_appx_unsigned: { - binary_path: File.join(BUILDS_FOLDER, 'Studio-appx-arm64-unsigned', "Studio #{appx_version} unsigned.appx"), - filename_core: 'win32-arm64', - suffix: 'unsigned', - extension: 'appx', - name: 'Windows ARM64 (Unsigned Appx)' + name: 'Windows - ARM64', + platform: 'Windows - ARM64', + arch: 'arm64', + install_type: 'Full Install' }, + windows_appx_unsigned: { + binary_path: File.join(BUILDS_FOLDER, 'Studio-appx-x64-unsigned', "Studio #{appx_version} unsigned.appx"), + name: 'Windows (Unsigned Appx)', + platform: 'Microsoft Store - x64', + arch: 'x64', + install_type: 'Full Install', + }, + windows_arm64_appx_unsigned: { + binary_path: File.join(BUILDS_FOLDER, 'Studio-appx-arm64-unsigned', "Studio #{appx_version} unsigned.appx"), + name: 'Windows (Unsigned Appx)', + platform: 'Microsoft Store - ARM64', + arch: 'arm64', + install_type: 'Full Install', + }, } - # Add computed fields - assemble_filename = lambda do |build, filename_suffix| - build_suffix = build[:suffix].to_s.empty? ? '' : "-#{build[:suffix]}" - "#{filename_root}-#{build[:filename_core]}-#{filename_suffix}#{build_suffix}.#{build[:extension]}" - end - builds.each_value do |build| - build[:filename] = assemble_filename.call(build, suffix) - build[:cdn_url] = "#{CDN_URL}/#{bucket_folder}/#{build[:filename]}" - end - - - bucket_name = 'a8c-apps-public-artifacts' - - builds.each_value do |build| - aws_upload_to_s3( - bucket: bucket_name, - key: "#{bucket_folder}/#{build[:filename]}", - file: build[:binary_path], - if_exists: :fail, - auto_prefix: false + builds_to_upload = release_tag.nil? ? update_builds : { **update_builds, **full_install_builds } + + UI.message("Uploading #{builds_to_upload.count} builds (#{release_tag.nil? ? 'dev: updates only' : 'release: all builds'})") + + # Upload to Apps CDN + builds_to_upload.each_value do |build| + result = upload_file_to_apps_cdn( + site_id: WPCOM_STUDIO_SITE_ID, + product: 'WordPress.com Studio', + file_path: build[:binary_path], + platform: build[:platform], + arch: build[:arch], + build_type: build_type, + install_type: build[:install_type], + visibility: 'external', + version: version, + build_number: build_number, + release_notes: release_notes, + sha: build[:sha], + error_on_duplicate: false ) + # Store the download URL for later use + build[:cdn_url] = result[:media_url] end - manifest_name = 'releases.json' - aws_upload_to_s3( - bucket: bucket_name, - key: "#{bucket_folder}/#{manifest_name}", - file: File.join(BUILDS_FOLDER, manifest_name), - if_exists: :replace, - auto_prefix: false - ) - - cache_paths_to_clear = [ - "/#{bucket_folder}/releases.json" - ] - - if update_latest - builds.each_value do |build| - filename = assemble_filename.call(build, 'latest') - key = "#{bucket_folder}/#{filename}" - - aws_upload_to_s3( - bucket: bucket_name, - key:, - file: build[:binary_path], - if_exists: :replace, - auto_prefix: false - ) - - cache_paths_to_clear.append("/#{key}") - end - end - - # Because we distribute via Cloudfront, we need to invalidate the manifest - # and the latest build (if building for release) after each upload, otherwise - # it'll be stale. - clear_cloudfront_cache( - paths: cache_paths_to_clear, - commit_hash: "#{commit_hash}-#{build_number}" - ) - unless DRY_RUN buildkite_annotate( context: 'cdn-link', style: 'info', - message: "🔗 Build available for #{builds.each_value.map { |build| "[#{build[:name]}](#{build[:cdn_url]})" }.join(', ')}" + message: "🔗 Build available for #{builds_to_upload.each_value.map { |build| "[#{build[:name]}](#{build[:cdn_url]})" }.join(', ')}" ) end # Return the builds data so callers can use them for further processing or messaging. - builds + builds_to_upload end -def clear_cloudfront_cache(paths:, commit_hash:, dry_run: DRY_RUN) - if dry_run - UI.message("[DRY RUN] Would invalidate CloudFront cache:") - UI.message(" Distribution: #{CLOUDFRONT_DISTRIBUTION_ID}") - UI.message(" Paths: #{paths.join(', ')}") - UI.message(" Commit hash: #{commit_hash}") - return true +# Create unique filenames using the current version and the architecture. +def create_versioned_file(original_file_path:, version:, arch:) + original_filename = File.basename(original_file_path) + if original_filename.match(/\.app\.zip$/i) + original_filename = original_filename.gsub(/\.app\.zip$/i, '') + extension = '.zip' + else + extension = File.extname(original_filename) + end + base_name = File.basename(original_filename, extension) + + if base_name.match(/-#{arch}/i) + versioned_filename = "#{base_name}-#{version}#{extension}" + else + versioned_filename = "#{base_name}-#{arch}-#{version}#{extension}" end - Aws::CloudFront::Client.new.create_invalidation( - distribution_id: CLOUDFRONT_DISTRIBUTION_ID, - invalidation_batch: { - paths: { - quantity: paths.length, - items: paths - }, - caller_reference: commit_hash + versioned_file_path = File.join(File.dirname(original_file_path), versioned_filename) + UI.message("Copying #{original_file_path} to #{versioned_file_path}") + FileUtils.cp(original_file_path, versioned_file_path) + versioned_file_path +end + +# Helper method to upload a file to Apps CDN with dry run support +def upload_file_to_apps_cdn(site_id:, product:, file_path:, platform:, arch:, build_type:, install_type: 'Full Install', visibility:, version:, build_number:, release_notes:, sha: nil, error_on_duplicate:, dry_run: DRY_RUN) + versioned_file_path = create_versioned_file(original_file_path: file_path, version:, arch:) + + if !File.exist?(versioned_file_path) + UI.user_error!("File #{versioned_file_path} does not exist") + end + + if dry_run + media_url = "https://appscdn.wordpress.com/downloads/wordpress-com-studio/#{platform}/#{version}/#{build_number}" + # Check if the file exists + UI.message("[DRY RUN] Upload step skipped due to dry run mode.") + UI.message(" File exists at: #{versioned_file_path}") + UI.message(" file size: #{File.size(versioned_file_path)/1024/1024} MB") + UI.message(" for platform: #{platform}") + UI.message(" media url: #{media_url}") + UI.message(" build type: #{build_type}") + UI.message(" install type: #{install_type}") + UI.message(" visibility: #{visibility}") + UI.message(" version: #{version}") + UI.message(" build number: #{build_number}") + UI.message(" release notes: #{release_notes}") + UI.message(" sha: #{sha}") + UI.message(" error on duplicate: #{error_on_duplicate}") + + return { + media_url: media_url } + end + + UI.message("Uploading file: #{versioned_file_path}") + + result = upload_build_to_apps_cdn( + site_id:, + product:, + platform:, + build_type:, + install_type:, + visibility:, + version:, + build_number:, + release_notes:, + sha:, + file_path: versioned_file_path, + error_on_duplicate: ) + + UI.message("--------------------------------") + UI.message("✅ Uploaded file: #{versioned_file_path} to #{result[:media_url]}") + + result end diff --git a/scripts/generate-releases-manifest.mjs b/scripts/generate-releases-manifest.mjs deleted file mode 100644 index 736d0ddc78..0000000000 --- a/scripts/generate-releases-manifest.mjs +++ /dev/null @@ -1,194 +0,0 @@ -// This script creates a manifest file that gets uploaded to the CDN so the update API can check for new versions. -// The file is uploaded to `https://cdn.a8c-ci.services/studio/releases.json` and looks like -// -// { -// "dev": { -// "darwin": { -// "arm64": { -// "sha": "30a8251", -// "url": "https://cdn.a8c-ci.services/studio/studio-darwin-arm64-v1.2.3-42.app.zip" -// }, -// "x64": { -// "sha": "30a8251", -// "url": "https://cdn.a8c-ci.services/studio/studio-darwin-x64-v1.2.3-42.app.zip" -// } -// }, -// "win32": { -// "arm64": { -// "sha": "30a8251", -// "url": "https://cdn.a8c-ci.services/studio/studio-win32-arm64-v1.2.3-42-full.nupkg" -// }, -// "x64": { -// "sha": "30a8251", -// "url": "https://cdn.a8c-ci.services/studio/studio-win32-x64-v1.2.3-42-full.nupkg" -// } -// } -// }, -// "1.0.0": { -// "darwin": { -// "arm64": { -// "sha": "abcdef1234567890", -// "url": "https://cdn.a8c-ci.services/studio/studio-darwin-arm64-v1.0.0.app.zip" -// }, -// ... etc. -// }, -// ... etc. -// }, -// "1.0.1-beta1": { ... }, -// "1.2.1-rc3": { ... }, -// } -// -// The "dev" entry will be replaced with the latest build from trunk. - -import * as child_process from 'child_process'; -import { createWriteStream } from 'fs'; -import fs from 'fs/promises'; -import https from 'https'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import packageJson from '../package.json' with { type: 'json' }; - -const cdnURL = 'https://cdn.a8c-ci.services/studio'; -const baseName = 'studio'; - -const currentCommit = child_process.execSync( 'git rev-parse --short HEAD' ).toString().trim(); -const { version } = packageJson; -const isDevBuild = process.env.IS_DEV_BUILD; - -const __dirname = path.dirname( fileURLToPath( import.meta.url ) ); - -console.log( `Version: ${ version }` ); -console.log( `Is dev build: ${ isDevBuild }` ); -console.log( `Current commit: ${ currentCommit }` ); - -console.log( 'Downloading current manifest ...' ); - -const releasesPath = path.join( __dirname, '..', 'out', 'releases.json' ); - -async function getWindowsReleaseInfo( arch = 'x64' ) { - let windowsReleaseInfo = {}; - try { - windowsReleaseInfo = await fs.readFile( - path.join( __dirname, '..', 'out', 'make', 'squirrel.windows', arch, 'RELEASES' ), - 'utf8' - ); - } catch ( error ) { - console.log( - `Couldn't read RELEASES file of Windows ${ arch } build, please ensure that the file exists to generate the release manifest.` - ); - process.exit( 1 ); - } - - const [ _, sha1, filename, size ] = windowsReleaseInfo.match( - /([a-zA-Z\d]{40})\s(.*\.nupkg)\s(\d+)/ - ); - - return { sha1, filename, size }; -} - -try { - await fs.mkdir( path.dirname( releasesPath ) ); -} catch ( err ) { - if ( err.code !== 'EEXIST' ) throw err; -} - -const releasesFile = createWriteStream( releasesPath, { flags: 'w' } ); // 'w', we want to override any existing file - -const downloaded = await new Promise( ( resolve, reject ) => { - https.get( `${ cdnURL }/releases.json`, ( response ) => { - if ( response.statusCode === 404 || response.statusCode === 403 ) { - resolve( false ); - return; - } - - response.pipe( releasesFile ); - response.on( 'end', () => { - console.log( '\nDownload complete' ); - releasesFile.close( () => resolve( true ) ); - } ); - response.on( 'error', ( err ) => reject( err ) ); - } ); -} ); - -if ( ! downloaded ) { - console.log( 'Creating releases.json for the first time ...' ); - await releasesFile.write( '{}\n' ); - await new Promise( ( resolve ) => { - releasesFile.close( resolve ); - } ); - console.log( 'Done' ); -} - -console.log( 'Parsing current release info ...' ); -const releasesData = JSON.parse( await fs.readFile( releasesPath, 'utf8' ) ); - -if ( isDevBuild ) { - console.log( 'Overriding latest dev release ...' ); - - releasesData[ 'dev' ] = releasesData[ 'dev' ] ?? {}; - - // macOS - releasesData[ 'dev' ][ 'darwin' ] = releasesData[ 'dev' ][ 'darwin' ] ?? {}; - releasesData[ 'dev' ][ 'darwin' ][ 'x64' ] = { - sha: currentCommit, - url: `${ cdnURL }/${ baseName }-darwin-x64-v${ version }.app.zip`, - }; - releasesData[ 'dev' ][ 'darwin' ][ 'arm64' ] = { - sha: currentCommit, - url: `${ cdnURL }/${ baseName }-darwin-arm64-v${ version }.app.zip`, - }; - - // Windows - releasesData[ 'dev' ][ 'win32' ] = releasesData[ 'dev' ][ 'win32' ] ?? {}; - const windowsX64ReleaseInfo = await getWindowsReleaseInfo( 'x64' ); - releasesData[ 'dev' ][ 'win32' ][ 'x64' ] = { - sha: windowsX64ReleaseInfo.sha1, - url: `${ cdnURL }/${ baseName }-win32-x64-v${ version }-full.nupkg`, - size: windowsX64ReleaseInfo.size, - }; - const windowsArm64ReleaseInfo = await getWindowsReleaseInfo( 'arm64' ); - releasesData[ 'dev' ][ 'win32' ][ 'arm64' ] = { - sha: windowsArm64ReleaseInfo.sha1, - url: `${ cdnURL }/${ baseName }-win32-arm64-v${ version }-full.nupkg`, - size: windowsArm64ReleaseInfo.size, - }; - - await fs.writeFile( releasesPath, JSON.stringify( releasesData, null, 2 ) ); - console.log( 'Overwrote latest dev release' ); -} else { - console.log( 'Adding latest release ...' ); - - releasesData[ version ] = releasesData[ version ] ?? {}; - - // macOS - releasesData[ version ][ 'darwin' ] = releasesData[ version ][ 'darwin' ] ?? {}; - releasesData[ version ][ 'darwin' ][ 'x64' ] = { - sha: currentCommit, - url: `${ cdnURL }/${ baseName }-darwin-x64-v${ version }.app.zip`, - }; - releasesData[ version ][ 'darwin' ][ 'arm64' ] = { - sha: currentCommit, - url: `${ cdnURL }/${ baseName }-darwin-arm64-v${ version }.app.zip`, - }; - - // Windows - releasesData[ version ][ 'win32' ] = releasesData[ version ][ 'win32' ] ?? {}; - const windowsX64Release = await getWindowsReleaseInfo( 'x64' ); - releasesData[ version ][ 'win32' ][ 'x64' ] = { - sha: windowsX64Release.sha1, - url: `${ cdnURL }/${ baseName }-win32-x64-v${ version }-full.nupkg`, - size: windowsX64Release.size, - }; - const windowsArm64Release = await getWindowsReleaseInfo( 'arm64' ); - releasesData[ version ][ 'win32' ][ 'arm64' ] = { - sha: windowsArm64Release.sha1, - url: `${ cdnURL }/${ baseName }-win32-arm64-v${ version }-full.nupkg`, - size: windowsArm64Release.size, - }; - - await fs.writeFile( releasesPath, JSON.stringify( releasesData, null, 2 ) ); - console.log( 'Added latest release' ); -} - -console.log( 'Done generating releases manifest.' ); -process.exit( 0 );