mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-02-01 22:48:03 +00:00
298 lines
9.8 KiB
JavaScript
298 lines
9.8 KiB
JavaScript
#!/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,
|
|
})
|
|
.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 (_statusError) {
|
|
// 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 (_e) {
|
|
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);
|
|
});
|