@mnci/cli 4.36.0 → 4.37.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
@@ -429,6 +429,131 @@ function fileExists(path) {
429
429
  return (0, import_node_fs3.existsSync)(path);
430
430
  }
431
431
 
432
+ // src/pipeline-customization/pipeline-markers.algorithm.ts
433
+ var CHOOSABLE_PHASES = ["pack", "release"];
434
+ var SLOT_BEGIN = /^(\s*)# mnci:slot (\S+)/;
435
+ var SLOT_END = /^\s*# mnci:slot-end \S+/;
436
+ var PHASE_BEGIN = /^(\s*)# mnci:phase (\S+)/;
437
+ var PHASE_END = /^\s*# mnci:phase-end (\S+)/;
438
+ var SWITCHED_OFF_HINT = "# mnci: switched off. Uncomment the lines below to run it again (mnci doctor lists it).";
439
+ function slotMarkers(name, indent) {
440
+ return [
441
+ `${indent}# mnci:slot ${name} (your own steps go between these two lines; mnci upgrade keeps them)`,
442
+ `${indent}# mnci:slot-end ${name}`
443
+ ].join("\n");
444
+ }
445
+ function phaseStart(name, indent) {
446
+ return `${indent}# mnci:phase ${name} (delete or comment this block to switch it off; mnci upgrade keeps it off)`;
447
+ }
448
+ function phaseEnd(name, indent) {
449
+ return `${indent}# mnci:phase-end ${name}`;
450
+ }
451
+ function extractSlots(text) {
452
+ const slots = /* @__PURE__ */ new Map();
453
+ const lines = text.split("\n");
454
+ for (let index = 0; index < lines.length; index++) {
455
+ const begin = SLOT_BEGIN.exec(lines[index]);
456
+ if (!begin) {
457
+ continue;
458
+ }
459
+ const [, indent, name] = begin;
460
+ const content = [];
461
+ for (index++; index < lines.length && !SLOT_END.test(lines[index]); index++) {
462
+ content.push(lines[index].startsWith(indent) ? lines[index].slice(indent.length) : lines[index].trimStart());
463
+ }
464
+ slots.set(name, content);
465
+ index--;
466
+ }
467
+ return slots;
468
+ }
469
+ function injectSlots(generated, slots) {
470
+ const lines = generated.split("\n");
471
+ const result = [];
472
+ for (let index = 0; index < lines.length; index++) {
473
+ result.push(lines[index]);
474
+ const begin = SLOT_BEGIN.exec(lines[index]);
475
+ if (!begin) {
476
+ continue;
477
+ }
478
+ const [, indent, name] = begin;
479
+ const content = slots.get(name) ?? [];
480
+ for (const line of content) {
481
+ result.push(line === "" ? "" : `${indent}${line}`);
482
+ }
483
+ while (index + 1 < lines.length && !SLOT_END.test(lines[index + 1])) {
484
+ index++;
485
+ }
486
+ }
487
+ return result.join("\n");
488
+ }
489
+ function switchedOffPhases(text) {
490
+ const lines = text.split("\n");
491
+ if (lines.every((line) => !(PHASE_BEGIN.test(line) || SLOT_BEGIN.test(line)))) {
492
+ return [];
493
+ }
494
+ return CHOOSABLE_PHASES.filter((phase) => {
495
+ const begin = lines.findIndex((line) => PHASE_BEGIN.exec(line)?.[2] === phase);
496
+ if (begin === -1) {
497
+ return true;
498
+ }
499
+ for (let index = begin + 1; index < lines.length && PHASE_END.exec(lines[index])?.[1] !== phase; index++) {
500
+ const line = lines[index].trim();
501
+ if (line !== "" && !line.startsWith("#")) {
502
+ return false;
503
+ }
504
+ }
505
+ return true;
506
+ });
507
+ }
508
+ function commentOutPhases(generated, phases) {
509
+ if (phases.length === 0) {
510
+ return generated;
511
+ }
512
+ const lines = generated.split("\n");
513
+ const result = [];
514
+ for (let index = 0; index < lines.length; index++) {
515
+ result.push(lines[index]);
516
+ const begin = PHASE_BEGIN.exec(lines[index]);
517
+ if (!begin || !phases.includes(begin[2])) {
518
+ continue;
519
+ }
520
+ const [, indent, name] = begin;
521
+ result.push(`${indent}${SWITCHED_OFF_HINT}`);
522
+ for (index++; index < lines.length && PHASE_END.exec(lines[index])?.[1] !== name; index++) {
523
+ const line = lines[index];
524
+ result.push(line.trim() === "" || line.trimStart().startsWith("#") ? line : `${indent}# ${line.slice(indent.length)}`);
525
+ }
526
+ index--;
527
+ }
528
+ return result.join("\n");
529
+ }
530
+ function inspectPipeline(text) {
531
+ return {
532
+ verifyActive: text.split("\n").some((line) => !line.trimStart().startsWith("#") && /\bci verify\b/.test(line)),
533
+ switchedOff: switchedOffPhases(text)
534
+ };
535
+ }
536
+
537
+ // src/pipeline-customization/merge-pipeline.use-case.ts
538
+ function mergePipeline(existing, generated) {
539
+ if (existing === void 0) {
540
+ return { text: generated, notes: [] };
541
+ }
542
+ const normalized = existing.replaceAll("\r\n", "\n");
543
+ if (!/^\s*# mnci:(?:slot|phase) /m.test(normalized)) {
544
+ return {
545
+ text: generated,
546
+ 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.']
547
+ };
548
+ }
549
+ const off = switchedOffPhases(normalized);
550
+ const text = commentOutPhases(injectSlots(generated, extractSlots(normalized)), off);
551
+ return {
552
+ text,
553
+ notes: off.map((phase) => `The ${phase} phase is switched off in this pipeline and stays off.`)
554
+ };
555
+ }
556
+
432
557
  // src/workspace-overlay/overlay.use-case.ts
433
558
  var DEFAULT_STACK = { testRunner: "jest" };
434
559
  var TS_COMPILER_DEPENDENCIES = {
@@ -918,6 +1043,13 @@ function isUnmodifiedMnciEslintConfig(content) {
918
1043
  const code = content.split("\n").map((line) => line.trim()).filter((line) => line !== "" && !line.startsWith("//"));
919
1044
  return code.length === 2 && code[0] === "import mnci from '@mnci/eslint-config'" && code[1] === "export default mnci({ workspaceRoot: import.meta.dirname })";
920
1045
  }
1046
+ function writePipeline(path, onProgress, generated) {
1047
+ const { text, notes } = mergePipeline((0, import_node_fs4.existsSync)(path) ? (0, import_node_fs4.readFileSync)(path, "utf8") : void 0, generated);
1048
+ for (const note of notes) {
1049
+ onProgress(` ${note}`);
1050
+ }
1051
+ writeFileEnsured(path, text);
1052
+ }
921
1053
  function writeEslintEntryPoint(workspaceRoot, onProgress) {
922
1054
  const path = (0, import_node_path3.join)(workspaceRoot, ESLINT_USER_FILENAME);
923
1055
  if ((0, import_node_fs4.existsSync)(path) && !isUnmodifiedMnciEslintConfig((0, import_node_fs4.readFileSync)(path, "utf8"))) {
@@ -1429,6 +1561,8 @@ steps:
1429
1561
  ${npmAuthenticateStep} - script: npm ci
1430
1562
  displayName: Install dependencies${npmCiEnv}
1431
1563
 
1564
+ ${slotMarkers("after-install", " ")}
1565
+
1432
1566
  # Everything from here to the release is a command of the mnci CLI, a
1433
1567
  # devDependency of this workspace, so the version is the lockfile's and a
1434
1568
  # developer reproduces CI by running the same line. The guards that used to be
@@ -1472,6 +1606,7 @@ ${npmAuthenticateStep} - script: npm ci
1472
1606
  - script: npx mnci ci verify
1473
1607
  displayName: Verify (sync check, then affected on a PR, every project on main)
1474
1608
 
1609
+ ${phaseStart("pack", " ")}
1475
1610
  # Pack every app into dist/drop/<type>-<name>.zip via each app's 'package'
1476
1611
  # target; skipped cleanly when the workspace has no apps yet.
1477
1612
  - script: npx mnci ci pack
@@ -1491,7 +1626,11 @@ ${npmAuthenticateStep} - script: npm ci
1491
1626
  - script: node -e "const fs=require('node:fs');const path=require('node:path');for(const f of fs.globSync('dist/drop/*.zip')){console.log('##vso[build.addbuildtag]'+path.basename(f,'.zip'))}"
1492
1627
  displayName: Tag the run per app (type-name)
1493
1628
  condition: ${onMain}
1629
+ ${phaseEnd("pack", " ")}
1494
1630
 
1631
+ ${slotMarkers("before-release", " ")}
1632
+
1633
+ ${phaseStart("release", " ")}
1495
1634
  # The release, in the order that keeps a failure safe: refuse a shallow
1496
1635
  # clone, prove the npm token and name the PyPI projects it would create (all
1497
1636
  # BEFORE anything is tagged), then version + tag + publish in one 'nx release'
@@ -1517,6 +1656,9 @@ ${npmAuthenticateStep} - script: npm ci
1517
1656
  # A VS Code extension publishes with it; tools/vscode-extension.cjs skips the
1518
1657
  # Marketplace when it is unset, including when Azure leaves it as '$(VSCE_PAT)'.
1519
1658
  VSCE_PAT: $(VSCE_PAT)
1659
+ ${phaseEnd("release", " ")}
1660
+
1661
+ ${slotMarkers("after-release", " ")}
1520
1662
  `;
1521
1663
  if (!nativeApps) {
1522
1664
  return document;
@@ -1643,6 +1785,8 @@ jobs:
1643
1785
  env:
1644
1786
  ${npmAuthName}: ${npmAuthValue}
1645
1787
 
1788
+ ${slotMarkers("after-install", " ".repeat(6))}
1789
+
1646
1790
  # Everything from here to the release is a command of the mnci CLI, a
1647
1791
  # devDependency of this workspace, so the version is the lockfile's and a
1648
1792
  # developer reproduces CI by running the same line. The guards that used to be
@@ -1682,6 +1826,7 @@ jobs:
1682
1826
  - run: npx mnci ci verify
1683
1827
  name: Verify (sync check, then affected on a PR, every project on main)
1684
1828
 
1829
+ ${phaseStart("pack", " ".repeat(6))}
1685
1830
  # Pack every app into dist/drop/<type>-<name>.zip via each app's 'package'
1686
1831
  # target; skipped cleanly when the workspace has no apps yet.
1687
1832
  - run: npx mnci ci pack
@@ -1694,7 +1839,11 @@ jobs:
1694
1839
  name: drop
1695
1840
  path: dist/drop
1696
1841
  if-no-files-found: ignore
1842
+ ${phaseEnd("pack", " ".repeat(6))}
1843
+
1844
+ ${slotMarkers("before-release", " ".repeat(6))}
1697
1845
 
1846
+ ${phaseStart("release", " ".repeat(6))}
1698
1847
  # Signs in to Microsoft Entra ID as the Marketplace publishing identity (an
1699
1848
  # app registration with a federated credential for this repository's main
1700
1849
  # branch), so the release step publishes VS Code extensions with
@@ -1739,6 +1888,9 @@ jobs:
1739
1888
  VSCE_PAT: \${{ secrets.VSCE_PAT }}${githubReleases ? `
1740
1889
  GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
1741
1890
  GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}` : ""}
1891
+ ${phaseEnd("release", " ".repeat(6))}
1892
+
1893
+ ${slotMarkers("after-release", " ".repeat(6))}
1742
1894
  ${nativeApps ? githubNativeJob(npmAuthName, npmAuthValue) : ""}`;
1743
1895
  }
1744
1896
  var PIP_PROJECT_GLOBS = ["apps", "python-packages", "libs"];
@@ -2059,8 +2211,9 @@ function applyOverlay(workspaceRoot, options, onProgress = () => {
2059
2211
  const nativeApps = hasNativeGoApp(workspaceRoot);
2060
2212
  if (options.ci === "azure" || options.ci === "both") {
2061
2213
  onProgress("azure-pipelines.yml \u2014 build, verify, pack and release");
2062
- writeFileEnsured(
2214
+ writePipeline(
2063
2215
  (0, import_node_path3.join)(workspaceRoot, "azure-pipelines.yml"),
2216
+ onProgress,
2064
2217
  azurePipelinesYaml(
2065
2218
  options.agent,
2066
2219
  options.variableGroup,
@@ -2074,8 +2227,9 @@ function applyOverlay(workspaceRoot, options, onProgress = () => {
2074
2227
  }
2075
2228
  if (options.ci === "github" || options.ci === "both") {
2076
2229
  onProgress(".github/workflows/ci.yml and dependabot.yml");
2077
- writeFileEnsured(
2230
+ writePipeline(
2078
2231
  (0, import_node_path3.join)(workspaceRoot, ".github/workflows/ci.yml"),
2232
+ onProgress,
2079
2233
  githubActionsYaml(options.agent, publishUrl, options.registry.kind, options.ci, nugetUrl, nativeApps)
2080
2234
  );
2081
2235
  writeFileEnsured((0, import_node_path3.join)(workspaceRoot, ".github/dependabot.yml"), dependabotConfig(workspaceRoot));
@@ -7633,6 +7787,26 @@ function checkDeclarationSpecifiers(workspaceRoot) {
7633
7787
  ];
7634
7788
  });
7635
7789
  }
7790
+ function checkPipelinePhases(workspaceRoot) {
7791
+ const files = ["azure-pipelines.yml", ".github/workflows/ci.yml"];
7792
+ return files.filter((file) => fileExists((0, import_node_path31.join)(workspaceRoot, file))).flatMap((file) => {
7793
+ const { verifyActive, switchedOff } = inspectPipeline((0, import_node_fs25.readFileSync)((0, import_node_path31.join)(workspaceRoot, file), "utf8"));
7794
+ const verify = {
7795
+ check: `${file} runs the enforced verify phase`,
7796
+ ok: verifyActive,
7797
+ detail: `${file} has no active \`mnci ci verify\` call, so CI would build and release code nothing checked`,
7798
+ remedy: "run `mnci upgrade`, which restores it"
7799
+ };
7800
+ const warnings = switchedOff.map((phase) => ({
7801
+ check: `${file}: the ${phase} phase is switched off`,
7802
+ ok: true,
7803
+ warning: true,
7804
+ detail: `${file} does not run \`mnci ci ${phase}\``,
7805
+ remedy: `uncomment the ${phase} block in ${file} to run it again`
7806
+ }));
7807
+ return [verify, ...warnings];
7808
+ });
7809
+ }
7636
7810
  function collectFindings(workspaceRoot) {
7637
7811
  const nxJsonPath = (0, import_node_path31.join)(workspaceRoot, "nx.json");
7638
7812
  if (!fileExists(nxJsonPath)) {
@@ -7657,7 +7831,8 @@ function collectFindings(workspaceRoot) {
7657
7831
  checkSync(workspaceRoot),
7658
7832
  checkGoPluginRegistered(workspaceRoot),
7659
7833
  checkGoWorkInSync(workspaceRoot),
7660
- ...checkNativeApps(workspaceRoot)
7834
+ ...checkNativeApps(workspaceRoot),
7835
+ ...checkPipelinePhases(workspaceRoot)
7661
7836
  ].filter((finding) => finding !== void 0);
7662
7837
  }
7663
7838
  function runDoctor(workspaceRoot, options = {}) {
@@ -7671,7 +7846,12 @@ function runDoctor(workspaceRoot, options = {}) {
7671
7846
  return;
7672
7847
  }
7673
7848
  for (const finding of findings) {
7674
- if (finding.ok) {
7849
+ if (finding.ok && finding.warning === true) {
7850
+ logger.warn(`${finding.check} \u2014 ${finding.detail ?? ""}`);
7851
+ if (finding.remedy) {
7852
+ logger.info(` to change it: ${finding.remedy}`);
7853
+ }
7854
+ } else if (finding.ok) {
7675
7855
  logger.success(finding.check);
7676
7856
  } else {
7677
7857
  logger.error(`${finding.check} \u2014 ${finding.detail ?? "failed"}`);