@teambit/ci 1.0.494 → 1.0.495

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.
@@ -17,6 +17,15 @@ export declare const HALT_SUMMARY_PREFIX = "HALTED";
17
17
  * sync conflict.
18
18
  */
19
19
  export declare const REFUSED_SUMMARY_PREFIX = "REFUSED";
20
+ /**
21
+ * `recordLaneHeadOnBranch`'s ledger-commit push lost the race, AFTER the snap/export that precedes it
22
+ * already succeeded — the lane genuinely moved. Only the branch's own acknowledgment of that (the
23
+ * `.bitmap` pointer commit) failed to land, so the next run reads the branch as behind the lane it
24
+ * itself just updated and plans `import-lane` to catch it up. `policyClause` carries a destructive
25
+ * auto-resolution (onConflict git-wins/lane-wins) that already exported — it must not vanish from the
26
+ * summary just because the ledger push raced.
27
+ */
28
+ export declare function racedLedgerPushSummary(laneName: string, policyClause?: string): string;
20
29
  export type LaneSyncDeps = {
21
30
  lanes: LanesMain;
22
31
  /** for snapPrCommit + getDefaultBranchName + switchToLaneForSync */
@@ -144,7 +153,9 @@ export declare class LaneSyncExecutor {
144
153
  /**
145
154
  * Record on the branch which lane state it now mirrors: re-query the lane (the export just moved it,
146
155
  * so any earlier fingerprint is stale), commit — crucially the `.bitmap` the snap rewrote — and push.
147
- * Returns undefined when the lane can no longer be read, in which case the caller halts.
156
+ * `unreadable` means the lane could no longer be read back, in which case the caller halts;
157
+ * `raced` means a concurrent run's push already got there first, in which case the caller reports
158
+ * a benign noop instead.
148
159
  */
149
160
  private recordLaneHeadOnBranch;
150
161
  /**
@@ -219,9 +230,22 @@ export declare class LaneSyncExecutor {
219
230
  /**
220
231
  * Stage everything, commit with the annotated sync message, and push. NEVER force-pushes: a rejected
221
232
  * push means someone pushed concurrently and the next run should re-plan rather than clobber.
222
- * `--allow-empty` is only insurance against `git commit` failing the lane outright.
233
+ * `--allow-empty` is only insurance against `git commit` failing the lane outright. Returns whether
234
+ * the push was rejected by a CONFIRMED race (`classifyPushRejection`) — the caller's cue to report a
235
+ * benign noop instead of halting; any other push failure still throws.
223
236
  */
224
237
  private commitAllAndPush;
238
+ /** `origin/<branch>`'s tip as this local clone currently knows it — no fetch. Undefined if unknown. */
239
+ private localRemoteTip;
240
+ /**
241
+ * `confirmPushRace` (git-ops) with this executor's seam: `currentBranchTip` (shared with the close-pr
242
+ * retirement path) keeps the confirmation unit-testable without a real remote. Residual risk,
243
+ * accepted: "the branch moved" is not "a sync run won" — on a branch that moves during every run
244
+ * window, a persistent wording-matched failure still classifies as raced each time. The caller logs
245
+ * the original rejection text so that shape stays diagnosable. Only answers; the caller decides to
246
+ * swallow or rethrow.
247
+ */
248
+ private classifyPushRejection;
225
249
  private openPrForLane;
226
250
  /**
227
251
  * Put the workspace back on main with the default branch checked out. Best-effort, warn-only, so a
@@ -17,6 +17,7 @@ exports.haltCommentBody = haltCommentBody;
17
17
  exports.isProtectedBranch = isProtectedBranch;
18
18
  exports.laneHeadFingerprint = laneHeadFingerprint;
19
19
  exports.laneSyncPrBody = laneSyncPrBody;
20
+ exports.racedLedgerPushSummary = racedLedgerPushSummary;
20
21
  function _chalk() {
21
22
  const data = _interopRequireDefault(require("chalk"));
22
23
  _chalk = function () {
@@ -104,6 +105,38 @@ const HALT_SUMMARY_PREFIX = exports.HALT_SUMMARY_PREFIX = 'HALTED';
104
105
  * sync conflict.
105
106
  */
106
107
  const REFUSED_SUMMARY_PREFIX = exports.REFUSED_SUMMARY_PREFIX = 'REFUSED';
108
+
109
+ /**
110
+ * A raced verdict: the run lost a push race to a concurrent run and exits green — never a halt, never
111
+ * the `bit-sync-conflict` label; a raced push is not a content conflict. Nothing retries in-run, so
112
+ * convergence waits on the NEXT trigger (webhook or scheduled run): a deployment without the cron can
113
+ * mirror stale content until the next event. One builder so the `-> raced (` stem a consumer routes on
114
+ * lives at one site, like `HALT_SUMMARY_PREFIX`.
115
+ */
116
+ function racedSummary(laneName, detail) {
117
+ return `${laneName} -> raced (${detail})`;
118
+ }
119
+
120
+ /**
121
+ * `executeImportLane`'s own push (materializing the lane onto the branch) lost the race. Nothing on
122
+ * the LANE changed here — only the branch-side commit failed to land — so the next run's plan is
123
+ * unaffected: it simply retries the same import against the winner's tip.
124
+ */
125
+ function racedBranchPushSummary(laneName) {
126
+ return racedSummary(laneName, 'branch push lost the race to a concurrent run; next run re-plans');
127
+ }
128
+
129
+ /**
130
+ * `recordLaneHeadOnBranch`'s ledger-commit push lost the race, AFTER the snap/export that precedes it
131
+ * already succeeded — the lane genuinely moved. Only the branch's own acknowledgment of that (the
132
+ * `.bitmap` pointer commit) failed to land, so the next run reads the branch as behind the lane it
133
+ * itself just updated and plans `import-lane` to catch it up. `policyClause` carries a destructive
134
+ * auto-resolution (onConflict git-wins/lane-wins) that already exported — it must not vanish from the
135
+ * summary just because the ledger push raced.
136
+ */
137
+ function racedLedgerPushSummary(laneName, policyClause = '') {
138
+ return racedSummary(laneName, `${policyClause}lane updated; branch ledger commit lost the push race — next run re-plans`);
139
+ }
107
140
  /**
108
141
  * Fingerprint of a lane's content — same primitive as `branchStateFingerprint`, so "equal" means
109
142
  * exactly "the branch records every lane component at the lane's head".
@@ -578,7 +611,20 @@ class LaneSyncExecutor {
578
611
  pr
579
612
  });
580
613
  }
581
- await this.commitAllAndPush(branch, (0, _syncState().buildSyncCommitMessage)(laneIdStr, laneHead));
614
+ const pushResult = await this.commitAllAndPush(branch, (0, _syncState().buildSyncCommitMessage)(laneIdStr, laneHead));
615
+ if (pushResult.raced) {
616
+ // Still ensure the PR: this early return skips the file's only PR-creation site, and the
617
+ // converged state every later run reads plans a bare noop, which never repairs a missing PR.
618
+ if (!pr) await this.openPrForLane({
619
+ target,
620
+ laneIdStr,
621
+ branch,
622
+ defaultBranch,
623
+ laneHead,
624
+ remoteLane
625
+ });
626
+ return racedBranchPushSummary(laneName);
627
+ }
582
628
  let prUrl = pr?.htmlUrl;
583
629
  if (!pr) {
584
630
  prUrl = await this.openPrForLane({
@@ -625,8 +671,9 @@ class LaneSyncExecutor {
625
671
  pr: await this.findPr(branch)
626
672
  });
627
673
  }
628
- const laneHead = await this.recordLaneHeadOnBranch(target, laneIdStr, branch);
629
- if (!laneHead) {
674
+ const recorded = await this.recordLaneHeadOnBranch(target, laneIdStr, branch);
675
+ if (recorded.status === 'raced') return racedLedgerPushSummary(laneName);
676
+ if (recorded.status === 'unreadable') {
630
677
  return await this.executeHalt({
631
678
  laneName,
632
679
  laneIdStr,
@@ -635,7 +682,7 @@ class LaneSyncExecutor {
635
682
  pr: await this.findPr(branch)
636
683
  });
637
684
  }
638
- return `${laneName} -> export-branch (lane ${laneIdStr} @ ${laneHead.slice(0, 9)}, branch ${branch} updated)`;
685
+ return `${laneName} -> export-branch (lane ${laneIdStr} @ ${recorded.laneHead.slice(0, 9)}, ` + `branch ${branch} updated)`;
639
686
  } finally {
640
687
  await this.restoreWorkspace(defaultBranch);
641
688
  }
@@ -715,11 +762,12 @@ class LaneSyncExecutor {
715
762
  }
716
763
 
717
764
  // ---- step 3: record the new lane head on the branch --------------------------------------
718
- const laneHead = await this.recordLaneHeadOnBranch(target, laneIdStr, branch);
719
- if (!laneHead) {
765
+ const recorded = await this.recordLaneHeadOnBranch(target, laneIdStr, branch);
766
+ if (recorded.status === 'raced') return racedLedgerPushSummary(laneName, policyClause);
767
+ if (recorded.status === 'unreadable') {
720
768
  return await halt(`lane ${laneIdStr} could not be read back from the remote after the merge export`);
721
769
  }
722
- return `${laneName} -> merge-diverged (${policyClause}merged lane into branch, then exported; lane ${laneIdStr} @ ` + `${laneHead.slice(0, 9)}, branch ${branch} updated)`;
770
+ return `${laneName} -> merge-diverged (${policyClause}merged lane into branch, then exported; lane ${laneIdStr} @ ` + `${recorded.laneHead.slice(0, 9)}, branch ${branch} updated)`;
723
771
  } catch (e) {
724
772
  // Something unforeseen — halt anyway: no single lane may abort the rest of the run.
725
773
  return await halt(`unexpected failure while reconciling diverged lane ${laneIdStr} with branch ${branch}: ${e?.message || e}`);
@@ -758,14 +806,24 @@ class LaneSyncExecutor {
758
806
  /**
759
807
  * Record on the branch which lane state it now mirrors: re-query the lane (the export just moved it,
760
808
  * so any earlier fingerprint is stale), commit — crucially the `.bitmap` the snap rewrote — and push.
761
- * Returns undefined when the lane can no longer be read, in which case the caller halts.
809
+ * `unreadable` means the lane could no longer be read back, in which case the caller halts;
810
+ * `raced` means a concurrent run's push already got there first, in which case the caller reports
811
+ * a benign noop instead.
762
812
  */
763
813
  async recordLaneHeadOnBranch(target, laneIdStr, branch) {
764
814
  const remoteLane = await this.getRemoteLane(target);
765
- if (!remoteLane) return undefined;
815
+ if (!remoteLane) return {
816
+ status: 'unreadable'
817
+ };
766
818
  const laneHead = laneHeadFingerprint(remoteLane.components);
767
- await this.commitAllAndPush(branch, (0, _syncState().buildSyncCommitMessage)(laneIdStr, laneHead));
768
- return laneHead;
819
+ const pushResult = await this.commitAllAndPush(branch, (0, _syncState().buildSyncCommitMessage)(laneIdStr, laneHead));
820
+ if (pushResult.raced) return {
821
+ status: 'raced'
822
+ };
823
+ return {
824
+ status: 'ok',
825
+ laneHead
826
+ };
769
827
  }
770
828
 
771
829
  /**
@@ -1123,17 +1181,65 @@ class LaneSyncExecutor {
1123
1181
  /**
1124
1182
  * Stage everything, commit with the annotated sync message, and push. NEVER force-pushes: a rejected
1125
1183
  * push means someone pushed concurrently and the next run should re-plan rather than clobber.
1126
- * `--allow-empty` is only insurance against `git commit` failing the lane outright.
1184
+ * `--allow-empty` is only insurance against `git commit` failing the lane outright. Returns whether
1185
+ * the push was rejected by a CONFIRMED race (`classifyPushRejection`) — the caller's cue to report a
1186
+ * benign noop instead of halting; any other push failure still throws.
1127
1187
  */
1128
1188
  async commitAllAndPush(branch, message) {
1189
+ // The local knowledge of `origin/<branch>` right now — no fetch, `fetchOnce` already ran for this
1190
+ // whole executor run — is exactly what our about-to-be-made commit is based on. After a rejection,
1191
+ // this is the baseline `classifyPushRejection` checks the remote against to confirm a real race.
1192
+ const baseSha = await this.localRemoteTip(branch);
1129
1193
  await (0, _gitOps().addAllExceptScopeAndModules)();
1130
1194
  await (0, _gitOps().commitWithIdentity)(message, {
1131
1195
  extraArgs: ['--allow-empty']
1132
1196
  });
1133
- // `HEAD:refs/heads/<branch>`: a full-ref destination cannot be resolved as a tag or reinterpreted —
1134
- // the configured branch name is user input (see `sync-config.ts`).
1135
- await _git().git.push(['origin', `HEAD:refs/heads/${branch}`]);
1197
+ try {
1198
+ // `HEAD:refs/heads/<branch>`: a full-ref destination cannot be resolved as a tag or
1199
+ // reinterpreted — the configured branch name is user input (see `sync-config.ts`).
1200
+ await _git().git.push(['origin', `HEAD:refs/heads/${branch}`]);
1201
+ } catch (e) {
1202
+ if ((await this.classifyPushRejection(branch, baseSha, e)) !== 'confirmed-race') throw e;
1203
+ // Drop the losing commit: left on refs/heads/<branch>, its orphan sibling would trip
1204
+ // `checkoutPristine`'s guard on this clone's next branch-touching run. The confirmation just
1205
+ // refetched the winner's tip, so `origin/<branch>` resolves.
1206
+ const dropErr = await (0, _gitOps().dropLocalBranchToRemoteTip)(branch);
1207
+ if (dropErr) {
1208
+ this.deps.logger.consoleWarning(`Could not drop the unpushed sync commit on ${branch}: ${dropErr}`);
1209
+ }
1210
+ // Keep the original rejection text (redacted: git prints the remote URL, which may embed
1211
+ // credentials): the raced verdict exits green, and without the text a persistent wording-matched
1212
+ // failure on a busy branch would leave no diagnosable trace anywhere.
1213
+ this.deps.logger.console((0, _cli().formatWarningSummary)(`Push to ${branch} was rejected and the branch has since moved — a concurrent run got there first; ` + `re-planning on the next sync. The rejection: ${(0, _gitOps().redactUrlCredentials)(String(e?.message || e))}`));
1214
+ return {
1215
+ raced: true
1216
+ };
1217
+ }
1136
1218
  this.deps.logger.console(_chalk().default.green(`Pushed ${branch}`));
1219
+ return {
1220
+ raced: false
1221
+ };
1222
+ }
1223
+
1224
+ /** `origin/<branch>`'s tip as this local clone currently knows it — no fetch. Undefined if unknown. */
1225
+ async localRemoteTip(branch) {
1226
+ try {
1227
+ return String(await _git().git.raw(['rev-parse', `origin/${branch}`])).trim() || undefined;
1228
+ } catch {
1229
+ return undefined;
1230
+ }
1231
+ }
1232
+
1233
+ /**
1234
+ * `confirmPushRace` (git-ops) with this executor's seam: `currentBranchTip` (shared with the close-pr
1235
+ * retirement path) keeps the confirmation unit-testable without a real remote. Residual risk,
1236
+ * accepted: "the branch moved" is not "a sync run won" — on a branch that moves during every run
1237
+ * window, a persistent wording-matched failure still classifies as raced each time. The caller logs
1238
+ * the original rejection text so that shape stays diagnosable. Only answers; the caller decides to
1239
+ * swallow or rethrow.
1240
+ */
1241
+ async classifyPushRejection(branch, baseSha, e) {
1242
+ return (0, _gitOps().confirmPushRace)(branch, baseSha, String(e?.message || e), b => this.currentBranchTip(b));
1137
1243
  }
1138
1244
  async openPrForLane({
1139
1245
  target,