@bongos/core 1.19.650 → 1.19.652

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.
@@ -44,7 +44,7 @@ function test(name, fn) {
44
44
  }
45
45
  }
46
46
 
47
- const { parseDeployMode, effectiveDeployMode, buildPrCreateArgv, buildPrMergeArgv, deployRunSucceeded, shouldBailOnFetchFailure, classifyLandOutcome, strandNextStep, pickStaleGateReviewRun, landPollAfterAutoMerge, reconciledCardStatus, landed, landPending, landBailed, strandCommand, landPendingLines } = ship;
47
+ const { parseDeployMode, effectiveDeployMode, buildPrCreateArgv, buildPrMergeArgv, deployRunSucceeded, shouldBailOnFetchFailure, classifyLandOutcome, strandNextStep, pickStaleGateReviewRun, landPollAfterAutoMerge, reconciledCardStatus, landed, landPending, landBailed, strandCommand, landPendingLines, landBailRecoverableByPr, onMainNextStep, LAND_BAIL_MAIN_CHECKOUT_ON_BRANCH, LAND_BAIL_NO_TASK_BRANCH } = ship;
48
48
 
49
49
  // ---- 1. parseDeployMode precedence + safe default ----
50
50
 
@@ -628,6 +628,111 @@ test('1002645 reconcileBlockedLines: never reads as success (no "NOT a failure",
628
628
  assert.ok(!/Prod is live/.test(lines), 'nor the success line');
629
629
  });
630
630
 
631
+ // ---- task 1002572 (PART 2): the single-checkout land fallback ----
632
+ // A builder with ONE checkout can never complete a local merge — the tree that
633
+ // would RECEIVE the merge is the tree holding the feature branch. That used to
634
+ // park the task at 'confirmed', unpaid under ADR 0120, with nothing landing. The
635
+ // branch itself is landable, so that bail now falls back to the push+PR path.
636
+ // What must NOT happen is a blanket conversion: a merge conflict, a red
637
+ // post-merge smoke or a failed push are real failures, and a PR would carry the
638
+ // same breakage to main. The decision is therefore opt-in PER BAIL, and these
639
+ // tests pin both halves of it.
640
+
641
+ test('1002572 landBailed: carries the bail id, and defaults to none', () => {
642
+ assert.equal(landBailed('r', 'n', LAND_BAIL_MAIN_CHECKOUT_ON_BRANCH).bailId, LAND_BAIL_MAIN_CHECKOUT_ON_BRANCH);
643
+ // Fail-safe: every pre-existing bail site passes no id, so it stays terminal.
644
+ assert.equal(landBailed('r', 'n').bailId, null);
645
+ // Still a bail — the fallback must not have turned it into a success.
646
+ assert.equal(landBailed('r', 'n', LAND_BAIL_MAIN_CHECKOUT_ON_BRANCH).ok, false);
647
+ assert.equal(landBailed('r', 'n', LAND_BAIL_MAIN_CHECKOUT_ON_BRANCH).pending, false);
648
+ });
649
+
650
+ test('1002572 landBailRecoverableByPr: only the main-checkout-holds-the-branch bail falls back', () => {
651
+ assert.equal(landBailRecoverableByPr(LAND_BAIL_MAIN_CHECKOUT_ON_BRANCH), true);
652
+ // On main there is no head branch, so a PR has nothing to open FROM.
653
+ assert.equal(landBailRecoverableByPr(LAND_BAIL_NO_TASK_BRANCH), false);
654
+ });
655
+
656
+ test('1002572 landBailRecoverableByPr: an untagged bail is terminal (no blanket conversion)', () => {
657
+ // The real failures — conflict, red smoke, failed push, unreachable origin —
658
+ // all reach the caller as untagged bails. Each must stay loud.
659
+ for (const v of [null, undefined, '', 'merge-conflict', 'post-merge-smoke-failed', 'push-failed', 'fetch-failed']) {
660
+ assert.equal(landBailRecoverableByPr(v), false, `${JSON.stringify(v)} must not be PR-recoverable`);
661
+ }
662
+ });
663
+
664
+ test('1002572 onMainNextStep: names the task branch to re-ship from when it exists', () => {
665
+ const step = onMainNextStep(1002572, true);
666
+ assert.match(step, /task-1002572/, 'names the branch to go back to');
667
+ assert.match(step, /node scripts\/gds\/ship\.js 1002572/, 'and the re-ship command for THIS task');
668
+ // The old advice pointed back at the same dead end; that is the regression.
669
+ assert.notEqual(step, '/merge-mode');
670
+ });
671
+
672
+ test('1002572 onMainNextStep: falls back to /merge-mode rather than inventing a branch', () => {
673
+ assert.equal(onMainNextStep(1002572, false), '/merge-mode');
674
+ });
675
+
676
+ // The ROUTING — the actual deliverable. Driven through autoMerge with the merge
677
+ // chain and the PR land stubbed (a real mergeLocally needs a second checkout and
678
+ // an authed gh), so this proves behaviour rather than matching the source.
679
+ const shipMerge = require('../scripts/gds/ship-merge.js');
680
+
681
+ function driveAutoMerge(mergeOutcome) {
682
+ const calls = [];
683
+ const deps = {
684
+ mergeLocally: async () => mergeOutcome,
685
+ ciLand: async (taskId, branch, summary) => {
686
+ calls.push({ taskId, branch, summary });
687
+ return landPending();
688
+ },
689
+ currentBranch: () => 'task-1002572',
690
+ };
691
+ return shipMerge.autoMerge(1002572, 'a summary', {}, deps).then((outcome) => ({ outcome, calls }));
692
+ }
693
+
694
+ await (async () => {
695
+ const recoverable = await driveAutoMerge(landBailed('main checkout is on a branch', '/merge-mode', LAND_BAIL_MAIN_CHECKOUT_ON_BRANCH));
696
+ test('1002572 routing: the single-checkout bail lands via the PR path instead of stranding', () => {
697
+ assert.equal(recoverable.calls.length, 1, 'ciLand was called exactly once');
698
+ assert.deepEqual(recoverable.calls[0], { taskId: 1002572, branch: 'task-1002572', summary: 'a summary' });
699
+ // Pending, not bailed: the card may honestly say "landing automatically" now.
700
+ assert.equal(recoverable.outcome.pending, true);
701
+ assert.equal(recoverable.outcome.reason, null);
702
+ });
703
+
704
+ const terminal = await driveAutoMerge(landBailed('merging task-1002572 into main failed (conflicts?)', '/merge-mode'));
705
+ test('1002572 routing: a real failure stays loud — no PR is opened for a conflict', () => {
706
+ assert.equal(terminal.calls.length, 0, 'a conflict must NOT be converted into a PR');
707
+ assert.equal(terminal.outcome.pending, false);
708
+ assert.match(terminal.outcome.reason, /conflicts/);
709
+ });
710
+
711
+ const onMain = await driveAutoMerge(landBailed('nothing to merge — this checkout is on main, not a task branch', onMainNextStep(1002572, true), LAND_BAIL_NO_TASK_BRANCH));
712
+ test('1002572 routing: standing on main opens no PR (there is no head branch), but names the cure', () => {
713
+ assert.equal(onMain.calls.length, 0);
714
+ assert.match(onMain.outcome.nextStep, /task-1002572/);
715
+ });
716
+
717
+ const success = await driveAutoMerge(landed());
718
+ test('1002572 routing: a successful local merge is untouched (the worktree path does not regress)', () => {
719
+ assert.equal(success.calls.length, 0, 'a clean local merge must not also open a PR');
720
+ assert.equal(success.outcome.ok, true);
721
+ });
722
+ })();
723
+
724
+ // Only the two documented catch-22 bails may carry an id. Behaviour can't see
725
+ // this — a third tagged bail would silently gain a fallback it was never granted.
726
+ test('1002572 wiring: exactly the two catch-22 bails are tagged', () => {
727
+ const src = fs.readFileSync(path.join(__dirname, '..', 'scripts', 'gds', 'ship-merge.js'), 'utf8');
728
+ // Each id appears exactly twice: the require destructure, and its ONE bail site.
729
+ for (const id of [LAND_BAIL_MAIN_CHECKOUT_ON_BRANCH, LAND_BAIL_NO_TASK_BRANCH]) {
730
+ const name = `LAND_BAIL_${id.toUpperCase().replace(/-/g, '_')}`;
731
+ const hits = src.split(name).length - 1;
732
+ assert.equal(hits, 2, `${name}: imported once + used at exactly one bail site (saw ${hits})`);
733
+ }
734
+ });
735
+
631
736
  console.log('');
632
737
  console.log(`ship_ci_deploy: ${passed} passed, ${failed} failed`);
633
738
  process.exit(failed === 0 ? 0 : 1);