Download scripts/releasing/create-patch-pr.js from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 10 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/scripts/releasing/create-patch-pr.js
- Command line
-
hf download hf://SaylorTwift/gemini-cli/scripts/releasing/create-patch-pr.js
-
curl -L -o create-patch-pr.js https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/scripts/releasing/create-patch-pr.js
10 kB
| /** | |
| * @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, | |
| }) | |
| .option('pullRequestNumber', { | |
| alias: 'pr', | |
| description: "The pr number that we're cherry picking", | |
| type: 'number', | |
| demandOption: true, | |
| }) | |
| .option('channel', { | |
| alias: 'ch', | |
| description: 'The release channel to patch.', | |
| choices: ['stable', 'preview'], | |
| demandOption: true, | |
| }) | |
| .option('cli-package-name', { | |
| description: | |
| 'fully qualified package name with scope (e.g @google/gemini-cli)', | |
| string: true, | |
| default: '@google/gemini-cli', | |
| }) | |
| .option('dry-run', { | |
| description: 'Whether to run in dry-run mode.', | |
| type: 'boolean', | |
| default: false, | |
| }) | |
| .help() | |
| .alias('help', 'h').argv; | |
| const { commit, channel, dryRun, pullRequestNumber } = argv; | |
| console.log(`Starting patch process for commit: ${commit}`); | |
| console.log(`Targeting channel: ${channel}`); | |
| if (dryRun) { | |
| console.log('Running in dry-run mode.'); | |
| } | |
| run('git fetch --all --tags --prune', dryRun); | |
| const releaseInfo = getLatestReleaseInfo({ argv, channel }); | |
| const latestTag = releaseInfo.currentTag; | |
| const nextVersion = releaseInfo.nextVersion; | |
| const releaseBranch = `release/${latestTag}-pr-${pullRequestNumber}`; | |
| const hotfixBranch = `hotfix/${latestTag}/${nextVersion}/${channel}/cherry-pick-${commit.substring(0, 7)}/pr-${pullRequestNumber}`; | |
| // 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}...`, | |
| ); | |
| try { | |
| run(`git checkout -b ${releaseBranch} ${latestTag}`, dryRun); | |
| run(`git push origin ${releaseBranch}`, dryRun); | |
| } catch (error) { | |
| // Check if this is a GitHub App workflows permission error | |
| if ( | |
| error.message.match(/refusing to allow a GitHub App/i) && | |
| error.message.match(/workflows?['`]? permission/i) | |
| ) { | |
| 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( | |
| `\nAfter running these commands, you can run the patch command again.`, | |
| ); | |
| process.exit(1); | |
| } else { | |
| // Re-throw other errors | |
| throw error; | |
| } | |
| } | |
| } else { | |
| console.log(`Release branch ${releaseBranch} already exists.`); | |
| } | |
| // Check if hotfix branch already exists | |
| if (branchExists(hotfixBranch)) { | |
| console.log(`Hotfix branch ${hotfixBranch} already exists.`); | |
| // Check if there's already a PR for this branch | |
| try { | |
| const prInfo = execSync( | |
| `gh pr list --head ${hotfixBranch} --json number,url --jq '.[0] // empty'`, | |
| ) | |
| .toString() | |
| .trim(); | |
| 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.`); | |
| console.log( | |
| `You may need to delete the branch and run this command again.`, | |
| ); | |
| return { existingBranch: hotfixBranch }; | |
| } | |
| } catch (err) { | |
| console.error(`Error checking for existing PR: ${err.message}`); | |
| console.log(`Hotfix branch ${hotfixBranch} already exists.`); | |
| return { existingBranch: hotfixBranch }; | |
| } | |
| } | |
| // Create the hotfix branch from the release branch. | |
| console.log( | |
| `Creating hotfix branch ${hotfixBranch} from ${releaseBranch}...`, | |
| ); | |
| run(`git checkout -b ${hotfixBranch} origin/${releaseBranch}`, dryRun); | |
| // 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); | |
| // Cherry-pick the commit. | |
| console.log(`Cherry-picking commit ${commit} into ${hotfixBranch}...`); | |
| 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 --no-verify`); | |
| console.log(`โ Committed cherry-pick with conflict markers`); | |
| } else { | |
| // Re-throw if it's not a conflict error | |
| throw error; | |
| } | |
| } catch { | |
| // Re-throw original error if we can't determine the status | |
| throw error; | |
| } | |
| } | |
| } else { | |
| console.log(`[DRY RUN] Would cherry-pick ${commit}`); | |
| } | |
| // Push the hotfix branch. | |
| console.log(`Pushing hotfix branch ${hotfixBranch} to origin...`); | |
| run(`git push --set-upstream origin ${hotfixBranch}`, dryRun); | |
| // Create the pull request. | |
| console.log( | |
| `Creating pull request from ${hotfixBranch} to ${releaseBranch}...`, | |
| ); | |
| 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}.`; | |
| 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`; | |
| } | |
| if (dryRun) { | |
| prBody += '\n\n**[DRY RUN]**'; | |
| } | |
| const prCommand = `gh pr create --base ${releaseBranch} --head ${hotfixBranch} --title "${prTitle}" --body "${prBody}"`; | |
| run(prCommand, dryRun); | |
| if (hasConflicts) { | |
| console.log( | |
| 'โ ๏ธ Patch process completed with conflicts - manual resolution required!', | |
| ); | |
| } else { | |
| console.log('โ Patch process completed successfully!'); | |
| } | |
| 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('---------------------'); | |
| } | |
| return { newBranch: hotfixBranch, created: true, hasConflicts }; | |
| } | |
| function run(command, dryRun = false, throwOnError = true) { | |
| console.log(`> ${command}`); | |
| if (dryRun) { | |
| return; | |
| } | |
| try { | |
| return execSync(command).toString().trim(); | |
| } catch (err) { | |
| console.error(`Command failed: ${command}`); | |
| if (throwOnError) { | |
| throw err; | |
| } | |
| return null; | |
| } | |
| } | |
| function branchExists(branchName) { | |
| try { | |
| execSync(`git ls-remote --exit-code --heads origin ${branchName}`); | |
| return true; | |
| } catch { | |
| return false; | |
| } | |
| } | |
| function getLatestReleaseInfo({ argv, channel } = {}) { | |
| console.log(`Fetching latest release info for channel: ${channel}...`); | |
| const patchFrom = channel; // 'stable' or 'preview' | |
| const command = `node scripts/get-release-version.js --cli-package-name="${argv['cli-package-name']}" --type=patch --patch-from=${patchFrom}`; | |
| try { | |
| 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, | |
| }; | |
| } catch (err) { | |
| console.error(`Failed to get release info for channel: ${channel}`); | |
| throw err; | |
| } | |
| } | |
| main().catch((err) => { | |
| console.error(err); | |
| process.exit(1); | |
| }); | |