chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 11:58:09 +08:00
commit c48e26cdd0
2909 changed files with 1591131 additions and 0 deletions
+297
View File
@@ -0,0 +1,297 @@
#!/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);
});
+223
View File
@@ -0,0 +1,223 @@
#!/usr/bin/env node
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Script for commenting back to original PR with patch release results.
* Used by the patch release workflow (step 3).
*/
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('success', {
description: 'Whether the release succeeded',
type: 'boolean',
})
.option('release-version', {
description: 'The release version (e.g., 0.5.4)',
type: 'string',
demandOption: !process.env.GITHUB_ACTIONS,
})
.option('release-tag', {
description: 'The release tag (e.g., v0.5.4)',
type: 'string',
})
.option('npm-tag', {
description: 'The npm tag (latest or preview)',
type: 'string',
})
.option('channel', {
description: 'The channel (stable or preview)',
type: 'string',
choices: ['stable', 'preview'],
})
.option('dry-run', {
description: 'Whether this was a dry run',
type: 'boolean',
default: false,
})
.option('test', {
description: 'Test mode - validate logic without GitHub API calls',
type: 'boolean',
default: false,
})
.example(
'$0 --original-pr 8655 --success --release-version "0.5.4" --channel stable --test',
'Test success comment',
)
.example(
'$0 --original-pr 8655 --no-success --channel preview --test',
'Test failure comment',
)
.help()
.alias('help', 'h').argv;
const testMode = argv.test || process.env.TEST_MODE === 'true';
// Initialize GitHub API client only if not in test mode
let github;
if (!testMode) {
const { Octokit } = await import('@octokit/rest');
github = new Octokit({
auth: process.env.GITHUB_TOKEN,
});
}
const repo = {
owner: process.env.GITHUB_REPOSITORY_OWNER || 'google-gemini',
repo: process.env.GITHUB_REPOSITORY_NAME || 'gemini-cli',
};
// Get inputs from CLI args or environment
const originalPr = argv.originalPr || process.env.ORIGINAL_PR;
const success =
argv.success !== undefined ? argv.success : process.env.SUCCESS === 'true';
const releaseVersion = argv.releaseVersion || process.env.RELEASE_VERSION;
const releaseTag =
argv.releaseTag ||
process.env.RELEASE_TAG ||
(releaseVersion ? `v${releaseVersion}` : null);
const npmTag =
argv.npmTag ||
process.env.NPM_TAG ||
(argv.channel === 'stable' ? 'latest' : 'preview');
const channel = argv.channel || process.env.CHANNEL || 'stable';
const dryRun = argv.dryRun || process.env.DRY_RUN === 'true';
const runId = process.env.GITHUB_RUN_ID || '12345678';
const raceConditionFailure = process.env.RACE_CONDITION_FAILURE === 'true';
// Current version info for race condition failures
const currentReleaseVersion = process.env.CURRENT_RELEASE_VERSION;
const currentReleaseTag = process.env.CURRENT_RELEASE_TAG;
const currentPreviousTag = process.env.CURRENT_PREVIOUS_TAG;
if (!originalPr) {
console.log('No original PR specified, skipping comment');
return;
}
console.log(
`Commenting on original PR ${originalPr} with ${success ? 'success' : 'failure'} status`,
);
if (testMode) {
console.log('\n🧪 TEST MODE - No API calls will be made');
console.log('\n📋 Inputs:');
console.log(` - Original PR: ${originalPr}`);
console.log(` - Success: ${success}`);
console.log(` - Release Version: ${releaseVersion}`);
console.log(` - Release Tag: ${releaseTag}`);
console.log(` - NPM Tag: ${npmTag}`);
console.log(` - Channel: ${channel}`);
console.log(` - Dry Run: ${dryRun}`);
console.log(` - Run ID: ${runId}`);
}
let commentBody;
if (success) {
commentBody = `✅ **[Step 4/4] Patch Release Complete!**
**📦 Release Details:**
- **Version**: [\`${releaseVersion}\`](https://github.com/${repo.owner}/${repo.repo}/releases/tag/${releaseTag})
- **NPM Tag**: \`${npmTag}\`
- **Channel**: \`${channel}\`
- **Dry Run**: ${dryRun}
**🎉 Status:** Your patch has been successfully released and published to npm!
**📝 What's Available:**
- **GitHub Release**: [View release ${releaseTag}](https://github.com/${repo.owner}/${repo.repo}/releases/tag/${releaseTag})
- **NPM Package**: \`npm install @google/gemini-cli@${npmTag}\`
**🔗 Links:**
- [GitHub Release](https://github.com/${repo.owner}/${repo.repo}/releases/tag/${releaseTag})
- [This release workflow run](https://github.com/${repo.owner}/${repo.repo}/actions/runs/${runId})
- [Workflow History](https://github.com/${repo.owner}/${repo.repo}/actions/workflows/release-patch-3-release.yml)`;
} else if (raceConditionFailure) {
commentBody = `⚠️ **[Step 4/4] Patch Release Cancelled - Concurrent Release Detected**
**🚦 What Happened:**
Another patch release completed while this one was in progress, causing a version conflict.
**📋 Details:**
- **Originally planned**: \`${releaseVersion || 'Unknown'}\`
- **Channel**: \`${channel}\`
- **Issue**: Version numbers are no longer sequential due to concurrent releases
**📊 Current State:**${
currentReleaseVersion
? `
- **Latest ${channel} version**: \`${currentPreviousTag?.replace(/^v/, '') || 'unknown'}\`
- **Next patch should be**: \`${currentReleaseVersion}\`
- **New release tag**: \`${currentReleaseTag || 'unknown'}\``
: `
- **Status**: Version information updated since this release was triggered`
}
**🔄 Next Steps:**
1. **Request a new patch** - The version calculation will now be correct
2. No action needed on your part - simply request the patch again
3. The system detected this automatically to prevent invalid releases
**💡 Why This Happens:**
Multiple patch releases can't run simultaneously. When they do, the second one is automatically cancelled to maintain version consistency.
**🔗 Details:**
- [This release workflow run](https://github.com/${repo.owner}/${repo.repo}/actions/runs/${runId})
- [Workflow History](https://github.com/${repo.owner}/${repo.repo}/actions/workflows/release-patch-3-release.yml)`;
} else {
commentBody = `❌ **[Step 4/4] Patch Release Failed!**
**📋 Details:**
- **Version**: \`${releaseVersion || 'Unknown'}\`
- **Channel**: \`${channel}\`
- **Error**: The patch release workflow encountered an error
**🔍 Next Steps:**
1. Check the workflow logs for detailed error information
2. The maintainers have been notified via automatic issue creation
3. You may need to retry the patch once the issue is resolved
**🔗 Troubleshooting:**
- [This release workflow run](https://github.com/${repo.owner}/${repo.repo}/actions/runs/${runId})
- [View workflow logs](https://github.com/${repo.owner}/${repo.repo}/actions/runs/${runId})
- [Workflow History](https://github.com/${repo.owner}/${repo.repo}/actions/workflows/release-patch-3-release.yml)`;
}
if (testMode) {
console.log('\n💬 Would post comment:');
console.log('----------------------------------------');
console.log(commentBody);
console.log('----------------------------------------');
console.log('\n✅ Comment generation working correctly!');
} else if (github) {
await github.rest.issues.createComment({
owner: repo.owner,
repo: repo.repo,
issue_number: parseInt(originalPr),
body: commentBody,
});
console.log(`Successfully commented on PR ${originalPr}`);
} else {
console.log('No GitHub client available');
}
}
main().catch((error) => {
console.error('Error commenting on PR:', error);
process.exit(1);
});
+389
View File
@@ -0,0 +1,389 @@
#!/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);
});
+334
View File
@@ -0,0 +1,334 @@
#!/usr/bin/env node
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Script for patch release trigger workflow (step 2).
* Handles channel detection, workflow dispatch, and user feedback.
*/
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
/**
* Extract base version, original pr, and originalPr info from hotfix branch name.
* Formats:
* - New NEW: hotfix/v0.5.3/v0.5.4/preview/cherry-pick-abc/pr-1234 -> v0.5.4, preview, 1234
* - New format: hotfix/v0.5.3/preview/cherry-pick-abc -> v0.5.3 and preview
* - Old format: hotfix/v0.5.3/cherry-pick-abc -> v0.5.3 and stable (default)
* We check the formats from newest to oldest. If the channel found is invalid,
* an error is thrown.
*/
function getBranchInfo({ branchName, context }) {
const parts = branchName.split('/');
const version = parts[1];
let prNum;
let channel = 'stable'; // default for old format
if (parts.length >= 6 && (parts[3] === 'stable' || parts[3] === 'preview')) {
channel = parts[3];
const prMatch = parts[5].match(/pr-(\d+)/);
prNum = prMatch[1];
} else if (
parts.length >= 4 &&
(parts[2] === 'stable' || parts[2] === 'preview')
) {
// New format with explicit channel
channel = parts[2];
} else if (context.eventName === 'workflow_dispatch') {
// Manual dispatch, infer from version name
channel = version.includes('preview') ? 'preview' : 'stable';
}
// Validate channel
if (channel !== 'stable' && channel !== 'preview') {
throw new Error(
`Invalid channel: ${channel}. Must be 'stable' or 'preview'.`,
);
}
return { channel, prNum, version };
}
async function main() {
const argv = await yargs(hideBin(process.argv))
.option('head-ref', {
description:
'The hotfix branch name (e.g., hotfix/v0.5.3/preview/cherry-pick-abc1234)',
type: 'string',
demandOption: !process.env.GITHUB_ACTIONS,
})
.option('pr-body', {
description: 'The PR body content',
type: 'string',
default: '',
})
.option('dry-run', {
description: 'Run in test mode without actually triggering workflows',
type: 'boolean',
default: false,
})
.option('test', {
description: 'Test mode - validate logic without GitHub API calls',
type: 'boolean',
default: false,
})
.option('force-skip-tests', {
description: 'Skip the "Run Tests" step in testing',
type: 'boolean',
default: false,
})
.option('environment', {
choices: ['prod', 'dev'],
type: 'string',
default: process.env.ENVIRONMENT || 'prod',
})
.example(
'$0 --head-ref "hotfix/v0.5.3/preview/cherry-pick-abc1234" --test',
'Test channel detection logic',
)
.example(
'$0 --head-ref "hotfix/v0.5.3/stable/cherry-pick-abc1234" --dry-run',
'Test with GitHub API in dry-run mode',
)
.help()
.alias('help', 'h').argv;
const testMode = argv.test || process.env.TEST_MODE === 'true';
const context = {
eventName: process.env.GITHUB_EVENT_NAME || 'pull_request',
repo: {
owner: process.env.GITHUB_REPOSITORY_OWNER || 'google-gemini',
repo: process.env.GITHUB_REPOSITORY_NAME || 'gemini-cli',
},
payload: JSON.parse(process.env.GITHUB_EVENT_PAYLOAD || '{}'),
};
// Get inputs from CLI args or environment
const headRef = argv.headRef || process.env.HEAD_REF;
const environment = argv.environment;
const body = argv.prBody || process.env.PR_BODY || '';
const isDryRun = argv.dryRun || body.includes('[DRY RUN]');
const forceSkipTests =
argv.forceSkipTests || process.env.FORCE_SKIP_TESTS === 'true';
const runId = process.env.GITHUB_RUN_ID || '0';
if (!headRef) {
throw new Error(
'head-ref is required. Use --head-ref or set HEAD_REF environment variable.',
);
}
console.log(`Processing patch trigger for branch: ${headRef}`);
const { prNum, version, channel } = getBranchInfo({
branchName: headRef,
context,
});
let originalPr = prNum;
console.log(`Found originalPr: ${prNum} from hotfix branch`);
// Fallback to using PR search (inconsistent) if no pr found in branch name.
if (!testMode && !originalPr) {
try {
console.log('Looking for original PR using search...');
const { execFileSync } = await import('node:child_process');
// Split search string into searchArgs to prevent triple escaping on the quoted filters
const searchArgs =
`repo:${context.repo.owner}/${context.repo.repo} is:pr in:comments "${headRef}"`.split(
' ',
);
console.log('Search args:', searchArgs);
// Use gh CLI to search for PRs with comments referencing the hotfix branch
const result = execFileSync(
'gh',
[
'search',
'prs',
'--json',
'number,title',
'--limit',
'1',
...searchArgs,
'Patch PR Created',
],
{
encoding: 'utf8',
env: { ...process.env, GH_TOKEN: process.env.GITHUB_TOKEN },
},
);
const searchResults = JSON.parse(result);
if (searchResults && searchResults.length > 0) {
originalPr = searchResults[0].number;
console.log(`Found original PR: #${originalPr}`);
} else {
console.log('Could not find a matching original PR via search.');
}
} catch (e) {
console.log('Could not determine original PR:', e.message);
}
}
if (!originalPr && testMode) {
console.log('Skipping original PR lookup (test mode)');
originalPr = 8655; // Mock for testing
}
if (!originalPr) {
throw new Error(
'Could not find the original PR for this patch. Cannot proceed with release.',
);
}
const releaseRef = `release/${version}-pr-${originalPr}`;
const workflowId =
context.eventName === 'pull_request'
? 'release-patch-3-release.yml'
: process.env.WORKFLOW_ID || 'release-patch-3-release.yml';
console.log(`Detected channel: ${channel}, version: ${version}`);
console.log(`Release ref: ${releaseRef}`);
console.log(`Workflow ID: ${workflowId}`);
console.log(`Dry run: ${isDryRun}`);
if (testMode) {
console.log('\n🧪 TEST MODE - No API calls will be made');
console.log('\n📋 Parsed Results:');
console.log(` - Branch: ${headRef}`);
console.log(
` - Channel: ${channel} → npm tag: ${channel === 'stable' ? 'latest' : 'preview'}`,
);
console.log(` - Version: ${version}`);
console.log(` - Release ref: ${releaseRef}`);
console.log(` - Workflow: ${workflowId}`);
console.log(` - Dry run: ${isDryRun}`);
console.log('\n✅ Channel detection logic working correctly!');
return;
}
// Trigger the release workflow
console.log(`Triggering release workflow: ${workflowId}`);
if (!testMode) {
try {
const { execFileSync } = await import('node:child_process');
const args = [
'workflow',
'run',
workflowId,
'--ref',
'main',
'--field',
`type=${channel}`,
'--field',
`dry_run=${isDryRun.toString()}`,
'--field',
`force_skip_tests=${forceSkipTests.toString()}`,
'--field',
`release_ref=${releaseRef}`,
'--field',
`environment=${environment}`,
'--field',
originalPr ? `original_pr=${originalPr.toString()}` : 'original_pr=',
];
console.log(`Running command: gh ${args.join(' ')}`);
execFileSync('gh', args, {
stdio: 'inherit',
env: { ...process.env, GH_TOKEN: process.env.GITHUB_TOKEN },
});
console.log('✅ Workflow dispatch completed successfully');
} catch (e) {
console.error('❌ Failed to dispatch workflow:', e.message);
throw e;
}
} else {
console.log('✅ Would trigger workflow with inputs:', {
type: channel,
dry_run: isDryRun.toString(),
force_skip_tests: forceSkipTests.toString(),
release_ref: releaseRef,
original_pr: originalPr ? originalPr.toString() : '',
});
}
// Comment back to original PR if we found it
if (originalPr) {
console.log(`Commenting on original PR ${originalPr}...`);
const npmTag = channel === 'stable' ? 'latest' : 'preview';
const commentBody = `🚀 **[Step 3/4] Patch Release ${environment === 'prod' ? 'Waiting for Approval' : 'Triggered'}!**
**📋 Release Details:**
- **Environment**: \`${environment}\`
- **Channel**: \`${channel}\` → publishing to npm tag \`${npmTag}\`
- **Version**: \`${version}\`
- **Hotfix PR**: Merged ✅
- **Release Branch**: [\`${releaseRef}\`](https://github.com/${context.repo.owner}/${context.repo.repo}/tree/${releaseRef})
**⏳ Status:** The patch release has been triggered${environment === 'prod' ? ' and is waiting for deployment approval. Please visit the specific workflow run link below and approve the deployment' : ''}. You'll receive another update when it completes.
**🔗 Track Progress:**
- [View release workflow history](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/workflows/${workflowId})
- [This trigger workflow run](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId})`;
if (!testMode) {
let tempDir;
try {
const { execFileSync } = await import('node:child_process');
const { writeFileSync, mkdtempSync } = await import('node:fs');
const { join } = await import('node:path');
const { tmpdir } = await import('node:os');
// Create secure temporary directory and file
tempDir = mkdtempSync(join(tmpdir(), 'patch-trigger-'));
const tempFile = join(tempDir, 'comment.md');
writeFileSync(tempFile, commentBody);
execFileSync(
'gh',
['pr', 'comment', originalPr.toString(), '--body-file', tempFile],
{
stdio: 'inherit',
env: { ...process.env, GH_TOKEN: process.env.GITHUB_TOKEN },
},
);
console.log('✅ Comment posted successfully');
} catch (e) {
console.error('❌ Failed to post comment:', e.message);
// Don't throw here since the main workflow dispatch succeeded
} finally {
// Clean up temp directory and all its contents
if (tempDir) {
try {
const { rmSync } = await import('node:fs');
rmSync(tempDir, { recursive: true, force: true });
} catch (cleanupError) {
console.warn(
'⚠️ Failed to clean up temp directory:',
cleanupError.message,
);
}
}
}
} else {
console.log('✅ Would post comment:', commentBody);
}
}
console.log('Patch trigger completed successfully!');
}
main().catch((error) => {
console.error('Error in patch trigger:', error);
process.exit(1);
});