@teambit/ci 1.0.647 → 1.0.649

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1545,7 +1545,7 @@ export class LaneSyncExecutor {
1545
1545
  return undefined;
1546
1546
  }
1547
1547
 
1548
- private async fetchOnce() {
1548
+ async fetchOnce() {
1549
1549
  if (this.fetched) return;
1550
1550
  await fetchRemoteHeads();
1551
1551
  this.fetched = true;
@@ -9,7 +9,7 @@ import type { CheckoutMain } from '@teambit/checkout';
9
9
  import { CiAspect } from '../ci.aspect';
10
10
  import type { CiMain, CiWorkspaceConfig } from '../ci.main.runtime';
11
11
  import { git } from '../git';
12
- import type { CiSyncConfig } from './sync-config';
12
+ import type { CiSyncConfig, LaneTarget } from './sync-config';
13
13
  import {
14
14
  branchToLaneName,
15
15
  isValidGitBranchName,
@@ -23,6 +23,7 @@ import { HALT_SUMMARY_PREFIX, REFUSED_SUMMARY_PREFIX, LaneSyncExecutor } from '.
23
23
  import { MainSyncExecutor } from './main-sync-executor';
24
24
  import { selectGitHostProvider } from './git-host-provider';
25
25
  import { gitRepoRoot, isNonContentPath, listRemoteBranches } from './git-ops';
26
+ import { readBranchLanePointer } from './sync-state';
26
27
  import { chooseInitDefaultBranch, deriveOwnerRepo, renderInitReport, scaffoldWorkflowFiles } from './init-scaffold';
27
28
  import { UNKNOWN_REPO, scaffoldAgentsMd } from './agents-md';
28
29
 
@@ -171,11 +172,8 @@ export class SyncOrchestrator {
171
172
  if (!laneName) return `branch ${branch} does not map to a valid lane name; nothing to do`;
172
173
  const skipReason = this.laneNotSyncableReason(laneName, cfg, mainLaneName, defaultBranch);
173
174
  if (skipReason) return `${skipReason} (branch ${branch})`;
174
- // A branch name carries no scope, so this path can only resolve the lane against `defaultScope`;
175
- // resolving a foreign-hosted lane to the wrong scope is safe (no-op, never a deletion).
176
- return this.summarizeSync([
177
- await laneSync.syncLane({ hostScope: defaultScope, name: laneName }, { dryRun: opts.dryRun }),
178
- ]);
175
+ const target = await this.branchLaneTarget(laneSync, branch, laneName);
176
+ return this.summarizeSync([await laneSync.syncLane(target, { dryRun: opts.dryRun })]);
179
177
  }
180
178
 
181
179
  if (opts.lane) {
@@ -190,19 +188,17 @@ export class SyncOrchestrator {
190
188
  // `--all` (also the no-arguments default): every mapped lane, then the main scope. Sequential on
191
189
  // purpose — the lanes share one workspace and one git checkout.
192
190
  const lines: string[] = [];
193
- const { lanes: lanesToSync, errors } = await this.listLanesToSync(cfg, mainLaneName, defaultBranch);
191
+ const { lanes: lanesToSync, errors } = await this.listLanesToSync(cfg, mainLaneName, defaultBranch, laneSync);
194
192
  lines.push(...errors);
195
193
  this.deps.logger.console(
196
194
  formatTitle(
197
195
  `Reconciling ${lanesToSync.length} mapped lane(s) of ${defaultScope}` +
198
- `${lanesToSync.length ? `: ${lanesToSync.join(', ')}` : ''}`
196
+ `${lanesToSync.length ? `: ${lanesToSync.map(({ name }) => name).join(', ')}` : ''}`
199
197
  )
200
198
  );
201
- for (const laneName of lanesToSync) {
202
- // Every enumerated target is hosted on `defaultScope` by construction; foreign-hosted lanes are
203
- // explicit-target only.
199
+ for (const target of lanesToSync) {
204
200
  // oxlint-disable-next-line no-await-in-loop
205
- lines.push(await laneSync.syncLane({ hostScope: defaultScope, name: laneName }, { dryRun: opts.dryRun }));
201
+ lines.push(await laneSync.syncLane(target, { dryRun: opts.dryRun }));
206
202
  }
207
203
  lines.push(await mainSync.syncMain({ dryRun: opts.dryRun }));
208
204
  return this.summarizeSync(lines);
@@ -378,7 +374,7 @@ export class SyncOrchestrator {
378
374
  }
379
375
 
380
376
  /**
381
- * The lane names to reconcile on an `--all` run (sorted), plus one HALTED line per enumeration
377
+ * The lanes to reconcile on an `--all` run (sorted by name), plus one HALTED line per enumeration
382
378
  * failure. The list is the union of the remote's lanes AND the lane-mapped branches on `origin` —
383
379
  * a lane deleted on bit.cloud is gone from the first source, and only the branch side can make
384
380
  * `close-pr` fire for it. An enumeration failure is reported rather than thrown, so the half that
@@ -387,8 +383,9 @@ export class SyncOrchestrator {
387
383
  private async listLanesToSync(
388
384
  cfg: Required<CiSyncConfig>,
389
385
  mainLaneName: string,
390
- defaultBranch: string
391
- ): Promise<{ lanes: string[]; errors: string[] }> {
386
+ defaultBranch: string,
387
+ laneSync: LaneSyncExecutor
388
+ ): Promise<{ lanes: LaneTarget[]; errors: string[] }> {
392
389
  if (!cfg.lanes.length) {
393
390
  this.deps.logger.console(
394
391
  formatWarningSummary('sync.lanes is empty — lane mirroring is disabled, reconciling the main scope only')
@@ -396,14 +393,15 @@ export class SyncOrchestrator {
396
393
  return { lanes: [], errors: [] };
397
394
  }
398
395
  const errors: string[] = [];
399
- const names = new Set<string>();
396
+ const targets = new Map<string, LaneTarget>();
397
+ const add = (target: LaneTarget) => targets.set(`${target.hostScope}/${target.name}`, target);
400
398
  const remote = this.deps.workspace.defaultScope;
401
399
 
402
400
  try {
403
401
  const lanes = await this.deps.lanes.getLanes({ remote });
404
402
  lanes.forEach((lane) => {
405
403
  const name = lane.id?.name ?? lane.name;
406
- if (name) names.add(name);
404
+ if (name) add({ hostScope: remote, name });
407
405
  });
408
406
  } catch (e: any) {
409
407
  const msg = e?.toString() ?? '';
@@ -431,7 +429,8 @@ export class SyncOrchestrator {
431
429
  );
432
430
  continue;
433
431
  }
434
- names.add(laneName);
432
+ // oxlint-disable-next-line no-await-in-loop
433
+ add(await this.branchLaneTarget(laneSync, branch, laneName));
435
434
  }
436
435
  } catch (e: any) {
437
436
  errors.push(
@@ -440,13 +439,27 @@ export class SyncOrchestrator {
440
439
  );
441
440
  }
442
441
 
443
- const lanes = [...names]
444
- .filter((name) => name !== mainLaneName)
445
- .filter((name) => shouldSyncLane(name, cfg))
446
- .sort();
442
+ const lanes = [...targets.values()]
443
+ .filter(({ name }) => name !== mainLaneName)
444
+ .filter(({ name }) => shouldSyncLane(name, cfg))
445
+ .sort((a, b) => a.name.localeCompare(b.name));
447
446
  return { lanes, errors };
448
447
  }
449
448
 
449
+ /**
450
+ * The lane a lane-mapped branch mirrors. A branch name carries no scope, so a lane that another scope
451
+ * hosts is found only through the lane pointer in the branch's `.bitmap`. A pointer to a lane of
452
+ * another name comes from a branch cut from that lane's branch, and must not redirect this one.
453
+ */
454
+ private async branchLaneTarget(laneSync: LaneSyncExecutor, branch: string, laneName: string): Promise<LaneTarget> {
455
+ const defaultScope = this.deps.workspace.defaultScope;
456
+ // a failed fetch must not throw here: syncLane fetches again and turns the failure into a halt
457
+ await laneSync.fetchOnce().catch(() => undefined);
458
+ const pointer = await readBranchLanePointer(branch, defaultScope);
459
+ const target = pointer ? parseLaneTarget(pointer, defaultScope) : undefined;
460
+ return target?.name === laneName ? target : { hostScope: defaultScope, name: laneName };
461
+ }
462
+
450
463
  /**
451
464
  * Join the per-target summary lines, and turn any halt into a non-zero exit — the executors never
452
465
  * throw, so this is the single place "needs a human" becomes visible to CI. `BitError` rather than
@@ -222,6 +222,11 @@ export function parseDevCommitCount(raw: string): boolean {
222
222
  return Number.isNaN(count) ? true : count > 0;
223
223
  }
224
224
 
225
+ /** The lane id in `origin/<branch>`'s `.bitmap`, if it has an exported one. Assumes `git fetch origin` already ran. */
226
+ export async function readBranchLanePointer(branch: string, defaultScope: string): Promise<string | undefined> {
227
+ return parseBranchBitmap(await readFileAtRef(`origin/${branch}`, BIT_MAP), defaultScope)?.laneIdStr;
228
+ }
229
+
225
230
  /**
226
231
  * Read a remote branch's sync state from its committed `.bitmap`. Assumes `git fetch origin` already ran.
227
232
  * `--first-parent` is a correctness requirement: `git log` orders across all parents, so a `.bitmap`