Skip to content

Realign the release branch on its remote during checkout - #25856

Open
AliSoftware wants to merge 6 commits into
trunkfrom
ainfra-2725/reset-release-branch-checkout
Open

Realign the release branch on its remote during checkout#25856
AliSoftware wants to merge 6 commits into
trunkfrom
ainfra-2725/reset-release-branch-checkout

Conversation

@AliSoftware

@AliSoftware AliSoftware commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Note

Please do not merge this PR yet. It should wait until wordpress-mobile/release-toolkit#763 is merged and a new version of the release-toolkit gem has been released, so that the fastlane-plugin-wpmreleasetoolkit version can be bumped in Gemfile.lock as part of this same PR. Both halves of AINFRA-2725 then land together.

What does it do?

Part of AINFRA-2725, following the WooCommerce iOS 25.1 release incident. The same change is going out to every mobile product repo.

1. Realign the release branch on the remote (the actual fix)

.buildkite/commands/checkout-release-branch.sh fetched the release branch and checked it out, but never moved the local branch onto the fetched commit:

git fetch origin "$BRANCH_NAME"
git checkout "$BRANCH_NAME"

Buildkite cleans the working copy between jobs, but it can reuse it. A refs/heads/release/x.y left behind by an earlier job on the same agent therefore survives, and git checkout then simply switches to that stale local ref rather than to what was just fetched. Whatever runs next — the version bump, or the GitHub Release draft that finalize_release creates from HEAD — would then be based on the wrong commit.

The fix adds the missing realignment. reset --hard rather than git pull: no extra network round trip, and no merge commit if the refs diverged.

2. Reset to FETCH_HEAD rather than the remote-tracking ref

git fetch origin <branch> always writes FETCH_HEAD, but it only updates refs/remotes/origin/<branch> when the remote's fetch refspec covers that branch. With Buildkite's default +refs/heads/*:refs/remotes/origin/* the two are equivalent — but on a clone whose refspec was narrowed after origin/<branch> already existed, the fetch leaves that ref stale and reset --hard "origin/$BRANCH_NAME" lands on the old commit: the very failure this script exists to prevent, reintroduced through the back door. Resetting to FETCH_HEAD drops the refspec dependency entirely.

3. Standardize the script across all repos

Slightly tangent to the issue, but bundled deliberately: 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, that divergence is pure friction.

All repos now share one canonical script. 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 fires on a branch matching ^release/, and 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, both [[ -z "${RELEASE_VERSION}" ]] on an unset variable and a bare RELEASE_VERSION=$1 with no arguments abort with unbound variable before their intended usage message can print. And an argument that is passed but empty — what happens when a pipeline forwards an unset $RELEASE_VERSION — no longer resolves to a bare release/: it falls through to the environment variable, then to the release/* branch the build runs on, and finally to a clear error if none of those yields a version.

Testing instructions

No behaviour change on a fresh checkout, which is the normal case: the local branch is already at the fetched commit, so the reset is a no-op. The argument-resolution logic was exercised across all combinations (argument / empty argument / environment variable / release/* branch / trunk / feature branch / unset), and the resulting script passes shellcheck in every repo. The next release build exercising this script is the real check.

🤖 Generated with Claude Code

`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) <noreply@anthropic.com>
@AliSoftware
AliSoftware requested a review from a team as a code owner July 30, 2026 21:09
@AliSoftware AliSoftware added the Tooling Build, Release, and Validation Tools label Jul 30, 2026
@AliSoftware AliSoftware added this to the 27.2 milestone Jul 30, 2026
@wpmobilebot

wpmobilebot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor
App Icon📲 You can test the changes from this Pull Request in WordPress by scanning the QR code below to install the corresponding build.
App NameWordPress
ConfigurationRelease-Alpha
Build Number33557
VersionPR #25856
Bundle IDorg.wordpress.alpha
Commit0b07eee
Installation URL7b1291vbhjqm8
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@wpmobilebot

wpmobilebot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor
App Icon📲 You can test the changes from this Pull Request in Jetpack by scanning the QR code below to install the corresponding build.
App NameJetpack
ConfigurationRelease-Alpha
Build Number33557
VersionPR #25856
Bundle IDcom.jetpack.alpha
Commit0b07eee
Installation URL37ia17rl0qln0
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

Comment thread .buildkite/commands/checkout-release-branch.sh Outdated
AliSoftware and others added 3 commits July 31, 2026 19:01
"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) <noreply@anthropic.com>
`git fetch origin <branch>` always writes `FETCH_HEAD`, but it only updates `refs/remotes/origin/<branch>` 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/<branch>` 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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
@AliSoftware

Copy link
Copy Markdown
Contributor Author

@mokagio heads-up — these PRs grew since your review, so they're worth another look rather than assuming they're still the three-line change you saw. Same comment on all 13 repos.

Two commits were added on top of the wording fix you suggested:

1. Reset to FETCH_HEAD rather than origin/$BRANCH_NAME. git fetch origin <branch> always writes FETCH_HEAD, but it only updates refs/remotes/origin/<branch> when the remote's configured fetch refspec covers that branch. With Buildkite's default +refs/heads/*:refs/remotes/origin/* the two are equivalent — but on a clone whose refspec was narrowed after origin/<branch> already existed, the fetch leaves that ref stale and the reset lands on the old commit: the very failure this script exists to prevent, reintroduced through the back door. Resetting to FETCH_HEAD drops the refspec dependency entirely.

2. Standardized the script across all 13 repos. Tangent to the original issue, bundled deliberately. The script had drifted into five different shapes — 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 simplenote-android) no argument at all, reading RELEASE_VERSION from the environment. Having rolled the same one-line fix out thirteen times this week, that divergence is pure friction.

All 13 now share one canonical script. 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 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, both [[ -z "${RELEASE_VERSION}" ]] on an unset variable and a bare RELEASE_VERSION=$1 with no arguments abort with unbound variable before their intended usage message can print. And an argument that is passed but empty — what happens when a pipeline forwards an unset $RELEASE_VERSION — is now a hard error everywhere, instead of resolving to release/ or silently falling through to the current branch.

The scripts are byte-identical across all 13 repos and pass shellcheck everywhere; the argument resolution was exercised across every combination (argument / empty argument / environment variable / release/* / trunk / feature branch / unset). Full rationale in the updated PR description.

Worth flagging that the release-toolkit PR also picked up a real fix from your review — the tag-based lookup was preferring a leftover draft over the release that actually owns the tag.

@AliSoftware
AliSoftware requested a review from mokagio July 31, 2026 19:46
AliSoftware and others added 2 commits July 31, 2026 22:11
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
@wpmobilebot

Copy link
Copy Markdown
Contributor

🤖 Build Failure Analysis

This build has failures. Claude has analyzed them - check the build annotations for details.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Tooling Build, Release, and Validation Tools

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants