@teambit/ci 1.0.523 → 1.0.525

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.
@@ -19,6 +19,7 @@ exports.isProtectedBranch = isProtectedBranch;
19
19
  exports.laneHeadFingerprint = laneHeadFingerprint;
20
20
  exports.laneSummaryComponents = laneSummaryComponents;
21
21
  exports.laneSyncPrBody = laneSyncPrBody;
22
+ exports.ownLaneComponents = ownLaneComponents;
22
23
  exports.racedAdoptionPushSummary = racedAdoptionPushSummary;
23
24
  exports.racedLedgerPushSummary = racedLedgerPushSummary;
24
25
  exports.runSummaryCommentBody = runSummaryCommentBody;
@@ -173,15 +174,25 @@ function laneComponentIds(components) {
173
174
  }
174
175
 
175
176
  /**
176
- * The lane's components that do not belong to `defaultScope`. A lane may span scopes, and the
177
- * reconciler acts on a lane whole (fingerprint, materialize, snap, export), so foreign content is
178
- * refused rather than written into this repository. Soft-deleted components and `updateDependents`
179
- * entries are deliberately not read — the reconciler never materializes or exports them either.
177
+ * The lane's components that do not belong to `defaultScope`. A lane may span scopes; this
178
+ * repository reconciles only its own-scope slice (see `ownLaneComponents`), and the foreign entries
179
+ * ride along on the lane untouched — sourced in their own scopes' repositories and reachable here
180
+ * only as package dependencies. Soft-deleted components and `updateDependents` entries are
181
+ * deliberately not read — the reconciler never materializes or exports them either.
180
182
  */
181
183
  function foreignLaneComponents(components, defaultScope) {
182
184
  return components.filter(comp => comp.id.scope !== defaultScope).map(comp => comp.id.toStringWithoutVersion());
183
185
  }
184
186
 
187
+ /**
188
+ * The slice of the lane this repository mirrors: the components that belong to `defaultScope`.
189
+ * Every reconciliation act — fingerprint, materialize, snap, export — operates on this slice, so a
190
+ * foreign component's head moving neither wakes the mirror nor reads as divergence.
191
+ */
192
+ function ownLaneComponents(components, defaultScope) {
193
+ return components.filter(comp => comp.id.scope === defaultScope);
194
+ }
195
+
185
196
  /** How many foreign component ids a cross-scope message names before it summarizes the rest. */
186
197
  const MAX_LISTED_FOREIGN_COMPONENTS = 5;
187
198
 
@@ -201,30 +212,34 @@ function capEntries(entries, overflowPrefix = '', max = MAX_LISTED_COMPONENTS) {
201
212
  return [...entries.slice(0, max), `${overflowPrefix}…and ${entries.length - max} more`];
202
213
  }
203
214
 
204
- /** The shared clause describing why a lane is cross-scope, used by the skip/refusal/halt outcomes. */
215
+ /**
216
+ * The shared clause describing a lane with nothing of this repository's scope on it, used by the
217
+ * skip/refusal/halt outcomes. Only such a lane is refused: a lane that merely INCLUDES foreign
218
+ * components is reconciled over its own-scope slice.
219
+ */
205
220
  function crossScopeDescription(foreignIds, defaultScope) {
206
221
  // The scope is everything before the FIRST '/': a component id is `<scope>/<namespace…>/<name>`.
207
222
  const scopes = [...new Set(foreignIds.map(id => id.split('/', 1)[0]))].sort();
208
223
  const sample = capEntries(foreignIds, '', MAX_LISTED_FOREIGN_COMPONENTS).join(', ');
209
- return `components from scope(s) ${scopes.join(', ')} (this repo maps scope ${defaultScope}); ` + `foreign components: ${sample}`;
224
+ return `every component is from scope(s) ${scopes.join(', ')} (this repo maps scope ${defaultScope}); ` + `foreign components: ${sample}`;
210
225
  }
211
226
 
212
227
  /**
213
- * A cross-scope lane that was merely enumerated: a skip, and the run stays green — a standing, valid
214
- * lane must not turn every scheduled run permanently red.
228
+ * A lane with no own-scope components that was merely enumerated: a skip, and the run stays green — a
229
+ * standing, valid lane must not turn every scheduled run permanently red.
215
230
  */
216
231
  function crossScopeSkipSummary(laneName, foreignIds, defaultScope) {
217
- return `${laneName} -> skipped (cross-scope lane: ${crossScopeDescription(foreignIds, defaultScope)} — ` + `no branch created; see the docs' Cross-scope lanes section)`;
232
+ return `${laneName} -> skipped (nothing to mirror: ${crossScopeDescription(foreignIds, defaultScope)} — ` + `no branch created; see the docs' Cross-scope lanes section)`;
218
233
  }
219
234
 
220
235
  /**
221
- * A cross-scope lane the user named explicitly: exits non-zero with the explanation. A refusal, not a
222
- * halt — no PR is labelled, no branch is written. `existingBranch` keeps the closing sentence truthful
223
- * when a branch of that name already exists.
236
+ * A lane with no own-scope components that the user named explicitly: exits non-zero with the
237
+ * explanation. A refusal, not a halt — no PR is labelled, no branch is written. `existingBranch`
238
+ * keeps the closing sentence truthful when a branch of that name already exists.
224
239
  */
225
240
  function crossScopeRefusal(foreignIds, defaultScope, existingBranch) {
226
241
  const outcome = existingBranch ? `Nothing was written; the existing branch ${existingBranch} was left untouched` : 'No branch was created and nothing was written';
227
- return `cross-scope lane: ${crossScopeDescription(foreignIds, defaultScope)}; syncing cross-scope lanes is ` + `not supported yet — see the docs' Cross-scope lanes section. ${outcome}`;
242
+ return `nothing to mirror: ${crossScopeDescription(foreignIds, defaultScope)} — this repository reconciles ` + `only its own scope's slice of a lane, and this lane has none. See the docs' Cross-scope lanes ` + `section. ${outcome}`;
228
243
  }
229
244
 
230
245
  /**
@@ -252,11 +267,12 @@ Do NOT run the usual "bit lane import" resolution steps on this branch — they
252
267
  }
253
268
 
254
269
  /**
255
- * A lane that became cross-scope after this repository had already mirrored it onto a branch — the one
256
- * cross-scope shape that is a genuine halt: the pair is mid-flight and can no longer converge.
270
+ * A lane whose own-scope slice emptied out after this repository had already mirrored it onto a
271
+ * branch — the one cross-scope shape that is a genuine halt: the pair is mid-flight, and an empty
272
+ * slice fingerprints as a constant, which would read as converged forever over a stale mirror.
257
273
  */
258
274
  function crossScopeMidFlightHaltReason(branch, foreignIds, defaultScope) {
259
- return `lane became cross-scope after it was mirrored onto ${branch}: ${crossScopeDescription(foreignIds, defaultScope)}; ` + `the branch and the lane can no longer be reconciled automatically — see the docs' Cross-scope lanes section`;
275
+ return `every ${defaultScope} component left the lane after it was mirrored onto ${branch}: ` + `${crossScopeDescription(foreignIds, defaultScope)}; the branch and the lane can no longer be ` + `reconciled automatically — see the docs' Cross-scope lanes section`;
260
276
  }
261
277
 
262
278
  /**
@@ -379,7 +395,12 @@ class LaneSyncExecutor {
379
395
  return summary;
380
396
  }
381
397
  const remoteLane = await this.getRemoteLane(target);
382
- const laneHead = remoteLane ? laneHeadFingerprint(remoteLane.components) : undefined;
398
+
399
+ // The slice of the lane this repository reconciles. Everything below — fingerprint, materialize,
400
+ // snap, export — acts on it alone: foreign components stay on the lane untouched and reach this
401
+ // repository only as package dependencies, so a foreign head moving is invisible here.
402
+ const ownComponents = remoteLane ? ownLaneComponents(remoteLane.components, defaultScope) : [];
403
+ const laneHead = remoteLane ? laneHeadFingerprint(ownComponents) : undefined;
383
404
  const branchExists = await (0, _gitOps().branchExistsOnRemote)(branch);
384
405
  const branchState = branchExists ? await (0, _syncState().readBranchSyncState)(branch, defaultBranch, defaultScope) : {
385
406
  stateCommit: undefined,
@@ -424,11 +445,13 @@ class LaneSyncExecutor {
424
445
  });
425
446
  }
426
447
 
427
- // A cross-scope lane cannot be reconciled here — the reconciler acts on a lane whole, so mirroring
428
- // one would write another repository's components into this one. The check lives in the executor
429
- // because every trigger funnels through here, and it needs the lane's content.
448
+ // A lane with NO own-scope components cannot be reconciled here — there is nothing to mirror, and
449
+ // an empty slice fingerprints as a constant, which would read as converged forever. A lane that
450
+ // merely INCLUDES foreign components proceeds: it is reconciled over its own-scope slice. The
451
+ // check lives in the executor because every trigger funnels through here, and it needs the lane's
452
+ // content.
430
453
  const foreign = remoteLane ? foreignLaneComponents(remoteLane.components, defaultScope) : [];
431
- if (foreign.length) {
454
+ if (foreign.length && !ownComponents.length) {
432
455
  return this.crossScopeOutcome({
433
456
  laneName,
434
457
  laneIdStr,
@@ -443,14 +466,19 @@ class LaneSyncExecutor {
443
466
  dryRun
444
467
  });
445
468
  }
469
+ if (foreign.length) {
470
+ logger.console((0, _cli().formatWarningSummary)(`${laneName}: cross-scope lane — mirroring its ${ownComponents.length} ${defaultScope} component(s); ` + `${foreign.length} component(s) from other scope(s) stay on the lane as package dependencies ` + `(${capEntries(foreign, '', MAX_LISTED_FOREIGN_COMPONENTS).join(', ')})`));
471
+ }
446
472
  // Attribution to THIS lane is required: the claim was computed for whichever lane the branch's
447
473
  // `.bitmap` names, and a claim on someone else's behalf licenses nothing here.
448
474
  const laneIsGone = branchExists && !laneHead;
449
475
  const ownership = laneIsGone && mirroredLane === laneIdStr ? claim : 'inherited-or-none';
450
476
 
451
477
  // What the branch reflects, read off its own `.bitmap` — only when that `.bitmap` names THIS lane;
452
- // otherwise the planner's `!lastSyncedHead` rows handle "no state of ours".
453
- const lastSyncedHead = remoteLane && branchState.bitmap && mirroredLane === laneIdStr ? (0, _syncState().branchStateFingerprint)(branchState.bitmap, laneComponentIds(remoteLane.components)) : undefined;
478
+ // otherwise the planner's `!lastSyncedHead` rows handle "no state of ours". Read over the OWN
479
+ // slice's ids, the same set `laneHead` fingerprints — a foreign component is never in `.bitmap`
480
+ // and counting it would read the pair as permanently diverged.
481
+ const lastSyncedHead = remoteLane && branchState.bitmap && mirroredLane === laneIdStr ? (0, _syncState().branchStateFingerprint)(branchState.bitmap, laneComponentIds(ownComponents)) : undefined;
454
482
 
455
483
  // `isSyncAuthoredMessage`, NOT `hasSyncMarker`: this feeds a branch deletion, and a message that
456
484
  // merely quotes the marker must not read as one we wrote.
@@ -548,10 +576,12 @@ class LaneSyncExecutor {
548
576
  }
549
577
 
550
578
  /**
551
- * What a cross-scope lane means for this repository: a mid-flight halt when the branch is this
552
- * lane's live mirror (checked first — the problem is the state of the pair, not the phrasing of the
553
- * request); a refusal when the user named the lane explicitly; otherwise an enumerated skip that
554
- * stays green — one standing cross-scope lane must not fail every scheduled run forever.
579
+ * What a lane with NO own-scope components means for this repository (a lane that merely includes
580
+ * foreign components never reaches here — it is reconciled over its own-scope slice): a mid-flight
581
+ * halt when the branch is this lane's live mirror (checked first — the problem is the state of the
582
+ * pair, not the phrasing of the request); a refusal when the user named the lane explicitly;
583
+ * otherwise an enumerated skip that stays green — one standing foreign lane must not fail every
584
+ * scheduled run forever.
555
585
  */
556
586
  async crossScopeOutcome({
557
587
  laneName,
@@ -696,11 +726,15 @@ class LaneSyncExecutor {
696
726
  // moved in between must re-plan (merge-diverged owns that shape): comparing the branch's files
697
727
  // with the newer lane would misread the lane's own advance as branch work, and the export would
698
728
  // put the older content on top of a concurrent developer's push. The materialize above just
699
- // fetched, so this read and the status below see the same lane.
729
+ // fetched, so this read and the status below see the same lane. Own-slice fingerprints on both
730
+ // sides: a foreign head moving concurrently is not this pair's divergence.
700
731
  if (preExportLane) {
701
732
  const laneNow = await this.getRemoteLane(target);
702
- const movedTo = laneNow ? laneHeadFingerprint(laneNow.components) : undefined;
703
- if (movedTo !== laneHeadFingerprint(preExportLane.components)) {
733
+ const {
734
+ defaultScope
735
+ } = this.deps;
736
+ const movedTo = laneNow ? laneHeadFingerprint(ownLaneComponents(laneNow.components, defaultScope)) : undefined;
737
+ if (movedTo !== laneHeadFingerprint(ownLaneComponents(preExportLane.components, defaultScope))) {
704
738
  return `${laneName} -> noop (lane ${laneIdStr} moved while this run was planning; the next run re-plans)`;
705
739
  }
706
740
  }
@@ -855,11 +889,15 @@ class LaneSyncExecutor {
855
889
 
856
890
  // Same freshness rule as the export probe: the plan's inputs must still describe the world.
857
891
  // A lane that moved since planning re-plans — the next run merges the newer head — rather
858
- // than proceeding on a divergence assessment the planner never made.
892
+ // than proceeding on a divergence assessment the planner never made. Own-slice on both sides,
893
+ // like every fingerprint here.
859
894
  if (preExportLane) {
860
895
  const laneNow = await this.getRemoteLane(target);
861
- const movedTo = laneNow ? laneHeadFingerprint(laneNow.components) : undefined;
862
- if (movedTo !== laneHeadFingerprint(preExportLane.components)) {
896
+ const {
897
+ defaultScope
898
+ } = this.deps;
899
+ const movedTo = laneNow ? laneHeadFingerprint(ownLaneComponents(laneNow.components, defaultScope)) : undefined;
900
+ if (movedTo !== laneHeadFingerprint(ownLaneComponents(preExportLane.components, defaultScope))) {
863
901
  return `${laneName} -> noop (lane ${laneIdStr} moved while this run was planning; the next run re-plans)`;
864
902
  }
865
903
  }
@@ -982,7 +1020,8 @@ class LaneSyncExecutor {
982
1020
  if (!remoteLane) return {
983
1021
  status: 'unreadable'
984
1022
  };
985
- const laneHead = laneHeadFingerprint(remoteLane.components);
1023
+ // Own-slice, like the planner's `laneHead`: the ledger records what THIS repository mirrors.
1024
+ const laneHead = laneHeadFingerprint(ownLaneComponents(remoteLane.components, this.deps.defaultScope));
986
1025
  const pushResult = await this.commitAllAndPush(branch, (0, _syncState().buildSyncCommitMessage)(laneIdStr, laneHead, opts));
987
1026
  if (pushResult.raced) return {
988
1027
  status: 'raced'
@@ -1031,7 +1070,11 @@ class LaneSyncExecutor {
1031
1070
  mergeStrategy,
1032
1071
  promptMergeOptions: false,
1033
1072
  skipNpmInstall: true,
1034
- workspaceOnly: false
1073
+ workspaceOnly: false,
1074
+ // `workspaceOnly: false` also ADDS lane components the workspace lacks; on a cross-scope lane
1075
+ // that must stop at the own-scope slice — a foreign component merged in here would ride the
1076
+ // sync commit into this repository.
1077
+ restrictToScopes: [this.deps.defaultScope]
1035
1078
  });
1036
1079
  let conflictedFileCount = 0;
1037
1080
  const conflicts = [];
@@ -1336,6 +1379,10 @@ class LaneSyncExecutor {
1336
1379
  // opposite of `bit ci pr`, where the git checkout is the source of truth and `switchToLane`'s
1337
1380
  // `workspaceOnly` default keeps the switch from writing lane-only components.
1338
1381
  workspaceOnly: false,
1382
+ // A cross-scope lane's foreign components must never be written into this repository — they are
1383
+ // sourced elsewhere and reach this workspace only as package dependencies. The lane object
1384
+ // itself is still fetched and saved whole, so a later export preserves the foreign entries.
1385
+ restrictToScopes: [this.deps.defaultScope],
1339
1386
  // Follows the direction: in adopt mode the missing-component retry must not write the lane's
1340
1387
  // files over the branch's — the ledger commit would then push the lane's content onto the branch.
1341
1388
  writeAdoptedFiles: theirsWins
@@ -1429,7 +1476,8 @@ class LaneSyncExecutor {
1429
1476
  branch,
1430
1477
  branchTipSha,
1431
1478
  laneHead,
1432
- changed: changedLaneComponents(preComponents, postComponents)
1479
+ // Own-slice only: a foreign head that moved concurrently was not snapped by this export.
1480
+ changed: ownLaneComponents(changedLaneComponents(preComponents, postComponents), this.deps.defaultScope)
1433
1481
  });
1434
1482
  await gitHost.upsertComment(pr.number, RUN_SUMMARY_MARKER, body);
1435
1483
  } catch (e) {
@@ -1542,7 +1590,8 @@ class LaneSyncExecutor {
1542
1590
  laneUrl,
1543
1591
  branch,
1544
1592
  laneHead,
1545
- components: remoteLane.components
1593
+ components: remoteLane.components,
1594
+ defaultScope: this.deps.defaultScope
1546
1595
  });
1547
1596
  try {
1548
1597
  const created = await gitHost.createPr({
@@ -1595,18 +1644,29 @@ function isConflictFileStatus(status) {
1595
1644
  }
1596
1645
 
1597
1646
  /**
1598
- * The body of the pull request that mirrors a lane onto a branch. The component list is capped so the
1599
- * body stays inside the git host's size limit however large the lane is; the total is not.
1647
+ * The body of the pull request that mirrors a lane onto a branch. On a cross-scope lane only the
1648
+ * own-scope slice is mirrored, and the foreign components get their own section — the branch's diff
1649
+ * cannot show them, and a reviewer must know the lane carries more than this repository sees. Every
1650
+ * component list is capped so the body stays inside the git host's size limit however large the lane
1651
+ * is; the totals are not.
1600
1652
  */
1601
1653
  function laneSyncPrBody({
1602
1654
  laneIdStr,
1603
1655
  laneUrl,
1604
1656
  branch,
1605
1657
  laneHead,
1606
- components
1658
+ components,
1659
+ defaultScope
1607
1660
  }) {
1608
- const listed = capEntries(components.map(comp => `- \`${comp.id.toStringWithoutVersion()}\` @ \`${comp.head.slice(0, 9)}\``), '- ').join('\n');
1609
- return [`Mirrors the Bit lane [\`${laneIdStr}\`](${laneUrl}) onto \`${branch}\`.`, '', `- lane: ${laneUrl}`, `- lane head: \`${laneHead}\``, '', `Components on the lane (${components.length}):`, listed || '_none_', '', 'This PR is maintained by `bit ci sync` — push to the branch to send changes back to the lane.'].join('\n');
1661
+ const own = ownLaneComponents(components, defaultScope);
1662
+ const foreignIds = foreignLaneComponents(components, defaultScope);
1663
+ const listed = capEntries(own.map(comp => `- \`${comp.id.toStringWithoutVersion()}\` @ \`${comp.head.slice(0, 9)}\``), '- ').join('\n');
1664
+ const lines = [`Mirrors the Bit lane [\`${laneIdStr}\`](${laneUrl}) onto \`${branch}\`.`, '', `- lane: ${laneUrl}`, `- lane head: \`${laneHead}\``, '', foreignIds.length ? `Components mirrored from the lane (${own.length} of ${components.length}):` : `Components on the lane (${components.length}):`, listed || '_none_'];
1665
+ if (foreignIds.length) {
1666
+ lines.push('', `Also on this lane, from other scopes (${foreignIds.length}) — not mirrored into this repository; ` + `this branch consumes them as package dependencies at their lane versions:`, capEntries(foreignIds.map(id => `- \`${id}\``), '- ').join('\n'));
1667
+ }
1668
+ lines.push('', 'This PR is maintained by `bit ci sync` — push to the branch to send changes back to the lane.');
1669
+ return lines.join('\n');
1610
1670
  }
1611
1671
 
1612
1672
  /**