dealing with conflicts (#8772)

This commit is contained in:
matt korwel
2025-09-18 17:33:08 -07:00
committed by GitHub
parent 29852e9b08
commit b14a4f5ebc
6 changed files with 143 additions and 39 deletions

View File

@@ -1,4 +1,4 @@
name: 'Gemini CLI CI' name: 'Testing: CI'
on: on:
push: push:

View File

@@ -1,4 +1,4 @@
name: 'E2E Tests' name: 'Testing: E2E'
on: on:
push: push:

View File

@@ -164,20 +164,40 @@ function getPatchVersion(patchFrom) {
const distTag = patchFrom === 'stable' ? 'latest' : 'preview'; const distTag = patchFrom === 'stable' ? 'latest' : 'preview';
const pattern = distTag === 'latest' ? 'v[0-9].[0-9].[0-9]' : 'v*-preview*'; const pattern = distTag === 'latest' ? 'v[0-9].[0-9].[0-9]' : 'v*-preview*';
const { latestVersion, latestTag } = getAndVerifyTags(distTag, pattern); const { latestVersion, latestTag } = getAndVerifyTags(distTag, pattern);
const [version, ...prereleaseParts] = latestVersion.split('-');
const prerelease = prereleaseParts.join('-'); if (patchFrom === 'stable') {
const versionParts = version.split('.'); // For stable versions, increment the patch number: 0.5.4 -> 0.5.5
const major = versionParts[0]; const versionParts = latestVersion.split('.');
const minor = versionParts[1]; const major = versionParts[0];
const patch = versionParts[2] ? parseInt(versionParts[2]) : 0; const minor = versionParts[1];
const releaseVersion = prerelease const patch = versionParts[2] ? parseInt(versionParts[2]) : 0;
? `${major}.${minor}.${patch + 1}-${prerelease}` const releaseVersion = `${major}.${minor}.${patch + 1}`;
: `${major}.${minor}.${patch + 1}`; return {
return { releaseVersion,
releaseVersion, npmTag: distTag,
npmTag: distTag, previousReleaseTag: latestTag,
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 = {}) { export function getVersion(options = {}) {

View File

@@ -97,7 +97,53 @@ async function main() {
// Cherry-pick the commit. // Cherry-pick the commit.
console.log(`Cherry-picking commit ${commit} into ${hotfixBranch}...`); 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. // Push the hotfix branch.
console.log(`Pushing hotfix branch ${hotfixBranch} to origin...`); console.log(`Pushing hotfix branch ${hotfixBranch} to origin...`);
@@ -107,15 +153,45 @@ async function main() {
console.log( console.log(
`Creating pull request from ${hotfixBranch} to ${releaseBranch}...`, `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.`; 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) { if (dryRun) {
prBody += '\n\n**[DRY RUN]**'; prBody += '\n\n**[DRY RUN]**';
} }
const prCommand = `gh pr create --base ${releaseBranch} --head ${hotfixBranch} --title "${prTitle}" --body "${prBody}"`; const prCommand = `gh pr create --base ${releaseBranch} --head ${hotfixBranch} --title "${prTitle}" --body "${prBody}"`;
run(prCommand, dryRun); 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) { if (dryRun) {
console.log('\n--- Dry Run Summary ---'); console.log('\n--- Dry Run Summary ---');
@@ -125,7 +201,7 @@ async function main() {
console.log('---------------------'); console.log('---------------------');
} }
return { newBranch: hotfixBranch, created: true }; return { newBranch: hotfixBranch, created: true, hasConflicts };
} }
function run(command, dryRun = false, throwOnError = true) { function run(command, dryRun = false, throwOnError = true) {

View File

@@ -182,18 +182,22 @@ A patch branch [\`${branch}\`](https://github.com/${repository}/tree/${branch})
const mockPrNumber = Math.floor(Math.random() * 1000) + 8000; const mockPrNumber = Math.floor(Math.random() * 1000) + 8000;
const mockPrUrl = `https://github.com/${repository}/pull/${mockPrNumber}`; const mockPrUrl = `https://github.com/${repository}/pull/${mockPrNumber}`;
const hasConflicts =
logContent.includes('Cherry-pick has conflicts') ||
logContent.includes('[CONFLICTS]');
commentBody = `🚀 **Patch PR Created!** commentBody = `🚀 **Patch PR Created!**
**📋 Patch Details:** **📋 Patch Details:**
- **Channel**: \`${channel}\` → will publish to npm tag \`${npmTag}\` - **Channel**: \`${channel}\` → will publish to npm tag \`${npmTag}\`
- **Commit**: \`${commit}\` - **Commit**: \`${commit}\`
- **Hotfix Branch**: [\`${branch}\`](https://github.com/${repository}/tree/${branch}) - **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:** **📝 Next Steps:**
1. Review and approve the hotfix PR: [#${mockPrNumber}](${mockPrUrl}) 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' : ''}
2. Once merged, the patch release will automatically trigger ${hasConflicts ? '3' : '2'}. Once merged, the patch release will automatically trigger
3. You'll receive updates here when the release completes ${hasConflicts ? '4' : '3'}. You'll receive updates here when the release completes
**🔗 Track Progress:** **🔗 Track Progress:**
- [View hotfix PR #${mockPrNumber}](${mockPrUrl})`; - [View hotfix PR #${mockPrNumber}](${mockPrUrl})`;
@@ -209,18 +213,22 @@ A patch branch [\`${branch}\`](https://github.com/${repository}/tree/${branch})
if (prList.data.length > 0) { if (prList.data.length > 0) {
const pr = prList.data[0]; const pr = prList.data[0];
const hasConflicts =
logContent.includes('Cherry-pick has conflicts') ||
pr.title.includes('[CONFLICTS]');
commentBody = `🚀 **Patch PR Created!** commentBody = `🚀 **Patch PR Created!**
**📋 Patch Details:** **📋 Patch Details:**
- **Channel**: \`${channel}\` → will publish to npm tag \`${npmTag}\` - **Channel**: \`${channel}\` → will publish to npm tag \`${npmTag}\`
- **Commit**: \`${commit}\` - **Commit**: \`${commit}\`
- **Hotfix Branch**: [\`${branch}\`](https://github.com/${repository}/tree/${branch}) - **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:** **📝 Next Steps:**
1. Review and approve the hotfix PR: [#${pr.number}](${pr.html_url}) 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' : ''}
2. Once merged, the patch release will automatically trigger ${hasConflicts ? '3' : '2'}. Once merged, the patch release will automatically trigger
3. You'll receive updates here when the release completes ${hasConflicts ? '4' : '3'}. You'll receive updates here when the release completes
**🔗 Track Progress:** **🔗 Track Progress:**
- [View hotfix PR #${pr.number}](${pr.html_url})`; - [View hotfix PR #${pr.number}](${pr.html_url})`;

View File

@@ -21,7 +21,7 @@ describe('getVersion', () => {
if (command.includes('npm view') && command.includes('--tag=latest')) if (command.includes('npm view') && command.includes('--tag=latest'))
return '0.4.1'; return '0.4.1';
if (command.includes('npm view') && command.includes('--tag=preview')) 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')) if (command.includes('npm view') && command.includes('--tag=nightly'))
return '0.6.0-nightly.20250910.a31830a3'; 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]'")) if (command.includes("git tag --sort=-creatordate -l 'v[0-9].[0-9].[0-9]'"))
return 'v0.4.1'; return 'v0.4.1';
if (command.includes("git tag --sort=-creatordate -l 'v*-preview*'")) 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*'")) if (command.includes("git tag --sort=-creatordate -l 'v*-nightly*'"))
return 'v0.6.0-nightly.20250910.a31830a3'; return 'v0.6.0-nightly.20250910.a31830a3';
// GitHub Release Mocks // GitHub Release Mocks
if (command.includes('gh release view "v0.4.1"')) return 'v0.4.1'; if (command.includes('gh release view "v0.4.1"')) return 'v0.4.1';
if (command.includes('gh release view "v0.5.0-preview-2"')) if (command.includes('gh release view "v0.5.0-preview.2"'))
return 'v0.5.0-preview-2'; return 'v0.5.0-preview.2';
if (command.includes('gh release view "v0.6.0-nightly.20250910.a31830a3"')) if (command.includes('gh release view "v0.6.0-nightly.20250910.a31830a3"'))
return 'v0.6.0-nightly.20250910.a31830a3'; return 'v0.6.0-nightly.20250910.a31830a3';
@@ -71,7 +71,7 @@ describe('getVersion', () => {
const result = getVersion({ type: 'preview' }); const result = getVersion({ type: 'preview' });
expect(result.releaseVersion).toBe('0.6.0-preview.0'); expect(result.releaseVersion).toBe('0.6.0-preview.0');
expect(result.npmTag).toBe('preview'); 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', () => { 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.releaseVersion).toBe('4.5.6-preview.0');
expect(result.npmTag).toBe('preview'); 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', () => { 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', () => { it('should calculate the next patch version for a preview release', () => {
vi.mocked(execSync).mockImplementation(mockExecSync); vi.mocked(execSync).mockImplementation(mockExecSync);
const result = getVersion({ type: 'patch', 'patch-from': 'preview' }); 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.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); vi.mocked(execSync).mockImplementation(mockWithMismatchGitTag);
expect(() => getVersion({ type: 'stable' })).toThrow( 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', () => { it('should throw an error if the GitHub release is missing', () => {
const mockWithMissingRelease = (command) => { 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 throw new Error('gh command failed'); // Simulate gh failure
} }
return mockExecSync(command); return mockExecSync(command);
@@ -174,7 +174,7 @@ describe('getVersion', () => {
vi.mocked(execSync).mockImplementation(mockWithMissingRelease); vi.mocked(execSync).mockImplementation(mockWithMissingRelease);
expect(() => getVersion({ type: 'stable' })).toThrow( 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.',
); );
}); });
}); });