@teambit/ci 1.0.507 → 1.0.508

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.
Files changed (36) hide show
  1. package/dist/ci.main.runtime.d.ts +3 -2
  2. package/dist/ci.main.runtime.js +5 -6
  3. package/dist/ci.main.runtime.js.map +1 -1
  4. package/dist/{preview-1787147173446.js → preview-1787160027560.js} +1 -1
  5. package/dist/sync/git-host-provider.d.ts +9 -0
  6. package/dist/sync/git-host-provider.js.map +1 -1
  7. package/dist/sync/github-client.d.ts +28 -0
  8. package/dist/sync/github-client.js +81 -3
  9. package/dist/sync/github-client.js.map +1 -1
  10. package/dist/sync/github-client.spec.js +249 -0
  11. package/dist/sync/github-client.spec.js.map +1 -1
  12. package/dist/sync/lane-sync-executor.d.ts +47 -3
  13. package/dist/sync/lane-sync-executor.js +191 -39
  14. package/dist/sync/lane-sync-executor.js.map +1 -1
  15. package/dist/sync/lane-sync-executor.spec.js +336 -0
  16. package/dist/sync/lane-sync-executor.spec.js.map +1 -1
  17. package/dist/sync/sync-planner.d.ts +12 -2
  18. package/dist/sync/sync-planner.js +17 -8
  19. package/dist/sync/sync-planner.js.map +1 -1
  20. package/dist/sync/sync-planner.spec.js +64 -0
  21. package/dist/sync/sync-planner.spec.js.map +1 -1
  22. package/dist/sync/sync-state.d.ts +34 -3
  23. package/dist/sync/sync-state.js +61 -3
  24. package/dist/sync/sync-state.js.map +1 -1
  25. package/dist/sync/sync-state.spec.js +54 -0
  26. package/dist/sync/sync-state.spec.js.map +1 -1
  27. package/package.json +15 -15
  28. package/sync/git-host-provider.ts +8 -0
  29. package/sync/github-client.spec.ts +160 -0
  30. package/sync/github-client.ts +90 -3
  31. package/sync/lane-sync-executor.spec.ts +291 -0
  32. package/sync/lane-sync-executor.ts +233 -50
  33. package/sync/sync-planner.spec.ts +41 -0
  34. package/sync/sync-planner.ts +31 -10
  35. package/sync/sync-state.spec.ts +48 -0
  36. package/sync/sync-state.ts +104 -4
@@ -566,6 +566,10 @@ describe('executeExportBranch reports the ledger-race wording when only the ledg
566
566
  executor.lastNonSyncCommitMessage = async () => 'feat: some change';
567
567
  executor.checkoutFromRemote = async () => {};
568
568
  executor.restoreWorkspace = async () => {};
569
+ executor.materializeLane = async () => undefined;
570
+ executor.deps.ci = {
571
+ hasUnsyncedWorkChanges: async () => true
572
+ };
569
573
  // The export half already succeeded; only the ledger commit that follows raced.
570
574
  executor.snapAndExportOntoLane = async () => ({
571
575
  status: 'exported'
@@ -587,6 +591,157 @@ describe('executeExportBranch reports the ledger-race wording when only the ledg
587
591
  });
588
592
  });
589
593
 
594
+ // The probe's "clean" answer means converged only against the lane the PLAN read. A lane that moved
595
+ // in between must re-plan — comparing the branch's files with the newer lane would misread the
596
+ // lane's own advance as branch work and export a revert over a concurrent developer's export.
597
+ describe('executeExportBranch bails when the lane moved since planning', () => {
598
+ function stubExport(currentLaneComponents, plannedLaneComponents) {
599
+ const {
600
+ executor
601
+ } = stubExecutor();
602
+ const touched = [];
603
+ executor.checkoutFromRemote = async () => {};
604
+ executor.restoreWorkspace = async () => {};
605
+ executor.materializeLane = async () => undefined;
606
+ executor.lastNonSyncCommitMessage = async () => 'feat: some change';
607
+ executor.getRemoteLane = async () => ({
608
+ components: currentLaneComponents
609
+ });
610
+ executor.deps.ci = {
611
+ hasUnsyncedWorkChanges: async () => {
612
+ touched.push('status');
613
+ return false;
614
+ }
615
+ };
616
+ executor.snapAndExportOntoLane = async () => {
617
+ touched.push('snap');
618
+ return {
619
+ status: 'exported'
620
+ };
621
+ };
622
+ const run = () => executor.executeExportBranch({
623
+ target: {
624
+ hostScope: 'acme.shop',
625
+ name: 'my-lane'
626
+ },
627
+ laneIdStr: 'acme.shop/my-lane',
628
+ branch: 'my-lane',
629
+ defaultBranch: 'main',
630
+ preExportLane: {
631
+ components: plannedLaneComponents
632
+ }
633
+ });
634
+ return {
635
+ run,
636
+ touched
637
+ };
638
+ }
639
+ const L1 = [comp('acme.shop/comp1', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1')];
640
+ const L2 = [comp('acme.shop/comp1', 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb2')];
641
+ it('a moved lane returns a re-plan noop before the status read or any snap', async () => {
642
+ const {
643
+ run,
644
+ touched
645
+ } = stubExport(L2, L1);
646
+ const summary = await run();
647
+ (0, _chai().expect)(summary).to.include('my-lane -> noop');
648
+ (0, _chai().expect)(summary).to.include('moved');
649
+ (0, _chai().expect)(touched).to.deep.equal([]);
650
+ });
651
+ it('an unmoved lane proceeds to the probe', async () => {
652
+ const {
653
+ run,
654
+ touched
655
+ } = stubExport(L1, L1);
656
+ const summary = await run();
657
+ (0, _chai().expect)(summary).to.include('my-lane -> noop (converged)');
658
+ (0, _chai().expect)(touched).to.deep.equal(['status']);
659
+ });
660
+ });
661
+
662
+ // The merge path snaps whatever the merge produced — which can be nothing new, now routinely: a
663
+ // source-bundling tip plus a moved lane plans merge-diverged, and a branch with nothing of its own
664
+ // merges clean and leaves the snap a noop. The summary must say so, and the run-summary comment
665
+ // belongs only to runs that actually exported.
666
+ describe('executeMergeDiverged distinguishes an exporting merge from a nothing-new merge', () => {
667
+ function stubMerge(snapStatus, currentLaneComponents = []) {
668
+ const {
669
+ executor
670
+ } = stubExecutor();
671
+ const summaryCalls = [];
672
+ executor.checkoutFromRemote = async () => {};
673
+ executor.restoreWorkspace = async () => {};
674
+ executor.getRemoteLane = async () => ({
675
+ components: currentLaneComponents
676
+ });
677
+ executor.mergeLaneIntoBranchWorkingTree = async () => ({
678
+ conflicts: [],
679
+ conflictedFileCount: 0
680
+ });
681
+ executor.snapAndExportOntoLane = async () => ({
682
+ status: snapStatus
683
+ });
684
+ executor.recordLaneHeadOnBranch = async () => ({
685
+ status: 'ok',
686
+ laneHead: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1',
687
+ remoteLane: {
688
+ components: []
689
+ }
690
+ });
691
+ executor.postRunSummaryComment = async args => summaryCalls.push(args);
692
+ const run = () => executor.executeMergeDiverged({
693
+ target: {
694
+ hostScope: 'acme.shop',
695
+ name: 'my-lane'
696
+ },
697
+ laneIdStr: 'acme.shop/my-lane',
698
+ branch: 'my-lane',
699
+ defaultBranch: 'main',
700
+ preExportLane: {
701
+ components: []
702
+ }
703
+ });
704
+ return {
705
+ run,
706
+ summaryCalls
707
+ };
708
+ }
709
+ it('a nothing-new merge says so, and posts no run summary', async () => {
710
+ const {
711
+ run,
712
+ summaryCalls
713
+ } = stubMerge('noop');
714
+ const summary = await run();
715
+ (0, _chai().expect)(summary).to.include('my-lane -> merge-diverged');
716
+ (0, _chai().expect)(summary).to.include('nothing new to export');
717
+ (0, _chai().expect)(summary).to.not.include('then exported');
718
+ (0, _chai().expect)(summaryCalls).to.have.lengthOf(0);
719
+ });
720
+ it('an exporting merge keeps the exported wording and posts the run summary', async () => {
721
+ const {
722
+ run,
723
+ summaryCalls
724
+ } = stubMerge('exported');
725
+ const summary = await run();
726
+ (0, _chai().expect)(summary).to.include('then exported');
727
+ (0, _chai().expect)(summaryCalls).to.have.lengthOf(1);
728
+ (0, _chai().expect)(summaryCalls[0].laneHead).to.equal('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1');
729
+ });
730
+
731
+ // Same freshness rule as the export probe: the plan's inputs must still describe the world.
732
+ it('a lane that moved since planning bails to a re-plan before merging anything', async () => {
733
+ const moved = [comp('acme.shop/comp1', 'ccccccccccccccccccccccccccccccccccccccc3')];
734
+ const {
735
+ run,
736
+ summaryCalls
737
+ } = stubMerge('exported', moved);
738
+ const summary = await run();
739
+ (0, _chai().expect)(summary).to.include('my-lane -> noop');
740
+ (0, _chai().expect)(summary).to.include('moved');
741
+ (0, _chai().expect)(summaryCalls).to.have.lengthOf(0);
742
+ });
743
+ });
744
+
590
745
  // Adoption never snaps or exports, so its raced line must NOT claim the lane was updated — the lane
591
746
  // is untouched and only the branch's pointer commit lost the race.
592
747
  describe('executeAdoptBranch reports a lane-untouched race when its ledger push races', () => {
@@ -634,5 +789,186 @@ describe('isProtectedBranch', () => {
634
789
  (0, _chai().expect)((0, _laneSyncExecutor().isProtectedBranch)('my-lane', 'develop', 'bit-sync/main')).to.equal(false);
635
790
  });
636
791
  });
792
+ describe('laneSummaryComponents', () => {
793
+ it('includes hidden updateDependents — a cascade export must not read as "nothing changed"', () => {
794
+ const lane = {
795
+ components: [comp('acme.shop/comp1', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1')],
796
+ updateDependents: [comp('acme.shop/dep1', 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb2')]
797
+ };
798
+ (0, _chai().expect)((0, _laneSyncExecutor().laneSummaryComponents)(lane).map(c => c.id.toStringWithoutVersion())).to.deep.equal(['acme.shop/comp1', 'acme.shop/dep1']);
799
+ });
800
+ it('an absent lane lists nothing', () => {
801
+ (0, _chai().expect)((0, _laneSyncExecutor().laneSummaryComponents)(undefined)).to.deep.equal([]);
802
+ });
803
+ });
804
+ describe('changedLaneComponents', () => {
805
+ it('includes a component new to the lane, and one whose head moved', () => {
806
+ const before = [comp('acme.shop/comp1', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1')];
807
+ const after = [comp('acme.shop/comp1', 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb2'), comp('acme.shop/comp2', 'ccccccccccccccccccccccccccccccccccccccc3')];
808
+ const changed = (0, _laneSyncExecutor().changedLaneComponents)(before, after);
809
+ (0, _chai().expect)(changed.map(c => c.id.toStringWithoutVersion())).to.deep.equal(['acme.shop/comp1', 'acme.shop/comp2']);
810
+ });
811
+ it('excludes a component whose head did not move', () => {
812
+ const stable = comp('acme.shop/comp1', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1');
813
+ (0, _chai().expect)((0, _laneSyncExecutor().changedLaneComponents)([stable], [stable])).to.deep.equal([]);
814
+ });
815
+ it('treats an undefined "before" (never seen this lane) as everything being new', () => {
816
+ const after = [comp('acme.shop/comp1', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1')];
817
+ (0, _chai().expect)((0, _laneSyncExecutor().changedLaneComponents)(undefined, after)).to.deep.equal(after);
818
+ });
819
+ });
820
+ describe('runSummaryCommentBody', () => {
821
+ it('carries the marker, the changed components, and the branch/lane anchors', () => {
822
+ const body = (0, _laneSyncExecutor().runSummaryCommentBody)({
823
+ laneIdStr: 'acme.shop/my-lane',
824
+ laneUrl: 'https://bit.cloud/acme/shop/~lane/my-lane',
825
+ branch: 'my-lane',
826
+ branchTipSha: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1',
827
+ laneHead: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb2',
828
+ changed: [comp('acme.shop/comp1', 'ccccccccccccccccccccccccccccccccccccccc3')]
829
+ });
830
+ (0, _chai().expect)(body).to.include(_laneSyncExecutor().RUN_SUMMARY_MARKER);
831
+ (0, _chai().expect)(body).to.include('acme.shop/comp1` @ `ccccccccc');
832
+ (0, _chai().expect)(body).to.include('branch: `my-lane` @ `aaaaaaaaa`');
833
+ // the lane anchor is a LINK — the reader lands on the lane, not on a name to copy-paste
834
+ (0, _chai().expect)(body).to.include('lane: [`acme.shop/my-lane`](https://bit.cloud/acme/shop/~lane/my-lane) @ `bbbbbbbbb`');
835
+ });
836
+ it('says plainly that nothing changed, rather than an empty list', () => {
837
+ const body = (0, _laneSyncExecutor().runSummaryCommentBody)({
838
+ laneIdStr: 'acme.shop/my-lane',
839
+ laneUrl: 'https://bit.cloud/acme/shop/~lane/my-lane',
840
+ branch: 'my-lane',
841
+ branchTipSha: undefined,
842
+ laneHead: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb2',
843
+ changed: []
844
+ });
845
+ (0, _chai().expect)(body).to.include('none — nothing on the lane changed this run');
846
+ (0, _chai().expect)(body).to.not.include('@ `undefined');
847
+ });
848
+ });
849
+
850
+ // The comment is a pure surface effect: it must never gate the run's own outcome, and must degrade
851
+ // silently through every "nothing to comment on" state a real host, PR or branch can be in.
852
+ describe('postRunSummaryComment', () => {
853
+ const BEFORE = [comp('acme.shop/comp1', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1')];
854
+ const AFTER = [comp('acme.shop/comp1', 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb2')];
855
+ function executorWith(gitHost, warnings = []) {
856
+ const executor = new (_laneSyncExecutor().LaneSyncExecutor)({
857
+ lanes: {},
858
+ ci: {},
859
+ logger: {
860
+ console: () => {},
861
+ consoleWarning: msg => warnings.push(msg),
862
+ error: () => {}
863
+ },
864
+ gitHost,
865
+ cfg: (0, _syncConfig().resolveSyncConfig)({}),
866
+ defaultScope: 'acme.shop'
867
+ });
868
+ executor.currentBranchTip = async () => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1';
869
+ return executor;
870
+ }
871
+ const call = executor => executor.postRunSummaryComment({
872
+ target: {
873
+ hostScope: 'acme.shop',
874
+ name: 'my-lane'
875
+ },
876
+ laneIdStr: 'acme.shop/my-lane',
877
+ branch: 'my-lane',
878
+ laneHead: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb2',
879
+ branchTipSha: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1',
880
+ preComponents: BEFORE,
881
+ postComponents: AFTER
882
+ });
883
+
884
+ // The anchor is the ledger sha THIS run pushed — passed in, never refetched: a concurrent developer
885
+ // push must not be presented as the synced tip, and no remote read exists to fail the path.
886
+ it('anchors the body on the given pushed sha without any branch refetch', async () => {
887
+ const upserts = [];
888
+ const gitHost = {
889
+ findPrByBranch: async () => ({
890
+ number: 7,
891
+ htmlUrl: 'https://example.test/pr/7',
892
+ labels: []
893
+ }),
894
+ upsertComment: async (_n, _m, body) => upserts.push(body)
895
+ };
896
+ const executor = executorWith(gitHost);
897
+ executor.currentBranchTip = async () => {
898
+ throw new Error('must not be called');
899
+ };
900
+ await call(executor);
901
+ (0, _chai().expect)(upserts).to.have.lengthOf(1);
902
+ (0, _chai().expect)(upserts[0]).to.include('branch: `my-lane` @ `aaaaaaaaa`');
903
+ });
904
+ it('upserts the marked comment on the open PR when the host supports it', async () => {
905
+ const calls = [];
906
+ const gitHost = {
907
+ findPrByBranch: async () => ({
908
+ number: 7,
909
+ htmlUrl: 'https://example.test/pr/7',
910
+ labels: []
911
+ }),
912
+ upsertComment: async (prNumber, marker, body) => {
913
+ calls.push({
914
+ prNumber,
915
+ marker,
916
+ body
917
+ });
918
+ }
919
+ };
920
+ await call(executorWith(gitHost));
921
+ (0, _chai().expect)(calls).to.have.lengthOf(1);
922
+ (0, _chai().expect)(calls[0].prNumber).to.equal(7);
923
+ (0, _chai().expect)(calls[0].marker).to.equal(_laneSyncExecutor().RUN_SUMMARY_MARKER);
924
+ (0, _chai().expect)(calls[0].body).to.include('acme.shop/comp1` @ `bbbbbbbbb');
925
+ });
926
+ it('is a no-op with no configured git host', async () => {
927
+ await call(executorWith(undefined)); // would throw on any host call; nothing to assert but "did not throw"
928
+ });
929
+ it('is a no-op when the host does not implement upsertComment', async () => {
930
+ const calls = [];
931
+ const gitHost = {
932
+ findPrByBranch: async () => {
933
+ calls.push('findPrByBranch');
934
+ return {
935
+ number: 7,
936
+ htmlUrl: 'https://example.test/pr/7',
937
+ labels: []
938
+ };
939
+ }
940
+ };
941
+ await call(executorWith(gitHost));
942
+ // upsertComment is feature-tested BEFORE the PR lookup — an unsupported host must not even look up the PR.
943
+ (0, _chai().expect)(calls).to.deep.equal([]);
944
+ });
945
+ it('is a no-op when the branch has no open PR yet', async () => {
946
+ const calls = [];
947
+ const gitHost = {
948
+ findPrByBranch: async () => undefined,
949
+ upsertComment: async () => {
950
+ calls.push('upsertComment');
951
+ }
952
+ };
953
+ await call(executorWith(gitHost));
954
+ (0, _chai().expect)(calls).to.deep.equal([]);
955
+ });
956
+ it('warns and does not throw when the comment API call fails', async () => {
957
+ const warnings = [];
958
+ const gitHost = {
959
+ findPrByBranch: async () => ({
960
+ number: 7,
961
+ htmlUrl: 'https://example.test/pr/7',
962
+ labels: []
963
+ }),
964
+ upsertComment: async () => {
965
+ throw new Error('rate limited');
966
+ }
967
+ };
968
+ await call(executorWith(gitHost, warnings));
969
+ (0, _chai().expect)(warnings.join('\n')).to.contain('Could not post the run-summary comment');
970
+ (0, _chai().expect)(warnings.join('\n')).to.contain('rate limited');
971
+ });
972
+ });
637
973
 
638
974
  //# sourceMappingURL=lane-sync-executor.spec.js.map