From 36861b40d0c9b789613e2e735c462c91ce8ad3ec Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Fri, 13 Mar 2026 17:33:14 +0100 Subject: [PATCH 1/7] fix: prevent duplicate private sync pr in release script merged_untagged case was creating a private sync PR without checking if one already existed, unlike tagged_private_stale which had the guard. Co-Authored-By: Claude Sonnet 4.6 --- scripts/release.ts | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/scripts/release.ts b/scripts/release.ts index 0d643891fd0..19d6b58946d 100644 --- a/scripts/release.ts +++ b/scripts/release.ts @@ -610,15 +610,24 @@ const doRegularRelease = async () => { await git().push(['origin', '--tags']) console.log(chalk.green(`Tagged ${nextVersion}.`)) - console.log(chalk.green('Creating PR to sync private to main...')) - const privatePrUrl = await createPr({ - base: 'private', - head: 'main', - title: `chore: sync private to ${nextVersion}`, - body: `Sync private branch to main after release ${nextVersion}.`, - }) - console.log(chalk.green(`Private sync PR created: ${privatePrUrl}`)) - console.log(chalk.green('Merge it on GitHub to complete the release.')) + const existingPrivatePrAfterTag = await findOpenPr('main', 'private') + if (existingPrivatePrAfterTag) { + console.log( + chalk.yellow( + `Private sync PR already open: #${existingPrivatePrAfterTag.number}. Merge it on GitHub.`, + ), + ) + } else { + console.log(chalk.green('Creating PR to sync private to main...')) + const privatePrUrl = await createPr({ + base: 'private', + head: 'main', + title: `chore: sync private to ${nextVersion}`, + body: `Sync private branch to main after release ${nextVersion}.`, + }) + console.log(chalk.green(`Private sync PR created: ${privatePrUrl}`)) + console.log(chalk.green('Merge it on GitHub to complete the release.')) + } break } From 9b15248ef798a05ec498c0c5d24e54b76a2bc521 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Fri, 13 Mar 2026 17:53:42 +0100 Subject: [PATCH 2/7] fix: prevent duplicate private sync pr in release script tagged_private_stale case was creating a private sync PR even when private was already content-identical to main (SHA mismatch due to propagation delay after a sync PR merges). Added a content diff check to bail early in that case. Same guard applied to the hotfix path. The merged_untagged case also gets the open-PR guard for belt-and-suspenders. Co-Authored-By: Claude Sonnet 4.6 --- scripts/release.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/scripts/release.ts b/scripts/release.ts index 19d6b58946d..bec19b84fcc 100644 --- a/scripts/release.ts +++ b/scripts/release.ts @@ -634,6 +634,12 @@ const doRegularRelease = async () => { case 'tagged_private_stale': { console.log(chalk.yellow(`${nextVersion} is tagged but private is behind main.`)) + const privateDiff = await git().diff(['origin/main', 'origin/private']) + if (!privateDiff) { + console.log(chalk.green('Private is already in sync with main content-wise. Nothing to do.')) + break + } + const existingPrivatePr = await findOpenPr('main', 'private') if (existingPrivatePr) { console.log( @@ -796,6 +802,12 @@ const doHotfixRelease = async () => { case 'tagged_private_stale': { console.log(chalk.yellow(`${nextVersion} is tagged but private is behind main.`)) + const privateDiffHotfix = await git().diff(['origin/main', 'origin/private']) + if (!privateDiffHotfix) { + console.log(chalk.green('Private is already in sync with main content-wise. Nothing to do.')) + break + } + const existingPrivatePr = await findOpenPr('main', 'private') if (existingPrivatePr) { console.log( From 33f7c0c969df462c53906038faa56ee4b2598ed9 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Fri, 13 Mar 2026 19:27:59 +0100 Subject: [PATCH 3/7] fix: idle prereleaseMerged content diff + tagged_private_stale backmerge auto-merge - idle case: use git diff content check instead of SHA equality for prereleaseMerged - squash merges diverge SHAs even when content matches - tagged_private_stale (regular + hotfix): set auto-merge with merge commit strategy on backmerge PR so it lands without manual intervention Co-Authored-By: Claude Sonnet 4.6 --- scripts/release.ts | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/scripts/release.ts b/scripts/release.ts index bec19b84fcc..355e19d772d 100644 --- a/scripts/release.ts +++ b/scripts/release.ts @@ -515,7 +515,9 @@ const doRegularRelease = async () => { // Two sub-states within idle: // 1. Release branch ahead of main (prerelease merged) -> create release -> main PR // 2. Release branch matches main (fresh start) -> create develop -> release PR - const prereleaseMerged = releaseSha !== mainSha + // Use content diff (not SHA) to handle squash merges where SHAs diverge but content is identical + const releaseMatchesMain = !(await git().diff(['origin/main', 'origin/release'])) + const prereleaseMerged = releaseSha !== mainSha && !releaseMatchesMain if (prereleaseMerged) { const messages = await getCommitMessages(`${latestTag}..origin/release`) @@ -636,7 +638,9 @@ const doRegularRelease = async () => { const privateDiff = await git().diff(['origin/main', 'origin/private']) if (!privateDiff) { - console.log(chalk.green('Private is already in sync with main content-wise. Nothing to do.')) + console.log( + chalk.green('Private is already in sync with main content-wise. Nothing to do.'), + ) break } @@ -657,6 +661,26 @@ const doRegularRelease = async () => { }) console.log(chalk.green(`Private sync PR created: ${privatePrUrl}`)) } + + const existingBackmerge = await findOpenPr('main', 'develop') + if (!existingBackmerge) { + const mainDevelopCommits = await getCommitMessages('origin/develop..origin/main') + if (mainDevelopCommits.length > 0) { + console.log(chalk.green('Creating backmerge PR (main -> develop)...')) + const backmergeUrl = await createPr({ + base: 'develop', + head: 'main', + title: `chore: backmerge ${nextVersion} into develop`, + body: `Backmerge main into develop after release ${nextVersion}.`, + }) + console.log(chalk.green(`Backmerge PR created: ${backmergeUrl}`)) + console.log(chalk.green('Setting auto-merge with merge commit strategy...')) + await pify(exec)(`gh pr merge --auto --merge ${backmergeUrl}`) + console.log( + chalk.green('Auto-merge set. Backmerge will merge automatically when CI passes.'), + ) + } + } break } @@ -804,7 +828,9 @@ const doHotfixRelease = async () => { const privateDiffHotfix = await git().diff(['origin/main', 'origin/private']) if (!privateDiffHotfix) { - console.log(chalk.green('Private is already in sync with main content-wise. Nothing to do.')) + console.log( + chalk.green('Private is already in sync with main content-wise. Nothing to do.'), + ) break } @@ -838,6 +864,11 @@ const doHotfixRelease = async () => { body: `Backmerge main into develop after hotfix ${nextVersion}.`, }) console.log(chalk.green(`Backmerge PR created: ${backmergeUrl}`)) + console.log(chalk.green('Setting auto-merge with merge commit strategy...')) + await pify(exec)(`gh pr merge --auto --merge ${backmergeUrl}`) + console.log( + chalk.green('Auto-merge set. Backmerge will merge automatically when CI passes.'), + ) } } break From 66bb8404ae84ef0371c375c00729f0ab2ab31c08 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Fri, 13 Mar 2026 23:30:34 +0100 Subject: [PATCH 4/7] fix: coderabbitai review - prereleaseMerged ahead-check + no early break in tagged_private_stale - idle: replace SHA/content-diff prereleaseMerged with commit-ahead check (origin/main..origin/release) - prevents false positive when release is *behind* main (e.g. post-hotfix), which would have routed into release PR path with 0 commits - tagged_private_stale (regular + hotfix): remove early break when private is content-synced - script must still evaluate backmerge PR creation even when private sync is a no-op Co-Authored-By: Claude Sonnet 4.6 --- scripts/release.ts | 85 ++++++++++++++++++++++++---------------------- 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/scripts/release.ts b/scripts/release.ts index 355e19d772d..1e8e0ac0e9a 100644 --- a/scripts/release.ts +++ b/scripts/release.ts @@ -515,9 +515,12 @@ const doRegularRelease = async () => { // Two sub-states within idle: // 1. Release branch ahead of main (prerelease merged) -> create release -> main PR // 2. Release branch matches main (fresh start) -> create develop -> release PR - // Use content diff (not SHA) to handle squash merges where SHAs diverge but content is identical + // Use commit-ahead check (not SHA) to correctly detect if release is ahead of main. + // SHA equality breaks with squash merges; content diff alone breaks when release is *behind* main. const releaseMatchesMain = !(await git().diff(['origin/main', 'origin/release'])) - const prereleaseMerged = releaseSha !== mainSha && !releaseMatchesMain + const releaseHasCommitsNotInMain = + (await getCommitMessages('origin/main..origin/release')).length > 0 + const prereleaseMerged = releaseHasCommitsNotInMain && !releaseMatchesMain if (prereleaseMerged) { const messages = await getCommitMessages(`${latestTag}..origin/release`) @@ -637,29 +640,30 @@ const doRegularRelease = async () => { console.log(chalk.yellow(`${nextVersion} is tagged but private is behind main.`)) const privateDiff = await git().diff(['origin/main', 'origin/private']) - if (!privateDiff) { - console.log( - chalk.green('Private is already in sync with main content-wise. Nothing to do.'), - ) - break - } + const shouldSyncPrivate = Boolean(privateDiff) - const existingPrivatePr = await findOpenPr('main', 'private') - if (existingPrivatePr) { + if (!shouldSyncPrivate) { console.log( - chalk.yellow( - `Private sync PR already open: #${existingPrivatePr.number}. Merge it on GitHub.`, - ), + chalk.green('Private is already in sync with main content-wise. Skipping private sync PR.'), ) } else { - console.log(chalk.green('Creating PR to sync private to main...')) - const privatePrUrl = await createPr({ - base: 'private', - head: 'main', - title: `chore: sync private to ${nextVersion}`, - body: `Sync private branch to main after release ${nextVersion}.`, - }) - console.log(chalk.green(`Private sync PR created: ${privatePrUrl}`)) + const existingPrivatePr = await findOpenPr('main', 'private') + if (existingPrivatePr) { + console.log( + chalk.yellow( + `Private sync PR already open: #${existingPrivatePr.number}. Merge it on GitHub.`, + ), + ) + } else { + console.log(chalk.green('Creating PR to sync private to main...')) + const privatePrUrl = await createPr({ + base: 'private', + head: 'main', + title: `chore: sync private to ${nextVersion}`, + body: `Sync private branch to main after release ${nextVersion}.`, + }) + console.log(chalk.green(`Private sync PR created: ${privatePrUrl}`)) + } } const existingBackmerge = await findOpenPr('main', 'develop') @@ -827,29 +831,30 @@ const doHotfixRelease = async () => { console.log(chalk.yellow(`${nextVersion} is tagged but private is behind main.`)) const privateDiffHotfix = await git().diff(['origin/main', 'origin/private']) - if (!privateDiffHotfix) { - console.log( - chalk.green('Private is already in sync with main content-wise. Nothing to do.'), - ) - break - } + const shouldSyncPrivateHotfix = Boolean(privateDiffHotfix) - const existingPrivatePr = await findOpenPr('main', 'private') - if (existingPrivatePr) { + if (!shouldSyncPrivateHotfix) { console.log( - chalk.yellow( - `Private sync PR already open: #${existingPrivatePr.number}. Merge it on GitHub.`, - ), + chalk.green('Private is already in sync with main content-wise. Skipping private sync PR.'), ) } else { - console.log(chalk.green('Creating PR to sync private to main...')) - const privatePrUrl = await createPr({ - base: 'private', - head: 'main', - title: `chore: sync private to ${nextVersion}`, - body: `Sync private branch to main after hotfix ${nextVersion}.`, - }) - console.log(chalk.green(`Private sync PR created: ${privatePrUrl}`)) + const existingPrivatePr = await findOpenPr('main', 'private') + if (existingPrivatePr) { + console.log( + chalk.yellow( + `Private sync PR already open: #${existingPrivatePr.number}. Merge it on GitHub.`, + ), + ) + } else { + console.log(chalk.green('Creating PR to sync private to main...')) + const privatePrUrl = await createPr({ + base: 'private', + head: 'main', + title: `chore: sync private to ${nextVersion}`, + body: `Sync private branch to main after hotfix ${nextVersion}.`, + }) + console.log(chalk.green(`Private sync PR created: ${privatePrUrl}`)) + } } const existingBackmerge = await findOpenPr('main', 'develop') From c0f8456f50f53b9508559b90beb7b490a8a390d0 Mon Sep 17 00:00:00 2001 From: gomes-bot Date: Sat, 14 Mar 2026 00:11:41 +0100 Subject: [PATCH 5/7] fix: prettier formatting in release script --- scripts/release.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/release.ts b/scripts/release.ts index 1e8e0ac0e9a..faeaba4ff06 100644 --- a/scripts/release.ts +++ b/scripts/release.ts @@ -644,7 +644,9 @@ const doRegularRelease = async () => { if (!shouldSyncPrivate) { console.log( - chalk.green('Private is already in sync with main content-wise. Skipping private sync PR.'), + chalk.green( + 'Private is already in sync with main content-wise. Skipping private sync PR.', + ), ) } else { const existingPrivatePr = await findOpenPr('main', 'private') @@ -835,7 +837,9 @@ const doHotfixRelease = async () => { if (!shouldSyncPrivateHotfix) { console.log( - chalk.green('Private is already in sync with main content-wise. Skipping private sync PR.'), + chalk.green( + 'Private is already in sync with main content-wise. Skipping private sync PR.', + ), ) } else { const existingPrivatePr = await findOpenPr('main', 'private') From 7f2522501a3cdf395565833cee0b2d4c01da8315 Mon Sep 17 00:00:00 2001 From: gomes-bot Date: Mon, 16 Mar 2026 10:24:25 +0100 Subject: [PATCH 6/7] fix: enable auto-merge on existing backmerge PRs during reruns Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/release.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/scripts/release.ts b/scripts/release.ts index faeaba4ff06..d5b5372e830 100644 --- a/scripts/release.ts +++ b/scripts/release.ts @@ -669,7 +669,15 @@ const doRegularRelease = async () => { } const existingBackmerge = await findOpenPr('main', 'develop') - if (!existingBackmerge) { + if (existingBackmerge) { + console.log( + chalk.yellow(`Backmerge PR already open: #${existingBackmerge.number}. Enabling auto-merge...`), + ) + await pify(exec)(`gh pr merge --auto --merge ${existingBackmerge.number}`) + console.log( + chalk.green('Auto-merge set. Backmerge will merge automatically when CI passes.'), + ) + } else { const mainDevelopCommits = await getCommitMessages('origin/develop..origin/main') if (mainDevelopCommits.length > 0) { console.log(chalk.green('Creating backmerge PR (main -> develop)...')) @@ -862,7 +870,15 @@ const doHotfixRelease = async () => { } const existingBackmerge = await findOpenPr('main', 'develop') - if (!existingBackmerge) { + if (existingBackmerge) { + console.log( + chalk.yellow(`Backmerge PR already open: #${existingBackmerge.number}. Enabling auto-merge...`), + ) + await pify(exec)(`gh pr merge --auto --merge ${existingBackmerge.number}`) + console.log( + chalk.green('Auto-merge set. Backmerge will merge automatically when CI passes.'), + ) + } else { const mainDevelopDiff = await getCommitMessages('origin/develop..origin/main') if (mainDevelopDiff.length > 0) { console.log(chalk.green('Creating backmerge PR (main -> develop)...')) From 84775fbdcc182002cce4dc6c32cc017afc9d1c00 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Tue, 17 Mar 2026 01:18:45 +0100 Subject: [PATCH 7/7] fix: lint --- scripts/release.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/release.ts b/scripts/release.ts index d5b5372e830..4e42e1b4a21 100644 --- a/scripts/release.ts +++ b/scripts/release.ts @@ -671,7 +671,9 @@ const doRegularRelease = async () => { const existingBackmerge = await findOpenPr('main', 'develop') if (existingBackmerge) { console.log( - chalk.yellow(`Backmerge PR already open: #${existingBackmerge.number}. Enabling auto-merge...`), + chalk.yellow( + `Backmerge PR already open: #${existingBackmerge.number}. Enabling auto-merge...`, + ), ) await pify(exec)(`gh pr merge --auto --merge ${existingBackmerge.number}`) console.log( @@ -872,7 +874,9 @@ const doHotfixRelease = async () => { const existingBackmerge = await findOpenPr('main', 'develop') if (existingBackmerge) { console.log( - chalk.yellow(`Backmerge PR already open: #${existingBackmerge.number}. Enabling auto-merge...`), + chalk.yellow( + `Backmerge PR already open: #${existingBackmerge.number}. Enabling auto-merge...`, + ), ) await pify(exec)(`gh pr merge --auto --merge ${existingBackmerge.number}`) console.log(