From 7fcbc7174d0674a5e3fe6cdea0d417cec77c3613 Mon Sep 17 00:00:00 2001 From: Olivier Halligon Date: Thu, 30 Jul 2026 22:57:55 +0200 Subject: [PATCH 1/9] Realign the release branch on its remote during checkout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `checkout-release-branch.sh` fetched the release branch then checked it out, but never moved the local branch to the fetched commit. Buildkite cleans the working copy between jobs, yet can reuse it — so a `refs/heads/release/x.y` left behind by an earlier job on the same agent survives, and `git checkout` then just switches to that stale local ref instead of the freshly fetched remote one. Anything running afterwards, such as the version bump and the GitHub Release draft created by `finalize_release`, would target the wrong commit. Adding `git reset --hard "origin/$BRANCH_NAME"` after the checkout makes the branch match the remote unconditionally. `reset --hard` rather than `git pull`: it needs no extra network round trip and cannot produce a merge if the local and remote refs have diverged. This is the second part of AINFRA-2725, a follow-up to the WooCommerce iOS 25.1 release incident. Co-Authored-By: Claude Opus 5 (1M context) --- .buildkite/commands/checkout-release-branch.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.buildkite/commands/checkout-release-branch.sh b/.buildkite/commands/checkout-release-branch.sh index fe570fa93ad6..7dacc62fe11f 100755 --- a/.buildkite/commands/checkout-release-branch.sh +++ b/.buildkite/commands/checkout-release-branch.sh @@ -9,3 +9,6 @@ RELEASE_VERSION="${1?Please provide a release version as an argument.}" BRANCH_NAME="release/${RELEASE_VERSION}" git fetch origin "$BRANCH_NAME" git checkout "$BRANCH_NAME" +# Buildkite can reuse a working copy where "$BRANCH_NAME" was left at an older commit by a previous job, +# so realign it on the remote. `reset --hard` rather than `git pull`, to avoid merging if the two diverged. +git reset --hard "origin/$BRANCH_NAME" From 33acecb1e169cd9675fc9377ce4ae39c3495ddb1 Mon Sep 17 00:00:00 2001 From: Olivier Halligon Date: Fri, 31 Jul 2026 19:01:20 +0200 Subject: [PATCH 2/9] Clarify the wording of the realignment comment "realign it on the remote" read as though the operation happened *on* the remote, rather than describing what the local branch is realigned against. Say plainly what the reset does instead: force the local branch to the fetched commit. Wording suggested by @mokagio in review. Co-Authored-By: Claude Opus 5 (1M context) --- .buildkite/commands/checkout-release-branch.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.buildkite/commands/checkout-release-branch.sh b/.buildkite/commands/checkout-release-branch.sh index 7dacc62fe11f..9d7448882ff8 100755 --- a/.buildkite/commands/checkout-release-branch.sh +++ b/.buildkite/commands/checkout-release-branch.sh @@ -10,5 +10,6 @@ BRANCH_NAME="release/${RELEASE_VERSION}" git fetch origin "$BRANCH_NAME" git checkout "$BRANCH_NAME" # Buildkite can reuse a working copy where "$BRANCH_NAME" was left at an older commit by a previous job, -# so realign it on the remote. `reset --hard` rather than `git pull`, to avoid merging if the two diverged. +# so force the local branch to the fetched commit. `reset --hard` rather than +# `git pull`, to avoid merging if the two diverged. git reset --hard "origin/$BRANCH_NAME" From 44a669938e8001c609d376ec1dd848749c72b57a Mon Sep 17 00:00:00 2001 From: Olivier Halligon Date: Fri, 31 Jul 2026 19:41:07 +0200 Subject: [PATCH 3/9] Reset to FETCH_HEAD rather than the remote-tracking ref MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `git fetch origin ` always writes `FETCH_HEAD`, but it only updates `refs/remotes/origin/` when the remote's configured fetch refspec covers that branch. With the default `+refs/heads/*:refs/remotes/origin/*` that Buildkite sets up, the two are equivalent — but on a clone whose refspec was narrowed after `origin/` already existed, the fetch leaves that ref stale and `reset --hard "origin/$BRANCH_NAME"` silently lands on the old commit: exactly the failure this script is meant to prevent, reintroduced through the back door. Resetting to `FETCH_HEAD` removes the dependency on the refspec entirely — it is whatever the line above just fetched. Co-Authored-By: Claude Opus 5 (1M context) --- .buildkite/commands/checkout-release-branch.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.buildkite/commands/checkout-release-branch.sh b/.buildkite/commands/checkout-release-branch.sh index 9d7448882ff8..fc3a05cb5268 100755 --- a/.buildkite/commands/checkout-release-branch.sh +++ b/.buildkite/commands/checkout-release-branch.sh @@ -11,5 +11,6 @@ git fetch origin "$BRANCH_NAME" git checkout "$BRANCH_NAME" # Buildkite can reuse a working copy where "$BRANCH_NAME" was left at an older commit by a previous job, # so force the local branch to the fetched commit. `reset --hard` rather than -# `git pull`, to avoid merging if the two diverged. -git reset --hard "origin/$BRANCH_NAME" +# `git pull`, to avoid merging if the two diverged; `FETCH_HEAD` rather than +# `origin/$BRANCH_NAME`, which `git fetch ` only updates opportunistically. +git reset --hard FETCH_HEAD From 272144db73a028cc4b3bdba1bd9eff030044603d Mon Sep 17 00:00:00 2001 From: Olivier Halligon Date: Fri, 31 Jul 2026 20:08:23 +0200 Subject: [PATCH 4/9] Standardize checkout-release-branch.sh across all product repos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The script had drifted into five different shapes across the repos: argument required via `${1?…}` or `${1:?…}`, argument plus a `BUILDKITE_BRANCH` fallback, argument with a hand-rolled usage check, the same under a different variable name, and — in one repo — no argument at all, reading `RELEASE_VERSION` from the environment. Having rolled the same one-line fix out thirteen times this week, the divergence is pure friction, so this settles on a single canonical version. The resolution order is a superset of what every repo did before — argument, then `RELEASE_VERSION` from the environment, then the `release/*` branch the build runs on — so no call site needed changing. The `BUILDKITE_BRANCH` fallback only ever fires on a branch matching `^release/`, and it derives the branch name back from that same value, so it cannot select a branch other than the one the build was already triggered on. It also closes two latent bugs. Under `bash -eu`, `[[ -z "${RELEASE_VERSION}" ]]` on an unset variable and a bare `RELEASE_VERSION=$1` with no arguments both abort with `unbound variable` before their intended usage message can print. And an argument that is passed but empty — which happens when a pipeline forwards an unset `$RELEASE_VERSION` — is now a hard error everywhere, rather than resolving to `release/` or silently falling through to the current branch. The redundant `echo '--- :git: Checkout Release Branch'` in simplenote-android's pipelines is dropped, since the canonical script prints that group header itself. Co-Authored-By: Claude Opus 5 (1M context) --- .../commands/checkout-release-branch.sh | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/.buildkite/commands/checkout-release-branch.sh b/.buildkite/commands/checkout-release-branch.sh index fc3a05cb5268..2f70e033f7ba 100755 --- a/.buildkite/commands/checkout-release-branch.sh +++ b/.buildkite/commands/checkout-release-branch.sh @@ -1,11 +1,35 @@ #!/bin/bash -eu +# Checks out the `release/*` branch for a given release version. +# +# Usage: checkout-release-branch.sh [] +# +# The release version is taken from, in order of precedence: +# 1. the first argument, if one is passed +# 2. the `RELEASE_VERSION` environment variable +# 3. the `release/*` branch the build is running on, via `BUILDKITE_BRANCH` +# +# Buildkite, by default, checks out a specific commit, ending up in a detached HEAD state. +# But some release steps need to be on the `release/*` branch instead, namely: +# - when a `release-pipelines/*.yml` needs to `git push` to the `release/*` branch (for version bumps) +# - when a job `pipeline upload`'d by such a pipeline builds from that branch, so that the build +# includes the commit that the version bump just added. + echo "--- :git: Checkout Release Branch" -RELEASE_VERSION="${1?Please provide a release version as an argument.}" +if [[ $# -gt 0 ]]; then + # An argument was passed explicitly, so it must not be empty — that would mean the caller + # meant to forward a version but it resolved to nothing, which is a pipeline misconfiguration. + RELEASE_VERSION="${1:?release version argument was passed but is empty}" +elif [[ -n "${RELEASE_VERSION:-}" ]]; then + : # Already provided through the pipeline environment +elif [[ "${BUILDKITE_BRANCH:-}" =~ ^release/ ]]; then + RELEASE_VERSION="${BUILDKITE_BRANCH#release/}" +else + echo "Error: no release version. Pass it as \$1, set RELEASE_VERSION, or run on a release/* branch." >&2 + exit 1 +fi -# Buildkite, by default, checks out a specific commit. For many release actions, we need to be -# on a release branch instead. BRANCH_NAME="release/${RELEASE_VERSION}" git fetch origin "$BRANCH_NAME" git checkout "$BRANCH_NAME" From d6a5b9fe77187a374bea201d4acb49dc86e817f1 Mon Sep 17 00:00:00 2001 From: Olivier Halligon Date: Fri, 31 Jul 2026 22:11:19 +0200 Subject: [PATCH 5/9] Simplify the release version resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collapse the three-way `if/elif/else` into a plain assignment plus two guards. The `: # Already provided through the pipeline environment` no-op branch existed only to skip reassigning a value that was already correct, which reads oddly for anyone who has not just written it. The one behavioural difference is that an argument that is passed but empty no longer gets its own dedicated error: it now falls through to the environment variable, then to the `release/*` branch, and finally to the same generic error as the unset case. That is a rare enough situation to not be worth a distinct branch, and when the fallback does catch it, it resolves to the branch the build is already running on, which cannot be a different branch than intended. Note the nested guard in `${1:-${RELEASE_VERSION:-}}`: written as `${1:-$RELEASE_VERSION}`, the default expression itself dereferences an unset variable, so under `bash -eu` the script would abort with `RELEASE_VERSION: unbound variable` when neither is set — the same latent failure this standardization removed from a couple of the repos. Also say "a different commit" rather than "an older commit" when describing the reused working copy, since a stale local ref is not necessarily behind the remote. Co-Authored-By: Claude Opus 5 (1M context) --- .../commands/checkout-release-branch.sh | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/.buildkite/commands/checkout-release-branch.sh b/.buildkite/commands/checkout-release-branch.sh index 2f70e033f7ba..4a610f063ef7 100755 --- a/.buildkite/commands/checkout-release-branch.sh +++ b/.buildkite/commands/checkout-release-branch.sh @@ -4,8 +4,8 @@ # # Usage: checkout-release-branch.sh [] # -# The release version is taken from, in order of precedence: -# 1. the first argument, if one is passed +# The release version is taken from the first of these that is set and non-empty: +# 1. the first argument # 2. the `RELEASE_VERSION` environment variable # 3. the `release/*` branch the build is running on, via `BUILDKITE_BRANCH` # @@ -17,15 +17,13 @@ echo "--- :git: Checkout Release Branch" -if [[ $# -gt 0 ]]; then - # An argument was passed explicitly, so it must not be empty — that would mean the caller - # meant to forward a version but it resolved to nothing, which is a pipeline misconfiguration. - RELEASE_VERSION="${1:?release version argument was passed but is empty}" -elif [[ -n "${RELEASE_VERSION:-}" ]]; then - : # Already provided through the pipeline environment -elif [[ "${BUILDKITE_BRANCH:-}" =~ ^release/ ]]; then +RELEASE_VERSION="${1:-${RELEASE_VERSION:-}}" + +if [[ -z "$RELEASE_VERSION" && "${BUILDKITE_BRANCH:-}" =~ ^release/ ]]; then RELEASE_VERSION="${BUILDKITE_BRANCH#release/}" -else +fi + +if [[ -z "$RELEASE_VERSION" ]]; then echo "Error: no release version. Pass it as \$1, set RELEASE_VERSION, or run on a release/* branch." >&2 exit 1 fi @@ -33,7 +31,7 @@ fi BRANCH_NAME="release/${RELEASE_VERSION}" git fetch origin "$BRANCH_NAME" git checkout "$BRANCH_NAME" -# Buildkite can reuse a working copy where "$BRANCH_NAME" was left at an older commit by a previous job, +# Buildkite can reuse a working copy where "$BRANCH_NAME" was left at a different commit by a previous job, # so force the local branch to the fetched commit. `reset --hard` rather than # `git pull`, to avoid merging if the two diverged; `FETCH_HEAD` rather than # `origin/$BRANCH_NAME`, which `git fetch ` only updates opportunistically. From a243e48098efce930ee87f7aec168ed71397fc68 Mon Sep 17 00:00:00 2001 From: Olivier Halligon Date: Fri, 31 Jul 2026 22:27:32 +0200 Subject: [PATCH 6/9] Reformat the realignment comment as a bullet list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two justifications—`reset --hard` over `git pull`, and `FETCH_HEAD` over the remote-tracking ref—were run together in a prose paragraph that wrapped mid-clause, so neither stood out. Split them into bullets under the sentence stating what the reset does. Co-Authored-By: Claude Opus 5 (1M context) --- .buildkite/commands/checkout-release-branch.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.buildkite/commands/checkout-release-branch.sh b/.buildkite/commands/checkout-release-branch.sh index 4a610f063ef7..0650af0d1d41 100755 --- a/.buildkite/commands/checkout-release-branch.sh +++ b/.buildkite/commands/checkout-release-branch.sh @@ -32,7 +32,7 @@ BRANCH_NAME="release/${RELEASE_VERSION}" git fetch origin "$BRANCH_NAME" git checkout "$BRANCH_NAME" # Buildkite can reuse a working copy where "$BRANCH_NAME" was left at a different commit by a previous job, -# so force the local branch to the fetched commit. `reset --hard` rather than -# `git pull`, to avoid merging if the two diverged; `FETCH_HEAD` rather than -# `origin/$BRANCH_NAME`, which `git fetch ` only updates opportunistically. +# so force the local branch to the fetched commit. +# - `reset --hard` rather than `git pull`, to avoid merging if the two diverged +# - `FETCH_HEAD` rather than `origin/$BRANCH_NAME`, which `git fetch ` only updates opportunistically. git reset --hard FETCH_HEAD From addf735420e6c5929852f30ca0b1dbaf402dcc4e Mon Sep 17 00:00:00 2001 From: Olivier Halligon Date: Mon, 3 Aug 2026 15:09:44 +0200 Subject: [PATCH 7/9] Bump the development Ruby to 3.4.9 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `bundle update` fails on 3.2.2 while building nokogiri 1.19.4 from source—`gumbo.c: fatal error: 'nokogiri_gumbo.h' file not found`—which blocks picking up any new release-toolkit version. The lockfile pins `PLATFORMS: ruby`, so there is no precompiled gem to fall back on and the native build has to succeed. 3.4.9 is what the rest of the mobile repos already use. Co-Authored-By: Claude Opus 5 (1M context) --- .ruby-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ruby-version b/.ruby-version index be94e6f53db6..7bcbb3808b50 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.2.2 +3.4.9 From 7107cbdc1959349c62c35eb821d9426076eabc31 Mon Sep 17 00:00:00 2001 From: Olivier Halligon Date: Mon, 3 Aug 2026 15:10:15 +0200 Subject: [PATCH 8/9] Update release-toolkit to 14.11.2 Picks up wordpress-mobile/release-toolkit#763, which makes `publish_github_release` publish the most recently created GitHub Release when several share the same name rather than whichever one the API happened to list first. That is the other half of AINFRA-2725: without it, a re-run of `finalize_release` can still leave the git tag on the wrong commit, which is what caused the WooCommerce iOS 25.1 incident. `bundle update` also refreshed a few unrelated transitive gems that had newer releases, and bumped `BUNDLED WITH` to the current 4.0.17. Co-Authored-By: Claude Opus 5 (1M context) --- Gemfile.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 182b104c63d0..5c22e1264a4b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,7 +9,7 @@ GEM ast (2.4.3) atomos (0.1.3) aws-eventstream (1.4.0) - aws-partitions (1.1274.0) + aws-partitions (1.1277.0) aws-sdk-core (3.254.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) @@ -21,7 +21,7 @@ GEM aws-sdk-kms (1.130.0) aws-sdk-core (~> 3, >= 3.254.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.228.1) + aws-sdk-s3 (1.228.2) aws-sdk-core (~> 3, >= 3.254.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) @@ -168,7 +168,7 @@ GEM google-apis-firebaseappdistribution_v1alpha (>= 0.12.0) fastlane-plugin-sentry (1.29.0) os (~> 1.1, >= 1.1.4) - fastlane-plugin-wpmreleasetoolkit (14.11.1) + fastlane-plugin-wpmreleasetoolkit (14.11.2) buildkit (~> 1.5) chroma (= 0.2.0) diffy (~> 3.3) @@ -251,7 +251,7 @@ GEM mutex_m java-properties (0.3.0) jmespath (1.6.2) - json (2.21.1) + json (2.21.2) jwt (3.2.0) base64 kramdown (2.5.2) @@ -385,4 +385,4 @@ DEPENDENCIES rmagick (~> 7.1) BUNDLED WITH - 2.6.8 + 4.0.17 From d13de060f210150fd199a87bda79df0d39c7b0d4 Mon Sep 17 00:00:00 2001 From: Olivier Halligon Date: Mon, 3 Aug 2026 16:49:37 +0200 Subject: [PATCH 9/9] Keep the pinned Ruby and bundler, and only update the toolkit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts the earlier `.ruby-version` bump to 3.4.9 and re-resolves the lockfile with the bundler this repo was already pinned to. Ruby 3.4.9 is only present on the `xcode-26.6` CI image and newer—26.5 ships 3.4.2, 26.4.1 ships 3.4.0—so pinning it here breaks any job running on an earlier image. The bump was never needed to consume the new toolkit anyway: `fastlane-plugin-wpmreleasetoolkit` 14.11.2 declares `required_ruby_version >= 3.2.2`, exactly as 13.8.1 did. It was only needed to let `bundle update` compile nokogiri's native extension locally, which `bundle lock --update` sidesteps by resolving without installing. Updating Ruby across the release pipelines is a worthwhile change, but it is a bigger one than it looks and does not belong bundled with a gem bump. Co-Authored-By: Claude Opus 5 (1M context) --- .ruby-version | 2 +- Gemfile.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.ruby-version b/.ruby-version index 7bcbb3808b50..be94e6f53db6 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.4.9 +3.2.2 diff --git a/Gemfile.lock b/Gemfile.lock index 5c22e1264a4b..2b3f78474c6a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -385,4 +385,4 @@ DEPENDENCIES rmagick (~> 7.1) BUNDLED WITH - 4.0.17 + 2.6.8