@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 +184 -4
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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"}`);
|