@mnci/cli 4.39.0 → 4.40.0

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/cli.js CHANGED
@@ -492,6 +492,189 @@ function fileExists(path) {
492
492
  return (0, import_node_fs4.existsSync)(path);
493
493
  }
494
494
 
495
+ // src/pipeline-customization/legacy-pipeline-steps.algorithm.ts
496
+ var MNCI_STEP_NAMES = /* @__PURE__ */ new Set([
497
+ "Add Go tool bin to PATH",
498
+ "Add the Flutter SDK to PATH",
499
+ "Attach HEAD and fetch refs",
500
+ "Attach HEAD to the source branch",
501
+ "Attach the per-platform zips to the GitHub Release (releasable Go apps)",
502
+ "Attach this OS's zip to the GitHub Release (releasable native apps)",
503
+ "Audit dependencies (fails on an actionable npm advisory)",
504
+ "Authenticate npm against the feed (build identity, no PAT)",
505
+ "Authenticate npm registry",
506
+ "Build all deployables",
507
+ "Cache npm packages",
508
+ "Check formatting (run 'npm run format' locally to fix)",
509
+ "Detect .NET projects",
510
+ "Download Go module dependencies",
511
+ "Ensure the function-apps folder exists",
512
+ "Fetch branches and release tags",
513
+ "Install Azure Functions Core Tools",
514
+ "Install dependencies",
515
+ "Install golangci-lint",
516
+ "Install native prerequisites (Linux)",
517
+ "Install Python dependencies (ruff, pytest, build, twine)",
518
+ "Install Python dependencies (ruff, pytest, build, twine, pip-audit)",
519
+ "Install Python project dependencies (editable, workspace-wide)",
520
+ "Lint",
521
+ "Lint, test and build affected projects",
522
+ "Lint, test and build affected projects (PR)",
523
+ "Lint, test and build everything",
524
+ "Lint, test, build and package the native apps",
525
+ "Lint, typecheck, test and build everything",
526
+ "npm audit (fails on an actionable advisory)",
527
+ "npm audit (non-blocking)",
528
+ "Pack all apps (one zip per app -> dist/drop)",
529
+ "Package function apps",
530
+ "Pin npm to the major mnci verifies against",
531
+ "pip-audit (non-blocking)",
532
+ "Preflight \u2014 name the PyPI projects this release would create",
533
+ "Preflight \u2014 npm must accept the token before anything is tagged",
534
+ "Publish function app packages",
535
+ "Publish packages and tag main",
536
+ "Publish Python packages (uv)",
537
+ "Publish the drop (one zip per app)",
538
+ "Push release tags (nx release's own push never runs without a remote Release configured)",
539
+ "Release (version from commits, tag-only, publish)",
540
+ "Resolve Dart dependencies (one pub get for the whole workspace)",
541
+ "Set the git identity used for release tags",
542
+ "Set up the Go toolchain",
543
+ "Set up the language toolchains (Python, Go, Flutter)",
544
+ "Sign in to Microsoft Entra ID (VS Code Marketplace)",
545
+ "Tag the run per app (type-name)",
546
+ "Test and build everything",
547
+ "Verify the workspace is synced (run 'npx nx sync' locally and commit if this fails)",
548
+ "Verify this is a full checkout (nx release needs the real tag history)"
549
+ ]);
550
+ var MNCI_STEP_NAME_PATTERNS = [
551
+ /^Install the (\.NET SDK|Flutter SDK)/,
552
+ /^Release — version, tag/,
553
+ /^Verify \(/,
554
+ /^Publish this OS's native zips/
555
+ ];
556
+ var MNCI_USES = ["actions/checkout@", "actions/setup-node@", "actions/setup-dotnet@", "actions/upload-artifact@", "azure/login@"];
557
+ var MNCI_TASKS = ["UseNode@", "Cache@", "PublishBuildArtifacts@", "UseDotNet@", "npmAuthenticate@"];
558
+ var MNCI_COMMANDS = ["npm ci", "npm install -g npm@", "git fetch --all", 'git config user.name "github-actions', "git checkout -B", "npx mnci ci", "npx nx sync"];
559
+ function findFirstStepList(lines) {
560
+ const keyAt = lines.findIndex((line) => line.trim() === "steps:");
561
+ if (keyAt === -1) {
562
+ return void 0;
563
+ }
564
+ const firstItem = lines.findIndex((line, index) => index > keyAt && /^\s*- /.test(line));
565
+ if (firstItem === -1) {
566
+ return void 0;
567
+ }
568
+ const indent = lines[firstItem].length - lines[firstItem].trimStart().length;
569
+ const itemStart = " ".repeat(indent) + "- ";
570
+ const steps = [];
571
+ let pending = [];
572
+ let current;
573
+ for (let index = firstItem; index < lines.length; index++) {
574
+ const line = lines[index];
575
+ const lineIndent = line.length - line.trimStart().length;
576
+ const isBlank = line.trim() === "";
577
+ const isComment = line.trimStart().startsWith("#");
578
+ if (line.startsWith(itemStart)) {
579
+ current = { lead: pending, body: [line] };
580
+ steps.push(current);
581
+ pending = [];
582
+ } else if (isBlank || isComment && lineIndent <= indent) {
583
+ pending.push(line);
584
+ } else if (current && lineIndent > indent) {
585
+ current.body.push(...pending, line);
586
+ pending = [];
587
+ } else {
588
+ break;
589
+ }
590
+ }
591
+ return { steps, indent };
592
+ }
593
+ function topLevelFields(step) {
594
+ const fields = /* @__PURE__ */ new Map();
595
+ const keyIndent = step.body[0].length - step.body[0].trimStart().length + 2;
596
+ for (const [index, line] of step.body.entries()) {
597
+ const text = index === 0 ? line.trimStart().replace(/^- /, "") : line;
598
+ const lineIndent = index === 0 ? keyIndent : line.length - line.trimStart().length;
599
+ const trimmed = text.trimStart();
600
+ const colon = trimmed.indexOf(":");
601
+ const key = colon === -1 ? "" : trimmed.slice(0, colon);
602
+ if (lineIndent === keyIndent && /^[\w-]+$/.test(key) && !fields.has(key)) {
603
+ fields.set(key, trimmed.slice(colon + 1).trim().replaceAll(/^["']|["']$/g, ""));
604
+ }
605
+ }
606
+ return fields;
607
+ }
608
+ function isGeneratedStep(step) {
609
+ const fields = topLevelFields(step);
610
+ const name = fields.get("name") ?? fields.get("displayName");
611
+ if (name !== void 0 && (MNCI_STEP_NAMES.has(name) || MNCI_STEP_NAME_PATTERNS.some((pattern) => pattern.test(name)))) {
612
+ return true;
613
+ }
614
+ const uses = fields.get("uses");
615
+ if (uses !== void 0 && MNCI_USES.some((prefix) => uses.startsWith(prefix))) {
616
+ return true;
617
+ }
618
+ const task = fields.get("task");
619
+ if (task !== void 0 && MNCI_TASKS.some((prefix) => task.startsWith(prefix))) {
620
+ return true;
621
+ }
622
+ if (fields.has("checkout")) {
623
+ return true;
624
+ }
625
+ const command = fields.get("run") ?? fields.get("script");
626
+ return command !== void 0 && MNCI_COMMANDS.some((prefix) => command.startsWith(prefix));
627
+ }
628
+ function describeStep(step) {
629
+ const fields = topLevelFields(step);
630
+ return fields.get("name") ?? fields.get("displayName") ?? (fields.get("run") ?? fields.get("script") ?? step.body[0].trim()).slice(0, 60);
631
+ }
632
+ function landmark(step) {
633
+ const name = topLevelFields(step).get("name") ?? topLevelFields(step).get("displayName") ?? "";
634
+ if (/^(?:Verify \(|Lint|Test and build|Typecheck)/.test(name)) {
635
+ return "verify";
636
+ }
637
+ if (name.startsWith("Pack all apps")) {
638
+ return "pack";
639
+ }
640
+ if (/^(?:Release|Publish packages and tag)/.test(name)) {
641
+ return "release";
642
+ }
643
+ return void 0;
644
+ }
645
+ function readLegacyPipeline(lines) {
646
+ const reading = { slots: /* @__PURE__ */ new Map(), kept: [], hasPack: false, hasRelease: false };
647
+ const list = findFirstStepList(lines);
648
+ if (!list) {
649
+ return reading;
650
+ }
651
+ const marks = list.steps.map((step) => landmark(step));
652
+ const gate = marks.indexOf("verify");
653
+ const release = marks.indexOf("release");
654
+ reading.hasPack = marks.includes("pack");
655
+ reading.hasRelease = release !== -1;
656
+ for (const [index, step] of list.steps.entries()) {
657
+ if (isGeneratedStep(step)) {
658
+ continue;
659
+ }
660
+ let slot = "after-install";
661
+ if (release !== -1 && index > release) {
662
+ slot = "after-release";
663
+ } else if (gate !== -1 && index > gate) {
664
+ slot = "before-release";
665
+ }
666
+ const lines_ = [...step.lead, ...step.body].map((line) => line.startsWith(" ".repeat(list.indent)) ? line.slice(list.indent) : line.trimStart());
667
+ const existing = reading.slots.get(slot) ?? [];
668
+ reading.slots.set(slot, [...existing, ...existing.length > 0 ? [""] : [], ...trimBlank(lines_)]);
669
+ reading.kept.push(describeStep(step));
670
+ }
671
+ return reading;
672
+ }
673
+ function trimBlank(lines) {
674
+ const first = lines.findIndex((line) => line.trim() !== "");
675
+ return first === -1 ? [] : lines.slice(first);
676
+ }
677
+
495
678
  // src/pipeline-customization/pipeline-markers.algorithm.ts
496
679
  var CHOOSABLE_PHASES = ["pack", "release"];
497
680
  var SLOT_BEGIN = /^(\s*)# mnci:slot (\S+)/;
@@ -597,6 +780,31 @@ function inspectPipeline(text) {
597
780
  };
598
781
  }
599
782
 
783
+ // src/pipeline-customization/migrate-legacy-pipeline.use-case.ts
784
+ function migrateLegacyPipeline(legacy, generated) {
785
+ const reading = readLegacyPipeline(legacy.split("\n"));
786
+ const off = [];
787
+ if (reading.kept.length > 0 || reading.hasPack || reading.hasRelease) {
788
+ if (!reading.hasPack) {
789
+ off.push("pack");
790
+ }
791
+ if (!reading.hasRelease) {
792
+ off.push("release");
793
+ }
794
+ }
795
+ const text = commentOutPhases(injectSlots(generated, reading.slots), off);
796
+ const notes = [
797
+ "This pipeline was written before mnci kept user slots, so it was regenerated. Edits to the steps mnci generated, and anything outside the step list (triggers, variables, extra jobs), are in git history."
798
+ ];
799
+ if (reading.kept.length > 0) {
800
+ notes.push(`Kept ${reading.kept.length} step${reading.kept.length === 1 ? "" : "s"} of your own in the new slots: ${reading.kept.join("; ")}. Check their order.`);
801
+ }
802
+ for (const phase of off) {
803
+ notes.push(`The old pipeline had no ${phase} step, so the ${phase} phase is switched off.`);
804
+ }
805
+ return { text, notes };
806
+ }
807
+
600
808
  // src/pipeline-customization/merge-pipeline.use-case.ts
601
809
  function mergePipeline(existing, generated) {
602
810
  if (existing === void 0) {
@@ -604,10 +812,7 @@ function mergePipeline(existing, generated) {
604
812
  }
605
813
  const normalized = existing.replaceAll("\r\n", "\n");
606
814
  if (!/^\s*# mnci:(?:slot|phase) /m.test(normalized)) {
607
- return {
608
- text: generated,
609
- notes: ['This pipeline was written before mnci kept user slots, so it was regenerated whole. Edits outside the generated steps are in git history: re-apply them inside the new "# mnci:slot" blocks.']
610
- };
815
+ return migrateLegacyPipeline(normalized, generated);
611
816
  }
612
817
  const off = switchedOffPhases(normalized);
613
818
  const text = commentOutPhases(injectSlots(generated, extractSlots(normalized)), off);