Skip to content

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

Merged
0xApotheosis merged 10 commits intodevelopfrom
fix_release_duplicate_private_sync_pr_v2
Mar 17, 2026
Merged

fix: release script squash-merge compat and backmerge automation#12162
0xApotheosis merged 10 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 determine prerelease status, skip private syncs when content is identical, and prefer existing private PRs when available.
    • Private-sync and tagging flows create or reuse private PRs only when needed; logs include PR URLs and skip reasons.
    • Backmerge PRs (main -> develop) are created or reused and configured for auto-merge when required; logging clarified.

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

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

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

Changes

Cohort / File(s) Summary
Release script
scripts/release.ts
Replaced SHA-equality prerelease check with commit- and content-diff logic (origin/main vs origin/release); determine prereleaseMerged via commits+content; reuse or create private sync PRs only when branches differ; skip private sync when content-equal; create backmerge PRs (main→develop) and enable auto-merge (merge-commit) when applicable; updated 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: log prerelease merged / stop
    else
        Release->>GH: create release tag
        Release->>GH: query open private sync PRs
        alt open private PR exists
            GH-->>Release: return PR info
            Release->>GH: log reuse existing private PR
        else
            Release->>Git: compute private vs main content diff
            alt private differs from main
                Release->>GH: create private sync PR
                GH-->>Release: return PR URL
            else
                Release->>GH: log "private in sync, skip PR"
            end
        end
        Release->>Git: check commits main→develop
        alt backmerge needed
            Release->>GH: create or find 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 sniffed the branches, counted every hop,
Found commits that danced and ones that did stop,
Reused old PRs or spun new thread,
Pushed backmerges and set auto-merge ahead,
A rabbit's release, tidy on the hop.

🚥 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 title accurately summarizes the main changes: adding squash-merge compatibility and backmerge automation to the release script.
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 unit tests (beta)
  • Create PR with unit tests
  • 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

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

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)

828-836: Consider enabling auto-merge immediately in hotfix merged_untagged.

The backmerge PR created here doesn't have auto-merge enabled, unlike when it's created/found in tagged_private_stale. If the user doesn't re-run the script after this state, the backmerge will sit waiting for manual merge.

For consistency with the stated automation goals, consider enabling auto-merge immediately:

♻️ Optional: Enable auto-merge on backmerge creation
       console.log(chalk.green(`Backmerge PR created: ${backmergeUrl}`))
-      console.log(chalk.green('Merge both PRs on GitHub to complete the hotfix.'))
+      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.'),
+      )
+      console.log(chalk.green('Merge the private sync PR 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 828 - 836, The backmerge PR created by the
createPr call (creating backmergeUrl) in the hotfix merged_untagged path does
not enable auto-merge, so the PR can stall; update the implementation around
createPr (or immediately after obtaining backmergeUrl) to enable auto-merge for
that PR—either by extending createPr to accept and apply auto-merge/merge_method
options or by invoking the same auto-merge helper used elsewhere (the logic used
for tagged_private_stale) immediately after the PR is created; ensure you
reference createPr and backmergeUrl when adding the auto-merge step so the
backmerge PR is configured the same way as the other flow.
🤖 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 828-836: The backmerge PR created by the createPr call (creating
backmergeUrl) in the hotfix merged_untagged path does not enable auto-merge, so
the PR can stall; update the implementation around createPr (or immediately
after obtaining backmergeUrl) to enable auto-merge for that PR—either by
extending createPr to accept and apply auto-merge/merge_method options or by
invoking the same auto-merge helper used elsewhere (the logic used for
tagged_private_stale) immediately after the PR is created; ensure you reference
createPr and backmergeUrl when adding the auto-merge step so the backmerge PR is
configured the same way as the other flow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f9630391-d4e0-4133-b0f1-c0dca6a7fbbe

📥 Commits

Reviewing files that changed from the base of the PR and between c0f8456 and 7f25225.

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

Copy link
Member

@0xApotheosis 0xApotheosis left a comment

Choose a reason for hiding this comment

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

Logic sane, though can't test with the current release PR open. Happy for you to confirm it works by shipping the release!

@0xApotheosis 0xApotheosis merged commit e96bfe7 into develop Mar 17, 2026
4 checks passed
@0xApotheosis 0xApotheosis deleted the fix_release_duplicate_private_sync_pr_v2 branch March 17, 2026 00:34
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.

2 participants