File size: 13,466 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 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 | #!/usr/bin/env node
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Script for commenting on the original PR after patch creation (step 1).
* Handles parsing create-patch-pr.js output and creating appropriate feedback.
*/
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
async function main() {
const argv = await yargs(hideBin(process.argv))
.option('original-pr', {
description: 'The original PR number to comment on',
type: 'number',
demandOption: !process.env.GITHUB_ACTIONS,
})
.option('exit-code', {
description: 'Exit code from patch creation step',
type: 'number',
demandOption: !process.env.GITHUB_ACTIONS,
})
.option('commit', {
description: 'The commit SHA being patched',
type: 'string',
demandOption: !process.env.GITHUB_ACTIONS,
})
.option('channel', {
description: 'The channel (stable or preview)',
type: 'string',
choices: ['stable', 'preview'],
demandOption: !process.env.GITHUB_ACTIONS,
})
.option('repository', {
description: 'The GitHub repository (owner/repo format)',
type: 'string',
demandOption: !process.env.GITHUB_ACTIONS,
})
.option('run-id', {
description: 'The GitHub workflow run ID',
type: 'string',
})
.option('environment', {
choices: ['prod', 'dev'],
type: 'string',
default: process.env.ENVIRONMENT || 'prod',
})
.option('test', {
description: 'Test mode - validate logic without GitHub API calls',
type: 'boolean',
default: false,
})
.example(
'$0 --original-pr 8655 --exit-code 0 --commit abc1234 --channel preview --repository google-gemini/gemini-cli --test',
'Test success comment',
)
.example(
'$0 --original-pr 8655 --exit-code 1 --commit abc1234 --channel stable --repository google-gemini/gemini-cli --test',
'Test failure comment',
)
.help()
.alias('help', 'h').argv;
const testMode = argv.test || process.env.TEST_MODE === 'true';
// GitHub CLI is available in the workflow environment
const hasGitHubCli = !testMode;
// Get inputs from CLI args or environment
const originalPr = argv.originalPr || process.env.ORIGINAL_PR;
const exitCode =
argv.exitCode !== undefined
? argv.exitCode
: parseInt(process.env.EXIT_CODE || '1');
const commit = argv.commit || process.env.COMMIT;
const channel = argv.channel || process.env.CHANNEL;
const environment = argv.environment;
const repository =
argv.repository || process.env.REPOSITORY || 'google-gemini/gemini-cli';
const runId = argv.runId || process.env.GITHUB_RUN_ID || '0';
// Validate required parameters
if (!runId || runId === '0') {
console.warn(
'Warning: No valid GitHub run ID found, workflow links may not work correctly',
);
}
if (!originalPr) {
console.log('No original PR specified, skipping comment');
return;
}
console.log(
`Analyzing patch creation result for PR ${originalPr} (exit code: ${exitCode})`,
);
const [_owner, _repo] = repository.split('/');
const npmTag = channel === 'stable' ? 'latest' : 'preview';
if (testMode) {
console.log('\nπ§ͺ TEST MODE - No API calls will be made');
console.log('\nπ Inputs:');
console.log(` - Original PR: ${originalPr}`);
console.log(` - Exit Code: ${exitCode}`);
console.log(` - Commit: ${commit}`);
console.log(` - Channel: ${channel} β npm tag: ${npmTag}`);
console.log(` - Repository: ${repository}`);
console.log(` - Run ID: ${runId}`);
}
let commentBody;
let logContent = '';
// Get log content from environment variable or generate mock content for testing
if (testMode && !process.env.LOG_CONTENT) {
// Create mock log content for testing only if LOG_CONTENT is not provided
if (exitCode === 0) {
logContent = `Creating hotfix branch hotfix/v0.5.3/${channel}/cherry-pick-${commit.substring(0, 7)} from release/v0.5.3`;
} else {
logContent = 'Error: Failed to create patch';
}
} else {
// Use log content from environment variable
logContent = process.env.LOG_CONTENT || '';
}
if (
logContent.includes(
'Failed to create release branch due to insufficient GitHub App permissions',
)
) {
// GitHub App permission error - extract manual commands
const manualCommandsMatch = logContent.match(
/π Please run these commands manually to create the branch:[\s\S]*?```bash\s*([\s\S]*?)\s*```/,
);
let manualCommands = '';
if (manualCommandsMatch) {
manualCommands = manualCommandsMatch[1].trim();
}
commentBody = `π **[Step 2/4] GitHub App Permission Issue**
The patch creation failed due to insufficient GitHub App permissions for creating workflow files.
**π Manual Action Required:**
${
manualCommands
? `Please run these commands manually to create the release branch:
\`\`\`bash
${manualCommands}
\`\`\`
After running these commands, you can re-run the patch workflow.`
: 'Please check the workflow logs for manual commands to run.'
}
**π Links:**
- [View workflow run](https://github.com/${repository}/actions/runs/${runId})`;
} else if (logContent.includes('already has an open PR')) {
// Branch exists with existing PR
const prMatch = logContent.match(/Found existing PR #(\d+): (.*)/);
if (prMatch) {
const [, prNumber, prUrl] = prMatch;
commentBody = `βΉοΈ **[Step 2/4] Patch PR already exists!**
A patch PR for this change already exists: [#${prNumber}](${prUrl}).
**π Next Steps:**
1. Review and approve the existing patch PR
2. If it's incorrect, close it and run the patch command again
**π Links:**
- [View existing patch PR #${prNumber}](${prUrl})`;
}
} else if (logContent.includes('exists but has no open PR')) {
// Branch exists but no PR
const branchMatch = logContent.match(/Hotfix branch (.*) already exists/);
if (branchMatch) {
const [, branch] = branchMatch;
commentBody = `βΉοΈ **[Step 2/4] Patch branch exists but no PR found!**
A patch branch [\`${branch}\`](https://github.com/${repository}/tree/${branch}) exists but has no open PR.
**π Issue:** This might indicate an incomplete patch process.
**π Next Steps:**
1. Delete the branch: \`git branch -D ${branch}\`
2. Run the patch command again
**π Links:**
- [View branch on GitHub](https://github.com/${repository}/tree/${branch})`;
}
} else if (exitCode === 0) {
// Success - extract branch info
const branchMatch = logContent.match(/Creating hotfix branch (.*) from/);
if (branchMatch) {
const [, branch] = branchMatch;
if (testMode) {
// Mock PR info for testing
const mockPrNumber = Math.floor(Math.random() * 1000) + 8000;
const mockPrUrl = `https://github.com/${repository}/pull/${mockPrNumber}`;
const hasConflicts =
logContent.includes('Cherry-pick has conflicts') ||
logContent.includes('[CONFLICTS]');
commentBody = `π **[Step 2/4] Patch PR Created!**
**π Patch Details:**
- **Environment**: \`${environment}\`
- **Channel**: \`${channel}\` β will publish to npm tag \`${npmTag}\`
- **Commit**: \`${commit}\`
- **Hotfix Branch**: [\`${branch}\`](https://github.com/${repository}/tree/${branch})
- **Hotfix PR**: [#${mockPrNumber}](${mockPrUrl})${hasConflicts ? '\n- **β οΈ Status**: Cherry-pick conflicts detected - manual resolution required' : ''}
**π Next Steps:**
1. ${hasConflicts ? 'β οΈ **Resolve conflicts** in the hotfix PR first' : 'Review and approve the hotfix PR'}: [#${mockPrNumber}](${mockPrUrl})${hasConflicts ? '\n2. **Test your changes** after resolving conflicts' : ''}
${hasConflicts ? '3' : '2'}. Once merged, the patch release will automatically trigger
${hasConflicts ? '4' : '3'}. You'll receive updates here when the release completes
**π Track Progress:**
- [View hotfix PR #${mockPrNumber}](${mockPrUrl})
- [This patch creation workflow run](https://github.com/${repository}/actions/runs/${runId})`;
} else if (hasGitHubCli) {
// Find the actual PR for the new branch using gh CLI
try {
const { spawnSync } = await import('node:child_process');
const result = spawnSync(
'gh',
[
'pr',
'list',
'--head',
branch,
'--state',
'open',
'--json',
'number,title,url',
'--limit',
'1',
],
{ encoding: 'utf8' },
);
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
throw new Error(
`gh pr list failed with status ${result.status}: ${result.stderr}`,
);
}
const prListOutput = result.stdout;
const prList = JSON.parse(prListOutput);
if (prList.length > 0) {
const pr = prList[0];
const hasConflicts =
logContent.includes('Cherry-pick has conflicts') ||
pr.title.includes('[CONFLICTS]');
commentBody = `π **[Step 2/4] Patch PR Created!**
**π Patch Details:**
- **Environment**: \`${environment}\`
- **Channel**: \`${channel}\` β will publish to npm tag \`${npmTag}\`
- **Commit**: \`${commit}\`
- **Hotfix Branch**: [\`${branch}\`](https://github.com/${repository}/tree/${branch})
- **Hotfix PR**: [#${pr.number}](${pr.url})${hasConflicts ? '\n- **β οΈ Status**: Cherry-pick conflicts detected - manual resolution required' : ''}
**π Next Steps:**
1. ${hasConflicts ? 'β οΈ **Resolve conflicts** in the hotfix PR first' : 'Review and approve the hotfix PR'}: [#${pr.number}](${pr.url})${hasConflicts ? '\n2. **Test your changes** after resolving conflicts' : ''}
${hasConflicts ? '3' : '2'}. Once merged, the patch release will automatically trigger
${hasConflicts ? '4' : '3'}. You'll receive updates here when the release completes
**π Track Progress:**
- [View hotfix PR #${pr.number}](${pr.url})
- [This patch creation workflow run](https://github.com/${repository}/actions/runs/${runId})`;
} else {
// Fallback if PR not found yet
commentBody = `π **[Step 2/4] Patch PR Created!**
The patch release PR for this change has been created on branch [\`${branch}\`](https://github.com/${repository}/tree/${branch}).
**π Next Steps:**
1. Review and approve the patch PR
2. Once merged, the patch release will automatically trigger
**π Links:**
- [View all patch PRs](https://github.com/${repository}/pulls?q=is%3Apr+is%3Aopen+label%3Apatch)
- [This patch creation workflow run](https://github.com/${repository}/actions/runs/${runId})`;
}
} catch (error) {
console.log('Error finding PR for branch:', error.message);
// Fallback
commentBody = `π **[Step 2/4] Patch PR Created!**
The patch release PR for this change has been created.
**π Links:**
- [View all patch PRs](https://github.com/${repository}/pulls?q=is%3Apr+is%3Aopen+label%3Apatch)
- [This patch creation workflow run](https://github.com/${repository}/actions/runs/${runId})`;
}
}
}
} else {
// Failure
commentBody = `β **[Step 2/4] Patch creation failed!**
There was an error creating the patch release.
**π Troubleshooting:**
- Check the workflow logs for detailed error information
- Verify the commit SHA is valid and accessible
- Ensure you have permissions to create branches and PRs
**π Links:**
- [View workflow run](https://github.com/${repository}/actions/runs/${runId})`;
}
if (!commentBody) {
commentBody = `β **[Step 2/4] Patch creation failed!**
No output was generated during patch creation.
**π Links:**
- [View workflow run](https://github.com/${repository}/actions/runs/${runId})`;
}
if (testMode) {
console.log('\n㪠Would post comment:');
console.log('----------------------------------------');
console.log(commentBody);
console.log('----------------------------------------');
console.log('\nβ
Comment generation working correctly!');
} else if (hasGitHubCli) {
const { spawnSync } = await import('node:child_process');
const { writeFileSync, unlinkSync } = await import('node:fs');
const { join } = await import('node:path');
// Write comment to temporary file to avoid shell escaping issues
const tmpFile = join(process.cwd(), `comment-${Date.now()}.md`);
writeFileSync(tmpFile, commentBody);
try {
const result = spawnSync(
'gh',
['pr', 'comment', originalPr.toString(), '--body-file', tmpFile],
{
stdio: 'inherit',
},
);
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
throw new Error(`gh pr comment failed with status ${result.status}`);
}
console.log(`Successfully commented on PR ${originalPr}`);
} finally {
// Clean up temp file
try {
unlinkSync(tmpFile);
} catch {
// Ignore cleanup errors
}
}
} else {
console.log('No GitHub CLI available');
}
}
main().catch((error) => {
console.error('Error commenting on PR:', error);
process.exit(1);
});
|