From 771a952811b171477ea714252db04a58e9638fa8 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Thu, 12 Feb 2026 17:35:54 +0100 Subject: [PATCH 01/48] Add lanes for CI release automation --- fastlane/Fastfile | 340 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 337 insertions(+), 3 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 05796bf74e..82878b707e 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -31,6 +31,12 @@ APPLE_API_KEY_PATH = File.join(SECRETS_FOLDER, 'app_store_connect_fastlane_api_k # Site ID for WordPress.com Studio in the Apps CDN WPCOM_STUDIO_SITE_ID = '239164481' +GITHUB_REPO = 'Automattic/studio' +MAIN_BRANCH = 'trunk' +PACKAGE_JSON_PATH = File.join(PROJECT_ROOT_FOLDER, 'package.json') +BUILDKITE_ORG = 'automattic' +BUILDKITE_PIPELINE = 'studio' + # Use this instead of getting values from ENV directly # It will throw an error if the requested value is missing def get_required_env(key) @@ -81,9 +87,12 @@ lane :distribute_dev_build do |_options| distribute_builds end -desc 'Ship release build' -lane :distribute_release_build do |_options| - release_tag = get_required_env('BUILDKITE_TAG') +# Upload release builds to the Apps CDN and notify Slack. +# +# @param version [String] The release version (e.g., '1.7.4' or '1.7.4-beta1') +# +lane :distribute_release_build do |version: read_package_json_version| + release_tag = "v#{version}" builds = distribute_builds(release_tag:) if DRY_RUN @@ -106,6 +115,271 @@ lane :distribute_release_build do |_options| ) end +######################################################################## +# Release Management Lanes +######################################################################## + +# Create a new release branch from trunk and the first beta. +# +# - Creates a `release/` branch from trunk +# - Extracts translatable strings +# - Delegates to {new_beta_release} to bump to beta1, commit, push, create a GitHub prerelease, and trigger a build +# +# @param version [String] The version to freeze (e.g., '1.7.4') +# @param skip_confirm [Boolean] Skip interactive confirmation prompts (default: false) +# +lane :code_freeze do |version:, skip_confirm: false| + branch_name = "release/#{version}" + + UI.important <<~PROMPT + Freezing code for release #{version}. This will: + - Create a new `#{branch_name}` branch from `#{MAIN_BRANCH}` + - Extract translatable strings + - Bump version to #{version}-beta1 + - Create a GitHub prerelease for v#{version}-beta1 + - Trigger a release build + PROMPT + next unless skip_confirm || UI.confirm('Continue?') + + # Create release branch from trunk + Fastlane::Helper::GitHelper.checkout_and_pull(MAIN_BRANCH) + Fastlane::Helper::GitHelper.create_branch(branch_name) + + # Extract translatable strings + sh('rm', '-rf', './out/pots') + sh( + 'npx', 'wp-babel-makepot', + '{src,cli,common}/**/*.{js,jsx,ts,tsx}', + '--ignore', 'cli/node_modules/**/*,**/*.d.ts', + '--base', '.', + '--dir', './out/pots', + '--output', './out/pots/bundle-strings.pot' + ) + + # Create the first beta (bumps version, commits, pushes, creates GitHub prerelease, triggers build) + new_beta_release(version: "#{version}-beta1", skip_confirm: skip_confirm) + + UI.success("Code freeze complete! Created #{branch_name} with first beta") +end + +# Create a new beta release on the current release branch. +# +# - Bumps the beta number (e.g., beta1 -> beta2) +# - Creates a GitHub prerelease (which also creates the tag) +# - Triggers a release build in Buildkite +# +# @param version [String] (optional) The beta version to use (e.g., '1.7.4-beta1'). +# If provided, sets the version directly. Typically passed from {code_freeze}. +# If nil, increments the current beta number. +# @param skip_confirm [Boolean] Skip interactive confirmation prompts (default: false) +# +lane :new_beta_release do |version: nil, skip_confirm: false| + if version + new_version = version + base_version = version.sub(/-beta\d+$/, '') + else + current_version = read_package_json_version + UI.user_error!("Current version #{current_version} is not a beta") unless current_version.include?('beta') + + base_version = current_version.sub(/-beta\d+$/, '') + current_beta = current_version.match(/beta(\d+)$/)[1].to_i + new_version = "#{base_version}-beta#{current_beta + 1}" + end + + UI.important <<~PROMPT + Creating new beta release #{new_version}. This will: + - Bump version to #{new_version} + - Create a GitHub prerelease for v#{new_version} + - Trigger a release build + PROMPT + next unless skip_confirm || UI.confirm('Continue?') + + set_package_json_version(version: new_version) + + # Commit and push + git_add(path: ['package.json', 'package-lock.json']) + git_commit( + path: ['package.json', 'package-lock.json'], + message: "Bump version to #{new_version}" + ) + push_to_git_remote(set_upstream: true) + + # Create GitHub prerelease (also creates the tag) + tag_name = "v#{new_version}" + create_github_release( + repository: GITHUB_REPO, + version: tag_name, + target: "release/#{base_version}", + release_assets: [], + prerelease: true, + is_draft: false + ) + + trigger_release_build(version: new_version) + + UI.success("New beta release created: #{tag_name}") +end + +# Finalize the release by removing the beta suffix and preparing the final version. +# +# - Bumps version to the final release number (removes beta suffix) +# - Creates a draft GitHub release with release notes (which also creates the tag) +# - Triggers a release build in Buildkite +# - The draft is published later by the {publish_release} lane +# +# @param version [String] The final version number (e.g., '1.7.4') +# @param skip_confirm [Boolean] Skip interactive confirmation prompts (default: false) +# +lane :finalize_release do |version:, skip_confirm: false| + UI.important <<~PROMPT + Finalizing release #{version}. This will: + - Bump version to #{version} (remove beta suffix) + - Create a draft GitHub release with release notes + - Trigger a release build + PROMPT + next unless skip_confirm || UI.confirm('Continue?') + + set_package_json_version(version: version) + + # Commit and push + git_add(path: ['package.json', 'package-lock.json']) + git_commit( + path: ['package.json', 'package-lock.json'], + message: "Bump version to #{version}" + ) + push_to_git_remote + + # Extract release notes and write to a temp file for the GitHub release action + notes = extract_release_notes(version: version) + release_notes_path = File.join(PROJECT_ROOT_FOLDER, 'fastlane', 'github_release_notes.txt') + File.write(release_notes_path, notes) + + # Create draft GitHub release with release notes (also creates the tag) + tag_name = "v#{version}" + create_github_release( + repository: GITHUB_REPO, + version: tag_name, + target: "release/#{version}", + release_notes_file_path: release_notes_path, + release_assets: [], + prerelease: false, + is_draft: true + ) + + trigger_release_build(version: version) + + UI.success("Release finalized: #{tag_name} (draft release created)") +end + +# Publish the release by making the draft GitHub release public and creating a backmerge PR. +# +# - Publishes the draft GitHub release created by {finalize_release} +# - Creates a backmerge PR from the release branch to trunk (with intermediate branch for conflicts) +# +# @param version [String] The version to publish (e.g., '1.7.4') +# @param skip_confirm [Boolean] Skip interactive confirmation prompts (default: false) +# +lane :publish_release do |version:, skip_confirm: false| + UI.important <<~PROMPT + Publishing release #{version}. This will: + - Publish the draft GitHub release for v#{version} + - Create a backmerge PR from `release/#{version}` into `#{MAIN_BRANCH}` + PROMPT + next unless skip_confirm || UI.confirm('Continue?') + + # Publish the draft GitHub release + publish_github_release( + repository: GITHUB_REPO, + name: "v#{version}" + ) + + # Create backmerge PR with intermediate branch to handle conflicts + create_release_backmerge_pull_request( + repository: GITHUB_REPO, + source_branch: "release/#{version}", + target_branch: MAIN_BRANCH, + labels: ['Releases'] + ) + + UI.success("Release v#{version} published!") +end + +# Create a hotfix release branch from the latest release tag. +# +# - Finds the latest non-beta release tag +# - Creates a `release/` branch from that tag +# - Bumps the version number +# +# @param version [String] The hotfix version number (e.g., '1.7.5') +# @param skip_confirm [Boolean] Skip interactive confirmation prompts (default: false) +# +lane :new_hotfix_release do |version:, skip_confirm: false| + branch_name = "release/#{version}" + + # Extract the major.minor version to find the previous release tag + short_version = version.match(/^(\d+\.\d+)/)[1] + previous_tag = find_previous_tag(pattern: "v#{short_version}.*") + + # Determine the base for the hotfix branch: either a tag or a release branch + previous_release_branch = "release/#{short_version}" + base_ref = if previous_tag && !previous_tag.include?('beta') + previous_tag + elsif Fastlane::Helper::GitHelper.branch_exists_on_remote?(branch_name: previous_release_branch) + UI.message("Tag for version '#{short_version}' not found. Using release branch '#{previous_release_branch}' as the base for the hotfix.") + previous_release_branch + else + UI.user_error!("Neither a tag for version '#{short_version}' nor branch '#{previous_release_branch}' exists on the remote. A hotfix branch cannot be created.") + end + + UI.important <<~PROMPT + Creating hotfix release #{version}. This will: + - Create a new `#{branch_name}` branch from #{base_ref} + - Bump version to #{version} + PROMPT + next unless skip_confirm || UI.confirm('Continue?') + + Fastlane::Helper::GitHelper.create_branch(branch_name, from: base_ref) + + set_package_json_version(version: version) + + # Commit and push + git_add(path: ['package.json', 'package-lock.json']) + git_commit( + path: ['package.json', 'package-lock.json'], + message: "Bump version to #{version}" + ) + push_to_git_remote(set_upstream: true) + + UI.success("Hotfix branch #{branch_name} created from #{base_ref}") +end + +# Download the latest translations and commit them to the current branch. +# +# @param skip_confirm [Boolean] Skip interactive confirmation prompts (default: false) +# +lane :download_translations do |skip_confirm: false| + UI.important <<~PROMPT + Downloading translations. This will: + - Download latest translations from GlotPress + - Commit and push any changes to the current branch + PROMPT + next unless skip_confirm || UI.confirm('Continue?') + + sh('node', './scripts/download-available-site-translations.mjs') + + git_add(path: ['.']) + git_commit( + path: ['.'], + message: 'Update translations', + allow_nothing_to_commit: true + ) + push_to_git_remote +end + +######################################################################## +# Build and Distribution Helper Methods +######################################################################## + def get_windows_update_release_sha(arch: 'x64') releases_file_path = File.join(BUILDS_FOLDER, 'make', 'squirrel.windows', arch, 'RELEASES') @@ -334,3 +608,63 @@ def upload_file_to_apps_cdn(site_id:, product:, file_path:, platform:, arch:, bu result end + +######################################################################## +# Release Management Helper Methods +######################################################################## + +# Read the current version from package.json +def read_package_json_version + JSON.parse(File.read(PACKAGE_JSON_PATH))['version'] +end + +# Set the version in package.json (and update package-lock.json via npm) +def set_package_json_version(version:) + sh('npm', 'version', version, '--no-git-tag-version') +end + +# Extract release notes for a specific version from RELEASE-NOTES.txt +def extract_release_notes(version:) + notes_path = File.join(PROJECT_ROOT_FOLDER, 'RELEASE-NOTES.txt') + content = File.read(notes_path) + + escaped = Regexp.escape(version) + match = content.match(/^#{escaped}\n=+\n(.*?)(?=\n\d+\.\d+|\z)/m) + + if match + match[1].strip + else + UI.important("No release notes found for #{version} in RELEASE-NOTES.txt") + "Release #{version}" + end +end + +# Trigger a release build in Buildkite for the given version. +# +# Uses `buildkite_add_trigger_step` on CI (to create a separate build with proper Git mirroring) +# and `buildkite_trigger_build` when running locally. +def trigger_release_build(version:) + base_version = version.sub(/-beta\d+$/, '') + branch = "release/#{base_version}" + + environment = { + 'RELEASE_VERSION' => base_version + } + + common_args = { + pipeline_file: 'release-build-and-distribute.yml', + branch: branch, + message: "Release Build (v#{version})", + environment: environment + } + + if is_ci? + buildkite_add_trigger_step(**common_args) + else + buildkite_trigger_build( + buildkite_organization: BUILDKITE_ORG, + buildkite_pipeline: BUILDKITE_PIPELINE, + **common_args + ) + end +end From 489b5a54067ce1bdbcf499c7ffdaaefc8ae7ed98 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Thu, 12 Feb 2026 17:45:01 +0100 Subject: [PATCH 02/48] Update helper scripts --- .buildkite/commands/build-for-windows.ps1 | 2 -- .../commands/checkout-release-branch.sh | 26 +++++++++++++++++++ .gitignore | 1 + scripts/confirm-tag-matches-version.mjs | 26 ------------------- 4 files changed, 27 insertions(+), 28 deletions(-) create mode 100755 .buildkite/commands/checkout-release-branch.sh delete mode 100644 scripts/confirm-tag-matches-version.mjs diff --git a/.buildkite/commands/build-for-windows.ps1 b/.buildkite/commands/build-for-windows.ps1 index c2918b532f..82eff5b681 100644 --- a/.buildkite/commands/build-for-windows.ps1 +++ b/.buildkite/commands/build-for-windows.ps1 @@ -46,8 +46,6 @@ if ($BuildType -eq $BUILD_TYPE_DEV) { $env:IS_DEV_BUILD="true" } else { Write-Host "Preparing release build..." - node ./scripts/confirm-tag-matches-version.mjs - If ($LastExitCode -ne 0) { Exit $LastExitCode } } # Set architecture environment variable for AppX packaging diff --git a/.buildkite/commands/checkout-release-branch.sh b/.buildkite/commands/checkout-release-branch.sh new file mode 100755 index 0000000000..26aec8ddf6 --- /dev/null +++ b/.buildkite/commands/checkout-release-branch.sh @@ -0,0 +1,26 @@ +#!/bin/bash -eu + +# Script to checkout a specific release branch. +# Usage: ./checkout-release-branch.sh +# +# Buildkite, by default, checks out a specific commit, ending up in a detached HEAD state. +# But in some cases, we need to ensure to be checked out on the `release/*` branch instead, namely: +# - When a `release-pipelines/*.yml` will end up needing to do a `git push` to the `release/*` branch (for version bumps) +# - When doing a new build from a job that was `pipeline upload`'d by such a pipeline, +# to ensure that the job doing the build would include that recent extra commit before starting the build. + +echo "--- :git: Checkout Release Branch" + +if [[ -n "${1:-}" ]]; then + RELEASE_VERSION="$1" +elif [[ "${BUILDKITE_BRANCH:-}" =~ ^release/ ]]; then + RELEASE_VERSION="${BUILDKITE_BRANCH#release/}" +else + echo "Error: RELEASE_VERSION parameter missing and BUILDKITE_BRANCH is not a release branch" + exit 1 +fi +BRANCH_NAME="release/${RELEASE_VERSION}" + +git fetch origin "$BRANCH_NAME" +git checkout "$BRANCH_NAME" +git pull diff --git a/.gitignore b/.gitignore index 410796ef76..43d9f0b44f 100644 --- a/.gitignore +++ b/.gitignore @@ -101,6 +101,7 @@ wp-files/ vendor/* /fastlane/report.xml /fastlane/README.md +/fastlane/github_release_notes.txt # CLI npm artifacts cli/vendor/ diff --git a/scripts/confirm-tag-matches-version.mjs b/scripts/confirm-tag-matches-version.mjs deleted file mode 100644 index 8550c8dc96..0000000000 --- a/scripts/confirm-tag-matches-version.mjs +++ /dev/null @@ -1,26 +0,0 @@ -// Fails (thus halting the build) if the git tag doesn't match the version in package.json. -// This safety measure is part of the release build process. - -import packageJson from '../apps/studio/package.json' with { type: 'json' }; - -const tagTriggeringBuild = process.env.BUILDKITE_TAG; - -if ( ! tagTriggeringBuild ) { - // Are you trying to dev on the build scripts outside of CI? - // You will need to define the BUILDKITE_TAG environment variable before - // running this script. e.g. - // BUILDKITE_TAG=v1.2.3 node ./scripts/confirm-tag-matches-version.mjs - throw new Error( 'Build was not triggered by a new tag' ); -} - -if ( tagTriggeringBuild === packageJson.version ) { - throw new Error( 'The git tag used to trigger a release build must start with "v"' ); -} - -if ( tagTriggeringBuild !== 'v' + packageJson.version ) { - throw new Error( - `Tag which triggered the build (${ tagTriggeringBuild }) does not match version in package.json (${ packageJson.version })` - ); -} - -process.exit( 0 ); From accbfebd10e3e354cde9940bf84194c7b63e9db8 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Thu, 12 Feb 2026 18:04:53 +0100 Subject: [PATCH 03/48] Add release pipelines --- .buildkite/release-pipelines/code-freeze.yml | 29 +++++++++++++++++++ .../download-translations.yml | 26 +++++++++++++++++ .../release-pipelines/finalize-release.yml | 26 +++++++++++++++++ .../release-pipelines/new-beta-release.yml | 26 +++++++++++++++++ .../release-pipelines/new-hotfix-release.yml | 21 ++++++++++++++ .../release-pipelines/publish-release.yml | 23 +++++++++++++++ 6 files changed, 151 insertions(+) create mode 100644 .buildkite/release-pipelines/code-freeze.yml create mode 100644 .buildkite/release-pipelines/download-translations.yml create mode 100644 .buildkite/release-pipelines/finalize-release.yml create mode 100644 .buildkite/release-pipelines/new-beta-release.yml create mode 100644 .buildkite/release-pipelines/new-hotfix-release.yml create mode 100644 .buildkite/release-pipelines/publish-release.yml diff --git a/.buildkite/release-pipelines/code-freeze.yml b/.buildkite/release-pipelines/code-freeze.yml new file mode 100644 index 0000000000..b354466fb4 --- /dev/null +++ b/.buildkite/release-pipelines/code-freeze.yml @@ -0,0 +1,29 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json +--- + +env: + IMAGE_ID: $IMAGE_ID + +steps: + - label: ":snowflake: Code Freeze" + plugins: [$CI_TOOLKIT_PLUGIN, $NVM_PLUGIN] + command: | + echo "--- :robot_face: Use bot for Git operations" + source use-bot-for-git + + echo "--- :npm: Install Node dependencies" + .buildkite/commands/install-node-dependencies.sh + + echo "--- :ruby: Setup Ruby Tools" + install_gems + + echo "--- :snowflake: Execute Code Freeze" + bundle exec fastlane code_freeze version:"${RELEASE_VERSION}" skip_confirm:true + artifact_paths: + - out/pots/bundle-strings.pot + agents: + queue: mac + retry: + manual: + reason: If release jobs fail, you should always re-trigger the task from Releases V2 rather than retrying the individual job from Buildkite + allowed: false diff --git a/.buildkite/release-pipelines/download-translations.yml b/.buildkite/release-pipelines/download-translations.yml new file mode 100644 index 0000000000..18318ebc11 --- /dev/null +++ b/.buildkite/release-pipelines/download-translations.yml @@ -0,0 +1,26 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json +--- + +env: + IMAGE_ID: $IMAGE_ID + +steps: + - label: ":earth_asia: Download Translations" + plugins: [$CI_TOOLKIT_PLUGIN, $NVM_PLUGIN] + command: | + echo "--- :robot_face: Use bot for Git operations" + source use-bot-for-git + + .buildkite/commands/checkout-release-branch.sh "${RELEASE_VERSION}" + + echo "--- :ruby: Setup Ruby Tools" + install_gems + + echo "--- :earth_asia: Download Translations" + bundle exec fastlane download_translations skip_confirm:true + agents: + queue: mac + retry: + manual: + reason: If release jobs fail, you should always re-trigger the task from Releases V2 rather than retrying the individual job from Buildkite + allowed: false diff --git a/.buildkite/release-pipelines/finalize-release.yml b/.buildkite/release-pipelines/finalize-release.yml new file mode 100644 index 0000000000..bc1df915c0 --- /dev/null +++ b/.buildkite/release-pipelines/finalize-release.yml @@ -0,0 +1,26 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json +--- + +env: + IMAGE_ID: $IMAGE_ID + +steps: + - label: ":checkered_flag: Finalize Release" + plugins: [$CI_TOOLKIT_PLUGIN, $NVM_PLUGIN] + command: | + echo "--- :robot_face: Use bot for Git operations" + source use-bot-for-git + + .buildkite/commands/checkout-release-branch.sh "${RELEASE_VERSION}" + + echo "--- :ruby: Setup Ruby Tools" + install_gems + + echo "--- :checkered_flag: Finalize Release" + bundle exec fastlane finalize_release version:"${RELEASE_VERSION}" skip_confirm:true + agents: + queue: mac + retry: + manual: + reason: If release jobs fail, you should always re-trigger the task from Releases V2 rather than retrying the individual job from Buildkite + allowed: false diff --git a/.buildkite/release-pipelines/new-beta-release.yml b/.buildkite/release-pipelines/new-beta-release.yml new file mode 100644 index 0000000000..37bbd1bd19 --- /dev/null +++ b/.buildkite/release-pipelines/new-beta-release.yml @@ -0,0 +1,26 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json +--- + +env: + IMAGE_ID: $IMAGE_ID + +steps: + - label: ":package: New Beta Release" + plugins: [$CI_TOOLKIT_PLUGIN, $NVM_PLUGIN] + command: | + echo "--- :robot_face: Use bot for Git operations" + source use-bot-for-git + + .buildkite/commands/checkout-release-branch.sh "${RELEASE_VERSION}" + + echo "--- :ruby: Setup Ruby Tools" + install_gems + + echo "--- :package: Create New Beta" + bundle exec fastlane new_beta_release skip_confirm:true + agents: + queue: mac + retry: + manual: + reason: If release jobs fail, you should always re-trigger the task from Releases V2 rather than retrying the individual job from Buildkite + allowed: false diff --git a/.buildkite/release-pipelines/new-hotfix-release.yml b/.buildkite/release-pipelines/new-hotfix-release.yml new file mode 100644 index 0000000000..c179e2ac5a --- /dev/null +++ b/.buildkite/release-pipelines/new-hotfix-release.yml @@ -0,0 +1,21 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json +--- + +steps: + - label: ":fire: New Hotfix Release" + plugins: [$CI_TOOLKIT_PLUGIN, $NVM_PLUGIN] + command: | + echo "--- :robot_face: Use bot for Git operations" + source use-bot-for-git + + echo "--- :ruby: Setup Ruby Tools" + install_gems + + echo "--- :fire: Create New Hotfix" + bundle exec fastlane new_hotfix_release version:"${RELEASE_VERSION}" skip_confirm:true + agents: + queue: mac + retry: + manual: + reason: If release jobs fail, you should always re-trigger the task from Releases V2 rather than retrying the individual job from Buildkite + allowed: false diff --git a/.buildkite/release-pipelines/publish-release.yml b/.buildkite/release-pipelines/publish-release.yml new file mode 100644 index 0000000000..edc5eb150a --- /dev/null +++ b/.buildkite/release-pipelines/publish-release.yml @@ -0,0 +1,23 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json +--- + +steps: + - label: ":shipit: Publish Release" + plugins: [$CI_TOOLKIT_PLUGIN, $NVM_PLUGIN] + command: | + echo "--- :robot_face: Use bot for Git operations" + source use-bot-for-git + + .buildkite/commands/checkout-release-branch.sh "${RELEASE_VERSION}" + + echo "--- :ruby: Setup Ruby Tools" + install_gems + + echo "--- :shipit: Publish Release" + bundle exec fastlane publish_release version:"${RELEASE_VERSION}" skip_confirm:true + agents: + queue: mac + retry: + manual: + reason: If release jobs fail, you should always re-trigger the task from Releases V2 rather than retrying the individual job from Buildkite + allowed: false From 524a26137e90b1d94d59fa36779790c4f00c6389 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Thu, 12 Feb 2026 18:05:32 +0100 Subject: [PATCH 04/48] Add build/distribute release pipeline --- .buildkite/release-build-and-distribute.yml | 109 ++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 .buildkite/release-build-and-distribute.yml diff --git a/.buildkite/release-build-and-distribute.yml b/.buildkite/release-build-and-distribute.yml new file mode 100644 index 0000000000..0edb2e76ea --- /dev/null +++ b/.buildkite/release-build-and-distribute.yml @@ -0,0 +1,109 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json +# +# Release build and distribution pipeline. +# +# Triggered as a separate Buildkite build by the fastlane lanes (code_freeze, new_beta_release, +# finalize_release) via `buildkite_add_trigger_step` / `buildkite_trigger_build`. +# +# Expects `RELEASE_VERSION` env var to be set (e.g., '1.7.4'). +# Each step checks out the release branch to ensure it builds the latest commit. +--- + +steps: + - group: ":package: Build for Mac" + key: release-mac + steps: + - label: ":hammer: Mac Release Build - {{matrix}}" + agents: + queue: mac + command: | + .buildkite/commands/checkout-release-branch.sh "${RELEASE_VERSION}" + + .buildkite/commands/prepare-environment.sh + + .buildkite/commands/install-node-dependencies.sh + + echo "--- :node: Building Binary" + npm run make:macos-{{matrix}} + + # Local trial and error show this needs to run before the DMG generation (obviously) but after the binary has been built + echo "--- :hammer: Rebuild fs-attr if necessary before generating DMG" + case {{matrix}} in + x64) + echo "Rebuilding fs-xattr for {{matrix}} architecture" + npm rebuild fs-xattr --cpu universal + ;; + arm64) + echo "No need to rebuild fs-xattr because it works out of the box on Apple Silicon" + ;; + *) + echo "^^^ +++ Unexpected architecture {{matrix}}" + exit 1 + ;; + esac + + echo "--- :node: Packaging in DMG" + npm run make:dmg-{{matrix}} + + echo "--- :scroll: Notarizing Binary" + bundle exec fastlane notarize_binary + plugins: [$CI_TOOLKIT_PLUGIN, $NVM_PLUGIN] + artifact_paths: + - out/**/*.app.zip + - out/*.dmg + matrix: + - x64 + - arm64 + notify: + - github_commit_status: + context: All Mac Release Builds + + - group: ":package: Build for Windows" + key: release-windows + steps: + - label: ":hammer: Windows Release Build - {{matrix}}" + agents: + queue: windows + command: | + bash .buildkite/commands/checkout-release-branch.sh "${RELEASE_VERSION}" + powershell -File .buildkite/commands/build-for-windows.ps1 -BuildType release -Architecture {{matrix}} + artifact_paths: + - out\**\studio-setup.exe + - out\**\studio-update.nupkg + - out\**\RELEASES + - out\**\*.appx + plugins: [$CI_TOOLKIT_PLUGIN, $NVM_PLUGIN] + matrix: + - x64 + - arm64 + notify: + - github_commit_status: + context: All Windows Release Builds + + - label: ":rocket: Publish Release Builds" + command: | + .buildkite/commands/checkout-release-branch.sh "${RELEASE_VERSION}" + + echo "--- :node: Downloading Binaries" + buildkite-agent artifact download "*.zip" . + buildkite-agent artifact download "*.dmg" . + buildkite-agent artifact download "*.exe" . + buildkite-agent artifact download "*.appx" . + buildkite-agent artifact download "*.nupkg" . + buildkite-agent artifact download "*\\RELEASES" . + + .buildkite/commands/install-node-dependencies.sh + + echo "--- :fastlane: Distributing Release Builds" + install_gems + VERSION=$(node -p "require('./package.json').version") + bundle exec fastlane distribute_release_build version:"${VERSION}" + agents: + queue: mac + depends_on: + - step: release-mac + - step: release-windows + plugins: [$CI_TOOLKIT_PLUGIN, $NVM_PLUGIN] + notify: + - github_commit_status: + context: Publish Release Builds From 6197af624702115c8b36119b6e79ec3315e47479 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Thu, 12 Feb 2026 18:05:39 +0100 Subject: [PATCH 05/48] Update main pipeline --- .buildkite/pipeline.yml | 95 ----------------------------------------- 1 file changed, 95 deletions(-) diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index e0d75e03aa..117aa79c09 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -181,98 +181,3 @@ steps: notify: - github_commit_status: context: Distribute Dev Builds - - - group: šŸ“¦ Build for Mac - key: release-mac - steps: - - label: šŸ”Ø Mac Release Build - {{matrix}} - agents: - queue: mac - command: | - .buildkite/commands/prepare-environment.sh - - .buildkite/commands/install-node-dependencies.sh - node ./scripts/confirm-tag-matches-version.mjs - - echo "--- :node: Building Binary" - npm run make:macos-{{matrix}} - - # Local trial and error show this needs to run before the DMG generation (obviously) but after the binary has been built - echo "--- :hammer: Rebuild fs-attr if necessary before generating DMG" - case {{matrix}} in - x64) - echo "Rebuilding fs-xattr for {{matrix}} architecture" - npm rebuild fs-xattr --cpu universal - ;; - arm64) - echo "No need to rebuild fs-xattr because it works out of the box on Apple Silicon" - ;; - *) - echo "^^^ +++ Unexpected architecture {{matrix}}" - exit 1 - ;; - esac - - echo "--- :node: Packaging in DMG" - npm run make:dmg-{{matrix}} - - echo "--- šŸ“ƒ Notarizing Binary" - bundle exec fastlane notarize_binary - plugins: [$CI_TOOLKIT_PLUGIN, $NVM_PLUGIN] - artifact_paths: - - apps/studio/out/**/*.app.zip - - apps/studio/out/*.dmg - matrix: - - x64 - - arm64 - notify: - - github_commit_status: - context: All Mac Release Builds - if: build.tag =~ /^v[0-9]+/ - - - group: šŸ“¦ Build for Windows - key: release-windows - steps: - - label: šŸ”Ø Windows Release Build - {{matrix}} - agents: - queue: windows - command: powershell -File .buildkite/commands/build-for-windows.ps1 -BuildType release -Architecture {{matrix}} - artifact_paths: - - apps\studio\out\**\studio-setup.exe - - apps\studio\out\**\studio-update.nupkg - - apps\studio\out\**\RELEASES - - apps\studio\out\**\*.appx - plugins: [$CI_TOOLKIT_PLUGIN, $NVM_PLUGIN] - matrix: - - x64 - - arm64 - notify: - - github_commit_status: - context: All Windows Release Builds - if: build.tag =~ /^v[0-9]+/ - - - label: ":rocket: Publish Release Builds" - command: | - echo "--- :node: Downloading Binaries" - buildkite-agent artifact download "*.zip" . - buildkite-agent artifact download "*.dmg" . - buildkite-agent artifact download "*.exe" . - buildkite-agent artifact download "*.appx" . - buildkite-agent artifact download "*.nupkg" . - buildkite-agent artifact download "*\\RELEASES" . - - .buildkite/commands/install-node-dependencies.sh - - echo "--- :fastlane: Distributing Release Builds" - install_gems - bundle exec fastlane distribute_release_build - agents: - queue: mac - depends_on: - - step: release-mac - - step: release-windows - plugins: [$CI_TOOLKIT_PLUGIN, $NVM_PLUGIN] - if: build.tag =~ /^v[0-9]+/ - notify: - - github_commit_status: - context: Publish Release Builds From 1e99f59f3c17de0ea48d65da98d4fa16eace63fa Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Fri, 13 Feb 2026 21:18:38 +0100 Subject: [PATCH 06/48] Improve localization automation --- .buildkite/release-pipelines/code-freeze.yml | 2 +- fastlane/Fastfile | 83 +++++++++++--------- package.json | 1 - scripts/make-pot.mjs | 53 ------------- 4 files changed, 47 insertions(+), 92 deletions(-) delete mode 100755 scripts/make-pot.mjs diff --git a/.buildkite/release-pipelines/code-freeze.yml b/.buildkite/release-pipelines/code-freeze.yml index b354466fb4..5fab384aac 100644 --- a/.buildkite/release-pipelines/code-freeze.yml +++ b/.buildkite/release-pipelines/code-freeze.yml @@ -20,7 +20,7 @@ steps: echo "--- :snowflake: Execute Code Freeze" bundle exec fastlane code_freeze version:"${RELEASE_VERSION}" skip_confirm:true artifact_paths: - - out/pots/bundle-strings.pot + - i18n/bundle-strings.pot agents: queue: mac retry: diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 82878b707e..c980ba11aa 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -18,7 +18,7 @@ BUILDS_FOLDER = File.join(PROJECT_ROOT_FOLDER, 'apps', 'studio', 'out') 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.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 @@ -121,8 +121,8 @@ end # Create a new release branch from trunk and the first beta. # +# - Extracts translatable strings and commits them to trunk (for GlotPress import) # - Creates a `release/` branch from trunk -# - Extracts translatable strings # - Delegates to {new_beta_release} to bump to beta1, commit, push, create a GitHub prerelease, and trigger a build # # @param version [String] The version to freeze (e.g., '1.7.4') @@ -133,28 +133,40 @@ lane :code_freeze do |version:, skip_confirm: false| UI.important <<~PROMPT Freezing code for release #{version}. This will: + - Extract translatable strings and commit to `#{MAIN_BRANCH}` - Create a new `#{branch_name}` branch from `#{MAIN_BRANCH}` - - Extract translatable strings - Bump version to #{version}-beta1 - Create a GitHub prerelease for v#{version}-beta1 - Trigger a release build PROMPT next unless skip_confirm || UI.confirm('Continue?') - # Create release branch from trunk + # Extract translatable strings on trunk before branching Fastlane::Helper::GitHelper.checkout_and_pull(MAIN_BRANCH) - Fastlane::Helper::GitHelper.create_branch(branch_name) - # Extract translatable strings sh('rm', '-rf', './out/pots') + sh('mkdir', '-p', './i18n') sh( 'npx', 'wp-babel-makepot', '{src,cli,common}/**/*.{js,jsx,ts,tsx}', '--ignore', 'cli/node_modules/**/*,**/*.d.ts', '--base', '.', '--dir', './out/pots', - '--output', './out/pots/bundle-strings.pot' + '--output', './i18n/bundle-strings.pot' + ) + + # Commit and push the .pot file to trunk so the wpcom cron can import it to GlotPress + git_add(path: ['./i18n/bundle-strings.pot']) + git_commit( + path: ['./i18n/bundle-strings.pot'], + message: "Code freeze: Update translatable strings for #{version}", + allow_nothing_to_commit: true ) + push_to_git_remote + + # Create release branch from trunk (inherits the .pot commit) + Fastlane::Helper::GitHelper.create_branch(branch_name) + push_to_git_remote(set_upstream: true) # Create the first beta (bumps version, commits, pushes, creates GitHub prerelease, triggers build) new_beta_release(version: "#{version}-beta1", skip_confirm: skip_confirm) @@ -202,7 +214,7 @@ lane :new_beta_release do |version: nil, skip_confirm: false| path: ['package.json', 'package-lock.json'], message: "Bump version to #{new_version}" ) - push_to_git_remote(set_upstream: true) + push_to_git_remote # Create GitHub prerelease (also creates the tag) tag_name = "v#{new_version}" @@ -385,12 +397,12 @@ def get_windows_update_release_sha(arch: 'x64') begin releases_content = File.read(releases_file_path) - rescue => error + rescue StandardError => e 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 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 + UI.user_error!('Could not parse Windows RELEASES file format') unless match_data sha1, filename, size = match_data.captures sha1 @@ -401,14 +413,13 @@ def distribute_builds( build_number: get_required_env('BUILDKITE_BUILD_NUMBER'), release_tag: nil ) - build_type = if release_tag.nil? - 'Nightly' - elsif release_tag.downcase.include?('beta') - 'Beta' - else - 'Production' - end + '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}" @@ -431,7 +442,7 @@ def distribute_builds( sha: commit_hash }, windows_update: { - binary_path: File.join(BUILDS_FOLDER, 'make', 'squirrel.windows', 'x64', "studio-update.nupkg"), + binary_path: File.join(BUILDS_FOLDER, 'make', 'squirrel.windows', 'x64', 'studio-update.nupkg'), name: 'Windows - x64 Update', platform: 'Windows - x64', arch: 'x64', @@ -439,7 +450,7 @@ def distribute_builds( sha: get_windows_update_release_sha(arch: 'x64') }, windows_arm64_update: { - binary_path: File.join(BUILDS_FOLDER, 'make', 'squirrel.windows', 'arm64', "studio-update.nupkg"), + binary_path: File.join(BUILDS_FOLDER, 'make', 'squirrel.windows', 'arm64', 'studio-update.nupkg'), name: 'Windows - ARM64 Update', platform: 'Windows - ARM64', arch: 'arm64', @@ -461,7 +472,7 @@ def distribute_builds( name: 'Mac Apple Silicon (DMG)', platform: 'Mac - Silicon', arch: 'arm64', - install_type: 'Full Install', + install_type: 'Full Install' }, windows: { binary_path: File.join(BUILDS_FOLDER, 'make', 'squirrel.windows', 'x64', 'studio-setup.exe'), @@ -470,7 +481,7 @@ def distribute_builds( arch: 'x64', install_type: 'Full Install' }, - windows_arm64: { + windows_arm64: { binary_path: File.join(BUILDS_FOLDER, 'make', 'squirrel.windows', 'arm64', 'studio-setup.exe'), name: 'Windows - ARM64', platform: 'Windows - ARM64', @@ -482,15 +493,15 @@ def distribute_builds( name: 'Windows - x64 (Unsigned Appx)', platform: 'Microsoft Store - x64', arch: 'x64', - install_type: 'Full Install', + 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 - ARM64 (Unsigned Appx)', platform: 'Microsoft Store - ARM64', arch: 'arm64', - install_type: 'Full Install', - }, + install_type: 'Full Install' + } } builds_to_upload = release_tag.nil? ? update_builds : { **update_builds, **full_install_builds } @@ -544,11 +555,11 @@ def create_versioned_file(original_file_path:, version:, arch:) 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 + versioned_filename = if base_name.match(/-#{arch}/i) + "#{base_name}-#{version}#{extension}" + else + "#{base_name}-#{arch}-#{version}#{extension}" + end versioned_file_path = File.join(File.dirname(original_file_path), versioned_filename) UI.message("Copying #{original_file_path} to #{versioned_file_path}") @@ -557,19 +568,17 @@ def create_versioned_file(original_file_path:, version:, arch:) 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) +def upload_file_to_apps_cdn(site_id:, product:, file_path:, platform:, arch:, build_type:, visibility:, version:, build_number:, release_notes:, error_on_duplicate:, install_type: 'Full Install', sha: nil, 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 + UI.user_error!("File #{versioned_file_path} does not exist") unless File.exist?(versioned_file_path) 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.") + # 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(" 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}") @@ -603,7 +612,7 @@ def upload_file_to_apps_cdn(site_id:, product:, file_path:, platform:, arch:, bu error_on_duplicate: ) - UI.message("--------------------------------") + UI.message('--------------------------------') UI.message("āœ… Uploaded file: #{versioned_file_path} to #{result[:media_url]}") result diff --git a/package.json b/package.json index da105172d0..5396f32a9a 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,6 @@ "test:watch": "vitest", "e2e": "npx playwright install && npx playwright test", "test:metrics": "npx playwright test --config=./tools/metrics/playwright.metrics.config.ts", - "make-pot": "node ./scripts/make-pot.mjs", "download-language-packs": "ts-node ./scripts/download-language-packs.ts" }, "devDependencies": { diff --git a/scripts/make-pot.mjs b/scripts/make-pot.mjs deleted file mode 100755 index 079c784765..0000000000 --- a/scripts/make-pot.mjs +++ /dev/null @@ -1,53 +0,0 @@ -import { execSync } from 'child_process'; -import { dirname, join } from 'path'; -import { fileURLToPath } from 'url'; - -const __filename = fileURLToPath( import.meta.url ); -const __dirname = dirname( __filename ); -const projectRoot = dirname( __dirname ); - -const POT_DIR = join( projectRoot, 'apps', 'studio', 'out', 'pots' ); -const POT_FILE = join( POT_DIR, 'bundle-strings.pot' ); -const IMPORT_PAGE = 'https://translate.wordpress.com/projects/studio/import-originals/'; - -function executeCommand( command, description ) { - try { - console.log( `šŸ”„ ${ description }` ); - execSync( command, { stdio: 'inherit', cwd: projectRoot } ); - return true; - } catch ( error ) { - console.error( `āŒ Error: ${ description } failed` ); - console.error( error.message ); - return false; - } -} - -console.log( '✨ Starting pot files generation...\n' ); - -const commands = [ - { - command: 'rm -rf ./apps/studio/out/pots', - description: 'Removing existing pot files', - }, - { - command: - 'npx wp-babel-makepot "{apps/studio/src,apps/cli,tools/common}/**/*.{js,jsx,ts,tsx}" --ignore "apps/cli/node_modules/**/*,**/*.d.ts" --base "." --dir "./apps/studio/out/pots" --output "./apps/studio/out/pots/bundle-strings.pot"', - description: 'Generating pot file with wp-babel-makepot', - }, - { - command: `open -R "${ POT_FILE }"`, - description: `Revealing pot file in Finder: ${ POT_FILE }`, - }, - { - command: `open "${ IMPORT_PAGE }"`, - description: `Opening the translation import page ${ IMPORT_PAGE } in the browser`, - }, -]; - -for ( const { command, description } of commands ) { - if ( ! executeCommand( command, description ) ) { - process.exit( 1 ); - } -} - -console.log( '\nāœ… Success! Now drag and drop the "bundle-strings.pot" file into GlotPress.' ); From 00a7fc369f529b7ff080928cecefc9081eadb4d4 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Mon, 16 Feb 2026 14:11:08 +0100 Subject: [PATCH 07/48] Update localization documentation --- docs/localization.md | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/docs/localization.md b/docs/localization.md index 0eaef6a14a..ed2532859a 100644 --- a/docs/localization.md +++ b/docs/localization.md @@ -15,19 +15,14 @@ If you want to add support for another language you will need to add it to the ### Extract and Import -#### Step 1: Extract Strings: +String extraction and GlotPress import are automated as part of the release process: - 1. Run `npm run make-pot` to get the text out of the source files. +1. During **code freeze**, the `code_freeze` Fastlane lane extracts all translatable strings + and commits the resulting `i18n/bundle-strings.pot` file to trunk. +2. A **wpcom cron job** (`import-github-originals.php`) periodically fetches the `.pot` file + from trunk and imports it into [GlotPress](https://translate.wordpress.com/projects/studio/). - This will remove the `out/pots/` directory and create a `*.pot` file for each module, as well as a bundle - of all translatable strings in `out/pots/bundle-strings.pot`. - It will also open the import page in your browser and select the `bundle-strings.pot` file. - -#### Step 2: Import to GlotPress: - - 1. Drag and drop the `out/pots/bundle-strings.pot` to the file input in the GlotPress Studio page https://translate.wordpress.com/projects/studio/import-originals/ - 2. Leave the format as "_Auto Detect_". - 3. Click **Import** and wait for the import to complete. +No manual steps are needed for string extraction or import. ### Export and Add From aaa9fec046bd601398a50d0d24fdcfb9b6884447 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Mon, 16 Feb 2026 14:51:03 +0100 Subject: [PATCH 08/48] Update release process documentation --- AGENTS.md | 2 +- docs/release-process.md | 89 +++++++++++++++++++++++++++++++++++------ 2 files changed, 77 insertions(+), 14 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b2647b5518..df46f130ce 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -103,7 +103,7 @@ For in-depth information, see these docs: - **CLI Design**: `docs/design-docs/cli.md` - CLI architecture, installation, IPC communication, data flow - **Custom Domains/SSL**: `docs/design-docs/custom-domains-and-ssl.md` - Proxy server, certificates, hosts file - **Localization**: `docs/localization.md` - GlotPress workflow, translation process -- **Release Process**: `docs/release-process.md` - Version tagging, Buildkite builds +- **Release Process**: `docs/release-process.md` - ReleasesV2 + Fastlane lifecycle, running lanes locally - **Overview**: `README.md` - Features, download links, contribution guidelines ## Quick Reference diff --git a/docs/release-process.md b/docs/release-process.md index 4975b4e082..abe554ff96 100644 --- a/docs/release-process.md +++ b/docs/release-process.md @@ -1,19 +1,82 @@ # Release Process -Release builds are built, signed, notarized, and uploaded to the CDN by [Buildkite](https://buildkite.com/automattic/studio). -Once the release is on the CDN the auto-update process will start downloading the new version. +Releases are managed through [ReleasesV2](https://releases.a8c.com/) and automated via Fastlane + Buildkite. Each step below has a corresponding button in the ReleasesV2 UI that triggers a Buildkite pipeline, which runs the appropriate Fastlane lane. -## Creating a Release +Builds are signed, notarized (macOS), and uploaded to the Apps CDN automatically. Once on the CDN, auto-update delivers the new version to users. -These instructions are for creating version 0.1.0-alpha5, but the steps are the same for releases with no pre-release tag. +## Release Lifecycle -1. Create a PR which updates the `version` field in `package.json` to `'0.1.0-alpha5'`. - - Remember to run `npm install` so the version in `package-lock.json` gets updated too. -2. Merge this PR. -3. Make a note of the commit hash of the PR which was just merged into `trunk`, e.g. `a1c70f3a3be5d28922a48f7f298f6152d6001516` -4. Tag this commit with `v0.1.0-alpha5`: - 1. On your local machine get the latest code: `git checkout trunk && git pull` - 2. Create the tag: `git tag v0.1.0-alpha5 a1c70f3a3be5d28922a48f7f298f6152d6001516` - 3. Push the tag to the GitHub repo: `git push origin v0.1.0-alpha5` +### 1. Code Freeze -Pushing the tag will automatically start the build and release process, and is complete when the build finishes cleanly. +**ReleasesV2 milestone**: Code Freeze | **Fastlane lane**: `code_freeze` + +- Extracts translatable strings and commits them to `trunk` (a wpcom cron imports them to GlotPress) +- Creates `release/` branch from `trunk` +- Bumps version to `-beta1`, creates a GitHub prerelease, and triggers a build + +### 2. Beta Releases + +**ReleasesV2 milestone**: Beta Release | **Fastlane lane**: `new_beta_release` + +- Increments the beta number (e.g. beta1 → beta2) +- Creates a GitHub prerelease and triggers a build +- Repeat as needed for additional betas + +### 3. Pre-Release + +**ReleasesV2 milestone**: Pre-Release + +- **Download translations**: Button triggers `download_translations` lane, which fetches translations from GlotPress and commits them to the release branch +- **Release notes**: Manually update `RELEASE-NOTES.txt` on the `release/` branch (required before finalizing) +- **Smoke tests**: Verify betas on macOS and Windows + +### 4. Finalize Release + +**ReleasesV2 milestone**: Release | **Fastlane lane**: `finalize_release` + +- Removes beta suffix (sets version to ``) +- Creates a **draft** GitHub release with notes from `RELEASE-NOTES.txt` +- Triggers the final release build + +### 5. Publish Release + +**Fastlane lane**: `publish_release` + +- Publishes the draft GitHub release +- Creates a backmerge PR from `release/` into `trunk` + +### 6. Post-Release (manual) + +- Publish Windows build to the Microsoft Store +- Update Slack channel bookmark +- Notify team for changelog update +- Notify next Release Wrangler + +## Hotfix Releases + +**Fastlane lane**: `new_hotfix_release` + +- Creates a `release/` branch from the latest release tag (or existing release branch) +- Bumps the version number +- After committing fixes, use `finalize_release` and `publish_release` as normal + +## Running Lanes Locally + +Lanes can be run locally for testing (requires Ruby + Bundler setup): + +```sh +# Dry run (no pushes, no uploads) +DRY_RUN=true bundle exec fastlane code_freeze version:"1.8.0" skip_confirm:true + +# Other lanes +bundle exec fastlane new_beta_release skip_confirm:true +bundle exec fastlane finalize_release version:"1.8.0" skip_confirm:true +bundle exec fastlane publish_release version:"1.8.0" skip_confirm:true +``` + +## Reference + +- [Buildkite pipelines](https://buildkite.com/automattic/studio) +- [ReleasesV2 scenarios](https://releases.a8c.com/) +- [Fastfile](../fastlane/Fastfile) — all lane implementations +- [Localization](localization.md) — string extraction and translation workflow From fe3188a4ab1fc42299bcf3f98c55dee5dcc9065f Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Mon, 16 Feb 2026 19:08:34 +0100 Subject: [PATCH 09/48] Add backmerge PR after .po strings bundle generation --- fastlane/Fastfile | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index c980ba11aa..d2a66561c3 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -133,17 +133,20 @@ lane :code_freeze do |version:, skip_confirm: false| UI.important <<~PROMPT Freezing code for release #{version}. This will: - - Extract translatable strings and commit to `#{MAIN_BRANCH}` - Create a new `#{branch_name}` branch from `#{MAIN_BRANCH}` + - Extract translatable strings and commit to `#{branch_name}` + - Create a backmerge PR from `#{branch_name}` to `#{MAIN_BRANCH}` - Bump version to #{version}-beta1 - Create a GitHub prerelease for v#{version}-beta1 - Trigger a release build PROMPT next unless skip_confirm || UI.confirm('Continue?') - # Extract translatable strings on trunk before branching Fastlane::Helper::GitHelper.checkout_and_pull(MAIN_BRANCH) + # Create release branch from MAIN_BRANCH + Fastlane::Helper::GitHelper.create_branch(branch_name) + sh('rm', '-rf', './out/pots') sh('mkdir', '-p', './i18n') sh( @@ -155,19 +158,24 @@ lane :code_freeze do |version:, skip_confirm: false| '--output', './i18n/bundle-strings.pot' ) - # Commit and push the .pot file to trunk so the wpcom cron can import it to GlotPress + # Commit and push the .pot file so the wpcom cron can import it to GlotPress git_add(path: ['./i18n/bundle-strings.pot']) git_commit( path: ['./i18n/bundle-strings.pot'], message: "Code freeze: Update translatable strings for #{version}", allow_nothing_to_commit: true ) - push_to_git_remote - # Create release branch from trunk (inherits the .pot commit) - Fastlane::Helper::GitHelper.create_branch(branch_name) push_to_git_remote(set_upstream: true) + # Create backmerge PR from the release branch to MAIN_BRANCH so that the strings bundle is uploaded to GlotPress + create_release_backmerge_pull_request( + repository: GITHUB_REPO, + source_branch: branch_name, + target_branch: MAIN_BRANCH, + labels: ['Releases'] + ) + # Create the first beta (bumps version, commits, pushes, creates GitHub prerelease, triggers build) new_beta_release(version: "#{version}-beta1", skip_confirm: skip_confirm) From 35ae16eaff2b1e0b5e31634709e112d737bdde62 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Mon, 16 Feb 2026 20:25:38 +0100 Subject: [PATCH 10/48] Add `skip ci` to release process commits --- fastlane/Fastfile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index d2a66561c3..1b50f8d2ac 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -162,7 +162,7 @@ lane :code_freeze do |version:, skip_confirm: false| git_add(path: ['./i18n/bundle-strings.pot']) git_commit( path: ['./i18n/bundle-strings.pot'], - message: "Code freeze: Update translatable strings for #{version}", + message: "[skip ci] Code freeze: Update translatable strings for #{version}", allow_nothing_to_commit: true ) @@ -220,7 +220,7 @@ lane :new_beta_release do |version: nil, skip_confirm: false| git_add(path: ['package.json', 'package-lock.json']) git_commit( path: ['package.json', 'package-lock.json'], - message: "Bump version to #{new_version}" + message: "[skip ci] Bump version to #{new_version}" ) push_to_git_remote @@ -265,7 +265,7 @@ lane :finalize_release do |version:, skip_confirm: false| git_add(path: ['package.json', 'package-lock.json']) git_commit( path: ['package.json', 'package-lock.json'], - message: "Bump version to #{version}" + message: "[skip ci] Bump version to #{version}" ) push_to_git_remote @@ -366,7 +366,7 @@ lane :new_hotfix_release do |version:, skip_confirm: false| git_add(path: ['package.json', 'package-lock.json']) git_commit( path: ['package.json', 'package-lock.json'], - message: "Bump version to #{version}" + message: "[skip ci] Bump version to #{version}" ) push_to_git_remote(set_upstream: true) @@ -390,7 +390,7 @@ lane :download_translations do |skip_confirm: false| git_add(path: ['.']) git_commit( path: ['.'], - message: 'Update translations', + message: '[skip ci] Update translations', allow_nothing_to_commit: true ) push_to_git_remote From 84329519a2a040f1364c8790533241f8fca753d2 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Tue, 17 Feb 2026 22:05:17 +0100 Subject: [PATCH 11/48] Delete release branch once published --- fastlane/Fastfile | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 1b50f8d2ac..7dc9bb049c 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -300,10 +300,13 @@ end # @param skip_confirm [Boolean] Skip interactive confirmation prompts (default: false) # lane :publish_release do |version:, skip_confirm: false| + release_branch = "release/#{version}" + UI.important <<~PROMPT Publishing release #{version}. This will: - Publish the draft GitHub release for v#{version} - - Create a backmerge PR from `release/#{version}` into `#{MAIN_BRANCH}` + - Create a backmerge PR from `#{release_branch}` into `#{MAIN_BRANCH}` + - Delete the `#{release_branch}` branch after creating the backmerge PR PROMPT next unless skip_confirm || UI.confirm('Continue?') @@ -316,11 +319,15 @@ lane :publish_release do |version:, skip_confirm: false| # Create backmerge PR with intermediate branch to handle conflicts create_release_backmerge_pull_request( repository: GITHUB_REPO, - source_branch: "release/#{version}", + source_branch: release_branch, target_branch: MAIN_BRANCH, labels: ['Releases'] ) + Fastlane::Helper::GitHelper.delete_remote_branch_if_exists!(release_branch) + Fastlane::Helper::GitHelper.checkout_and_pull(MAIN_BRANCH) + Fastlane::Helper::GitHelper.delete_local_branch_if_exists!(release_branch) + UI.success("Release v#{version} published!") end From 8a7814de73cae1752ddc6ae37079da9beda3899b Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Tue, 17 Feb 2026 22:22:37 +0100 Subject: [PATCH 12/48] Add a reviewer to backmerge PRs --- fastlane/Fastfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 7dc9bb049c..23590b27c3 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -173,7 +173,8 @@ lane :code_freeze do |version:, skip_confirm: false| repository: GITHUB_REPO, source_branch: branch_name, target_branch: MAIN_BRANCH, - labels: ['Releases'] + labels: ['Releases'], + reviewers: Array(ENV.fetch('GITHUB_USERNAME', nil)) ) # Create the first beta (bumps version, commits, pushes, creates GitHub prerelease, triggers build) @@ -321,7 +322,8 @@ lane :publish_release do |version:, skip_confirm: false| repository: GITHUB_REPO, source_branch: release_branch, target_branch: MAIN_BRANCH, - labels: ['Releases'] + labels: ['Releases'], + reviewers: Array(ENV.fetch('GITHUB_USERNAME', nil)) ) Fastlane::Helper::GitHelper.delete_remote_branch_if_exists!(release_branch) From d30b4d5c06e761713ca7f457407bf5aeecc2ecaf Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Tue, 17 Feb 2026 22:40:03 +0100 Subject: [PATCH 13/48] Tag instead of create prerelease --- docs/release-process.md | 4 ++-- fastlane/Fastfile | 22 ++++++++-------------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/docs/release-process.md b/docs/release-process.md index abe554ff96..1aedf70d28 100644 --- a/docs/release-process.md +++ b/docs/release-process.md @@ -12,14 +12,14 @@ Builds are signed, notarized (macOS), and uploaded to the Apps CDN automatically - Extracts translatable strings and commits them to `trunk` (a wpcom cron imports them to GlotPress) - Creates `release/` branch from `trunk` -- Bumps version to `-beta1`, creates a GitHub prerelease, and triggers a build +- Bumps version to `-beta1`, tags it, and triggers a build ### 2. Beta Releases **ReleasesV2 milestone**: Beta Release | **Fastlane lane**: `new_beta_release` - Increments the beta number (e.g. beta1 → beta2) -- Creates a GitHub prerelease and triggers a build +- Tags the new beta version and triggers a build - Repeat as needed for additional betas ### 3. Pre-Release diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 23590b27c3..4d9a5fb6fc 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -123,7 +123,7 @@ end # # - Extracts translatable strings and commits them to trunk (for GlotPress import) # - Creates a `release/` branch from trunk -# - Delegates to {new_beta_release} to bump to beta1, commit, push, create a GitHub prerelease, and trigger a build +# - Delegates to {new_beta_release} to bump to beta1, commit, push, tag, and trigger a build # # @param version [String] The version to freeze (e.g., '1.7.4') # @param skip_confirm [Boolean] Skip interactive confirmation prompts (default: false) @@ -137,7 +137,7 @@ lane :code_freeze do |version:, skip_confirm: false| - Extract translatable strings and commit to `#{branch_name}` - Create a backmerge PR from `#{branch_name}` to `#{MAIN_BRANCH}` - Bump version to #{version}-beta1 - - Create a GitHub prerelease for v#{version}-beta1 + - Tag v#{version}-beta1 - Trigger a release build PROMPT next unless skip_confirm || UI.confirm('Continue?') @@ -177,7 +177,7 @@ lane :code_freeze do |version:, skip_confirm: false| reviewers: Array(ENV.fetch('GITHUB_USERNAME', nil)) ) - # Create the first beta (bumps version, commits, pushes, creates GitHub prerelease, triggers build) + # Create the first beta (bumps version, commits, pushes, tags, triggers build) new_beta_release(version: "#{version}-beta1", skip_confirm: skip_confirm) UI.success("Code freeze complete! Created #{branch_name} with first beta") @@ -186,7 +186,7 @@ end # Create a new beta release on the current release branch. # # - Bumps the beta number (e.g., beta1 -> beta2) -# - Creates a GitHub prerelease (which also creates the tag) +# - Tags the release # - Triggers a release build in Buildkite # # @param version [String] (optional) The beta version to use (e.g., '1.7.4-beta1'). @@ -210,7 +210,7 @@ lane :new_beta_release do |version: nil, skip_confirm: false| UI.important <<~PROMPT Creating new beta release #{new_version}. This will: - Bump version to #{new_version} - - Create a GitHub prerelease for v#{new_version} + - Tag v#{new_version} - Trigger a release build PROMPT next unless skip_confirm || UI.confirm('Continue?') @@ -225,16 +225,10 @@ lane :new_beta_release do |version: nil, skip_confirm: false| ) push_to_git_remote - # Create GitHub prerelease (also creates the tag) + # Tag the beta release tag_name = "v#{new_version}" - create_github_release( - repository: GITHUB_REPO, - version: tag_name, - target: "release/#{base_version}", - release_assets: [], - prerelease: true, - is_draft: false - ) + add_git_tag(tag: tag_name) + push_git_tags(tag: tag_name) trigger_release_build(version: new_version) From e6340a16f3db0da659d8a486d50dc2b5622ec816 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Wed, 18 Feb 2026 12:37:57 +0100 Subject: [PATCH 14/48] Add buildkite annotation to backmerge prs --- fastlane/Fastfile | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 4d9a5fb6fc..0bdb12cf1c 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -169,13 +169,7 @@ lane :code_freeze do |version:, skip_confirm: false| push_to_git_remote(set_upstream: true) # Create backmerge PR from the release branch to MAIN_BRANCH so that the strings bundle is uploaded to GlotPress - create_release_backmerge_pull_request( - repository: GITHUB_REPO, - source_branch: branch_name, - target_branch: MAIN_BRANCH, - labels: ['Releases'], - reviewers: Array(ENV.fetch('GITHUB_USERNAME', nil)) - ) + create_backmerge_pr(source_branch: branch_name) # Create the first beta (bumps version, commits, pushes, tags, triggers build) new_beta_release(version: "#{version}-beta1", skip_confirm: skip_confirm) @@ -197,7 +191,6 @@ end lane :new_beta_release do |version: nil, skip_confirm: false| if version new_version = version - base_version = version.sub(/-beta\d+$/, '') else current_version = read_package_json_version UI.user_error!("Current version #{current_version} is not a beta") unless current_version.include?('beta') @@ -312,13 +305,7 @@ lane :publish_release do |version:, skip_confirm: false| ) # Create backmerge PR with intermediate branch to handle conflicts - create_release_backmerge_pull_request( - repository: GITHUB_REPO, - source_branch: release_branch, - target_branch: MAIN_BRANCH, - labels: ['Releases'], - reviewers: Array(ENV.fetch('GITHUB_USERNAME', nil)) - ) + create_backmerge_pr(source_branch: release_branch) Fastlane::Helper::GitHelper.delete_remote_branch_if_exists!(release_branch) Fastlane::Helper::GitHelper.checkout_and_pull(MAIN_BRANCH) @@ -659,6 +646,26 @@ def extract_release_notes(version:) end end +# Create a backmerge PR from the given source branch into the main branch +# and post a Buildkite annotation with links to the created PRs. +def create_backmerge_pr(source_branch:) + created_prs = create_release_backmerge_pull_request( + repository: GITHUB_REPO, + source_branch: source_branch, + target_branch: MAIN_BRANCH, + labels: ['Releases'], + reviewers: Array(ENV.fetch('GITHUB_USERNAME', nil)) + ) + + return unless is_ci? && created_prs && !created_prs.empty? + + buildkite_annotate( + context: 'backmerge-prs', + style: 'info', + message: "Backmerge Pull Requests:\n#{created_prs.map { |pr| "* #{pr}\n" }.join}" + ) +end + # Trigger a release build in Buildkite for the given version. # # Uses `buildkite_add_trigger_step` on CI (to create a separate build with proper Git mirroring) From 3859e0a2e9e9f30d167bb85be0c1cf08d0f5f51e Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Wed, 18 Feb 2026 12:48:46 +0100 Subject: [PATCH 15/48] Fix Rubocop violations --- fastlane/Fastfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 0bdb12cf1c..98c336879e 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -395,14 +395,14 @@ def get_windows_update_release_sha(arch: 'x64') begin releases_content = File.read(releases_file_path) - rescue StandardError => e + rescue StandardError 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 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, = match_data.captures sha1 end From 80c2b6832476d0df1503fb7eab2f565c6678c2d4 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Wed, 18 Feb 2026 17:29:11 +0100 Subject: [PATCH 16/48] Use `PROJECT_ROOT_FOLDER` to run external commands --- fastlane/Fastfile | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 98c336879e..075c746ad6 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -147,15 +147,19 @@ lane :code_freeze do |version:, skip_confirm: false| # Create release branch from MAIN_BRANCH Fastlane::Helper::GitHelper.create_branch(branch_name) - sh('rm', '-rf', './out/pots') - sh('mkdir', '-p', './i18n') + # Fastlane sh() runs from the fastlane/ directory, so use absolute paths + pot_dir = File.join(PROJECT_ROOT_FOLDER, 'out', 'pots') + pot_output = File.join(PROJECT_ROOT_FOLDER, 'i18n', 'bundle-strings.pot') + + sh('rm', '-rf', pot_dir) + sh('mkdir', '-p', File.join(PROJECT_ROOT_FOLDER, 'i18n')) sh( 'npx', 'wp-babel-makepot', - '{src,cli,common}/**/*.{js,jsx,ts,tsx}', - '--ignore', 'cli/node_modules/**/*,**/*.d.ts', - '--base', '.', - '--dir', './out/pots', - '--output', './i18n/bundle-strings.pot' + "#{PROJECT_ROOT_FOLDER}/{src,cli,common}/**/*.{js,jsx,ts,tsx}", + '--ignore', "#{PROJECT_ROOT_FOLDER}/cli/node_modules/**/*,**/*.d.ts", + '--base', PROJECT_ROOT_FOLDER, + '--dir', pot_dir, + '--output', pot_output ) # Commit and push the .pot file so the wpcom cron can import it to GlotPress @@ -375,7 +379,7 @@ lane :download_translations do |skip_confirm: false| PROMPT next unless skip_confirm || UI.confirm('Continue?') - sh('node', './scripts/download-available-site-translations.mjs') + sh('node', File.join(PROJECT_ROOT_FOLDER, 'scripts', 'download-available-site-translations.mjs')) git_add(path: ['.']) git_commit( @@ -627,7 +631,7 @@ end # Set the version in package.json (and update package-lock.json via npm) def set_package_json_version(version:) - sh('npm', 'version', version, '--no-git-tag-version') + sh('npm', 'version', version, '--no-git-tag-version', '--prefix', PROJECT_ROOT_FOLDER) end # Extract release notes for a specific version from RELEASE-NOTES.txt From 13c04a5e0a88cec032a95f53f26bef18a00d7b04 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Wed, 18 Feb 2026 17:37:21 +0100 Subject: [PATCH 17/48] Fix parameter name --- fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 075c746ad6..08db8a1501 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -656,7 +656,7 @@ def create_backmerge_pr(source_branch:) created_prs = create_release_backmerge_pull_request( repository: GITHUB_REPO, source_branch: source_branch, - target_branch: MAIN_BRANCH, + default_branch: MAIN_BRANCH, labels: ['Releases'], reviewers: Array(ENV.fetch('GITHUB_USERNAME', nil)) ) From 2c6813f6cf54e09ce101615b949d7c34902f38f1 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Wed, 18 Feb 2026 18:02:03 +0100 Subject: [PATCH 18/48] Create a PR when downloading translations --- fastlane/Fastfile | 51 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 08db8a1501..0fbc25311f 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -367,27 +367,68 @@ lane :new_hotfix_release do |version:, skip_confirm: false| UI.success("Hotfix branch #{branch_name} created from #{base_ref}") end -# Download the latest translations and commit them to the current branch. +# Download the latest translations and create a PR to merge them into the release branch. # # @param skip_confirm [Boolean] Skip interactive confirmation prompts (default: false) # lane :download_translations do |skip_confirm: false| + current_branch = Fastlane::Helper::GitHelper.current_branch_name + UI.important <<~PROMPT Downloading translations. This will: - Download latest translations from GlotPress - - Commit and push any changes to the current branch + - Create a PR to merge them into `#{current_branch}` PROMPT next unless skip_confirm || UI.confirm('Continue?') sh('node', File.join(PROJECT_ROOT_FOLDER, 'scripts', 'download-available-site-translations.mjs')) + translations_branch = 'update/latest-translations' + Fastlane::Helper::GitHelper.delete_local_branch_if_exists!(translations_branch) + Fastlane::Helper::GitHelper.create_branch(translations_branch) + git_add(path: ['.']) - git_commit( + result = git_commit( path: ['.'], - message: '[skip ci] Update translations', + message: 'Update translations', allow_nothing_to_commit: true ) - push_to_git_remote + + if result.nil? + Fastlane::Helper::GitHelper.checkout_and_pull(current_branch) + Fastlane::Helper::GitHelper.delete_local_branch_if_exists!(translations_branch) + UI.important('No translation changes detected.') + + if is_ci? + buildkite_annotate( + context: 'download-translations', + style: 'info', + message: 'No translation changes detected. No PR was created.' + ) + end + next + end + + Fastlane::Helper::GitHelper.delete_remote_branch_if_exists!(translations_branch) + push_to_git_remote(set_upstream: true, tags: false) + + pr_url = create_pull_request( + repo: GITHUB_REPO, + title: 'Update translations', + body: 'Merges the latest translations from GlotPress.', + labels: 'Releases', + base: current_branch, + head: translations_branch, + reviewers: Array(ENV.fetch('GITHUB_USERNAME', nil)) + ) + + if is_ci? && pr_url + buildkite_annotate( + context: 'download-translations', + style: 'info', + message: "Translations Pull Request: #{pr_url}" + ) + end end ######################################################################## From 4bdf07cfe7617954930ff41a83085e262b5cb47b Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Wed, 18 Feb 2026 18:20:01 +0100 Subject: [PATCH 19/48] Forward GITHUB_USERNAME to the lanes --- .buildkite/release-pipelines/code-freeze.yml | 2 +- .../release-pipelines/download-translations.yml | 2 +- .buildkite/release-pipelines/publish-release.yml | 2 +- fastlane/Fastfile | 16 ++++++++-------- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.buildkite/release-pipelines/code-freeze.yml b/.buildkite/release-pipelines/code-freeze.yml index 5fab384aac..78ce67dd58 100644 --- a/.buildkite/release-pipelines/code-freeze.yml +++ b/.buildkite/release-pipelines/code-freeze.yml @@ -18,7 +18,7 @@ steps: install_gems echo "--- :snowflake: Execute Code Freeze" - bundle exec fastlane code_freeze version:"${RELEASE_VERSION}" skip_confirm:true + bundle exec fastlane code_freeze version:"${RELEASE_VERSION}" github_username:"${GITHUB_USERNAME}" skip_confirm:true artifact_paths: - i18n/bundle-strings.pot agents: diff --git a/.buildkite/release-pipelines/download-translations.yml b/.buildkite/release-pipelines/download-translations.yml index 18318ebc11..ba3cbc31e2 100644 --- a/.buildkite/release-pipelines/download-translations.yml +++ b/.buildkite/release-pipelines/download-translations.yml @@ -17,7 +17,7 @@ steps: install_gems echo "--- :earth_asia: Download Translations" - bundle exec fastlane download_translations skip_confirm:true + bundle exec fastlane download_translations github_username:"${GITHUB_USERNAME}" skip_confirm:true agents: queue: mac retry: diff --git a/.buildkite/release-pipelines/publish-release.yml b/.buildkite/release-pipelines/publish-release.yml index edc5eb150a..501fdcf246 100644 --- a/.buildkite/release-pipelines/publish-release.yml +++ b/.buildkite/release-pipelines/publish-release.yml @@ -14,7 +14,7 @@ steps: install_gems echo "--- :shipit: Publish Release" - bundle exec fastlane publish_release version:"${RELEASE_VERSION}" skip_confirm:true + bundle exec fastlane publish_release version:"${RELEASE_VERSION}" github_username:"${GITHUB_USERNAME}" skip_confirm:true agents: queue: mac retry: diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 0fbc25311f..16018359c0 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -128,7 +128,7 @@ end # @param version [String] The version to freeze (e.g., '1.7.4') # @param skip_confirm [Boolean] Skip interactive confirmation prompts (default: false) # -lane :code_freeze do |version:, skip_confirm: false| +lane :code_freeze do |version:, skip_confirm: false, github_username: nil| branch_name = "release/#{version}" UI.important <<~PROMPT @@ -173,7 +173,7 @@ lane :code_freeze do |version:, skip_confirm: false| push_to_git_remote(set_upstream: true) # Create backmerge PR from the release branch to MAIN_BRANCH so that the strings bundle is uploaded to GlotPress - create_backmerge_pr(source_branch: branch_name) + create_backmerge_pr(source_branch: branch_name, github_username: github_username) # Create the first beta (bumps version, commits, pushes, tags, triggers build) new_beta_release(version: "#{version}-beta1", skip_confirm: skip_confirm) @@ -291,7 +291,7 @@ end # @param version [String] The version to publish (e.g., '1.7.4') # @param skip_confirm [Boolean] Skip interactive confirmation prompts (default: false) # -lane :publish_release do |version:, skip_confirm: false| +lane :publish_release do |version:, skip_confirm: false, github_username: nil| release_branch = "release/#{version}" UI.important <<~PROMPT @@ -309,7 +309,7 @@ lane :publish_release do |version:, skip_confirm: false| ) # Create backmerge PR with intermediate branch to handle conflicts - create_backmerge_pr(source_branch: release_branch) + create_backmerge_pr(source_branch: release_branch, github_username: github_username) Fastlane::Helper::GitHelper.delete_remote_branch_if_exists!(release_branch) Fastlane::Helper::GitHelper.checkout_and_pull(MAIN_BRANCH) @@ -371,7 +371,7 @@ end # # @param skip_confirm [Boolean] Skip interactive confirmation prompts (default: false) # -lane :download_translations do |skip_confirm: false| +lane :download_translations do |skip_confirm: false, github_username: nil| current_branch = Fastlane::Helper::GitHelper.current_branch_name UI.important <<~PROMPT @@ -419,7 +419,7 @@ lane :download_translations do |skip_confirm: false| labels: 'Releases', base: current_branch, head: translations_branch, - reviewers: Array(ENV.fetch('GITHUB_USERNAME', nil)) + reviewers: Array(github_username) ) if is_ci? && pr_url @@ -693,13 +693,13 @@ end # Create a backmerge PR from the given source branch into the main branch # and post a Buildkite annotation with links to the created PRs. -def create_backmerge_pr(source_branch:) +def create_backmerge_pr(source_branch:, github_username: nil) created_prs = create_release_backmerge_pull_request( repository: GITHUB_REPO, source_branch: source_branch, default_branch: MAIN_BRANCH, labels: ['Releases'], - reviewers: Array(ENV.fetch('GITHUB_USERNAME', nil)) + reviewers: Array(github_username) ) return unless is_ci? && created_prs && !created_prs.empty? From 5bb7680ff4f9a36932075d7fe355313129776b72 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Wed, 18 Feb 2026 19:21:44 +0100 Subject: [PATCH 20/48] Add IMAGE_ID to release pipeline --- .buildkite/release-build-and-distribute.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.buildkite/release-build-and-distribute.yml b/.buildkite/release-build-and-distribute.yml index 0edb2e76ea..06bea0e507 100644 --- a/.buildkite/release-build-and-distribute.yml +++ b/.buildkite/release-build-and-distribute.yml @@ -9,6 +9,10 @@ # Each step checks out the release branch to ensure it builds the latest commit. --- +# Used by mac agents only +env: + IMAGE_ID: $IMAGE_ID + steps: - group: ":package: Build for Mac" key: release-mac From be073c77b2fb679063e6b3c8ac8427ee8471a10f Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Fri, 20 Feb 2026 12:49:51 +0100 Subject: [PATCH 21/48] Adjust paths after monorepo merge --- .buildkite/release-build-and-distribute.yml | 22 ++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.buildkite/release-build-and-distribute.yml b/.buildkite/release-build-and-distribute.yml index 06bea0e507..18c04a5477 100644 --- a/.buildkite/release-build-and-distribute.yml +++ b/.buildkite/release-build-and-distribute.yml @@ -14,10 +14,10 @@ env: IMAGE_ID: $IMAGE_ID steps: - - group: ":package: Build for Mac" + - group: šŸ“¦ Build for Mac key: release-mac steps: - - label: ":hammer: Mac Release Build - {{matrix}}" + - label: šŸ”Ø Mac Release Build - {{matrix}} agents: queue: mac command: | @@ -49,12 +49,12 @@ steps: echo "--- :node: Packaging in DMG" npm run make:dmg-{{matrix}} - echo "--- :scroll: Notarizing Binary" + echo "--- šŸ“ƒ Notarizing Binary" bundle exec fastlane notarize_binary plugins: [$CI_TOOLKIT_PLUGIN, $NVM_PLUGIN] artifact_paths: - - out/**/*.app.zip - - out/*.dmg + - apps/studio/out/**/*.app.zip + - apps/studio/out/*.dmg matrix: - x64 - arm64 @@ -62,20 +62,20 @@ steps: - github_commit_status: context: All Mac Release Builds - - group: ":package: Build for Windows" + - group: šŸ“¦ Build for Windows key: release-windows steps: - - label: ":hammer: Windows Release Build - {{matrix}}" + - label: šŸ”Ø Windows Release Build - {{matrix}} agents: queue: windows command: | bash .buildkite/commands/checkout-release-branch.sh "${RELEASE_VERSION}" powershell -File .buildkite/commands/build-for-windows.ps1 -BuildType release -Architecture {{matrix}} artifact_paths: - - out\**\studio-setup.exe - - out\**\studio-update.nupkg - - out\**\RELEASES - - out\**\*.appx + - apps\studio\out\**\studio-setup.exe + - apps\studio\out\**\studio-update.nupkg + - apps\studio\out\**\RELEASES + - apps\studio\out\**\*.appx plugins: [$CI_TOOLKIT_PLUGIN, $NVM_PLUGIN] matrix: - x64 From a5b24c16e34086ad568c558bce2f71f818304e75 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Fri, 20 Feb 2026 13:52:00 +0100 Subject: [PATCH 22/48] Adjust paths in Fastlane --- fastlane/Fastfile | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 16018359c0..59d78eba9c 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -21,8 +21,8 @@ DRY_RUN = ENV['DRY_RUN'] == 'true' 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'] +# Path to the Studio app's package.json (monorepo workspace) +PACKAGE_JSON_PATH = File.join(PROJECT_ROOT_FOLDER, 'apps', 'studio', 'package.json') APPLE_TEAM_ID = 'PZYM8XX95Q' APPLE_BUNDLE_IDENTIFIER = 'com.automattic.studio' @@ -33,7 +33,6 @@ WPCOM_STUDIO_SITE_ID = '239164481' GITHUB_REPO = 'Automattic/studio' MAIN_BRANCH = 'trunk' -PACKAGE_JSON_PATH = File.join(PROJECT_ROOT_FOLDER, 'package.json') BUILDKITE_ORG = 'automattic' BUILDKITE_PIPELINE = 'studio' @@ -155,8 +154,8 @@ lane :code_freeze do |version:, skip_confirm: false, github_username: nil| sh('mkdir', '-p', File.join(PROJECT_ROOT_FOLDER, 'i18n')) sh( 'npx', 'wp-babel-makepot', - "#{PROJECT_ROOT_FOLDER}/{src,cli,common}/**/*.{js,jsx,ts,tsx}", - '--ignore', "#{PROJECT_ROOT_FOLDER}/cli/node_modules/**/*,**/*.d.ts", + "#{PROJECT_ROOT_FOLDER}/{apps/studio/src,apps/cli,tools/common}/**/*.{js,jsx,ts,tsx}", + '--ignore', "#{PROJECT_ROOT_FOLDER}/apps/cli/node_modules/**/*,**/*.d.ts", '--base', PROJECT_ROOT_FOLDER, '--dir', pot_dir, '--output', pot_output @@ -215,9 +214,9 @@ lane :new_beta_release do |version: nil, skip_confirm: false| set_package_json_version(version: new_version) # Commit and push - git_add(path: ['package.json', 'package-lock.json']) + git_add(path: ['apps/studio/package.json', 'package-lock.json']) git_commit( - path: ['package.json', 'package-lock.json'], + path: ['apps/studio/package.json', 'package-lock.json'], message: "[skip ci] Bump version to #{new_version}" ) push_to_git_remote @@ -254,9 +253,9 @@ lane :finalize_release do |version:, skip_confirm: false| set_package_json_version(version: version) # Commit and push - git_add(path: ['package.json', 'package-lock.json']) + git_add(path: ['apps/studio/package.json', 'package-lock.json']) git_commit( - path: ['package.json', 'package-lock.json'], + path: ['apps/studio/package.json', 'package-lock.json'], message: "[skip ci] Bump version to #{version}" ) push_to_git_remote @@ -357,9 +356,9 @@ lane :new_hotfix_release do |version:, skip_confirm: false| set_package_json_version(version: version) # Commit and push - git_add(path: ['package.json', 'package-lock.json']) + git_add(path: ['apps/studio/package.json', 'package-lock.json']) git_commit( - path: ['package.json', 'package-lock.json'], + path: ['apps/studio/package.json', 'package-lock.json'], message: "[skip ci] Bump version to #{version}" ) push_to_git_remote(set_upstream: true) @@ -463,7 +462,7 @@ def distribute_builds( else 'Production' end - version = release_tag.nil? ? "v#{PACKAGE_VERSION}" : release_tag + version = release_tag.nil? ? "v#{read_package_json_version}" : release_tag appx_version = "#{version[/\d+\.\d+\.\d+/]}.0" release_notes = release_tag.nil? ? "Development build #{version}-#{build_number}" : "Release #{release_tag}" @@ -670,9 +669,9 @@ def read_package_json_version JSON.parse(File.read(PACKAGE_JSON_PATH))['version'] end -# Set the version in package.json (and update package-lock.json via npm) +# Set the version in apps/studio/package.json (and update root package-lock.json via npm workspaces) def set_package_json_version(version:) - sh('npm', 'version', version, '--no-git-tag-version', '--prefix', PROJECT_ROOT_FOLDER) + sh('npm', 'version', version, '--no-git-tag-version', '-w', 'studio-app', '--prefix', PROJECT_ROOT_FOLDER) end # Extract release notes for a specific version from RELEASE-NOTES.txt From 92276da9d0b98a4350317519e9afb6c7f3ce1816 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Fri, 20 Feb 2026 14:07:22 +0100 Subject: [PATCH 23/48] Fix monorepo path --- .buildkite/release-build-and-distribute.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.buildkite/release-build-and-distribute.yml b/.buildkite/release-build-and-distribute.yml index 18c04a5477..8b21d9c1a7 100644 --- a/.buildkite/release-build-and-distribute.yml +++ b/.buildkite/release-build-and-distribute.yml @@ -100,7 +100,7 @@ steps: echo "--- :fastlane: Distributing Release Builds" install_gems - VERSION=$(node -p "require('./package.json').version") + VERSION=$(node -p "require('./apps/studio/package.json').version") bundle exec fastlane distribute_release_build version:"${VERSION}" agents: queue: mac From 531a17f0abce77de6efd220869395ed0fcf85aac Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Fri, 20 Feb 2026 15:36:47 +0100 Subject: [PATCH 24/48] Add Studio links to build in the GitHub release --- fastlane/Fastfile | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 59d78eba9c..4f4b874956 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -112,6 +112,10 @@ lane :distribute_release_build do |version: read_package_json_version| }, default_payloads: [] ) + + # For final releases (not betas), append download links to the draft GitHub release. + # Betas only have tags (no GitHub release), so get_github_release returns nil and this is skipped. + append_download_links_to_github_release(release_tag: release_tag, builds: builds) end ######################################################################## @@ -710,6 +714,39 @@ def create_backmerge_pr(source_branch:, github_username: nil) ) end +# Append download links to an existing draft GitHub release. +# Fetches the current release body, appends a formatted download section, and updates the release. +# Silently skips if no GitHub release exists for the tag (e.g., beta releases only have tags). +def append_download_links_to_github_release(release_tag:, builds:) + release_info = get_github_release(url: GITHUB_REPO, version: release_tag) + return unless release_info + + version = release_tag.delete_prefix('v') + + download_link_keys = %i[x64_dmg arm64_dmg windows windows_arm64] + links = download_link_keys.filter_map do |key| + build = builds[key] + next unless build&.dig(:cdn_url) + + label = build[:name].sub(' - ', ' – ') # Use en-dash like existing releases + "[#{label}](#{build[:cdn_url]})" + end + return if links.empty? + + download_section = "---\nDownload Studio #{version}: #{links.join(', ')}\n\n" \ + 'The latest version is always available on the [WordPress Studio](https://developer.wordpress.com/studio/) site.' + updated_body = "#{release_info['body']}\n\n#{download_section}" + + github_api( + server_url: 'https://api.github.com', + http_method: 'PATCH', + path: "/repos/#{GITHUB_REPO}/releases/#{release_info['id']}", + body: { body: updated_body }.to_json + ) + + UI.success("Updated GitHub release #{release_tag} with download links") +end + # Trigger a release build in Buildkite for the given version. # # Uses `buildkite_add_trigger_step` on CI (to create a separate build with proper Git mirroring) From f60b174a2cabee9f490d1feeeeeaa427c3072cb1 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Fri, 20 Feb 2026 15:42:14 +0100 Subject: [PATCH 25/48] Create release notes draft --- fastlane/Fastfile | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 4f4b874956..9c91133076 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -138,6 +138,7 @@ lane :code_freeze do |version:, skip_confirm: false, github_username: nil| Freezing code for release #{version}. This will: - Create a new `#{branch_name}` branch from `#{MAIN_BRANCH}` - Extract translatable strings and commit to `#{branch_name}` + - Generate draft release notes from merged PRs - Create a backmerge PR from `#{branch_name}` to `#{MAIN_BRANCH}` - Bump version to #{version}-beta1 - Tag v#{version}-beta1 @@ -173,6 +174,15 @@ lane :code_freeze do |version:, skip_confirm: false, github_username: nil| allow_nothing_to_commit: true ) + # Generate draft release notes from PRs merged since the last published release + update_release_notes_draft(version: version) + git_add(path: ['RELEASE-NOTES.txt']) + git_commit( + path: ['RELEASE-NOTES.txt'], + message: "[skip ci] Code freeze: Add draft release notes for #{version}", + allow_nothing_to_commit: true + ) + push_to_git_remote(set_upstream: true) # Create backmerge PR from the release branch to MAIN_BRANCH so that the strings bundle is uploaded to GlotPress @@ -694,6 +704,42 @@ def extract_release_notes(version:) end end +# Generate a draft release notes section from merged PRs and prepend it to RELEASE-NOTES.txt. +# Uses GitHub's generate-notes API to list PRs since the last published release. +def update_release_notes_draft(version:) + # Find the latest stable release tag reachable from HEAD to use as the starting point + previous_tag = find_previous_tag(pattern: 'v*') + + changelog = get_prs_between_tags( + repository: GITHUB_REPO, + tag_name: "v#{version}", + previous_tag: previous_tag, + target_commitish: 'HEAD' + ) + + # Convert "* Title by @user in https://github.com/Org/Repo/pull/123" to "* Title #123" + lines = changelog.lines.reject { |l| l.start_with?('##') || l.strip.empty? } + formatted = lines.map do |line| + line.sub(%r{ by @\S+ in https://github\.com/\S+/pull/(\d+)}, ' #\1') + end.join.strip + + if formatted.empty? + UI.important('No PRs found for draft release notes — skipping RELEASE-NOTES.txt update') + return + end + + notes_path = File.join(PROJECT_ROOT_FOLDER, 'RELEASE-NOTES.txt') + content = File.read(notes_path) + + new_section = "#{version}\n=====\n#{formatted}\n" + + # Insert the new version section after the "Unreleased" header + content.sub!(/^(Unreleased\n=+\n)\n*/, "\\1\n#{new_section}\n") + File.write(notes_path, content) + + UI.success("Draft release notes for #{version} added to RELEASE-NOTES.txt") +end + # Create a backmerge PR from the given source branch into the main branch # and post a Buildkite annotation with links to the created PRs. def create_backmerge_pr(source_branch:, github_username: nil) From 7ef21b9e85ea3788274d922afdcc01f3ece2d0b8 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Fri, 20 Feb 2026 21:22:00 +0100 Subject: [PATCH 26/48] Use RELEASE_VERSION also for build distribution --- .buildkite/release-build-and-distribute.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.buildkite/release-build-and-distribute.yml b/.buildkite/release-build-and-distribute.yml index 8b21d9c1a7..446ea0ff4e 100644 --- a/.buildkite/release-build-and-distribute.yml +++ b/.buildkite/release-build-and-distribute.yml @@ -100,8 +100,7 @@ steps: echo "--- :fastlane: Distributing Release Builds" install_gems - VERSION=$(node -p "require('./apps/studio/package.json').version") - bundle exec fastlane distribute_release_build version:"${VERSION}" + bundle exec fastlane distribute_release_build version:"${RELEASE_VERSION}" agents: queue: mac depends_on: From 6bd284f4f232418042922a8c3ed05731142b95b4 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Fri, 20 Feb 2026 21:58:48 +0100 Subject: [PATCH 27/48] Simplify new beta lane --- .../release-pipelines/new-beta-release.yml | 2 +- fastlane/Fastfile | 26 ++++++------------- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/.buildkite/release-pipelines/new-beta-release.yml b/.buildkite/release-pipelines/new-beta-release.yml index 37bbd1bd19..e2043880ca 100644 --- a/.buildkite/release-pipelines/new-beta-release.yml +++ b/.buildkite/release-pipelines/new-beta-release.yml @@ -17,7 +17,7 @@ steps: install_gems echo "--- :package: Create New Beta" - bundle exec fastlane new_beta_release skip_confirm:true + bundle exec fastlane new_beta_release version:"${RELEASE_VERSION}" skip_confirm:true agents: queue: mac retry: diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 9c91133076..faff1e3936 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -189,33 +189,23 @@ lane :code_freeze do |version:, skip_confirm: false, github_username: nil| create_backmerge_pr(source_branch: branch_name, github_username: github_username) # Create the first beta (bumps version, commits, pushes, tags, triggers build) - new_beta_release(version: "#{version}-beta1", skip_confirm: skip_confirm) + new_beta_release(version: version, skip_confirm: skip_confirm) UI.success("Code freeze complete! Created #{branch_name} with first beta") end # Create a new beta release on the current release branch. # -# - Bumps the beta number (e.g., beta1 -> beta2) -# - Tags the release -# - Triggers a release build in Buildkite +# - Determines the next beta number automatically from package.json +# - Bumps the version, commits, tags, and triggers a release build # -# @param version [String] (optional) The beta version to use (e.g., '1.7.4-beta1'). -# If provided, sets the version directly. Typically passed from {code_freeze}. -# If nil, increments the current beta number. +# @param version [String] The base release version (e.g., '1.7.4') # @param skip_confirm [Boolean] Skip interactive confirmation prompts (default: false) # -lane :new_beta_release do |version: nil, skip_confirm: false| - if version - new_version = version - else - current_version = read_package_json_version - UI.user_error!("Current version #{current_version} is not a beta") unless current_version.include?('beta') - - base_version = current_version.sub(/-beta\d+$/, '') - current_beta = current_version.match(/beta(\d+)$/)[1].to_i - new_version = "#{base_version}-beta#{current_beta + 1}" - end +lane :new_beta_release do |version:, skip_confirm: false| + current_version = read_package_json_version + current_beta = current_version[/beta(\d+)$/, 1].to_i + new_version = "#{version}-beta#{current_beta + 1}" UI.important <<~PROMPT Creating new beta release #{new_version}. This will: From e6681b00cbcc008ab5dc8e323a842e3012d48dd5 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Fri, 20 Feb 2026 21:59:42 +0100 Subject: [PATCH 28/48] Use `current_git_branch` --- fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index faff1e3936..bcddc30281 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -375,7 +375,7 @@ end # @param skip_confirm [Boolean] Skip interactive confirmation prompts (default: false) # lane :download_translations do |skip_confirm: false, github_username: nil| - current_branch = Fastlane::Helper::GitHelper.current_branch_name + current_branch = Fastlane::Helper::GitHelper.current_git_branch UI.important <<~PROMPT Downloading translations. This will: From 95dc04b092f55852a0c7ae9a69b96bbb51083310 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Fri, 20 Feb 2026 22:11:21 +0100 Subject: [PATCH 29/48] =?UTF-8?q?Fix=20error=20when=20downloading=20files?= =?UTF-8?q?=20mkdirSync=20with=20{=20recursive:=20true=20}=20creates=20all?= =?UTF-8?q?=20missing=20parent=20directories=20and=20is=20a=20no-op=20if?= =?UTF-8?q?=20they=20already=20exist=20=E2=80=94=20so=20the=20try/catch=20?= =?UTF-8?q?for=20EEXIST=20is=20no=20longer=20needed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/download-available-site-translations.mjs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/scripts/download-available-site-translations.mjs b/scripts/download-available-site-translations.mjs index 0de9b0c144..05f20e3863 100644 --- a/scripts/download-available-site-translations.mjs +++ b/scripts/download-available-site-translations.mjs @@ -16,11 +16,7 @@ const jsonFilePath = path.join( 'latest', 'available-site-translations.json' ); -try { - fs.mkdirSync( path.dirname( jsonFilePath ) ); -} catch ( err ) { - if ( err.code !== 'EEXIST' ) throw err; -} +fs.mkdirSync( path.dirname( jsonFilePath ), { recursive: true } ); const jsonFile = fs.createWriteStream( jsonFilePath ); await new Promise( ( resolve, reject ) => { From 2ae98531dc95867f7745b90af9b6e8dc8faee28b Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Fri, 20 Feb 2026 22:22:55 +0100 Subject: [PATCH 30/48] Improve log / confirmation message --- fastlane/Fastfile | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index bcddc30281..e43b3d75d8 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -140,9 +140,9 @@ lane :code_freeze do |version:, skip_confirm: false, github_username: nil| - Extract translatable strings and commit to `#{branch_name}` - Generate draft release notes from merged PRs - Create a backmerge PR from `#{branch_name}` to `#{MAIN_BRANCH}` - - Bump version to #{version}-beta1 - - Tag v#{version}-beta1 - - Trigger a release build + - Trigger the first beta build for all platforms (macOS, Windows), which will then: + - Upload build artifacts to the Apps CDN + - Notify #dotcom-studio on Slack PROMPT next unless skip_confirm || UI.confirm('Continue?') @@ -211,7 +211,9 @@ lane :new_beta_release do |version:, skip_confirm: false| Creating new beta release #{new_version}. This will: - Bump version to #{new_version} - Tag v#{new_version} - - Trigger a release build + - Trigger a release build for all platforms (macOS, Windows), which will then: + - Upload build artifacts to the Apps CDN + - Notify #dotcom-studio on Slack PROMPT next unless skip_confirm || UI.confirm('Continue?') @@ -250,7 +252,10 @@ lane :finalize_release do |version:, skip_confirm: false| Finalizing release #{version}. This will: - Bump version to #{version} (remove beta suffix) - Create a draft GitHub release with release notes - - Trigger a release build + - Trigger a release build for all platforms (macOS, Windows), which will then: + - Upload build artifacts to the Apps CDN + - Add download links to the draft GitHub release + - Notify #dotcom-studio on Slack PROMPT next unless skip_confirm || UI.confirm('Continue?') From 8f63241fc106ca5bdf41ff8711e5063076116d71 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Fri, 20 Feb 2026 22:48:32 +0100 Subject: [PATCH 31/48] Add IMAGE_ID --- .buildkite/release-pipelines/new-hotfix-release.yml | 3 +++ .buildkite/release-pipelines/publish-release.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.buildkite/release-pipelines/new-hotfix-release.yml b/.buildkite/release-pipelines/new-hotfix-release.yml index c179e2ac5a..0eca77b136 100644 --- a/.buildkite/release-pipelines/new-hotfix-release.yml +++ b/.buildkite/release-pipelines/new-hotfix-release.yml @@ -1,6 +1,9 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json --- +env: + IMAGE_ID: $IMAGE_ID + steps: - label: ":fire: New Hotfix Release" plugins: [$CI_TOOLKIT_PLUGIN, $NVM_PLUGIN] diff --git a/.buildkite/release-pipelines/publish-release.yml b/.buildkite/release-pipelines/publish-release.yml index 501fdcf246..cd6c18e61e 100644 --- a/.buildkite/release-pipelines/publish-release.yml +++ b/.buildkite/release-pipelines/publish-release.yml @@ -1,6 +1,9 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json --- +env: + IMAGE_ID: $IMAGE_ID + steps: - label: ":shipit: Publish Release" plugins: [$CI_TOOLKIT_PLUGIN, $NVM_PLUGIN] From d4d3a3952b4d8c32fc3eb99c6e279bc6e6aa0c6c Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Mon, 23 Feb 2026 14:27:09 +0100 Subject: [PATCH 32/48] Let the version in `distribute_release_build` to be read from package.json --- .buildkite/release-build-and-distribute.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.buildkite/release-build-and-distribute.yml b/.buildkite/release-build-and-distribute.yml index 446ea0ff4e..0d117e8248 100644 --- a/.buildkite/release-build-and-distribute.yml +++ b/.buildkite/release-build-and-distribute.yml @@ -100,7 +100,7 @@ steps: echo "--- :fastlane: Distributing Release Builds" install_gems - bundle exec fastlane distribute_release_build version:"${RELEASE_VERSION}" + bundle exec fastlane distribute_release_build agents: queue: mac depends_on: From 13a71824254e03768bac55bb05c080291b321cdf Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Mon, 23 Feb 2026 14:27:31 +0100 Subject: [PATCH 33/48] Update release-process.md documentation --- docs/release-process.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/release-process.md b/docs/release-process.md index 1aedf70d28..3efc6b7f24 100644 --- a/docs/release-process.md +++ b/docs/release-process.md @@ -10,8 +10,10 @@ Builds are signed, notarized (macOS), and uploaded to the Apps CDN automatically **ReleasesV2 milestone**: Code Freeze | **Fastlane lane**: `code_freeze` -- Extracts translatable strings and commits them to `trunk` (a wpcom cron imports them to GlotPress) - Creates `release/` branch from `trunk` +- Extracts translatable strings and commits them to the release branch (a wpcom cron imports them to GlotPress via a backmerge PR) +- Generates draft release notes from merged PRs and commits them to the release branch +- Creates a backmerge PR from the release branch into `trunk` - Bumps version to `-beta1`, tags it, and triggers a build ### 2. Beta Releases @@ -27,7 +29,7 @@ Builds are signed, notarized (macOS), and uploaded to the Apps CDN automatically **ReleasesV2 milestone**: Pre-Release - **Download translations**: Button triggers `download_translations` lane, which fetches translations from GlotPress and commits them to the release branch -- **Release notes**: Manually update `RELEASE-NOTES.txt` on the `release/` branch (required before finalizing) +- **Release notes**: Review and refine the draft release notes in `RELEASE-NOTES.txt` on the `release/` branch (a draft is auto-generated during code freeze) - **Smoke tests**: Verify betas on macOS and Windows ### 4. Finalize Release @@ -36,7 +38,7 @@ Builds are signed, notarized (macOS), and uploaded to the Apps CDN automatically - Removes beta suffix (sets version to ``) - Creates a **draft** GitHub release with notes from `RELEASE-NOTES.txt` -- Triggers the final release build +- Triggers the final release build, which uploads to the Apps CDN, appends download links to the draft GitHub release, and notifies Slack ### 5. Publish Release @@ -69,7 +71,7 @@ Lanes can be run locally for testing (requires Ruby + Bundler setup): DRY_RUN=true bundle exec fastlane code_freeze version:"1.8.0" skip_confirm:true # Other lanes -bundle exec fastlane new_beta_release skip_confirm:true +bundle exec fastlane new_beta_release version:"1.8.0" skip_confirm:true bundle exec fastlane finalize_release version:"1.8.0" skip_confirm:true bundle exec fastlane publish_release version:"1.8.0" skip_confirm:true ``` From f8732e7d4b494a3c1ab277f35837ca5559922759 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Mon, 23 Feb 2026 14:29:15 +0100 Subject: [PATCH 34/48] Use `git checkout -B "$BRANCH_NAME" "origin/$BRANCH_NAME"` instead of checkout + pull --- .buildkite/commands/checkout-release-branch.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.buildkite/commands/checkout-release-branch.sh b/.buildkite/commands/checkout-release-branch.sh index 26aec8ddf6..b15c5902e3 100755 --- a/.buildkite/commands/checkout-release-branch.sh +++ b/.buildkite/commands/checkout-release-branch.sh @@ -22,5 +22,4 @@ fi BRANCH_NAME="release/${RELEASE_VERSION}" git fetch origin "$BRANCH_NAME" -git checkout "$BRANCH_NAME" -git pull +git checkout -B "$BRANCH_NAME" "origin/$BRANCH_NAME" From b008eb725aceb491c73427f1b33cebc7d99c90c3 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Mon, 23 Feb 2026 14:30:07 +0100 Subject: [PATCH 35/48] Add RELEASE-NOTES.txt validation --- fastlane/Fastfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index e43b3d75d8..fb7f828271 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -729,7 +729,9 @@ def update_release_notes_draft(version:) new_section = "#{version}\n=====\n#{formatted}\n" # Insert the new version section after the "Unreleased" header - content.sub!(/^(Unreleased\n=+\n)\n*/, "\\1\n#{new_section}\n") + unless content.sub!(/^(Unreleased\n=+\n)\n*/, "\\1\n#{new_section}\n") + UI.user_error!("Could not find 'Unreleased' header in RELEASE-NOTES.txt. Please ensure the file has an 'Unreleased' section.") + end File.write(notes_path, content) UI.success("Draft release notes for #{version} added to RELEASE-NOTES.txt") From bcec68ac4876ed2009d315532fa6d890aa611cfc Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Mon, 23 Feb 2026 14:31:00 +0100 Subject: [PATCH 36/48] Improve extract_release_notes regex --- fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index fb7f828271..0b822fe2a7 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -689,7 +689,7 @@ def extract_release_notes(version:) content = File.read(notes_path) escaped = Regexp.escape(version) - match = content.match(/^#{escaped}\n=+\n(.*?)(?=\n\d+\.\d+|\z)/m) + match = content.match(/^#{escaped}\n=+\n(.*?)(?=^\d+\.\d+\S*\n=+|\z)/m) if match match[1].strip From 0b834711d48f3efea4a537a109a62fc5450d3696 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Mon, 23 Feb 2026 17:46:36 +0100 Subject: [PATCH 37/48] Consolidate version bump, commit and push --- fastlane/Fastfile | 42 +++++++++++++++--------------------------- 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 0b822fe2a7..bf52ec9893 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -217,15 +217,7 @@ lane :new_beta_release do |version:, skip_confirm: false| PROMPT next unless skip_confirm || UI.confirm('Continue?') - set_package_json_version(version: new_version) - - # Commit and push - git_add(path: ['apps/studio/package.json', 'package-lock.json']) - git_commit( - path: ['apps/studio/package.json', 'package-lock.json'], - message: "[skip ci] Bump version to #{new_version}" - ) - push_to_git_remote + bump_version_commit_and_push(version: new_version) # Tag the beta release tag_name = "v#{new_version}" @@ -259,15 +251,7 @@ lane :finalize_release do |version:, skip_confirm: false| PROMPT next unless skip_confirm || UI.confirm('Continue?') - set_package_json_version(version: version) - - # Commit and push - git_add(path: ['apps/studio/package.json', 'package-lock.json']) - git_commit( - path: ['apps/studio/package.json', 'package-lock.json'], - message: "[skip ci] Bump version to #{version}" - ) - push_to_git_remote + bump_version_commit_and_push(version: version) # Extract release notes and write to a temp file for the GitHub release action notes = extract_release_notes(version: version) @@ -362,15 +346,7 @@ lane :new_hotfix_release do |version:, skip_confirm: false| Fastlane::Helper::GitHelper.create_branch(branch_name, from: base_ref) - set_package_json_version(version: version) - - # Commit and push - git_add(path: ['apps/studio/package.json', 'package-lock.json']) - git_commit( - path: ['apps/studio/package.json', 'package-lock.json'], - message: "[skip ci] Bump version to #{version}" - ) - push_to_git_remote(set_upstream: true) + bump_version_commit_and_push(version: version, set_upstream: true) UI.success("Hotfix branch #{branch_name} created from #{base_ref}") end @@ -673,6 +649,18 @@ end # Release Management Helper Methods ######################################################################## +# Bump version in package.json, commit, and push. +# Used by new_beta_release, finalize_release, and new_hotfix_release. +def bump_version_commit_and_push(version:, set_upstream: false) + set_package_json_version(version: version) + git_add(path: ['apps/studio/package.json', 'package-lock.json']) + git_commit( + path: ['apps/studio/package.json', 'package-lock.json'], + message: "[skip ci] Bump version to #{version}" + ) + push_to_git_remote(set_upstream: set_upstream) +end + # Read the current version from package.json def read_package_json_version JSON.parse(File.read(PACKAGE_JSON_PATH))['version'] From 1881972cbf7b1371b1b24fb2411b65e24b36e9ac Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Mon, 23 Feb 2026 17:54:40 +0100 Subject: [PATCH 38/48] Update release-process.md section about running lanes locally --- docs/release-process.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/release-process.md b/docs/release-process.md index 3efc6b7f24..c5e307b812 100644 --- a/docs/release-process.md +++ b/docs/release-process.md @@ -64,16 +64,15 @@ Builds are signed, notarized (macOS), and uploaded to the Apps CDN automatically ## Running Lanes Locally -Lanes can be run locally for testing (requires Ruby + Bundler setup): +Lanes can be run locally for testing (requires Ruby + Bundler + GITHUB_TOKEN + BUILDKITE_TOKEN): ```sh -# Dry run (no pushes, no uploads) -DRY_RUN=true bundle exec fastlane code_freeze version:"1.8.0" skip_confirm:true -# Other lanes -bundle exec fastlane new_beta_release version:"1.8.0" skip_confirm:true -bundle exec fastlane finalize_release version:"1.8.0" skip_confirm:true -bundle exec fastlane publish_release version:"1.8.0" skip_confirm:true +# Running these lanes locally will always print a description of what the lane will do with a confirmation prompt +bundle exec fastlane code_freeze version:"1.8.0" +bundle exec fastlane new_beta_release version:"1.8.0" +bundle exec fastlane finalize_release version:"1.8.0" +bundle exec fastlane publish_release version:"1.8.0" ``` ## Reference From bd2de1621a1a467ef36f5bdeb1a2eb86ea50581d Mon Sep 17 00:00:00 2001 From: "Ian G. Maia" Date: Mon, 23 Feb 2026 19:41:54 +0100 Subject: [PATCH 39/48] Update fastlane/Fastfile code_freeze comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index bf52ec9893..be8b36d26d 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -124,7 +124,7 @@ end # Create a new release branch from trunk and the first beta. # -# - Extracts translatable strings and commits them to trunk (for GlotPress import) +# - Extracts translatable strings and commits them to the release branch (for GlotPress import) # - Creates a `release/` branch from trunk # - Delegates to {new_beta_release} to bump to beta1, commit, push, tag, and trigger a build # From dcb9cf8304010046e4cdfefa14677b15f0bbb826 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Mon, 23 Feb 2026 21:25:48 +0100 Subject: [PATCH 40/48] Exclude beta tags from find_previous_tag --- fastlane/Fastfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index be8b36d26d..51ba3802b4 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -690,8 +690,8 @@ end # Generate a draft release notes section from merged PRs and prepend it to RELEASE-NOTES.txt. # Uses GitHub's generate-notes API to list PRs since the last published release. def update_release_notes_draft(version:) - # Find the latest stable release tag reachable from HEAD to use as the starting point - previous_tag = find_previous_tag(pattern: 'v*') + # Find the latest stable (non-beta) release tag reachable from HEAD + previous_tag = sh('git', 'describe', '--tags', '--abbrev=0', '--match=v*', '--exclude=*beta*', log: false).strip changelog = get_prs_between_tags( repository: GITHUB_REPO, From cdbc5a80fa57f6c518fe270f2d857e5107617687 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Mon, 23 Feb 2026 21:35:38 +0100 Subject: [PATCH 41/48] Add version mismatch guard in new_beta_release --- fastlane/Fastfile | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 51ba3802b4..fde2898ac7 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -205,6 +205,13 @@ end lane :new_beta_release do |version:, skip_confirm: false| current_version = read_package_json_version current_beta = current_version[/beta(\d+)$/, 1].to_i + + # Guard against version mismatch (e.g. package.json has 1.7.4-beta3 but version param is 1.8.0) + # Skip the check when current_beta is 0, meaning this is the first beta (called from code_freeze before any bump). + if current_beta > 0 + base_version = current_version.sub(/-beta\d+$/, '') + UI.user_error!("Version mismatch: package.json has #{current_version} but expected #{version}-betaN") unless base_version == version + end new_version = "#{version}-beta#{current_beta + 1}" UI.important <<~PROMPT From f7708f34a9068d5f04cd2187908638c34bf39d6b Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Mon, 23 Feb 2026 21:41:46 +0100 Subject: [PATCH 42/48] Use absolute path constant for .pot file --- fastlane/Fastfile | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index fde2898ac7..0ae68d5a39 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -24,6 +24,9 @@ UI.message("Running in #{DRY_RUN ? 'DRY RUN' : 'NORMAL'} mode") # Path to the Studio app's package.json (monorepo workspace) PACKAGE_JSON_PATH = File.join(PROJECT_ROOT_FOLDER, 'apps', 'studio', 'package.json') +# Path to the translatable strings file (committed to the repo for GlotPress import) +POT_FILE_PATH = File.join(PROJECT_ROOT_FOLDER, 'i18n', 'bundle-strings.pot') + 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') @@ -151,25 +154,24 @@ lane :code_freeze do |version:, skip_confirm: false, github_username: nil| # Create release branch from MAIN_BRANCH Fastlane::Helper::GitHelper.create_branch(branch_name) - # Fastlane sh() runs from the fastlane/ directory, so use absolute paths + # Fastlane sh() runs from the fastlane/ directory, so use absolute paths for shell commands pot_dir = File.join(PROJECT_ROOT_FOLDER, 'out', 'pots') - pot_output = File.join(PROJECT_ROOT_FOLDER, 'i18n', 'bundle-strings.pot') sh('rm', '-rf', pot_dir) - sh('mkdir', '-p', File.join(PROJECT_ROOT_FOLDER, 'i18n')) + sh('mkdir', '-p', File.dirname(POT_FILE_PATH)) sh( 'npx', 'wp-babel-makepot', "#{PROJECT_ROOT_FOLDER}/{apps/studio/src,apps/cli,tools/common}/**/*.{js,jsx,ts,tsx}", '--ignore', "#{PROJECT_ROOT_FOLDER}/apps/cli/node_modules/**/*,**/*.d.ts", '--base', PROJECT_ROOT_FOLDER, '--dir', pot_dir, - '--output', pot_output + '--output', POT_FILE_PATH ) # Commit and push the .pot file so the wpcom cron can import it to GlotPress - git_add(path: ['./i18n/bundle-strings.pot']) + git_add(path: [POT_FILE_PATH]) git_commit( - path: ['./i18n/bundle-strings.pot'], + path: [POT_FILE_PATH], message: "[skip ci] Code freeze: Update translatable strings for #{version}", allow_nothing_to_commit: true ) @@ -212,6 +214,7 @@ lane :new_beta_release do |version:, skip_confirm: false| base_version = current_version.sub(/-beta\d+$/, '') UI.user_error!("Version mismatch: package.json has #{current_version} but expected #{version}-betaN") unless base_version == version end + new_version = "#{version}-beta#{current_beta + 1}" UI.important <<~PROMPT From 93bfe7f7bcaee9d30d3dc1b2350d9ff888a44980 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Mon, 23 Feb 2026 21:47:24 +0100 Subject: [PATCH 43/48] Add changes to replicate find_previous_tag --- fastlane/Fastfile | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 0ae68d5a39..e787996ee8 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -700,8 +700,13 @@ end # Generate a draft release notes section from merged PRs and prepend it to RELEASE-NOTES.txt. # Uses GitHub's generate-notes API to list PRs since the last published release. def update_release_notes_draft(version:) - # Find the latest stable (non-beta) release tag reachable from HEAD - previous_tag = sh('git', 'describe', '--tags', '--abbrev=0', '--match=v*', '--exclude=*beta*', log: false).strip + # Find the latest stable (non-beta) release tag reachable from HEAD. + # Mirrors find_previous_tag logic but adds --exclude for beta tags (not yet supported by the action, see AINFRA-2056). + sh('git', 'fetch', '--tags', '--force', log: false) { nil } + current_tag = sh('git', 'describe', '--tags', '--exact-match', log: false) { |_, stdout, _| stdout.chomp }.to_s.strip + git_cmd = %w[git describe --tags --abbrev=0 --match=v* --exclude=*beta*] + git_cmd += ['--exclude', current_tag] unless current_tag.empty? + previous_tag = sh(*git_cmd, log: false).strip changelog = get_prs_between_tags( repository: GITHUB_REPO, From b694d47c1180bca4b4658cb35f42143ce76de658 Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Tue, 24 Feb 2026 18:32:47 +0100 Subject: [PATCH 44/48] Create beta tags and GitHub releases only after the release is built --- .buildkite/release-build-and-distribute.yml | 3 + docs/release-process.md | 7 +- fastlane/Fastfile | 95 +++++++++------------ 3 files changed, 47 insertions(+), 58 deletions(-) diff --git a/.buildkite/release-build-and-distribute.yml b/.buildkite/release-build-and-distribute.yml index 0d117e8248..f6fb6fde30 100644 --- a/.buildkite/release-build-and-distribute.yml +++ b/.buildkite/release-build-and-distribute.yml @@ -86,6 +86,9 @@ steps: - label: ":rocket: Publish Release Builds" command: | + echo "--- :robot_face: Use bot for Git operations" + source use-bot-for-git + .buildkite/commands/checkout-release-branch.sh "${RELEASE_VERSION}" echo "--- :node: Downloading Binaries" diff --git a/docs/release-process.md b/docs/release-process.md index c5e307b812..b2af02b36e 100644 --- a/docs/release-process.md +++ b/docs/release-process.md @@ -14,14 +14,14 @@ Builds are signed, notarized (macOS), and uploaded to the Apps CDN automatically - Extracts translatable strings and commits them to the release branch (a wpcom cron imports them to GlotPress via a backmerge PR) - Generates draft release notes from merged PRs and commits them to the release branch - Creates a backmerge PR from the release branch into `trunk` -- Bumps version to `-beta1`, tags it, and triggers a build +- Bumps version to `-beta1` and triggers a build (the build tags and uploads to CDN) ### 2. Beta Releases **ReleasesV2 milestone**: Beta Release | **Fastlane lane**: `new_beta_release` - Increments the beta number (e.g. beta1 → beta2) -- Tags the new beta version and triggers a build +- Triggers a build (the build tags the new version and uploads to CDN) - Repeat as needed for additional betas ### 3. Pre-Release @@ -37,8 +37,7 @@ Builds are signed, notarized (macOS), and uploaded to the Apps CDN automatically **ReleasesV2 milestone**: Release | **Fastlane lane**: `finalize_release` - Removes beta suffix (sets version to ``) -- Creates a **draft** GitHub release with notes from `RELEASE-NOTES.txt` -- Triggers the final release build, which uploads to the Apps CDN, appends download links to the draft GitHub release, and notifies Slack +- Triggers the final release build, which uploads to the Apps CDN, creates a **draft** GitHub release with notes and download links, and notifies Slack ### 5. Publish Release diff --git a/fastlane/Fastfile b/fastlane/Fastfile index e787996ee8..b0e8224e8f 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -103,6 +103,15 @@ lane :distribute_release_build do |version: read_package_json_version| next end + # Create the tag or draft GitHub release only after the build succeeds. + # This avoids orphan tags or draft releases if the build fails. + if version.include?('beta') + add_git_tag(tag: release_tag) + push_git_tags(tag: release_tag) + else + create_draft_github_release(version: version, release_tag: release_tag, builds: builds) + end + slack( username: 'CI Bot', icon_url: 'https://octodex.github.com/images/jenktocat.jpg', @@ -115,10 +124,6 @@ lane :distribute_release_build do |version: read_package_json_version| }, default_payloads: [] ) - - # For final releases (not betas), append download links to the draft GitHub release. - # Betas only have tags (no GitHub release), so get_github_release returns nil and this is skipped. - append_download_links_to_github_release(release_tag: release_tag, builds: builds) end ######################################################################## @@ -210,7 +215,7 @@ lane :new_beta_release do |version:, skip_confirm: false| # Guard against version mismatch (e.g. package.json has 1.7.4-beta3 but version param is 1.8.0) # Skip the check when current_beta is 0, meaning this is the first beta (called from code_freeze before any bump). - if current_beta > 0 + if current_beta.positive? base_version = current_version.sub(/-beta\d+$/, '') UI.user_error!("Version mismatch: package.json has #{current_version} but expected #{version}-betaN") unless base_version == version end @@ -220,30 +225,25 @@ lane :new_beta_release do |version:, skip_confirm: false| UI.important <<~PROMPT Creating new beta release #{new_version}. This will: - Bump version to #{new_version} - - Tag v#{new_version} - Trigger a release build for all platforms (macOS, Windows), which will then: - Upload build artifacts to the Apps CDN + - Tag v#{new_version} - Notify #dotcom-studio on Slack PROMPT next unless skip_confirm || UI.confirm('Continue?') bump_version_commit_and_push(version: new_version) - # Tag the beta release - tag_name = "v#{new_version}" - add_git_tag(tag: tag_name) - push_git_tags(tag: tag_name) - trigger_release_build(version: new_version) - UI.success("New beta release created: #{tag_name}") + UI.success("New beta release created: v#{new_version}") end -# Finalize the release by removing the beta suffix and preparing the final version. +# Finalize the release by removing the beta suffix and triggering the final build. # # - Bumps version to the final release number (removes beta suffix) -# - Creates a draft GitHub release with release notes (which also creates the tag) # - Triggers a release build in Buildkite +# - The build creates a draft GitHub release (with notes and download links) after uploading to CDN # - The draft is published later by the {publish_release} lane # # @param version [String] The final version number (e.g., '1.7.4') @@ -253,41 +253,23 @@ lane :finalize_release do |version:, skip_confirm: false| UI.important <<~PROMPT Finalizing release #{version}. This will: - Bump version to #{version} (remove beta suffix) - - Create a draft GitHub release with release notes - Trigger a release build for all platforms (macOS, Windows), which will then: - Upload build artifacts to the Apps CDN - - Add download links to the draft GitHub release + - Create a draft GitHub release with release notes and download links - Notify #dotcom-studio on Slack PROMPT next unless skip_confirm || UI.confirm('Continue?') bump_version_commit_and_push(version: version) - # Extract release notes and write to a temp file for the GitHub release action - notes = extract_release_notes(version: version) - release_notes_path = File.join(PROJECT_ROOT_FOLDER, 'fastlane', 'github_release_notes.txt') - File.write(release_notes_path, notes) - - # Create draft GitHub release with release notes (also creates the tag) - tag_name = "v#{version}" - create_github_release( - repository: GITHUB_REPO, - version: tag_name, - target: "release/#{version}", - release_notes_file_path: release_notes_path, - release_assets: [], - prerelease: false, - is_draft: true - ) - trigger_release_build(version: version) - UI.success("Release finalized: #{tag_name} (draft release created)") + UI.success("Release finalized: v#{version} (build triggered)") end # Publish the release by making the draft GitHub release public and creating a backmerge PR. # -# - Publishes the draft GitHub release created by {finalize_release} +# - Publishes the draft GitHub release created by {distribute_release_build} # - Creates a backmerge PR from the release branch to trunk (with intermediate branch for conflicts) # # @param version [String] The version to publish (e.g., '1.7.4') @@ -760,37 +742,42 @@ def create_backmerge_pr(source_branch:, github_username: nil) ) end -# Append download links to an existing draft GitHub release. -# Fetches the current release body, appends a formatted download section, and updates the release. -# Silently skips if no GitHub release exists for the tag (e.g., beta releases only have tags). -def append_download_links_to_github_release(release_tag:, builds:) - release_info = get_github_release(url: GITHUB_REPO, version: release_tag) - return unless release_info - - version = release_tag.delete_prefix('v') +# Create a draft GitHub release with release notes and download links. +# Called from distribute_release_build after a successful final (non-beta) build. +def create_draft_github_release(version:, release_tag:, builds:) + notes = extract_release_notes(version: version) + # Format download links from CDN URLs download_link_keys = %i[x64_dmg arm64_dmg windows windows_arm64] links = download_link_keys.filter_map do |key| build = builds[key] next unless build&.dig(:cdn_url) - label = build[:name].sub(' - ', ' – ') # Use en-dash like existing releases + label = build[:name].sub(' - ', ' \u2013 ') # Use en-dash like existing releases "[#{label}](#{build[:cdn_url]})" end - return if links.empty? - download_section = "---\nDownload Studio #{version}: #{links.join(', ')}\n\n" \ - 'The latest version is always available on the [WordPress Studio](https://developer.wordpress.com/studio/) site.' - updated_body = "#{release_info['body']}\n\n#{download_section}" + body = notes + unless links.empty? + body += "\n\n---\nDownload Studio #{version}: #{links.join(', ')}\n\n" \ + 'The latest version is always available on the [WordPress Studio](https://developer.wordpress.com/studio/) site.' + end + + release_notes_path = File.join(PROJECT_ROOT_FOLDER, 'fastlane', 'github_release_notes.txt') + File.write(release_notes_path, body) - github_api( - server_url: 'https://api.github.com', - http_method: 'PATCH', - path: "/repos/#{GITHUB_REPO}/releases/#{release_info['id']}", - body: { body: updated_body }.to_json + base_version = version.sub(/-beta\d+$/, '') + create_github_release( + repository: GITHUB_REPO, + version: release_tag, + target: "release/#{base_version}", + release_notes_file_path: release_notes_path, + release_assets: [], + prerelease: false, + is_draft: true ) - UI.success("Updated GitHub release #{release_tag} with download links") + UI.success("Created draft GitHub release #{release_tag} with download links") end # Trigger a release build in Buildkite for the given version. From 54b21a7d909abf6de4d20b4f80528936f35b6ba7 Mon Sep 17 00:00:00 2001 From: "Ian G. Maia" Date: Tue, 24 Feb 2026 19:03:37 +0100 Subject: [PATCH 45/48] Add validation to version parameter Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- fastlane/Fastfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index b0e8224e8f..876ae12faf 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -315,7 +315,9 @@ lane :new_hotfix_release do |version:, skip_confirm: false| branch_name = "release/#{version}" # Extract the major.minor version to find the previous release tag - short_version = version.match(/^(\d+\.\d+)/)[1] + match_data = version.match(/^(\d+\.\d+)/) + UI.user_error!("Invalid version '#{version}'. Expected format like '1.7.5' so that a 'major.minor' (e.g., '1.7') can be extracted.") if match_data.nil? + short_version = match_data[1] previous_tag = find_previous_tag(pattern: "v#{short_version}.*") # Determine the base for the hotfix branch: either a tag or a release branch From 83505a2786e3893fca447097c93399d49269cb5d Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Tue, 24 Feb 2026 19:36:45 +0100 Subject: [PATCH 46/48] Use latest release-toolkit to use the new `find_previous_tag` --- Gemfile | 2 +- Gemfile.lock | 36 ++++++++++++++++++++++++++---------- fastlane/Fastfile | 7 +------ 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/Gemfile b/Gemfile index 41fbc12b0d..bf8b0f62b4 100644 --- a/Gemfile +++ b/Gemfile @@ -3,7 +3,7 @@ source 'https://rubygems.org' gem 'fastlane', '~> 2.232' -gem 'fastlane-plugin-wpmreleasetoolkit', '~> 13.7' +gem 'fastlane-plugin-wpmreleasetoolkit', '~> 14.1' gem 'aws-sdk-cloudfront', '~> 1.87' diff --git a/Gemfile.lock b/Gemfile.lock index db0a46d515..ce5055fd4c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -22,7 +22,7 @@ GEM ast (2.4.3) atomos (0.1.3) aws-eventstream (1.4.0) - aws-partitions (1.1213.0) + aws-partitions (1.1218.0) aws-sdk-cloudfront (1.141.0) aws-sdk-core (~> 3, >= 3.241.4) aws-sigv4 (~> 1.5) @@ -34,7 +34,7 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.121.0) + aws-sdk-kms (1.122.0) aws-sdk-core (~> 3, >= 3.241.4) aws-sigv4 (~> 1.5) aws-sdk-s3 (1.213.0) @@ -66,6 +66,7 @@ GEM dotenv (2.8.1) drb (2.2.3) emoji_regex (3.2.3) + erubi (1.13.1) excon (0.112.0) faraday (1.10.5) faraday-em_http (~> 1.0) @@ -96,7 +97,7 @@ GEM faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.4.0) - fastlane (2.232.0) + fastlane (2.232.1) CFPropertyList (>= 2.3, < 4.0.0) abbrev (~> 0.1.2) addressable (>= 2.8, < 3.0.0) @@ -146,12 +147,13 @@ GEM xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.4.1) xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) - fastlane-plugin-wpmreleasetoolkit (13.8.1) + fastlane-plugin-wpmreleasetoolkit (14.1.0) activesupport (>= 6.1.7.1) buildkit (~> 1.5) chroma (= 0.2.0) diffy (~> 3.3) - fastlane (~> 2.213) + fastlane (~> 2.231) + gettext (~> 3.5) git (~> 1.3) google-cloud-storage (~> 1.31) java-properties (~> 0.3.0) @@ -165,11 +167,18 @@ GEM xcodeproj (~> 1.22) fastlane-sirp (1.0.0) sysrandom (~> 1.0) + forwardable (1.4.0) + gettext (3.5.1) + erubi + locale (>= 2.0.5) + prime + racc + text (>= 1.3.0) gh_inspector (1.1.3) git (1.19.1) addressable (~> 2.8) rchardet (~> 1.8) - google-apis-androidpublisher_v3 (0.95.0) + google-apis-androidpublisher_v3 (0.96.0) google-apis-core (>= 0.15.0, < 2.a) google-apis-core (0.18.0) addressable (~> 2.5, >= 2.5.1) @@ -183,7 +192,7 @@ GEM google-apis-core (>= 0.15.0, < 2.a) google-apis-playcustomapp_v1 (0.17.0) google-apis-core (>= 0.15.0, < 2.a) - google-apis-storage_v1 (0.59.0) + google-apis-storage_v1 (0.61.0) google-apis-core (>= 0.15.0, < 2.a) google-cloud-core (1.8.0) google-cloud-env (>= 1.0, < 3.a) @@ -221,10 +230,12 @@ GEM base64 language_server-protocol (3.17.0.5) lint_roller (1.1.0) + locale (2.1.4) logger (1.7.0) mini_magick (4.13.2) mini_mime (1.1.5) - minitest (6.0.1) + minitest (6.0.2) + drb (~> 2.0) prism (~> 1.5) multi_json (1.19.1) multipart-post (2.4.1) @@ -261,6 +272,9 @@ GEM ast (~> 2.4.1) racc plist (3.7.2) + prime (0.1.4) + forwardable + singleton prism (1.9.0) progress_bar (1.3.4) highline (>= 1.6) @@ -277,7 +291,7 @@ GEM declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) - retriable (3.1.2) + retriable (3.2.1) rexml (3.4.4) rouge (3.28.0) rubocop (1.84.1) @@ -310,10 +324,12 @@ GEM simctl (1.6.10) CFPropertyList naturally + singleton (0.3.0) sysrandom (1.0.5) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) + text (1.3.1) trailblazer-option (0.1.2) tty-cursor (0.7.1) tty-screen (0.8.2) @@ -350,7 +366,7 @@ PLATFORMS DEPENDENCIES aws-sdk-cloudfront (~> 1.87) fastlane (~> 2.232) - fastlane-plugin-wpmreleasetoolkit (~> 13.7) + fastlane-plugin-wpmreleasetoolkit (~> 14.1) openssl rubocop (~> 1.42) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 876ae12faf..cb8bd997fb 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -685,12 +685,7 @@ end # Uses GitHub's generate-notes API to list PRs since the last published release. def update_release_notes_draft(version:) # Find the latest stable (non-beta) release tag reachable from HEAD. - # Mirrors find_previous_tag logic but adds --exclude for beta tags (not yet supported by the action, see AINFRA-2056). - sh('git', 'fetch', '--tags', '--force', log: false) { nil } - current_tag = sh('git', 'describe', '--tags', '--exact-match', log: false) { |_, stdout, _| stdout.chomp }.to_s.strip - git_cmd = %w[git describe --tags --abbrev=0 --match=v* --exclude=*beta*] - git_cmd += ['--exclude', current_tag] unless current_tag.empty? - previous_tag = sh(*git_cmd, log: false).strip + previous_tag = find_previous_tag(pattern: 'v*', exclude: '*beta*') changelog = get_prs_between_tags( repository: GITHUB_REPO, From 14650ea335b0070a50f6e4e5495174b33bd32b9d Mon Sep 17 00:00:00 2001 From: "Ian G. Maia" Date: Tue, 24 Feb 2026 19:54:57 +0100 Subject: [PATCH 47/48] Update docs/release-process.md Co-authored-by: Olivier Halligon --- docs/release-process.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-process.md b/docs/release-process.md index b2af02b36e..841326306a 100644 --- a/docs/release-process.md +++ b/docs/release-process.md @@ -43,7 +43,7 @@ Builds are signed, notarized (macOS), and uploaded to the Apps CDN automatically **Fastlane lane**: `publish_release` -- Publishes the draft GitHub release +- Publishes the draft GitHub release (which creates the corresponding GitHub tag too) - Creates a backmerge PR from `release/` into `trunk` ### 6. Post-Release (manual) From 575d06e9bfa09da1e313b977274a6bfeb119e6aa Mon Sep 17 00:00:00 2001 From: Ian Maia Date: Wed, 25 Feb 2026 17:48:29 +0100 Subject: [PATCH 48/48] Update download_translations lane to download translations directly from GlotPress --- docs/localization.md | 45 ++++++++++++++++++-------------------------- fastlane/Fastfile | 31 +++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 28 deletions(-) diff --git a/docs/localization.md b/docs/localization.md index ed2532859a..bf46ce4425 100644 --- a/docs/localization.md +++ b/docs/localization.md @@ -1,45 +1,36 @@ # Localization -Text is translated using [GlotPress](https://translate.wordpress.com) but the -process of getting original strings into GlotPress and the translations back -into the app is somewhat manual at the moment. +Text is translated using [GlotPress](https://translate.wordpress.com/projects/studio/). +The process of getting original strings into GlotPress and the translations back +into the app is fully automated as part of the release process. ## Supported Languages -We currently support the magnificent 16 languages defined in `common/lib/locale.ts`, -as well as Polish, Vietnamese and Ukrainian. +We currently support the magnificent 16 languages defined in `common/lib/locale.ts`, +as well as Polish, Vietnamese, Ukrainian and Hungarian. If you want to add support for another language you will need to add it to the -`supportedLocales` array. +`supportedLocales` array and add a corresponding `studio-.jed.json` file +in `tools/common/translations/`. ## Translation Process -### Extract and Import - -String extraction and GlotPress import are automated as part of the release process: +### Extract and Import (automated) 1. During **code freeze**, the `code_freeze` Fastlane lane extracts all translatable strings - and commits the resulting `i18n/bundle-strings.pot` file to trunk. + and commits the resulting `i18n/bundle-strings.pot` file to the release branch. 2. A **wpcom cron job** (`import-github-originals.php`) periodically fetches the `.pot` file - from trunk and imports it into [GlotPress](https://translate.wordpress.com/projects/studio/). + from trunk (via the backmerge PR) and imports it into [GlotPress](https://translate.wordpress.com/projects/studio/). No manual steps are needed for string extraction or import. -### Export and Add - -#### Step 1: Export from GlotPress: +### Export and Add (automated) -We will export the translations as Jed-formatted JSON, which is a format -`@wordpress/i18n` can understand. It's ok if some translations are missing, +During **pre-release**, the `download_translations` Fastlane lane downloads translations +from GlotPress in Jed 1.x JSON format (which `@wordpress/i18n` understands) and creates +a PR to merge them into the release branch. It's ok if some translations are missing — they will be left as English in the app. - 1. Open [our project in GlotPress](https://translate.wordpress.com/projects/studio/). - 2. Click the **Project actions** menu. - 3. Click **Bulk Export**. - 4. Click **Select WP.Com Priority Languages** to only the magnificent 16 languages. - 5. Select **Polish**, **Vietnamese**, **Ukrainian** and **Hungarian** too. - 6. Change the format to `Jed 1.x (.json)`. - 7. Leave the other fields as default and click **Export**. - -#### Step 2: Add Translations to Project: - 1. Unzip the exported strings and add them to the `common/translations`. Overwrite - the files in there with your new files. +The lane discovers locales from the existing `studio-*.jed.json` files in +`tools/common/translations/` and downloads each one from GlotPress. + +No manual steps are needed for translation export. diff --git a/fastlane/Fastfile b/fastlane/Fastfile index cb8bd997fb..edb4075c8f 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -4,6 +4,8 @@ fastlane_require 'digest' fastlane_require 'zip' fastlane_require 'aws-sdk-cloudfront' fastlane_require 'json' +fastlane_require 'net/http' +fastlane_require 'uri' UI.user_error!('Please run fastlane via `bundle exec`') unless FastlaneCore::Helper.bundler? @@ -27,6 +29,10 @@ PACKAGE_JSON_PATH = File.join(PROJECT_ROOT_FOLDER, 'apps', 'studio', 'package.js # Path to the translatable strings file (committed to the repo for GlotPress import) POT_FILE_PATH = File.join(PROJECT_ROOT_FOLDER, 'i18n', 'bundle-strings.pot') +# GlotPress project URL and output directory for Studio UI translations (Jed 1.x JSON) +GLOTPRESS_PROJECT_URL = 'https://translate.wordpress.com/projects/studio' +TRANSLATIONS_DIR = File.join(PROJECT_ROOT_FOLDER, 'tools', 'common', 'translations') + 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') @@ -345,6 +351,29 @@ lane :new_hotfix_release do |version:, skip_confirm: false| UI.success("Hotfix branch #{branch_name} created from #{base_ref}") end +# Download Studio UI translations from GlotPress (Jed 1.x JSON) into tools/common/translations/. +# Discovers locales from existing files in TRANSLATIONS_DIR (e.g. studio-es.jed.json → "es"). +# +lane :fetch_glotpress_translations do + files = Dir.glob(File.join(TRANSLATIONS_DIR, 'studio-*.jed.json')) + locales = files.map { |f| File.basename(f, '.jed.json').delete_prefix('studio-') }.sort + UI.user_error!('No existing translation files found in translations directory') if locales.empty? + + UI.message("Downloading translations for #{locales.length} locales from GlotPress...") + + locales.each do |locale| + url = URI("#{GLOTPRESS_PROJECT_URL}/#{locale}/default/export-translations/?format=jed1x") + response = Net::HTTP.get_response(url) + UI.user_error!("Failed to download translations for '#{locale}': HTTP #{response.code}") unless response.is_a?(Net::HTTPSuccess) + + output_path = File.join(TRANSLATIONS_DIR, "studio-#{locale}.jed.json") + File.write(output_path, response.body) + UI.message(" Downloaded #{locale}") + end + + UI.success("Downloaded translations for #{locales.length} locales") +end + # Download the latest translations and create a PR to merge them into the release branch. # # @param skip_confirm [Boolean] Skip interactive confirmation prompts (default: false) @@ -359,7 +388,7 @@ lane :download_translations do |skip_confirm: false, github_username: nil| PROMPT next unless skip_confirm || UI.confirm('Continue?') - sh('node', File.join(PROJECT_ROOT_FOLDER, 'scripts', 'download-available-site-translations.mjs')) + fetch_glotpress_translations translations_branch = 'update/latest-translations' Fastlane::Helper::GitHelper.delete_local_branch_if_exists!(translations_branch)