2025-09-11 09:19:07 -07:00
#!/usr/bin/env node
/ * *
* @ license
* Copyright 2025 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
import { execSync } from 'node:child_process' ;
import yargs from 'yargs' ;
import { hideBin } from 'yargs/helpers' ;
async function main ( ) {
const argv = await yargs ( hideBin ( process . argv ) )
. option ( 'commit' , {
alias : 'c' ,
description : 'The commit SHA to cherry-pick for the patch.' ,
type : 'string' ,
demandOption : true ,
} )
2025-09-29 13:14:53 -04:00
. option ( 'pullRequestNumber' , {
alias : 'pr' ,
description : "The pr number that we're cherry picking" ,
type : 'number' ,
demandOption : true ,
} )
2025-09-11 09:19:07 -07:00
. option ( 'channel' , {
alias : 'ch' ,
description : 'The release channel to patch.' ,
choices : [ 'stable' , 'preview' ] ,
demandOption : true ,
} )
. option ( 'dry-run' , {
description : 'Whether to run in dry-run mode.' ,
type : 'boolean' ,
default : false ,
} )
. help ( )
. alias ( 'help' , 'h' ) . argv ;
2025-09-29 13:14:53 -04:00
const { commit , channel , dryRun , pullRequestNumber } = argv ;
2025-09-11 09:19:07 -07:00
console . log ( ` Starting patch process for commit: ${ commit } ` ) ;
console . log ( ` Targeting channel: ${ channel } ` ) ;
if ( dryRun ) {
console . log ( 'Running in dry-run mode.' ) ;
}
2025-09-19 03:51:01 -07:00
run ( 'git fetch --all --tags --prune' , dryRun ) ;
2025-09-11 09:19:07 -07:00
2025-09-24 21:02:00 -07:00
const releaseInfo = getLatestReleaseInfo ( channel ) ;
const latestTag = releaseInfo . currentTag ;
2025-10-02 16:21:37 -04:00
const nextVersion = releaseInfo . nextVersion ;
2025-09-11 09:19:07 -07:00
2025-09-29 13:14:53 -04:00
const releaseBranch = ` release/ ${ latestTag } -pr- ${ pullRequestNumber } ` ;
2025-10-02 16:21:37 -04:00
const hotfixBranch = ` hotfix/ ${ latestTag } / ${ nextVersion } / ${ channel } /cherry-pick- ${ commit . substring ( 0 , 7 ) } ` ;
2025-09-11 09:19:07 -07:00
// Create the release branch from the tag if it doesn't exist.
if ( ! branchExists ( releaseBranch ) ) {
console . log (
` Release branch ${ releaseBranch } does not exist. Creating it from tag ${ latestTag } ... ` ,
) ;
2025-09-19 01:50:59 -07:00
try {
run ( ` git checkout -b ${ releaseBranch } ${ latestTag } ` , dryRun ) ;
run ( ` git push origin ${ releaseBranch } ` , dryRun ) ;
} catch ( error ) {
2025-09-19 03:27:47 -07:00
// Check if this is a GitHub App workflows permission error
if (
2025-09-19 03:51:01 -07:00
error . message . match ( /refusing to allow a GitHub App/i ) &&
error . message . match ( /workflows?['`]? permission/i )
2025-09-19 03:27:47 -07:00
) {
console . error (
` ❌ Failed to create release branch due to insufficient GitHub App permissions. ` ,
) ;
console . log (
` \n 📋 Please run these commands manually to create the branch: ` ,
) ;
console . log ( ` \n \` \` \` bash ` ) ;
console . log ( ` git checkout -b ${ releaseBranch } ${ latestTag } ` ) ;
console . log ( ` git push origin ${ releaseBranch } ` ) ;
console . log ( ` \` \` \` ` ) ;
console . log (
` \n After running these commands, you can run the patch command again. ` ,
) ;
process . exit ( 1 ) ;
} else {
// Re-throw other errors
throw error ;
2025-09-19 01:50:59 -07:00
}
}
2025-09-11 09:19:07 -07:00
} else {
console . log ( ` Release branch ${ releaseBranch } already exists. ` ) ;
}
2025-09-18 09:56:45 -07:00
// Check if hotfix branch already exists
if ( branchExists ( hotfixBranch ) ) {
console . log ( ` Hotfix branch ${ hotfixBranch } already exists. ` ) ;
2025-09-18 10:45:42 -07:00
// Check if there's already a PR for this branch
try {
2025-09-18 11:19:43 -07:00
const prInfo = execSync (
` gh pr list --head ${ hotfixBranch } --json number,url --jq '.[0] // empty' ` ,
)
. toString ( )
. trim ( ) ;
2025-09-18 10:45:42 -07:00
if ( prInfo && prInfo !== 'null' && prInfo !== '' ) {
const pr = JSON . parse ( prInfo ) ;
console . log ( ` Found existing PR # ${ pr . number } : ${ pr . url } ` ) ;
console . log ( ` Hotfix branch ${ hotfixBranch } already has an open PR. ` ) ;
return { existingBranch : hotfixBranch , existingPR : pr } ;
} else {
console . log ( ` Hotfix branch ${ hotfixBranch } exists but has no open PR. ` ) ;
2025-09-18 11:19:43 -07:00
console . log (
` You may need to delete the branch and run this command again. ` ,
) ;
2025-09-18 10:45:42 -07:00
return { existingBranch : hotfixBranch } ;
}
} catch ( err ) {
console . error ( ` Error checking for existing PR: ${ err . message } ` ) ;
console . log ( ` Hotfix branch ${ hotfixBranch } already exists. ` ) ;
return { existingBranch : hotfixBranch } ;
}
2025-09-18 09:56:45 -07:00
}
2025-09-11 09:19:07 -07:00
// Create the hotfix branch from the release branch.
console . log (
` Creating hotfix branch ${ hotfixBranch } from ${ releaseBranch } ... ` ,
) ;
run ( ` git checkout -b ${ hotfixBranch } origin/ ${ releaseBranch } ` , dryRun ) ;
2025-09-18 20:29:39 -07:00
// Ensure git user is configured properly for commits
console . log ( 'Configuring git user for cherry-pick commits...' ) ;
run ( 'git config user.name "gemini-cli-robot"' , dryRun ) ;
run ( 'git config user.email "gemini-cli-robot@google.com"' , dryRun ) ;
2025-09-11 09:19:07 -07:00
// Cherry-pick the commit.
console . log ( ` Cherry-picking commit ${ commit } into ${ hotfixBranch } ... ` ) ;
2025-09-18 17:33:08 -07:00
let hasConflicts = false ;
if ( ! dryRun ) {
try {
execSync ( ` git cherry-pick ${ commit } ` , { stdio : 'pipe' } ) ;
console . log ( ` ✅ Cherry-pick successful - no conflicts detected ` ) ;
} catch ( error ) {
// Check if this is a cherry-pick conflict
try {
const status = execSync ( 'git status --porcelain' , { encoding : 'utf8' } ) ;
const conflictFiles = status
. split ( '\n' )
. filter (
( line ) =>
line . startsWith ( 'UU ' ) ||
line . startsWith ( 'AA ' ) ||
line . startsWith ( 'DU ' ) ||
line . startsWith ( 'UD ' ) ,
) ;
if ( conflictFiles . length > 0 ) {
hasConflicts = true ;
console . log (
` ⚠️ Cherry-pick has conflicts in ${ conflictFiles . length } file(s): ` ,
) ;
conflictFiles . forEach ( ( file ) =>
console . log ( ` - ${ file . substring ( 3 ) } ` ) ,
) ;
// Add all files (including conflict markers) and commit
console . log (
` 📝 Creating commit with conflict markers for manual resolution... ` ,
) ;
execSync ( 'git add .' ) ;
execSync ( ` git commit --no-edit ` ) ;
console . log ( ` ✅ Committed cherry-pick with conflict markers ` ) ;
} else {
// Re-throw if it's not a conflict error
throw error ;
}
} catch ( _statusError ) {
// Re-throw original error if we can't determine the status
throw error ;
}
}
} else {
console . log ( ` [DRY RUN] Would cherry-pick ${ commit } ` ) ;
}
2025-09-11 09:19:07 -07:00
// Push the hotfix branch.
console . log ( ` Pushing hotfix branch ${ hotfixBranch } to origin... ` ) ;
run ( ` git push --set-upstream origin ${ hotfixBranch } ` , dryRun ) ;
2025-09-19 03:51:01 -07:00
// Create the pull request.
2025-09-11 09:19:07 -07:00
console . log (
` Creating pull request from ${ hotfixBranch } to ${ releaseBranch } ... ` ,
) ;
2025-10-02 16:21:37 -04:00
let prTitle = ` fix(patch): cherry-pick ${ commit . substring ( 0 , 7 ) } to ${ releaseBranch } to patch version ${ releaseInfo . currentTag } and create version ${ releaseInfo . nextVersion } ` ;
let prBody = ` This PR automatically cherry-picks commit ${ commit } to patch version ${ releaseInfo . currentTag } in the ${ channel } release to create version ${ releaseInfo . nextVersion } . ` ;
2025-09-18 17:33:08 -07:00
if ( hasConflicts ) {
prTitle = ` fix(patch): cherry-pick ${ commit . substring ( 0 , 7 ) } to ${ releaseBranch } [CONFLICTS] ` ;
prBody += `
# # ⚠ ️ Merge Conflicts Detected
This cherry - pick resulted in merge conflicts that need manual resolution .
# # # 🔧 Next Steps :
1. * * Review the conflicts * * : Check out this branch and review the conflict markers
2. * * Resolve conflicts * * : Edit the affected files to resolve the conflicts
3. * * Test the changes * * : Ensure the patch works correctly after resolution
4. * * Update this PR * * : Push your conflict resolution
# # # 📋 Files with conflicts :
The commit has been created with conflict markers for easier manual resolution .
# # # 🚨 Important :
- Do not merge this PR until conflicts are resolved
- The automated patch release will trigger once this PR is merged ` ;
}
2025-09-11 09:19:07 -07:00
if ( dryRun ) {
prBody += '\n\n**[DRY RUN]**' ;
}
2025-09-18 17:33:08 -07:00
2025-09-17 21:16:08 -07:00
const prCommand = ` gh pr create --base ${ releaseBranch } --head ${ hotfixBranch } --title " ${ prTitle } " --body " ${ prBody } " ` ;
run ( prCommand , dryRun ) ;
2025-09-11 09:19:07 -07:00
2025-09-18 17:33:08 -07:00
if ( hasConflicts ) {
console . log (
'⚠️ Patch process completed with conflicts - manual resolution required!' ,
) ;
} else {
console . log ( '✅ Patch process completed successfully!' ) ;
}
2025-09-17 21:16:08 -07:00
if ( dryRun ) {
console . log ( '\n--- Dry Run Summary ---' ) ;
console . log ( ` Release Branch: ${ releaseBranch } ` ) ;
console . log ( ` Hotfix Branch: ${ hotfixBranch } ` ) ;
console . log ( ` Pull Request Command: ${ prCommand } ` ) ;
console . log ( '---------------------' ) ;
}
2025-09-18 09:56:45 -07:00
2025-09-18 17:33:08 -07:00
return { newBranch : hotfixBranch , created : true , hasConflicts } ;
2025-09-11 09:19:07 -07:00
}
2025-09-18 09:56:45 -07:00
function run ( command , dryRun = false , throwOnError = true ) {
2025-09-11 09:19:07 -07:00
console . log ( ` > ${ command } ` ) ;
if ( dryRun ) {
return ;
}
try {
return execSync ( command ) . toString ( ) . trim ( ) ;
} catch ( err ) {
console . error ( ` Command failed: ${ command } ` ) ;
2025-09-18 09:56:45 -07:00
if ( throwOnError ) {
throw err ;
}
return null ;
2025-09-11 09:19:07 -07:00
}
}
function branchExists ( branchName ) {
try {
execSync ( ` git ls-remote --exit-code --heads origin ${ branchName } ` ) ;
return true ;
} catch ( _e ) {
return false ;
}
}
2025-09-24 21:02:00 -07:00
function getLatestReleaseInfo ( channel ) {
console . log ( ` Fetching latest release info for channel: ${ channel } ... ` ) ;
const patchFrom = channel ; // 'stable' or 'preview'
const command = ` node scripts/get-release-version.js --type=patch --patch-from= ${ patchFrom } ` ;
2025-09-11 09:19:07 -07:00
try {
2025-09-24 21:02:00 -07:00
const result = JSON . parse ( execSync ( command ) . toString ( ) . trim ( ) ) ;
console . log ( ` Current ${ channel } tag: ${ result . previousReleaseTag } ` ) ;
console . log ( ` Next ${ channel } version would be: ${ result . releaseVersion } ` ) ;
return {
currentTag : result . previousReleaseTag ,
nextVersion : result . releaseVersion ,
} ;
2025-09-11 09:19:07 -07:00
} catch ( err ) {
2025-09-24 21:02:00 -07:00
console . error ( ` Failed to get release info for channel: ${ channel } ` ) ;
2025-09-11 09:19:07 -07:00
throw err ;
}
}
main ( ) . catch ( ( err ) => {
console . error ( err ) ;
process . exit ( 1 ) ;
} ) ;