Download scripts/releasing/patch-trigger.js from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 11 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/scripts/releasing/patch-trigger.js
- Command line
-
hf download hf://SaylorTwift/gemini-cli/scripts/releasing/patch-trigger.js
-
curl -L -o patch-trigger.js https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/scripts/releasing/patch-trigger.js
11 kB
| /** | |
| * @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); | |
| }); | |