File size: 10,002 Bytes
7a1ad33 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 | #!/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 {
// 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);
});
|