diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2c61752cd..8a57f1803d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: 'Gemini CLI CI' +name: 'Testing: CI' on: push: diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index b0c29226b4..5127836ff3 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -1,4 +1,4 @@ -name: 'E2E Tests' +name: 'Testing: E2E' on: push: diff --git a/scripts/get-release-version.js b/scripts/get-release-version.js index 3981a1a4cf..862ef542dd 100644 --- a/scripts/get-release-version.js +++ b/scripts/get-release-version.js @@ -164,20 +164,40 @@ function getPatchVersion(patchFrom) { const distTag = patchFrom === 'stable' ? 'latest' : 'preview'; const pattern = distTag === 'latest' ? 'v[0-9].[0-9].[0-9]' : 'v*-preview*'; const { latestVersion, latestTag } = getAndVerifyTags(distTag, pattern); - const [version, ...prereleaseParts] = latestVersion.split('-'); - const prerelease = prereleaseParts.join('-'); - const versionParts = version.split('.'); - const major = versionParts[0]; - const minor = versionParts[1]; - const patch = versionParts[2] ? parseInt(versionParts[2]) : 0; - const releaseVersion = prerelease - ? `${major}.${minor}.${patch + 1}-${prerelease}` - : `${major}.${minor}.${patch + 1}`; - return { - releaseVersion, - npmTag: distTag, - previousReleaseTag: latestTag, - }; + + if (patchFrom === 'stable') { + // For stable versions, increment the patch number: 0.5.4 -> 0.5.5 + const versionParts = latestVersion.split('.'); + const major = versionParts[0]; + const minor = versionParts[1]; + const patch = versionParts[2] ? parseInt(versionParts[2]) : 0; + const releaseVersion = `${major}.${minor}.${patch + 1}`; + return { + releaseVersion, + npmTag: distTag, + previousReleaseTag: latestTag, + }; + } else { + // For preview versions, increment the preview number: 0.6.0-preview.2 -> 0.6.0-preview.3 + const [version, prereleasePart] = latestVersion.split('-'); + if (!prereleasePart || !prereleasePart.startsWith('preview.')) { + throw new Error( + `Invalid preview version format: ${latestVersion}. Expected format like "0.6.0-preview.2"`, + ); + } + + const previewNumber = parseInt(prereleasePart.split('.')[1]); + if (isNaN(previewNumber)) { + throw new Error(`Could not parse preview number from: ${prereleasePart}`); + } + + const releaseVersion = `${version}-preview.${previewNumber + 1}`; + return { + releaseVersion, + npmTag: distTag, + previousReleaseTag: latestTag, + }; + } } export function getVersion(options = {}) { diff --git a/scripts/releasing/create-patch-pr.js b/scripts/releasing/create-patch-pr.js index 1874f3f1ca..f9e34bb898 100644 --- a/scripts/releasing/create-patch-pr.js +++ b/scripts/releasing/create-patch-pr.js @@ -97,7 +97,53 @@ async function main() { // Cherry-pick the commit. console.log(`Cherry-picking commit ${commit} into ${hotfixBranch}...`); - run(`git cherry-pick ${commit}`, dryRun); + 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`); + console.log(`✅ Committed cherry-pick with conflict markers`); + } else { + // Re-throw if it's not a conflict error + throw error; + } + } catch (_statusError) { + // 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...`); @@ -107,15 +153,45 @@ async function main() { console.log( `Creating pull request from ${hotfixBranch} to ${releaseBranch}...`, ); - const prTitle = `fix(patch): cherry-pick ${commit.substring(0, 7)} to ${releaseBranch}`; + let 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 (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); - console.log('Patch process completed successfully!'); + 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 ---'); @@ -125,7 +201,7 @@ async function main() { console.log('---------------------'); } - return { newBranch: hotfixBranch, created: true }; + return { newBranch: hotfixBranch, created: true, hasConflicts }; } function run(command, dryRun = false, throwOnError = true) { diff --git a/scripts/releasing/patch-create-comment.js b/scripts/releasing/patch-create-comment.js index 71cd248e2a..a0d6751197 100644 --- a/scripts/releasing/patch-create-comment.js +++ b/scripts/releasing/patch-create-comment.js @@ -182,18 +182,22 @@ A patch branch [\`${branch}\`](https://github.com/${repository}/tree/${branch}) 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 = `🚀 **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}) +- **Hotfix PR**: [#${mockPrNumber}](${mockPrUrl})${hasConflicts ? '\n- **⚠️ Status**: Cherry-pick conflicts detected - manual resolution required' : ''} **📝 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 +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})`; @@ -209,18 +213,22 @@ A patch branch [\`${branch}\`](https://github.com/${repository}/tree/${branch}) if (prList.data.length > 0) { const pr = prList.data[0]; + const hasConflicts = + logContent.includes('Cherry-pick has conflicts') || + pr.title.includes('[CONFLICTS]'); + 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}) +- **Hotfix PR**: [#${pr.number}](${pr.html_url})${hasConflicts ? '\n- **⚠️ Status**: Cherry-pick conflicts detected - manual resolution required' : ''} **📝 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 +1. ${hasConflicts ? '⚠️ **Resolve conflicts** in the hotfix PR first' : 'Review and approve the hotfix PR'}: [#${pr.number}](${pr.html_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.html_url})`; diff --git a/scripts/tests/get-release-version.test.js b/scripts/tests/get-release-version.test.js index d5f9a3cbf0..6e9e194433 100644 --- a/scripts/tests/get-release-version.test.js +++ b/scripts/tests/get-release-version.test.js @@ -21,7 +21,7 @@ describe('getVersion', () => { if (command.includes('npm view') && command.includes('--tag=latest')) return '0.4.1'; if (command.includes('npm view') && command.includes('--tag=preview')) - return '0.5.0-preview-2'; + return '0.5.0-preview.2'; if (command.includes('npm view') && command.includes('--tag=nightly')) return '0.6.0-nightly.20250910.a31830a3'; @@ -29,14 +29,14 @@ describe('getVersion', () => { if (command.includes("git tag --sort=-creatordate -l 'v[0-9].[0-9].[0-9]'")) return 'v0.4.1'; if (command.includes("git tag --sort=-creatordate -l 'v*-preview*'")) - return 'v0.5.0-preview-2'; + return 'v0.5.0-preview.2'; if (command.includes("git tag --sort=-creatordate -l 'v*-nightly*'")) return 'v0.6.0-nightly.20250910.a31830a3'; // GitHub Release Mocks if (command.includes('gh release view "v0.4.1"')) return 'v0.4.1'; - if (command.includes('gh release view "v0.5.0-preview-2"')) - return 'v0.5.0-preview-2'; + if (command.includes('gh release view "v0.5.0-preview.2"')) + return 'v0.5.0-preview.2'; if (command.includes('gh release view "v0.6.0-nightly.20250910.a31830a3"')) return 'v0.6.0-nightly.20250910.a31830a3'; @@ -71,7 +71,7 @@ describe('getVersion', () => { const result = getVersion({ type: 'preview' }); expect(result.releaseVersion).toBe('0.6.0-preview.0'); expect(result.npmTag).toBe('preview'); - expect(result.previousReleaseTag).toBe('v0.5.0-preview-2'); + expect(result.previousReleaseTag).toBe('v0.5.0-preview.2'); }); it('should use the override version for preview if provided', () => { @@ -82,7 +82,7 @@ describe('getVersion', () => { }); expect(result.releaseVersion).toBe('4.5.6-preview.0'); expect(result.npmTag).toBe('preview'); - expect(result.previousReleaseTag).toBe('v0.5.0-preview-2'); + expect(result.previousReleaseTag).toBe('v0.5.0-preview.2'); }); it('should calculate the next nightly version from the latest nightly', () => { @@ -106,9 +106,9 @@ describe('getVersion', () => { it('should calculate the next patch version for a preview release', () => { vi.mocked(execSync).mockImplementation(mockExecSync); const result = getVersion({ type: 'patch', 'patch-from': 'preview' }); - expect(result.releaseVersion).toBe('0.5.1-preview-2'); + expect(result.releaseVersion).toBe('0.5.0-preview.3'); expect(result.npmTag).toBe('preview'); - expect(result.previousReleaseTag).toBe('v0.5.0-preview-2'); + expect(result.previousReleaseTag).toBe('v0.5.0-preview.2'); }); }); @@ -160,13 +160,13 @@ describe('getVersion', () => { vi.mocked(execSync).mockImplementation(mockWithMismatchGitTag); expect(() => getVersion({ type: 'stable' })).toThrow( - 'Discrepancy found! NPM preview tag (0.5.0-preview-2) does not match latest git preview tag (v0.4.0-preview-99).', + 'Discrepancy found! NPM preview tag (0.5.0-preview.2) does not match latest git preview tag (v0.4.0-preview-99).', ); }); it('should throw an error if the GitHub release is missing', () => { const mockWithMissingRelease = (command) => { - if (command.includes('gh release view "v0.5.0-preview-2"')) { + if (command.includes('gh release view "v0.5.0-preview.2"')) { throw new Error('gh command failed'); // Simulate gh failure } return mockExecSync(command); @@ -174,7 +174,7 @@ describe('getVersion', () => { vi.mocked(execSync).mockImplementation(mockWithMissingRelease); expect(() => getVersion({ type: 'stable' })).toThrow( - 'Discrepancy found! Failed to verify GitHub release for v0.5.0-preview-2.', + 'Discrepancy found! Failed to verify GitHub release for v0.5.0-preview.2.', ); }); });