@teambit/ci 1.0.494 → 1.0.497
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/ci.docs.mdx +39 -9
- package/dist/ci.docs.mdx +39 -9
- package/dist/ci.main.runtime.d.ts +6 -0
- package/dist/ci.main.runtime.js +24 -3
- package/dist/ci.main.runtime.js.map +1 -1
- package/dist/{preview-1786400681343.js → preview-1786718956243.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 +81 -15
- package/dist/sync/lane-sync-executor.js +315 -50
- package/dist/sync/lane-sync-executor.js.map +1 -1
- package/dist/sync/lane-sync-executor.spec.js +235 -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/dist/sync/sync-planner.d.ts +10 -1
- package/dist/sync/sync-planner.js +37 -13
- package/dist/sync/sync-planner.js.map +1 -1
- package/dist/sync/sync-planner.spec.js +44 -2
- package/dist/sync/sync-planner.spec.js.map +1 -1
- package/dist/sync/sync-state.d.ts +33 -1
- package/dist/sync/sync-state.js +60 -3
- package/dist/sync/sync-state.js.map +1 -1
- package/dist/sync/sync-state.spec.js +28 -0
- package/dist/sync/sync-state.spec.js.map +1 -1
- package/package.json +20 -20
- package/sync/git-ops.spec.ts +60 -0
- package/sync/git-ops.ts +72 -0
- package/sync/lane-sync-executor.spec.ts +215 -9
- package/sync/lane-sync-executor.ts +354 -47
- package/sync/main-sync-executor.ts +37 -5
- package/sync/sync-planner.spec.ts +36 -1
- package/sync/sync-planner.ts +36 -16
- package/sync/sync-state.spec.ts +33 -0
- package/sync/sync-state.ts +62 -1
|
@@ -13,9 +13,28 @@ import {
|
|
|
13
13
|
laneHeadFingerprint,
|
|
14
14
|
LaneSyncExecutor,
|
|
15
15
|
laneSyncPrBody,
|
|
16
|
+
racedLedgerPushSummary,
|
|
16
17
|
} from './lane-sync-executor';
|
|
18
|
+
import type { LaneOwnershipEvidence } from './sync-planner';
|
|
17
19
|
import { resolveSyncConfig } from './sync-config';
|
|
18
20
|
|
|
21
|
+
/**
|
|
22
|
+
* One stub executor for every seam-based test: real executor code over stubbed deps, no repository.
|
|
23
|
+
* `warnings` collects `consoleWarning` lines for tests that assert on them.
|
|
24
|
+
*/
|
|
25
|
+
function stubExecutor(): { executor: LaneSyncExecutor; warnings: string[] } {
|
|
26
|
+
const warnings: string[] = [];
|
|
27
|
+
const executor = new LaneSyncExecutor({
|
|
28
|
+
lanes: {} as any,
|
|
29
|
+
ci: {} as any,
|
|
30
|
+
logger: { console: () => {}, consoleWarning: (msg: string) => warnings.push(msg), error: () => {} } as any,
|
|
31
|
+
gitHost: undefined,
|
|
32
|
+
cfg: resolveSyncConfig({}),
|
|
33
|
+
defaultScope: 'acme.shop',
|
|
34
|
+
});
|
|
35
|
+
return { executor, warnings };
|
|
36
|
+
}
|
|
37
|
+
|
|
19
38
|
type LaneComponents = Parameters<typeof laneHeadFingerprint>[0];
|
|
20
39
|
|
|
21
40
|
/** A stand-in for `LaneData`'s component entry; the helpers only read the id string, scope and head. */
|
|
@@ -314,16 +333,8 @@ describe('close-pr re-verifies the tip before retiring a branch', () => {
|
|
|
314
333
|
const MOVED_SHA = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb2';
|
|
315
334
|
|
|
316
335
|
function retirer(currentTip: string | undefined, pushError?: Error) {
|
|
317
|
-
const warnings
|
|
336
|
+
const { executor, warnings } = stubExecutor();
|
|
318
337
|
const pushes: string[][] = [];
|
|
319
|
-
const executor = new LaneSyncExecutor({
|
|
320
|
-
lanes: {} as any,
|
|
321
|
-
ci: {} as any,
|
|
322
|
-
logger: { console: () => {}, consoleWarning: (msg: string) => warnings.push(msg), error: () => {} } as any,
|
|
323
|
-
gitHost: undefined,
|
|
324
|
-
cfg: resolveSyncConfig({}),
|
|
325
|
-
defaultScope: 'acme.shop',
|
|
326
|
-
});
|
|
327
338
|
(executor as any).currentBranchTip = async () => currentTip;
|
|
328
339
|
(executor as any).pushBranchDeletion = async (branch: string, sha: string) => {
|
|
329
340
|
pushes.push([branch, sha]);
|
|
@@ -377,6 +388,201 @@ describe('close-pr re-verifies the tip before retiring a branch', () => {
|
|
|
377
388
|
});
|
|
378
389
|
});
|
|
379
390
|
|
|
391
|
+
describe('differentLaneStillClaims uses .bitmap blob equality, not commit reachability', () => {
|
|
392
|
+
function executorWithInheritance(inherited: boolean) {
|
|
393
|
+
const executor = new LaneSyncExecutor({
|
|
394
|
+
lanes: {} as any,
|
|
395
|
+
ci: {} as any,
|
|
396
|
+
logger: { console: () => {}, consoleWarning: () => {}, error: () => {} } as any,
|
|
397
|
+
gitHost: undefined,
|
|
398
|
+
cfg: resolveSyncConfig({}),
|
|
399
|
+
defaultScope: 'acme.shop',
|
|
400
|
+
});
|
|
401
|
+
(executor as any).branchInheritsBitmapFromDefault = async () => inherited;
|
|
402
|
+
return (mirroredLane: string | undefined, laneIdStr: string) =>
|
|
403
|
+
(executor as any).differentLaneStillClaims(mirroredLane, laneIdStr, 'my-branch', 'main') as Promise<boolean>;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
it('does not claim when there is no mirrored lane pointer at all — never even checks the blob', async () => {
|
|
407
|
+
const differentLaneStillClaims = executorWithInheritance(true);
|
|
408
|
+
expect(await differentLaneStillClaims(undefined, 'acme.shop/my-lane')).to.equal(false);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("does not claim for this lane's own pointer — never even checks the blob", async () => {
|
|
412
|
+
const differentLaneStillClaims = executorWithInheritance(true);
|
|
413
|
+
expect(await differentLaneStillClaims('acme.shop/my-lane', 'acme.shop/my-lane')).to.equal(false);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("does not claim when the different-lane .bitmap is byte-identical to the default branch's — inherited", async () => {
|
|
417
|
+
const differentLaneStillClaims = executorWithInheritance(true);
|
|
418
|
+
expect(await differentLaneStillClaims('acme.shop/other-lane', 'acme.shop/my-lane')).to.equal(false);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it("STILL claims when the different-lane .bitmap has diverged from the default branch's — halt, not adopt", async () => {
|
|
422
|
+
const differentLaneStillClaims = executorWithInheritance(false);
|
|
423
|
+
expect(await differentLaneStillClaims('acme.shop/other-lane', 'acme.shop/my-lane')).to.equal(true);
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
describe('computeHasIndependentHistory gates the ancestry check to own-live claims with a state commit', () => {
|
|
428
|
+
function executorWithAncestryResult(result: boolean) {
|
|
429
|
+
const calls: Array<[string, string]> = [];
|
|
430
|
+
const executor = new LaneSyncExecutor({
|
|
431
|
+
lanes: {} as any,
|
|
432
|
+
ci: {} as any,
|
|
433
|
+
logger: { console: () => {}, consoleWarning: () => {}, error: () => {} } as any,
|
|
434
|
+
gitHost: undefined,
|
|
435
|
+
cfg: resolveSyncConfig({}),
|
|
436
|
+
defaultScope: 'acme.shop',
|
|
437
|
+
});
|
|
438
|
+
(executor as any).hasIndependentHistoryBelowStateCommit = async (stateCommit: string, defaultBranch: string) => {
|
|
439
|
+
calls.push([stateCommit, defaultBranch]);
|
|
440
|
+
return result;
|
|
441
|
+
};
|
|
442
|
+
const compute = (ownership: LaneOwnershipEvidence, stateCommit: string | undefined, defaultBranch: string) =>
|
|
443
|
+
(executor as any).computeHasIndependentHistory(ownership, stateCommit, defaultBranch) as Promise<boolean>;
|
|
444
|
+
return { compute, calls };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
it('skips the ancestry check entirely for a reachable claim (own-merged)', async () => {
|
|
448
|
+
const { compute, calls } = executorWithAncestryResult(true);
|
|
449
|
+
expect(await compute('own-merged', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1', 'main')).to.equal(false);
|
|
450
|
+
expect(calls).to.deep.equal([]);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('skips the ancestry check entirely for own-superseded too', async () => {
|
|
454
|
+
const { compute, calls } = executorWithAncestryResult(true);
|
|
455
|
+
expect(await compute('own-superseded', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1', 'main')).to.equal(false);
|
|
456
|
+
expect(calls).to.deep.equal([]);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('skips the check when there is no state commit to inspect, even for an own-live claim', async () => {
|
|
460
|
+
const { compute, calls } = executorWithAncestryResult(true);
|
|
461
|
+
expect(await compute('own-live', undefined, 'main')).to.equal(false);
|
|
462
|
+
expect(calls).to.deep.equal([]);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('defers to the ancestry check for an own-live claim with a state commit', async () => {
|
|
466
|
+
const { compute, calls } = executorWithAncestryResult(true);
|
|
467
|
+
expect(await compute('own-live', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1', 'main')).to.equal(true);
|
|
468
|
+
expect(calls).to.deep.equal([['aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1', 'main']]);
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
describe('classifyPushRejection confirms a race before calling it one', () => {
|
|
473
|
+
const BASE_SHA = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1';
|
|
474
|
+
const MOVED_SHA = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb2';
|
|
475
|
+
const REJECTION = new Error('! [rejected] HEAD -> feature (fetch first)\nerror: failed to push some refs');
|
|
476
|
+
|
|
477
|
+
function classifier(currentTip: string | undefined) {
|
|
478
|
+
const { executor } = stubExecutor();
|
|
479
|
+
(executor as any).currentBranchTip = async () => currentTip;
|
|
480
|
+
return (baseSha: string | undefined, e: Error) =>
|
|
481
|
+
(executor as any).classifyPushRejection('my-branch', baseSha, e) as Promise<'confirmed-race' | 'not-a-race'>;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
it('confirms the race when the branch actually moved off the base we pushed against', async () => {
|
|
485
|
+
const classify = classifier(MOVED_SHA);
|
|
486
|
+
expect(await classify(BASE_SHA, REJECTION)).to.equal('confirmed-race');
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('confirms the race for a brand-new branch that now exists on the remote', async () => {
|
|
490
|
+
const classify = classifier(MOVED_SHA);
|
|
491
|
+
expect(await classify(undefined, REJECTION)).to.equal('confirmed-race');
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// Message-text matching alone is not proof: a persistent, unrelated rejection could repeat the same
|
|
495
|
+
// wording forever. Without this re-fetch-and-compare, that would go permanently green.
|
|
496
|
+
it('answers not-a-race when the branch did NOT move — the wording matched but nothing raced', async () => {
|
|
497
|
+
const classify = classifier(BASE_SHA);
|
|
498
|
+
expect(await classify(BASE_SHA, REJECTION)).to.equal('not-a-race');
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('answers not-a-race when a brand-new branch still does not exist on the remote', async () => {
|
|
502
|
+
const classify = classifier(undefined);
|
|
503
|
+
expect(await classify(undefined, REJECTION)).to.equal('not-a-race');
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// The polarity bug this guards: `currentTip === undefined` here means the re-fetch FAILED (network
|
|
507
|
+
// blip, auth hiccup) — unknown, not evidence the branch moved. `undefined !== baseSha` reads as
|
|
508
|
+
// truthy if compared naively, which would misreport an unanswerable check as a confirmed race and
|
|
509
|
+
// paper over a real, persistent failure. Withhold like every other unknown-answer path in this file.
|
|
510
|
+
it('answers not-a-race when the re-fetch itself fails, even though a base sha is known', async () => {
|
|
511
|
+
const classify = classifier(undefined);
|
|
512
|
+
expect(await classify(BASE_SHA, REJECTION)).to.equal('not-a-race');
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it('answers not-a-race for a rejection whose wording is not a non-fast-forward race at all', async () => {
|
|
516
|
+
const classify = classifier(MOVED_SHA);
|
|
517
|
+
const unrelated = new Error('remote: GH006: Protected branch update failed');
|
|
518
|
+
expect(await classify(BASE_SHA, unrelated)).to.equal('not-a-race');
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// The live incident's actual shape: the snap/export onto the lane already succeeded — the lane truly
|
|
523
|
+
// moved — and only the branch's OWN ledger-commit push (recordLaneHeadOnBranch) lost the race. Nothing
|
|
524
|
+
// exercised `racedLedgerPushSummary` before this; a wording regression there would have gone unnoticed.
|
|
525
|
+
describe('executeExportBranch reports the ledger-race wording when only the ledger push races', () => {
|
|
526
|
+
it('surfaces "lane updated; branch ledger commit lost the push race" without halting', async () => {
|
|
527
|
+
const { executor } = stubExecutor();
|
|
528
|
+
(executor as any).lastNonSyncCommitMessage = async () => 'feat: some change';
|
|
529
|
+
(executor as any).checkoutFromRemote = async () => {};
|
|
530
|
+
(executor as any).restoreWorkspace = async () => {};
|
|
531
|
+
// The export half already succeeded; only the ledger commit that follows raced.
|
|
532
|
+
(executor as any).snapAndExportOntoLane = async () => ({ status: 'exported' });
|
|
533
|
+
(executor as any).recordLaneHeadOnBranch = async () => ({ status: 'raced' });
|
|
534
|
+
|
|
535
|
+
const summary = await (executor as any).executeExportBranch({
|
|
536
|
+
target: { hostScope: 'acme.shop', name: 'my-lane' },
|
|
537
|
+
laneIdStr: 'acme.shop/my-lane',
|
|
538
|
+
branch: 'my-lane',
|
|
539
|
+
defaultBranch: 'main',
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
expect(summary).to.equal(
|
|
543
|
+
'my-lane -> raced (lane updated; branch ledger commit lost the push race — next run re-plans)'
|
|
544
|
+
);
|
|
545
|
+
expect(summary).to.not.include('HALTED');
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
// Adoption never snaps or exports, so its raced line must NOT claim the lane was updated — the lane
|
|
550
|
+
// is untouched and only the branch's pointer commit lost the race.
|
|
551
|
+
describe('executeAdoptBranch reports a lane-untouched race when its ledger push races', () => {
|
|
552
|
+
it('surfaces "lane untouched; adoption ledger commit lost the push race" without halting', async () => {
|
|
553
|
+
const { executor } = stubExecutor();
|
|
554
|
+
(executor as any).deps.ci = { hasUnsyncedWorkChanges: async () => false };
|
|
555
|
+
(executor as any).checkoutFromRemote = async () => {};
|
|
556
|
+
(executor as any).restoreWorkspace = async () => {};
|
|
557
|
+
(executor as any).materializeLane = async () => undefined;
|
|
558
|
+
(executor as any).recordLaneHeadOnBranch = async () => ({ status: 'raced' });
|
|
559
|
+
|
|
560
|
+
const summary = await (executor as any).executeAdoptBranch({
|
|
561
|
+
target: { hostScope: 'acme.shop', name: 'my-lane' },
|
|
562
|
+
laneIdStr: 'acme.shop/my-lane',
|
|
563
|
+
branch: 'my-lane',
|
|
564
|
+
defaultBranch: 'main',
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
expect(summary).to.equal(
|
|
568
|
+
'my-lane -> raced (lane untouched; adoption ledger commit lost the push race — next run re-plans)'
|
|
569
|
+
);
|
|
570
|
+
expect(summary).to.not.include('lane updated');
|
|
571
|
+
expect(summary).to.not.include('HALTED');
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// A destructive auto-resolution (onConflict git-wins/lane-wins) that already exported must not vanish
|
|
576
|
+
// from the summary just because the ledger push raced.
|
|
577
|
+
describe('racedLedgerPushSummary keeps the conflict-policy clause', () => {
|
|
578
|
+
it('carries the merge path’s policyClause through the raced ledger line', () => {
|
|
579
|
+
expect(racedLedgerPushSummary('my-lane', 'conflicts auto-resolved: git-wins on 3 file(s); ')).to.equal(
|
|
580
|
+
'my-lane -> raced (conflicts auto-resolved: git-wins on 3 file(s); lane updated; branch ledger commit ' +
|
|
581
|
+
'lost the push race — next run re-plans)'
|
|
582
|
+
);
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
|
|
380
586
|
describe('isProtectedBranch', () => {
|
|
381
587
|
it('refuses the default branch and the main sync branch', () => {
|
|
382
588
|
expect(isProtectedBranch('develop', 'develop', 'bit-sync/main')).to.equal(true);
|