patch e2e vnext (#8767)

This commit is contained in:
matt korwel
2025-09-18 16:31:39 -07:00
committed by GitHub
parent 509444d059
commit 29852e9b08
8 changed files with 905 additions and 95 deletions

View File

@@ -0,0 +1,174 @@
#!/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('channel', {
alias: 'ch',
description: 'The release channel to patch.',
choices: ['stable', 'preview'],
demandOption: true,
})
.option('dry-run', {
description: 'Whether to run in dry-run mode.',
type: 'boolean',
default: false,
})
.help()
.alias('help', 'h').argv;
const { commit, channel, dryRun } = 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 latestTag = getLatestTag(channel);
console.log(`Found latest tag for ${channel}: ${latestTag}`);
const releaseBranch = `release/${latestTag}`;
const hotfixBranch = `hotfix/${latestTag}/${channel}/cherry-pick-${commit.substring(0, 7)}`;
// 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}...`,
);
run(`git checkout -b ${releaseBranch} ${latestTag}`, dryRun);
run(`git push origin ${releaseBranch}`, dryRun);
} 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);
// Cherry-pick the commit.
console.log(`Cherry-picking commit ${commit} into ${hotfixBranch}...`);
run(`git cherry-pick ${commit}`, dryRun);
// 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}...`,
);
const prTitle = `fix(patch): cherry-pick ${commit.substring(0, 7)} to ${releaseBranch}`;
let prBody = `This PR automatically cherry-picks commit ${commit} to patch the ${channel} release.`;
if (dryRun) {
prBody += '\n\n**[DRY RUN]**';
}
const prCommand = `gh pr create --base ${releaseBranch} --head ${hotfixBranch} --title "${prTitle}" --body "${prBody}"`;
run(prCommand, dryRun);
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 };
}
function run(command, dryRun = false, throwOnError = true) {
console.log(`> ${command}`);
if (dryRun) {
return;
}
try {
return execSync(command).toString().trim();
} catch (err) {
console.error(`Command failed: ${command}`);
if (throwOnError) {
throw err;
}
return null;
}
}
function branchExists(branchName) {
try {
execSync(`git ls-remote --exit-code --heads origin ${branchName}`);
return true;
} catch (_e) {
return false;
}
}
function getLatestTag(channel) {
console.log(`Fetching latest tag for channel: ${channel}...`);
const pattern =
channel === 'stable'
? '(contains("nightly") or contains("preview")) | not'
: '(contains("preview"))';
const command = `gh release list --limit 30 --json tagName | jq -r '[.[] | select(.tagName | ${pattern})] | .[0].tagName'`;
try {
return execSync(command).toString().trim();
} catch (err) {
console.error(`Failed to get latest tag for channel: ${channel}`);
throw err;
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});

View File

@@ -0,0 +1,220 @@
#!/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 = `✅ **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})
- [Workflow Run](https://github.com/${repo.owner}/${repo.repo}/actions/runs/${runId})`;
} else if (raceConditionFailure) {
commentBody = `⚠️ **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 started`
}
**🔄 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:**
- [View cancelled workflow run](https://github.com/${repo.owner}/${repo.repo}/actions/runs/${runId})`;
} else {
commentBody = `❌ **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:**
- [View 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})`;
}
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);
});

View File

@@ -0,0 +1,299 @@
#!/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';
import { readFileSync } from 'node:fs';
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('output-log', {
description: 'Path to the patch output log file',
type: 'string',
default: 'patch_output.log',
})
.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',
default: '0',
})
.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';
// 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.GH_TOKEN || process.env.GITHUB_TOKEN,
});
}
// 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 outputLog =
argv.outputLog || process.env.OUTPUT_LOG || 'patch_output.log';
const commit = argv.commit || process.env.COMMIT;
const channel = argv.channel || process.env.CHANNEL;
const repository =
argv.repository || process.env.REPOSITORY || 'google-gemini/gemini-cli';
const runId = argv.runId || process.env.GITHUB_RUN_ID || '0';
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(` - Output Log: ${outputLog}`);
console.log(` - Commit: ${commit}`);
console.log(` - Channel: ${channel} → npm tag: ${npmTag}`);
console.log(` - Repository: ${repository}`);
console.log(` - Run ID: ${runId}`);
}
let commentBody;
let logContent = '';
// Try to read the output log
try {
if (testMode) {
// Create mock log content for testing
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 {
logContent = readFileSync(outputLog, 'utf8');
}
} catch (error) {
console.log(`Could not read output log ${outputLog}:`, error.message);
}
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 = ` **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 = ` **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}`;
commentBody = `🚀 **Patch PR Created!**
**📋 Patch Details:**
- **Channel**: \`${channel}\` → will publish to npm tag \`${npmTag}\`
- **Commit**: \`${commit}\`
- **Hotfix Branch**: [\`${branch}\`](https://github.com/${repository}/tree/${branch})
- **Hotfix PR**: [#${mockPrNumber}](${mockPrUrl})
**📝 Next Steps:**
1. Review and approve the hotfix PR: [#${mockPrNumber}](${mockPrUrl})
2. Once merged, the patch release will automatically trigger
3. You'll receive updates here when the release completes
**🔗 Track Progress:**
- [View hotfix PR #${mockPrNumber}](${mockPrUrl})`;
} else if (github) {
// Find the actual PR for the new branch
try {
const prList = await github.rest.pulls.list({
owner,
repo,
head: `${owner}:${branch}`,
state: 'open',
});
if (prList.data.length > 0) {
const pr = prList.data[0];
commentBody = `🚀 **Patch PR Created!**
**📋 Patch Details:**
- **Channel**: \`${channel}\` → will publish to npm tag \`${npmTag}\`
- **Commit**: \`${commit}\`
- **Hotfix Branch**: [\`${branch}\`](https://github.com/${repository}/tree/${branch})
- **Hotfix PR**: [#${pr.number}](${pr.html_url})
**📝 Next Steps:**
1. Review and approve the hotfix PR: [#${pr.number}](${pr.html_url})
2. Once merged, the patch release will automatically trigger
3. You'll receive updates here when the release completes
**🔗 Track Progress:**
- [View hotfix PR #${pr.number}](${pr.html_url})`;
} else {
// Fallback if PR not found yet
commentBody = `🚀 **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)`;
}
} catch (error) {
console.log('Error finding PR for branch:', error.message);
// Fallback
commentBody = `🚀 **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)`;
}
}
}
} else {
// Failure
commentBody = `❌ **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 = `❌ **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 (github) {
await github.rest.issues.createComment({
owner,
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);
});

View File

@@ -0,0 +1,221 @@
#!/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';
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,
})
.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';
// 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 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 body = argv.prBody || process.env.PR_BODY || '';
const isDryRun = argv.dryRun || body.includes('[DRY RUN]');
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}`);
// Extract base version and channel from hotfix branch name
// 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)
const parts = headRef.split('/');
const version = parts[1];
let channel = 'stable'; // default for old format
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'.`,
);
}
const releaseRef = `release/${version}`;
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;
}
// Try to find the original PR that requested this patch
let originalPr = null;
if (!testMode && github) {
try {
console.log('Looking for original PR using search...');
// Use GitHub search to find the PR with a comment referencing the hotfix branch.
// This is much more efficient than listing PRs and their comments.
const query = `repo:${context.repo.owner}/${context.repo.repo} is:pr is:all in:comments "Patch PR Created" "${headRef}"`;
const searchResults = await github.rest.search.issuesAndPullRequests({
q: query,
sort: 'updated',
order: 'desc',
per_page: 1,
});
if (searchResults.data.items.length > 0) {
originalPr = searchResults.data.items[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);
}
} else {
console.log('Skipping original PR lookup (test mode)');
originalPr = 8655; // Mock for testing
}
// Trigger the release workflow
console.log(`Triggering release workflow: ${workflowId}`);
if (!testMode && github) {
await github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: workflowId,
ref: 'main',
inputs: {
type: channel,
dry_run: isDryRun.toString(),
release_ref: releaseRef,
original_pr: originalPr ? originalPr.toString() : '',
},
});
} else {
console.log('✅ Would trigger workflow with inputs:', {
type: channel,
dry_run: isDryRun.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 = `🚀 **Patch Release Started!**
**📋 Release Details:**
- **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 is now running. You'll receive another update when it completes.
**🔗 Track Progress:**
- [View release workflow](https://github.com/${context.repo.owner}/${context.repo.repo}/actions)`;
if (!testMode && github) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: originalPr,
body: commentBody,
});
} 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);
});