@teambit/ci 1.0.493 → 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.
- package/dist/{preview-1786365057144.js → preview-1786461108763.js} +1 -1
- package/dist/sync/git-ops.d.ts +41 -0
- package/dist/sync/git-ops.js +71 -0
- package/dist/sync/git-ops.js.map +1 -1
- package/dist/sync/git-ops.spec.js +39 -0
- package/dist/sync/git-ops.spec.js.map +1 -1
- package/dist/sync/lane-sync-executor.d.ts +26 -2
- package/dist/sync/lane-sync-executor.js +121 -15
- package/dist/sync/lane-sync-executor.js.map +1 -1
- package/dist/sync/lane-sync-executor.spec.js +111 -13
- package/dist/sync/lane-sync-executor.spec.js.map +1 -1
- package/dist/sync/main-sync-executor.js +30 -6
- package/dist/sync/main-sync-executor.js.map +1 -1
- package/package.json +14 -14
- package/sync/git-ops.spec.ts +60 -0
- package/sync/git-ops.ts +72 -0
- package/sync/lane-sync-executor.spec.ts +108 -9
- package/sync/lane-sync-executor.ts +122 -17
- package/sync/main-sync-executor.ts +37 -5
|
@@ -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
|
-
*
|
|
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
|
|
629
|
-
if (
|
|
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
|
|
719
|
-
if (
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
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,
|