Skip to content

feat: add release-fix subcommand to release script#12272

Merged
kaladinlight merged 4 commits intodevelopfrom
feat/release-fix-subcommand
Apr 10, 2026
Merged

feat: add release-fix subcommand to release script#12272
kaladinlight merged 4 commits intodevelopfrom
feat/release-fix-subcommand

Conversation

@kaladinlight
Copy link
Copy Markdown
Contributor

@kaladinlight kaladinlight commented Apr 10, 2026

Description

Adds a "Release fix" option to pnpm release that cherry-picks selected develop commits onto the release branch with -x when a release PR is already open. This closes the last manual gap in the release flow - previously, cherry-picking a fix onto release required manually running git cherry-pick -x and remembering the -x flag.

Also adds a safety check in handleReleaseReady that warns if any commit on release was cherry-picked without -x (missing the (cherry picked from commit ...) trailer), which would cause it to be re-included in a future release.

Changes:

  • New "Release fix" release type option
  • handleReleaseFix handler: validates open release PR exists, shows only commits not yet on release (compares origin/release...origin/develop), cherry-picks with -x in a worktree
  • Safety check in handleReleaseReady for missing cherry-pick trailers
  • inquireSelectCommits now accepts a custom message parameter for context-appropriate prompts
  • Exhaustive default case in main switch

Risk

Low - internal tooling only, no production code changes.

Testing

Engineering

  1. Run pnpm release and select "Release fix" with no open release PR - should error
  2. Run pnpm release and select "Release fix" with an open release PR - should show only commits not already on release
  3. Select commits and confirm - should cherry-pick with -x onto release

Operations

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

Summary by CodeRabbit

  • New Features

    • Added a "Release fix" release type to support targeted fixes onto the release branch.
    • New interactive flow to select and apply specific develop commits onto release, with automatic branch update.
  • Improvements

    • Clearer selection prompts for hotfix vs release-fix flows.
    • Pre-flight safety scan that warns about manually cherry-picked commits lacking proper trailers.
    • Ensures temporary workspace cleanup after cherry-pick operations.

Adds a "Release fix" option that cherry-picks selected develop commits
onto the release branch with -x when a release PR is already open. Also
adds a safety check in handleReleaseReady that warns about cherry-picks
missing the -x trailer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@kaladinlight kaladinlight requested a review from a team as a code owner April 10, 2026 16:49
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 10, 2026

📝 Walkthrough

Walkthrough

The release script adds a "Release fix" workflow to cherry-pick selected develop commits onto the release branch via a temporary worktree, parameterizes the commit selection prompt, and adds a pre-flight scan that warns about release commits missing cherry-pick trailers.

Changes

Cohort / File(s) Summary
Release script
scripts/release.ts
Added "Release fix" option and new handleReleaseFix flow that requires an open release→main PR, lists unreleased develop commits, prompts for selection (custom message), cherry-picks selected commits onto origin/release in a temporary worktree using -x, pushes changes, and ensures worktree cleanup. Also added pre-flight scan in handleReleaseReady to warn about non-merge release commits lacking cherry-pick trailers. inquireSelectCommits now accepts an optional message parameter.

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI as Release Script
    participant Git as Git/Remote
    participant Worktree as Temp Worktree

    User->>CLI: choose "Release fix"
    CLI->>Git: verify open release→main PR
    Git-->>CLI: PR exists / missing
    CLI->>Git: list unreleased commits (origin/release...origin/develop)
    Git-->>CLI: commits list
    CLI->>User: prompt selection (custom message)
    User->>CLI: select commits
    CLI->>User: confirm selection
    User->>CLI: confirm
    CLI->>Worktree: create temporary worktree on release
    CLI->>Worktree: cherry-pick selected commits with -x
    Worktree-->>Git: apply commits to release branch
    CLI->>Git: push updated release branch
    CLI->>Worktree: cleanup worktree
    CLI->>User: complete / report errors
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A hop, a pick, a tidy tree,
I cherry-pick the fixes free!
Temp worktree snug, trailers in tow,
Pushes done—release ready to go. 🎉

🚥 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 and concisely describes the main change: adding a 'release-fix' subcommand 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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/release-fix-subcommand

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
Copy Markdown
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

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

39-40: Rename this constant to RELEASE_TYPE_OPTIONS (or similar).

Since this declaration is being touched anyway, it would be a good time to align it with the repo’s constant naming convention.

As per coding guidelines, "Use UPPER_SNAKE_CASE for constants and configuration values with descriptive names".

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

In `@scripts/release.ts` around lines 39 - 40, Rename the exported constant
releaseType to RELEASE_TYPE_OPTIONS (or another UPPER_SNAKE_CASE name) and keep
it declared as a const assertion; then update the associated type alias
ReleaseType to reference the new symbol (i.e., type ReleaseType = (typeof
RELEASE_TYPE_OPTIONS)[number]) and update any usages of releaseType to the new
constant name throughout the module.

761-774: Consider reusing a shared helper for %H %s parsing.

This block duplicates the parsing logic from getUnreleasedCommits(). Extracting a getUnreleasedCommitsBetween(baseRef, headRef) helper would keep hotfix/release-fix selection consistent if the log format ever changes.

Refactor sketch
+const getUnreleasedCommitsBetween = async (
+  baseRef: string,
+  headRef: string,
+): Promise<UnreleasedCommit[]> => {
+  const result = await pify(exec)(
+    `git log --cherry-pick --right-only --no-merges --pretty=format:"%H %s" ${baseRef}...${headRef}`,
+  ).catch(() => '')
+  if (!result.trim()) return []
+
+  return result
+    .trim()
+    .split('\n')
+    .filter(Boolean)
+    .map((line: string) => {
+      const spaceIdx = line.indexOf(' ')
+      if (spaceIdx === -1) return { hash: line, message: '' }
+      return { hash: line.slice(0, spaceIdx), message: line.slice(spaceIdx + 1) }
+    })
+}
-
-  const result = await pify(exec)(
-    `git log --cherry-pick --right-only --no-merges --pretty=format:"%H %s" origin/release...origin/develop`,
-  ).catch(() => '')
-  const unreleased: UnreleasedCommit[] = !result.trim()
-    ? []
-    : result
-        .trim()
-        .split('\n')
-        .filter(Boolean)
-        .map((line: string) => {
-          const spaceIdx = line.indexOf(' ')
-          if (spaceIdx === -1) return { hash: line, message: '' }
-          return { hash: line.slice(0, spaceIdx), message: line.slice(spaceIdx + 1) }
-        })
+  const unreleased = await getUnreleasedCommitsBetween('origin/release', 'origin/develop')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/release.ts` around lines 761 - 774, This duplicates the
commit-parsing logic; extract a shared helper (e.g.,
getUnreleasedCommitsBetween(baseRef: string, headRef: string):
Promise<UnreleasedCommit[]>) that runs the git log command and parses lines of
"%H %s" into {hash,message} objects, then replace the inline logic here and in
getUnreleasedCommits() to call that helper so both hotfix/release-fix selection
use the same parsing and return type; ensure the helper trims empty output,
handles lines without spaces, and returns an empty array on errors.
🤖 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 611-639: The current cherry-pick heuristic uses `isFromMerge`
computed with `git log ... origin/main...${sha}` which compares a branch to a
single commit and can false-positive normal release commits; change the check to
test reachability from the develop branch instead. Replace the `isFromMerge`
invocation with a reachability/ancestor test such as running `git merge-base
--is-ancestor ${sha} origin/develop` (or equivalent `git rev-list
--ancestry-path`/`git branch --contains`) and treat the commit as coming from
develop when that command exits 0; keep using the same `sha`, `body`, and
warning logic so only commits that are not ancestors of `origin/develop` and
also lack the "(cherry picked from commit" trailer trigger the yellow -x
warning.

---

Nitpick comments:
In `@scripts/release.ts`:
- Around line 39-40: Rename the exported constant releaseType to
RELEASE_TYPE_OPTIONS (or another UPPER_SNAKE_CASE name) and keep it declared as
a const assertion; then update the associated type alias ReleaseType to
reference the new symbol (i.e., type ReleaseType = (typeof
RELEASE_TYPE_OPTIONS)[number]) and update any usages of releaseType to the new
constant name throughout the module.
- Around line 761-774: This duplicates the commit-parsing logic; extract a
shared helper (e.g., getUnreleasedCommitsBetween(baseRef: string, headRef:
string): Promise<UnreleasedCommit[]>) that runs the git log command and parses
lines of "%H %s" into {hash,message} objects, then replace the inline logic here
and in getUnreleasedCommits() to call that helper so both hotfix/release-fix
selection use the same parsing and return type; ensure the helper trims empty
output, handles lines without spaces, and returns an empty array on errors.
🪄 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: 55b72cc6-2960-49c8-9ec7-df0781017f4e

📥 Commits

Reviewing files that changed from the base of the PR and between 7f4080d and 0431b91.

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

Comment thread scripts/release.ts
kaladinlight and others added 2 commits April 10, 2026 11:07
Use `git merge-base --is-ancestor` to check if a commit on release is
reachable from develop, rather than the previous left-only log heuristic
which could false-positive on normal release commits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@kaladinlight kaladinlight enabled auto-merge (squash) April 10, 2026 17:21
Copy link
Copy Markdown
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

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

39-40: Rename releaseType to a constant-style identifier.

This is a static choice list, so RELEASE_TYPES would align better with the repo’s constant/config naming rule.

As per coding guidelines, "Use UPPER_SNAKE_CASE for constants and configuration values with descriptive names."

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

In `@scripts/release.ts` around lines 39 - 40, Rename the mutable-looking
identifier releaseType to a constant-style name RELEASE_TYPES and update its
type alias reference accordingly (currently ReleaseType uses (typeof
releaseType)[number]); change the const declaration to const RELEASE_TYPES =
['Regular', 'Hotfix', 'Release fix'] as const and update the type alias to type
ReleaseType = (typeof RELEASE_TYPES)[number] so the code follows
UPPER_SNAKE_CASE for constant/config values while preserving the ReleaseType
type.

762-775: Extract the shared unreleased-commit query/parser.

This reimplements the same git log parsing as getUnreleasedCommits(), with only the rev range changed. Pull the range into a parameter so the hotfix and release-fix paths stay aligned.

♻️ Possible extraction
-const getUnreleasedCommits = async (): Promise<UnreleasedCommit[]> => {
+const getUnreleasedCommits = async (
+  baseRef = 'origin/main',
+  compareRef = 'origin/develop',
+): Promise<UnreleasedCommit[]> => {
   const result = await pify(exec)(
-    `git log --cherry-pick --right-only --no-merges --pretty=format:"%H %s" origin/main...origin/develop`,
+    `git log --cherry-pick --right-only --no-merges --pretty=format:"%H %s" ${baseRef}...${compareRef}`,
   ).catch(() => '')
   if (!result.trim()) return []

   return result
     .trim()
     .split('\n')
     .filter(Boolean)
     .map((line: string) => {
       const spaceIdx = line.indexOf(' ')
       if (spaceIdx === -1) return { hash: line, message: '' }
       return { hash: line.slice(0, spaceIdx), message: line.slice(spaceIdx + 1) }
     })
 }
-  const result = await pify(exec)(
-    `git log --cherry-pick --right-only --no-merges --pretty=format:"%H %s" origin/release...origin/develop`,
-  ).catch(() => '')
-  const unreleased: UnreleasedCommit[] = !result.trim()
-    ? []
-    : result
-        .trim()
-        .split('\n')
-        .filter(Boolean)
-        .map((line: string) => {
-          const spaceIdx = line.indexOf(' ')
-          if (spaceIdx === -1) return { hash: line, message: '' }
-          return { hash: line.slice(0, spaceIdx), message: line.slice(spaceIdx + 1) }
-        })
+  const unreleased = await getUnreleasedCommits('origin/release', 'origin/develop')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/release.ts` around lines 762 - 775, Duplicate logic for running and
parsing the `git log` output should be extracted into a shared helper so both
the hotfix and release paths stay aligned: create or update a function (e.g.,
getUnreleasedCommits(range: string) or parseUnreleasedCommits(output: string)
called by getUnreleasedCommits) that accepts the rev-range string, runs the `git
log --cherry-pick --right-only --no-merges --pretty=format:"%H %s" <range>`
command (or parses its output), and returns UnreleasedCommit[] by splitting
lines, finding the first space to separate hash and message, and handling
missing message cases; replace the duplicated block with a call to this helper
from both places (the existing getUnreleasedCommits usage and the
hotfix/release-fix code paths) so only the range differs between callers.
🤖 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 799-813: After successfully pushing the fix commits and removing
the worktree in the try/catch block (around wt.push and the subsequent git
worktree remove calls), update the open release PR body so the new commits
appear in the release notes/testing checklist: call the existing routine that
generates the release PR body (invoke handleReleaseReady() or extract its
body-generation logic) or add a new helper (e.g., updateReleasePRBody(prNumber,
generatedBody)) to PATCH the PR via the GitHub API before exiting;
alternatively, if you want to force manual intervention, block exit with a clear
message instructing the user to update the PR body manually instead of silently
returning — ensure this change is placed just after the successful push and
before the final console.log so release_open() won't skip regenerating content.

---

Nitpick comments:
In `@scripts/release.ts`:
- Around line 39-40: Rename the mutable-looking identifier releaseType to a
constant-style name RELEASE_TYPES and update its type alias reference
accordingly (currently ReleaseType uses (typeof releaseType)[number]); change
the const declaration to const RELEASE_TYPES = ['Regular', 'Hotfix', 'Release
fix'] as const and update the type alias to type ReleaseType = (typeof
RELEASE_TYPES)[number] so the code follows UPPER_SNAKE_CASE for constant/config
values while preserving the ReleaseType type.
- Around line 762-775: Duplicate logic for running and parsing the `git log`
output should be extracted into a shared helper so both the hotfix and release
paths stay aligned: create or update a function (e.g.,
getUnreleasedCommits(range: string) or parseUnreleasedCommits(output: string)
called by getUnreleasedCommits) that accepts the rev-range string, runs the `git
log --cherry-pick --right-only --no-merges --pretty=format:"%H %s" <range>`
command (or parses its output), and returns UnreleasedCommit[] by splitting
lines, finding the first space to separate hash and message, and handling
missing message cases; replace the duplicated block with a call to this helper
from both places (the existing getUnreleasedCommits usage and the
hotfix/release-fix code paths) so only the range differs between callers.
🪄 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: 14d8a0e2-1024-4dd9-b17b-f94c9d5805cf

📥 Commits

Reviewing files that changed from the base of the PR and between 0431b91 and 6624d95.

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

Comment thread scripts/release.ts
@kaladinlight kaladinlight merged commit 8c4281c into develop Apr 10, 2026
4 checks passed
@kaladinlight kaladinlight deleted the feat/release-fix-subcommand branch April 10, 2026 22:14
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