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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 0 additions & 26 deletions .buildkite/release-pipelines/download-translations.yml

This file was deleted.

2 changes: 1 addition & 1 deletion .buildkite/release-pipelines/new-beta-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ steps:
install_gems

echo "--- :package: Create New Beta"
bundle exec fastlane new_beta_release version:"${RELEASE_VERSION}" skip_confirm:true
bundle exec fastlane new_beta_release version:"${RELEASE_VERSION}" github_username:"${GITHUB_USERNAME}" skip_confirm:true
agents:
queue: mac
retry:
Expand Down
11 changes: 6 additions & 5 deletions docs/localization.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ No manual steps are needed for string extraction or import.

### Export and Add (automated)

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.
During each **beta release**, the `new_beta_release` Fastlane lane downloads translations
from GlotPress in Jed 1.x JSON format (which `@wordpress/i18n` understands) and commits
them directly to the release branch before bumping the version. It's ok if some translations
are missing — they will be left as English in the app.

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.
No manual steps are needed for translation export. The standalone `fetch_glotpress_translations`
lane can be used to manually download translations if needed.
5 changes: 3 additions & 2 deletions docs/release-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,29 @@ 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 `<version>-beta1` and triggers a build (the build tags and uploads to CDN)

### 2. Beta Releases

**ReleasesV2 milestone**: Beta Release | **Fastlane lane**: `new_beta_release`

- Downloads latest translations from GlotPress and commits them to the release branch
- Increments the beta number (e.g. beta1 → beta2)
- Triggers a build (the build tags the new version and uploads to CDN)
- Creates a backmerge PR from the release branch into `trunk`
- 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**: Review and refine the draft release notes in `RELEASE-NOTES.txt` on the `release/<version>` branch (a draft is auto-generated during code freeze)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ ❓ Does the removal of this step means that if a release have it's first beta on the same day as the code freeze… but then no additional beta for the rest of the release (because no bugs was found during beta-testing), we won't download the latest translations during release finalization and might thus miss the opportunity to get more recent translations that had been translated by translators post-code-freeze while the app was in beta?!

I'd suggest to always download the latest translations at the time of release finalization scenario phase milestone (or the day before, like we do for Tumblr) to ensure that they get downloaded late in the release cycle and thus give the most time for translators to translate as much as possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a good point -- the previous implementation, at the "Pre-Release" stage, wasn't fully in line with the process either. Running a final fetch during finalize release makes total sense 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated on f4ccc01.

- **Smoke tests**: Verify betas on macOS and Windows

### 4. Finalize Release

**ReleasesV2 milestone**: Release | **Fastlane lane**: `finalize_release`

- Downloads latest translations from GlotPress (to capture any added during the beta period)
- Removes beta suffix (sets version to `<version>`)
- Triggers the final release build, which uploads to the Apps CDN, creates a **draft** GitHub release with notes and download links, and notifies Slack

Expand Down
119 changes: 40 additions & 79 deletions fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -166,14 +166,18 @@ end
# Release Management Lanes
########################################################################

# Create a new release branch from trunk and the first beta.
# Create a new release branch from trunk, extract translatable strings, and generate draft release notes.
#
# - Extracts translatable strings and commits them to the release branch (for GlotPress import)
# - Creates a `release/<version>` branch from trunk
# - Delegates to {new_beta_release} to bump to beta1, commit, push, tag, and trigger a build
# - Extracts translatable strings and commits them to the release branch (for GlotPress import)
# - Generates draft release notes from merged PRs and commits them to the release branch
# - Creates a backmerge PR from the release branch to trunk (so wpcom cron can import strings to GlotPress)
#
# The first beta is triggered separately via {new_beta_release}.
#
# @param version [String] The version to freeze (e.g., '1.7.4')
# @param skip_confirm [Boolean] Skip interactive confirmation prompts (default: false)
# @param github_username [String, nil] GitHub username to assign as reviewer on the backmerge PR
#
lane :code_freeze do |version:, skip_confirm: false, github_username: nil|
branch_name = "release/#{version}"
Expand All @@ -184,9 +188,6 @@ 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}`
- 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?')

Expand All @@ -211,30 +212,29 @@ lane :code_freeze do |version:, skip_confirm: false, github_username: nil|

push_to_git_remote(set_upstream: true)

# Create the first beta (bumps version, commits, pushes, tags, triggers build)
new_beta_release(version: version, skip_confirm: skip_confirm)

# Create backmerge PR from the release branch to MAIN_BRANCH so that the pot strings bundle is uploaded to GlotPress once it hits trunk
# Important: this must come after new_beta_release because the backmerge action switches branches.
# Create backmerge PR so that the .pot strings file reaches trunk for GlotPress import
create_backmerge_pr(source_branch: branch_name, github_username: github_username)

UI.success("Code freeze complete! Created #{branch_name} with first beta")
UI.success("Code freeze complete! Created #{branch_name}")
end

# Create a new beta release on the current release branch.
#
# - Downloads latest translations from GlotPress and commits them
# - Determines the next beta number automatically from package.json
# - Bumps the version, commits, tags, and triggers a release build
# - Bumps the version, commits, pushes, and triggers a release build
# - Creates a backmerge PR from the release branch to trunk
Copy link
Contributor

Choose a reason for hiding this comment

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

Note that this means that code-freeze will create one backmerge PR (to commit the version bump + extracted translatable strings etc), then on the step close after that in the scenario, we will separately trigger the first new_beta_release… which will also create yet another backmerge PR (whose diff this time will mostly only contain the beta version bump and not much else), right?

I'm OK with that if that's acknowledged, but just wanted to highlight that side effect of the separation, since that now means 2 backmerge PRs close to one another instead of a single one (and means waiting for CI to go green on each of those… but for that we can improve the RMs life via pr_changed_files-based filtering to reduce the number of CI jobs to run and having to wait for especially on the 2nd PR I guess)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, good point, I discussed this a bit with @sejas. I like the fact that we'll have backmerge PRs for each beta + download translations and I think that's the main point I wanted to address after the discussions.

But one point I'm not totally convinced about is the need (or not) of the "automatic" first beta build during code freeze (which could create the first backmerge instead of having it both on code freeze + first beta). The implementation removed it, but on a second thought I think having it saves a couple of clicks and binds the release process together a bit more. 🤷

Copy link
Contributor

@AliSoftware AliSoftware Feb 27, 2026

Choose a reason for hiding this comment

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

Another option would be to remove the backmerge PR at the time of code_freeze, relying on the fact that a new_beta_release will be triggered afterwards as a separate step in the release scenario.

Thinking about it, this is kinda what we do in some other product repos that need a pause between the start of the code freeze and the first beta (e.g. DOAndroid)… except in those repos we've used the convention to have a lane named complete_code_freeze for the similar case.
I'm ok to not have a lane called complete_code_freeze here if all it does in the case of Studio is doing the exact same as just calling new_beta_release directly and nothing more btw. Just drawing parallels.

This also re-enforces my suggestion on the ReleasesV2 PR (205520-ghe-Automattic/wpcom#discussion_r201295) that this step to create the first beta post code-freeze, even if it's now on demand and not done as part of the code_freeze lane, should still be a Task that already exists in the scenario from scratch when the scenario is created, instead of relying on the Release Manager having to remember to click the "New Beta" button manually.

[EDIT] At the time I wrote this comment I hadn't seen your intermediate reply (as I hadn't refreshed the GitHub page) [/EDIT]

Copy link
Contributor Author

Choose a reason for hiding this comment

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

One important point about the Code Freeze backmerge is the .pot file update, so that's the main reason I kept the backmerge.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah but if we're guaranteed to have a beta being done as a follow up step after the backmerge (be it because the code_freeze lane calls the new_beta_release lane itself or because we have a 'Task::buildkitein the scenario to trigger bynew_beta_releasewhen ready at the end of theMilestone::code_freeze), then that backmerge PR that is going to be created as part of that first beta will also contain the change to the .pot`. So that would just lead to one (not so) big PR containing everything, instead of 2 smaller ones back to back.

Copy link
Contributor

@AliSoftware AliSoftware Feb 27, 2026

Choose a reason for hiding this comment

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

By the way, tangentially related, one small bug we found in the first release was that, if we do an automated (or separate task, for that matter) first beta, the second beta created using "Add Beta" button will still say Beta Release 1 😄 not sure if this came up in other apps, but in Studio this became more visible as the increment is part of the version used everywhere.

I think this has always been the case even in other products? And probably part of why we've been calling the betas added via the button as "Intermediate beta" (as a cheat to suggest that there's the first beta made during code freeze, then a first intermediate beta after that, etc) 😅

So even in other products where we use different naming conventions of where we do the first beta as part of the code freeze lane this is also the case (eg first beta done during code freeze is named -rc-1 and Intermediate Beta 1 makes this bumped to -rc-2)

Copy link
Contributor

Choose a reason for hiding this comment

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

Random example with PockatCasts Android:

So that off-by-one in the title has always been there.

Maybe if we renamed the task in Releases V2 from "Intermediate Beta 1" to "Intermediate Bugfix build 1" to make the distinction even clearer between "the first build done during code freeze" vs "subsequent beta builds that are only done if we found a bug and merged a fix and want to do a new beta to include that bugfix". But at that point I think that'd be wayyy too nitpicky 😄

Besides, there is also the seldom but possible case that a failed beta build would need to be retried and end up bumping the version number multiple times (see the case we had this morning with DOAndroid — p1772201201312009-slack-C06CKSPHYA1) which would lead to the version being -rc-2 at the end of the code freeze, and the "Intermediate Beta 1" would then produce an -rc-3 😛

So in the end I think we don't need to be too much attached with this off-by-one offset between "Intermediate Beta" index and -betaN version name?

Copy link
Member

@sejas sejas Feb 27, 2026

Choose a reason for hiding this comment

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

Would it be possible to separate the pots generation and the submission of strings to GlotPress from the code freeze milestone?

  • Day 1: Submit strings (Back merge)
  • Day 3: Produce Beta1 (Codefreeze + beta + backmerge)
  • Day 3+: Produce more betas manually + backmerge
  • Day 5 : Build release
  • Day 8: Publish release

Copy link
Member

@sejas sejas Feb 27, 2026

Choose a reason for hiding this comment

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

We decided p1772217237435699/1771256551.851819-slack-C06DRMD6VPZ to produce the code freeze and then a different milestone to produce a new beta1 a couple of days later. We always can iterate in these decisions after trying the real process a couple of cycles.

Copy link
Contributor

Choose a reason for hiding this comment

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

So if I understand the discussion from Slack correctly, that will be:

  • Day 1: Freeze the code and the associated strings (i.e. Code Freeze to create the release branch and generate the pot then backmerge to trunk so that the strings are uploaded to GlotPress)
  • Day 3: Produce Beta1 (new beta build + backmerge)
  • Day 3+: Produce more betas manually + backmerge
  • Day 5 : Build release
  • Day 8: Publish release

If so, then that sounds good to me 👍

#
# @param version [String] The base release version (e.g., '1.7.4')
# @param skip_confirm [Boolean] Skip interactive confirmation prompts (default: false)
# @param github_username [String, nil] GitHub username to assign as reviewer on the backmerge PR
#
lane :new_beta_release do |version:, skip_confirm: false|
lane :new_beta_release do |version:, skip_confirm: false, github_username: nil|
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).
# Skip the check when current_beta is 0, meaning this is the first beta (before any bump).
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
Expand All @@ -244,23 +244,38 @@ lane :new_beta_release do |version:, skip_confirm: false|

UI.important <<~PROMPT
Creating new beta release #{new_version}. This will:
- Download latest translations from GlotPress
- Bump version to #{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
- Create a backmerge PR from the release branch to `#{MAIN_BRANCH}`
PROMPT
next unless skip_confirm || UI.confirm('Continue?')

# Download latest translations so they're included in the beta build
fetch_glotpress_translations
git_add(path: [TRANSLATIONS_DIR])
git_commit(
path: [TRANSLATIONS_DIR],
message: '[skip ci] Update translations',
allow_nothing_to_commit: true
)

bump_version_commit_and_push(version: new_version)

trigger_release_build(version: new_version)

UI.success("New beta release created: v#{new_version}")

# Create backmerge PR so that translations and version bump get merged back to trunk
create_backmerge_pr(source_branch: "release/#{version}", github_username: github_username)
end

# Finalize the release by removing the beta suffix and triggering the final build.
#
# - Downloads latest translations from GlotPress (to capture any translations added during the beta period)
# - Bumps version to the final release number (removes beta suffix)
# - Triggers a release build in Buildkite
# - The build creates a draft GitHub release (with notes and download links) after uploading to CDN
Expand All @@ -272,6 +287,7 @@ end
lane :finalize_release do |version:, skip_confirm: false|
UI.important <<~PROMPT
Finalizing release #{version}. This will:
- Download latest translations from GlotPress
- Bump version to #{version} (remove beta suffix)
- Trigger a release build for all platforms (macOS, Windows), which will then:
- Upload build artifacts to the Apps CDN
Expand All @@ -280,6 +296,15 @@ lane :finalize_release do |version:, skip_confirm: false|
PROMPT
next unless skip_confirm || UI.confirm('Continue?')

# Download latest translations to capture any added during the beta period
fetch_glotpress_translations
git_add(path: [TRANSLATIONS_DIR])
git_commit(
path: [TRANSLATIONS_DIR],
message: '[skip ci] Update translations',
allow_nothing_to_commit: true
)

bump_version_commit_and_push(version: version)

trigger_release_build(version: version)
Expand Down Expand Up @@ -387,70 +412,6 @@ lane :fetch_glotpress_translations do
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)
#
lane :download_translations do |skip_confirm: false, github_username: nil|
current_branch = Fastlane::Helper::GitHelper.current_git_branch

UI.important <<~PROMPT
Downloading translations. This will:
- Download latest translations from GlotPress
- Create a PR to merge them into `#{current_branch}`
PROMPT
next unless skip_confirm || UI.confirm('Continue?')

fetch_glotpress_translations

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: ['.'])
result = git_commit(
path: ['.'],
message: 'Update translations',
allow_nothing_to_commit: true
)

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(github_username)
)

if is_ci? && pr_url
buildkite_annotate(
context: 'download-translations',
style: 'info',
message: "Translations Pull Request: #{pr_url}"
)
end
end

########################################################################
# Build and Distribution Helper Methods
########################################################################
Expand Down