chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user