Skip to content

fix: release script squash-merge compat and backmerge automation#12162

Open
gomesalexandre wants to merge 6 commits intodevelopfrom
fix_release_duplicate_private_sync_pr_v2
Open

fix: release script squash-merge compat and backmerge automation#12162
gomesalexandre wants to merge 6 commits intodevelopfrom
fix_release_duplicate_private_sync_pr_v2

Conversation

@gomesalexandre
Copy link
Contributor

@gomesalexandre gomesalexandre commented Mar 13, 2026

Description

Three fixes to the release state machine in `scripts/release.ts`, all rooted in the same underlying problem: the script was designed (in #12110) assuming rebase merging (same SHAs post-merge), but the repo uses squash merging.

1. `idle` case - content diff instead of SHA equality for `prereleaseMerged`

Before:
```ts
const prereleaseMerged = releaseSha !== mainSha
```

After squash-merging a release PR, `releaseSha !== mainSha` is always true (squash creates a new commit), so the script always thought there was a pending release to cut even right after a fresh one. Fixed by comparing actual file content:

```ts
const releaseMatchesMain = !(await git().diff(['origin/main', 'origin/release']))
const prereleaseMerged = releaseSha !== mainSha && !releaseMatchesMain
```

2. `tagged_private_stale` (regular release) - add backmerge PR with auto-merge

After main gets tagged and private is synced, main has diverged from develop (the squash-merged release commit isn't in develop's history). The script was leaving this cleanup to be done manually.

Now creates a `main -> develop` backmerge PR automatically and sets auto-merge with merge commit strategy:

```ts
await pify(exec)(`gh pr merge --auto --merge ${backmergeUrl}`)
```

Merge commit strategy is intentional - preserves main's commit in develop's history so the merge base is correct for future releases.

3. `tagged_private_stale` (hotfix) - same auto-merge treatment

The hotfix path already created the backmerge PR but wasn't setting auto-merge. Now consistent with the regular release path.

Issue (if applicable)

closes #

Risk

Low - release script only, no production code touched. Worst case is a script error requiring manual intervention (same as before).

Testing

Engineering

Run `pnpm release` after a release has been tagged and private is stale:

  • should create private sync PR
  • should create backmerge PR (main -> develop) and immediately set auto-merge on it
  • should NOT create duplicate PRs if run again

For the `idle` fix: run `pnpm release` right after squash-merging a release - should land in the "fresh start" sub-state (create prerelease PR) instead of "prerelease merged".

Operations

  • 🏁 My feature is behind a flag and doesn't require operations testing (yet)

Summary by CodeRabbit

  • Chores
    • Release automation now uses content/diff checks to skip private syncs when no content differs and to reuse existing private PRs when possible.
    • Private-sync and tagging flows updated to create private PRs only when necessary.
    • Backmerge PRs (main -> develop) are created and auto-merged when main has new commits not in develop.
    • Logging improved for clearer status, skip, and PR URL messages.

gomesalexandre and others added 2 commits March 13, 2026 17:33
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 <[email protected]>
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 <[email protected]>
@gomesalexandre gomesalexandre requested a review from a team as a code owner March 13, 2026 16:54
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 13, 2026

📝 Walkthrough

Walkthrough

Release script now uses commit- and content-diff checks to decide prerelease state, conditionally creates or reuses private sync PRs, creates and auto-merges backmerge PRs from main→develop when needed, and updates logging across regular and hotfix flows.

Changes

Cohort / File(s) Summary
Release script
scripts/release.ts
Replaced simple prerelease-ahead check with commit- and content-diff logic (origin/main vs origin/release); only create private sync PRs when branches differ and reuse existing open PRs; skip private PR creation when no diff; add backmerge PR creation (main→develop) and enable auto-merge (merge-commit) when created; update logs across regular and hotfix paths.

Sequence Diagram(s)

sequenceDiagram
    participant Release as Release Script
    participant Git as Git (origin/main, origin/release, origin/private, develop)
    participant GH as GitHub (PRs, Tags)

    Release->>Git: fetch SHAs, commits and content diffs
    alt release has no unique commits OR content identical to main
        Release->>GH: mark prereleaseMerged / log and stop
    else
        Release->>GH: create release tag
        Release->>GH: query open PRs main→private
        alt open private PR exists
            GH-->>Release: return PR info
            Release->>GH: log "reusing existing private PR"
        else
            Release->>Git: compute private vs main content diff
            alt private behind main
                Release->>GH: create private-to-main PR
                GH-->>Release: return PR URL
            else
                Release->>GH: log "private in sync, skip PR"
            end
        end
        Release->>Git: compute commits on main since develop
        alt commits exist AND no backmerge PR exists
            Release->>GH: create backmerge PR (main→develop)
            Release->>GH: enable auto-merge (merge-commit)
            GH-->>Release: return PR + auto-merge status
        else
            Release->>GH: log "no backmerge needed"
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through branches, checked each trail,
Sniffed commits and diffs like a curious tail,
If a PR waited, I gave a small nod,
Else I stitched backmerges—merge-commit, applaud,
Carrots for CI, a neat release grail.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main changes: fixes to the release script for squash-merge compatibility and automated backmerge handling, which aligns with the detailed objectives and summary.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix_release_duplicate_private_sync_pr_v2
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
scripts/release.ts (1)

805-809: Content guard applied consistently to Hotfix path — but merged_untagged guard is missing.

The content diff check mirrors the Regular release path correctly. However, the Hotfix merged_untagged case (lines 781-798) still creates PRs unconditionally without checking for existing ones, unlike the guarded Regular release path at lines 613-630.

Per the PR description intent to "add an open-PR guard to merged_untagged", consider applying the same pattern to the Hotfix path for full consistency:

Proposed fix for Hotfix merged_untagged guard
     case 'merged_untagged': {
       console.log(chalk.green(`Hotfix merged to main. Tagging ${nextVersion}...`))
       await git().checkout(['main'])
       await git().pull()
       await git().tag(['-a', nextVersion, '-m', nextVersion])
       console.log(chalk.green('Pushing tag...'))
       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 hotfix ${nextVersion}.`,
-      })
-      console.log(chalk.green(`Private sync PR created: ${privatePrUrl}`))
+      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 hotfix ${nextVersion}.`,
+        })
+        console.log(chalk.green(`Private sync PR created: ${privatePrUrl}`))
+      }

-      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 hotfix ${nextVersion} to sync cherry-picked commits.`,
-      })
-      console.log(chalk.green(`Backmerge PR created: ${backmergeUrl}`))
-      console.log(chalk.green('Merge both PRs on GitHub to complete the hotfix.'))
+      const existingBackmergePrAfterTag = await findOpenPr('main', 'develop')
+      if (existingBackmergePrAfterTag) {
+        console.log(
+          chalk.yellow(
+            `Backmerge PR already open: #${existingBackmergePrAfterTag.number}. Merge it on GitHub.`,
+          ),
+        )
+      } else {
+        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 hotfix ${nextVersion} to sync cherry-picked commits.`,
+        })
+        console.log(chalk.green(`Backmerge PR created: ${backmergeUrl}`))
+      }
+      console.log(chalk.green('Merge PRs on GitHub to complete the hotfix.'))
       break
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/release.ts` around lines 805 - 809, The Hotfix `merged_untagged` path
is missing the open-PR guard present in the Regular release flow; modify the
Hotfix branch handling that uses `privateDiffHotfix`/`merged_untagged` so it
first queries for an existing open PR (reuse the same check used in the Regular
release path) and only calls the PR creation routine (e.g., the existing
`createPullRequest`/createPR call) when no open PR is found; if an open PR
exists, skip creating a new one and log or exit the same way the Regular path
does.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@scripts/release.ts`:
- Around line 805-809: The Hotfix `merged_untagged` path is missing the open-PR
guard present in the Regular release flow; modify the Hotfix branch handling
that uses `privateDiffHotfix`/`merged_untagged` so it first queries for an
existing open PR (reuse the same check used in the Regular release path) and
only calls the PR creation routine (e.g., the existing
`createPullRequest`/createPR call) when no open PR is found; if an open PR
exists, skip creating a new one and log or exit the same way the Regular path
does.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 63b51437-4657-4028-a4b4-653955dd6561

📥 Commits

Reviewing files that changed from the base of the PR and between ff0aa68 and 9b15248.

📒 Files selected for processing (1)
  • scripts/release.ts

@gomesalexandre gomesalexandre changed the title fix: prevent duplicate private sync pr in release script fix: release script squash-merge compat and backmerge automation Mar 13, 2026
…rge 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 <[email protected]>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/release.ts`:
- Around line 639-645: The early break when privateDiff is empty stops further
backmerge logic; remove the break (or replace it with no-op) so after logging
"Private is already in sync with main content-wise. Nothing to do." the script
continues to the subsequent backmerge checks and PR creation. Update both
occurrences around the privateDiff check (the const privateDiff = await
git().diff([...]) blocks and the similar block at the later lines) to log but
not exit the loop so the backmerge automation (main -> develop PR creation) can
still run.
- Around line 518-520: The prereleaseMerged boolean currently only checks
SHA/content mismatch (releaseMatchesMain) and can be true when origin/release is
behind origin/main; update the logic so prereleaseMerged is only true when
origin/release actually contains commits not present on origin/main (i.e.,
release is ahead), not merely when SHAs/content differ. Replace the current
check that sets prereleaseMerged (which references releaseSha, mainSha,
releaseMatchesMain and git().diff) with a check that runs a git rev-list/log
between origin/release and origin/main (e.g., list commits in origin/release
^origin/main) and considers prereleaseMerged true only if that list is
non-empty.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dcf3d564-1b1e-4465-9534-48d08f1e51a9

📥 Commits

Reviewing files that changed from the base of the PR and between 9b15248 and 33f7c0c.

📒 Files selected for processing (1)
  • scripts/release.ts

…eak 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 <[email protected]>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/release.ts`:
- Around line 669-687: The backmerge branch creates auto-merge only when a new
PR is created, so reruns where findOpenPr('main', 'develop') returns an
existingBackmerge never enable auto-merge; update the logic so after detecting
an existingBackmerge you extract its identifier/URL (from the existingBackmerge
object returned by findOpenPr) and run the same pify(exec)(`gh pr merge --auto
--merge ${prIdentifierOrUrl}`) call to enable auto-merge, and apply the
identical change to the hotfix backmerge block (the other block that uses
createPr/getCommitMessages and gh pr merge).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 248b07f2-f389-46e5-a7b9-f967173be922

📥 Commits

Reviewing files that changed from the base of the PR and between 33f7c0c and 66bb840.

📒 Files selected for processing (1)
  • scripts/release.ts

Comment on lines +669 to 687
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.'),
)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Auto-merge is skipped when backmerge PR already exists.

Right now, gh pr merge --auto --merge ... runs only in the “new PR created” branch. On reruns where a backmerge PR is already open, auto-merge is never set, so automation can stall.

🔧 Minimal fix (apply in both regular + hotfix backmerge blocks)
-      const existingBackmerge = await findOpenPr('main', 'develop')
-      if (!existingBackmerge) {
+      const existingBackmerge = await findOpenPr('main', 'develop')
+      if (existingBackmerge) {
+        console.log(chalk.yellow(`Backmerge PR already open: #${existingBackmerge.number}.`))
+        console.log(chalk.green('Setting auto-merge with merge commit strategy...'))
+        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)...'))
           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.'),
           )
         }
       }

Also applies to: 860-877

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/release.ts` around lines 669 - 687, The backmerge branch creates
auto-merge only when a new PR is created, so reruns where findOpenPr('main',
'develop') returns an existingBackmerge never enable auto-merge; update the
logic so after detecting an existingBackmerge you extract its identifier/URL
(from the existingBackmerge object returned by findOpenPr) and run the same
pify(exec)(`gh pr merge --auto --merge ${prIdentifierOrUrl}`) call to enable
auto-merge, and apply the identical change to the hotfix backmerge block (the
other block that uses createPr/getCommitMessages and gh pr merge).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
scripts/release.ts (1)

671-689: ⚠️ Potential issue | 🟠 Major

Auto-merge is still not enabled when a backmerge PR already exists.

When existingBackmerge is found, the script currently does nothing. On reruns, this can leave backmerge PRs open without auto-merge configured and stall automation.

🔧 Minimal fix (apply in both regular + hotfix backmerge blocks)
 const existingBackmerge = await findOpenPr('main', 'develop')
-if (!existingBackmerge) {
+if (existingBackmerge) {
+  console.log(chalk.yellow(`Backmerge PR already open: #${existingBackmerge.number}.`))
+  console.log(chalk.green('Setting auto-merge with merge commit strategy...'))
+  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)...'))
     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.'))
   }
 }

Also applies to: 864-882

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/release.ts` around lines 671 - 689, The code currently skips
configuring auto-merge when findOpenPr returns an existingBackmerge, leaving PRs
open without auto-merge; update the logic in the backmerge block around
existingBackmerge (and mirror the same change in the hotfix backmerge block) so
that if existingBackmerge is truthy you call the same auto-merge command used
after createPr (i.e., invoke pify(exec) with the gh pr merge --auto --merge
argument using existingBackmerge's URL or number), logging the same progress
messages as when a PR is newly created so reruns will enable auto-merge on
already-open backmerge PRs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@scripts/release.ts`:
- Around line 671-689: The code currently skips configuring auto-merge when
findOpenPr returns an existingBackmerge, leaving PRs open without auto-merge;
update the logic in the backmerge block around existingBackmerge (and mirror the
same change in the hotfix backmerge block) so that if existingBackmerge is
truthy you call the same auto-merge command used after createPr (i.e., invoke
pify(exec) with the gh pr merge --auto --merge argument using
existingBackmerge's URL or number), logging the same progress messages as when a
PR is newly created so reruns will enable auto-merge on already-open backmerge
PRs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2f82595b-7821-4229-bc4e-b8b0efe9c700

📥 Commits

Reviewing files that changed from the base of the PR and between 66bb840 and c0f8456.

📒 Files selected for processing (1)
  • scripts/release.ts

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant