fix(ci): recover from a stale remote lane on --keep-lane#10400
Merged
Conversation
When --keep-lane finds the lane on the remote, it calls switchToLane and trusts that as the way to land on the existing lane. If the stored lane is stale relative to the current PR (the PR has since removed/renamed a ModelComponent the lane still references, surfaced as 'unable to merge lane …, the component … was not found'), the switch fails — switchToLane logs and swallows the error, the post-switch guard then throws, and every subsequent 'bit ci pr --keep-lane' on that branch fails the same way until the lane is manually deleted on the remote. Replace the throw with a recovery path: when we observe that we did not land on the requested lane after switching, delete the stale remote lane and create a fresh one (same name) — the same shape the create-from-scratch path takes. The lane's prior history is lost on this one run, but the next run preserves history again, and we're no longer wedged. Surfaced by PR #10397 in CI; reachable on any long-running PR whose component shape changed after a lane was first created.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR updates the bit ci pr --keep-lane flow to recover when an existing remote lane is stale and cannot be switched to, avoiding repeated CI failures on the same PR branch.
Changes:
- Detects when switching to an existing remote lane did not actually land on the requested lane.
- Deletes the stale remote lane and recreates a fresh lane with the same name.
- Keeps the existing post-switch/current-lane validation before snapping and exporting.
…emote
switchToLane fetches the remote lane and persists it into the local scope's
lane index (importLaneObject → legacyScope.lanes.saveLane) BEFORE the
underlying merge step throws. removeLanes({remote: true, force: true}) only
deletes the remote — it returns early in remove-lanes.ts:8-19 and never
touches local state. createLane's existing-lane guard then sees the
locally-persisted stale copy and throws 'lane … already exists', and the
recovery crashes on the very next line — the exact failure mode the recovery
was added to fix.
Trash the local lane object via legacyScope.objects.moveObjectsToTrash
between the remote delete and createLane, mirroring the pattern used in
rebaseOntoRemoteLane. Also defensive nullish on .toString() so a non-Error
rejection doesn't compound the failure with a TypeError.
GiladShoham
approved these changes
May 29, 2026
…arker
Three follow-up fixes to the recovery path:
1. Only enter the destructive 'delete-remote + recreate' path when the switch
failure actually carries the stale-lane marker ('unable to merge lane').
For any other failure class (transient network blip during fetchLaneComponents,
auth/permission error, lane locked by Cloud UI, remote 5xx) we now rethrow
with the original error, instead of silently wiping the lane and its
Cloud UI history every time CI is flaky.
Mechanism: switchToLane now returns the caught Error (or undefined on
success); the caller inspects it. No existing caller of switchToLane uses
the return value, so the signature change is local.
2. Switch to main before createLane. createLane populates new lanes from
consumer.getCurrentLaneObject() regardless of forkLaneNewScope (which only
suppresses the cross-scope guard) — without this reset, the 'fresh' lane
silently inherits the bitmap's current-lane components when a developer
runs bit ci pr from a non-default lane.
3. Compare scope AND name when checking whether the switch landed. A same-named
lane in a different scope can no longer masquerade as a successful switch.
4. Tolerate 'was not found' from removeLanes — a concurrent recovery run on
the same PR can race and delete the lane first; the desired post-condition
(lane gone from remote) is already met, so we proceed instead of failing.
… return
Two follow-ups to Copilot review on the stale-lane recovery path:
1. Re-check the remote lane's hash immediately before the destructive
removeLanes. The central-hub delete API is name-based (no compare-and-swap),
so two CI jobs racing the same recovery could otherwise have job B delete
job A's freshly-recreated lane. Re-fetching here shrinks the TOCTOU window
from seconds to milliseconds; if A's recreate landed first the hash
differs, we skip the delete entirely, and the downstream export hits the
lane-hash mismatch and lands in exportWithAdoptOnConflict (which rebases
our snaps onto the winner's lane — no destroyed history).
2. Check switchToLane('main') return value (and follow-up getCurrentLane)
before createLane. The reset to main is load-bearing — createLane copies
from consumer.getCurrentLaneObject() regardless of forkLaneNewScope, so a
silent failure of the reset would silently fork the 'fresh' lane from
originalLane. Now we throw with a clear message instead of producing a
wrong-source lane.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Surfaced by #10397's
bit_prfailure. When--keep-lane(from #10388) found the lane on the remote, it calledswitchToLaneand assumed that landed us on the lane. If the stored lane was stale relative to the current PR — typically referencing aModelComponentthe PR has since removed/renamed, surfaced asunable to merge lane …, the component … was not found— the switch failed (switchToLanelogs-and-swallows the underlying error), the post-switch guard then threw, and every subsequentbit ci pr --keep-laneon that branch wedged the same way until the lane was manually deleted on the remote.Fix
When we observe that we did not land on the requested lane after switching, delete the stale remote lane and create a fresh one (same name) — the same shape the
elsebranch already takes when the lane doesn't exist on the remote. Lane history for the contested run is lost, but the next run preserves history again and CI is no longer blocked.Test plan
bit_prjob (which runsbit ci pr --build --keep-laneagainst bit's own repo) stays green.bit ci pr --keep-laneon the feat(internalize): addbit internalizeto hide components by default in the UI #10397 branch after merging — should print "Stale remote lane … — switching failed. Deleting it and creating a fresh lane to recover." once, then succeed; subsequent runs reuse the freshly-created lane normally.