Skip to content

fix(ci): use temp lane names in ci pr to avoid race conditions#10183

Merged
davidfirst merged 10 commits into
masterfrom
fix/ci-pr-race-condition-v2
Feb 4, 2026
Merged

fix(ci): use temp lane names in ci pr to avoid race conditions#10183
davidfirst merged 10 commits into
masterfrom
fix/ci-pr-race-condition-v2

Conversation

@davidfirst
Copy link
Copy Markdown
Member

When multiple CI jobs run bit ci pr on the same branch concurrently, a race condition could cause one job to fail with "lane exists with different hash".

The problem:

  1. Job1 deletes existing lane, creates new lane, starts long snap (~30+ min)
  2. Job2 checks for lane (not found), creates lane, snaps, exports
  3. Job1 finishes snap, tries to export → fails because lane hash changed

The fix:
Use a temporary lane name with a random suffix (e.g., pr-branch-a1b2c) during the snap operation, then rename to the final name right before export. This minimizes the race window to just the quick delete-rename-export operations.

Changes:

  • Modified snapPrCommit to create temp lane, snap, then delete/rename/export
  • Added e2e test verifying the temp lane mechanism

Fresh branch to rule out CI caching issues from #10172

When multiple CI jobs run `bit ci pr` on the same branch concurrently,
a race condition could cause one job to fail with 'lane exists with
different hash'. This happened because the long snap operation occurred
after lane creation, creating a large window for conflicts.

The fix uses a temporary lane name with a random suffix during snap,
then renames to the final name right before export. This minimizes the
race window to just the quick delete-rename-export operations.
- Use crypto-based generateRandomStr instead of Math.random() for better collision resistance
- Add throwOnError param to archiveLane for critical pre-export cleanup
- Clean up temp lane on 'no changes detected' early return
Copilot AI review requested due to automatic review settings February 4, 2026 15:32
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses a race condition that occurs when multiple CI jobs run bit ci pr concurrently on the same branch. The original implementation would delete the existing lane, create a new one with the final name, and then perform a long snap operation (30+ minutes). If a second job started during this window, both would try to export to the same lane, causing a "lane exists with different hash" error.

Changes:

  • Modified the snapPrCommit method to use temporary lane names with random suffixes during the snap operation
  • Added cleanup logic for the temporary lane when no changes are detected
  • Updated the finalization sequence to delete the remote lane, rename the local temp lane, and export atomically
  • Enhanced the archiveLane method to support throwing errors for critical operations and gracefully handle concurrent deletions
  • Added comprehensive e2e tests to verify the temp lane mechanism works correctly

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
scopes/git/ci/ci.main.runtime.ts Implements temporary lane strategy with random suffixes, updates finalization logic to minimize race window, and improves error handling in lane deletion
e2e/harmony/ci-commands.e2e.ts Adds test coverage for temp lane creation, renaming, and cleanup to verify race condition mitigation works as intended

Comment thread scopes/git/ci/ci.main.runtime.ts
Copilot AI review requested due to automatic review settings February 4, 2026 19:35
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comment on lines +468 to +474
this.logger.console(chalk.blue(`Cleaning up temporary lane ${tempLaneFullName}`));
try {
await this.lanes.removeLanes([tempLaneFullName], { remote: false, force: true });
this.logger.console(chalk.green(`Removed temporary lane ${tempLaneFullName}`));
} catch (cleanupErr: any) {
// Ignore cleanup errors to avoid masking the original error
this.logger.console(chalk.yellow(`Failed to clean up temporary lane: ${cleanupErr?.message || cleanupErr}`));
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error cleanup logic doesn't account for the case where the temp lane has already been renamed to the final name. If the rename operation (line 439) succeeds but the export (line 443) fails, the lane will have the final name (laneId.name), not the temp name. The cleanup code should check whether the rename completed and clean up the appropriately named lane. Consider tracking the rename status in a variable and using it to determine which lane name to clean up.

Suggested change
this.logger.console(chalk.blue(`Cleaning up temporary lane ${tempLaneFullName}`));
try {
await this.lanes.removeLanes([tempLaneFullName], { remote: false, force: true });
this.logger.console(chalk.green(`Removed temporary lane ${tempLaneFullName}`));
} catch (cleanupErr: any) {
// Ignore cleanup errors to avoid masking the original error
this.logger.console(chalk.yellow(`Failed to clean up temporary lane: ${cleanupErr?.message || cleanupErr}`));
const finalLaneFullName = `${laneId.scope}/${laneId.name}`;
this.logger.console(
chalk.blue(
`Cleaning up CI lane. Trying temporary lane ${tempLaneFullName}${
tempLaneFullName !== finalLaneFullName ? ` or final lane ${finalLaneFullName}` : ''
}`
)
);
try {
await this.lanes.removeLanes([tempLaneFullName], { remote: false, force: true });
this.logger.console(chalk.green(`Removed temporary lane ${tempLaneFullName}`));
} catch (cleanupErr: any) {
this.logger.console(
chalk.yellow(
`Failed to clean up temporary lane ${tempLaneFullName}: ${cleanupErr?.message || cleanupErr}`
)
);
// If the lane was renamed before the failure, it may now exist under the final name.
if (tempLaneFullName !== finalLaneFullName) {
try {
await this.lanes.removeLanes([finalLaneFullName], { remote: false, force: true });
this.logger.console(chalk.green(`Removed CI lane under final name ${finalLaneFullName}`));
} catch (finalCleanupErr: any) {
// Ignore cleanup errors to avoid masking the original error
this.logger.console(
chalk.yellow(
`Failed to clean up CI lane under final name ${finalLaneFullName}: ${
finalCleanupErr?.message || finalCleanupErr
}`
)
);
}
}

Copilot uses AI. Check for mistakes.
const tempLaneFullName = `${laneId.scope}/${tempLaneName}`;
this.logger.console(chalk.blue(`Cleaning up temporary lane ${tempLaneFullName}`));
try {
await this.lanes.removeLanes([tempLaneFullName], { remote: false, force: true });
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The temp lane cleanup uses the wrong format for the lane name. When removing a local lane (remote: false), the lane name should not include the scope prefix. This should be just tempLaneName instead of tempLaneFullName. The current code may fail to clean up the temporary lane on error, leaving orphaned lanes in the workspace.

Suggested change
await this.lanes.removeLanes([tempLaneFullName], { remote: false, force: true });
await this.lanes.removeLanes([tempLaneName], { remote: false, force: true });

Copilot uses AI. Check for mistakes.
@davidfirst davidfirst merged commit f7b91c2 into master Feb 4, 2026
18 checks passed
@davidfirst davidfirst deleted the fix/ci-pr-race-condition-v2 branch February 4, 2026 20:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants