@mnci/cli 4.4.0 → 4.6.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
@@ -313,8 +313,24 @@ function npmrcContent(registry, scope) {
313
313
  if (registry.kind === "npm") {
314
314
  return `; Publish authentication for the public npm registry.
315
315
  ;
316
- ; NODE_AUTH_TOKEN is exported by the generated CI's release step; an unset value
317
- ; is harmless locally, so no token is needed for day-to-day work.
316
+ ; NODE_AUTH_TOKEN is exported by the generated CI's release step. Nothing needs
317
+ ; it for day-to-day work here - installing, building and testing never
318
+ ; authenticate.
319
+ ;
320
+ ; It is NOT harmless the moment you DO want to authenticate from this directory,
321
+ ; which an earlier version of this comment claimed. A project .npmrc wins over
322
+ ; your user one, so with NODE_AUTH_TOKEN unset this line hands npm an empty
323
+ ; token and overrides 'npm login': inside this repo 'npm whoami' fails with a
324
+ ; 401 and any write is refused. The refusal is easy to misread, because for a
325
+ ; scoped package the registry answers a write you are not allowed to make with
326
+ ;
327
+ ; npm error 404 Not Found - PUT https://registry.npmjs.org/@scope%2fthing
328
+ ;
329
+ ; and everything above it is npm listing what it intended to send, so a long
330
+ ; healthy-looking run still changes nothing.
331
+ ;
332
+ ; To publish or deprecate by hand, run it from outside this directory, or export
333
+ ; a real token first.
318
334
  //registry.npmjs.org/:_authToken=\${NODE_AUTH_TOKEN}
319
335
 
320
336
  ; There is deliberately NO '${scope}:registry' line here. npmjs.org is already
@@ -415,6 +431,14 @@ function withSharedGlobals(nxJson) {
415
431
  namedInputs: { ...namedInputs, sharedGlobals: [...existing, ...missing] }
416
432
  };
417
433
  }
434
+ var PYTHON_GRAPH_PLUGIN = "@mnci/nx-python-pip/graph";
435
+ function withPythonGraphPlugin(nxJson, pluginInstalled) {
436
+ const plugins = nxJson.plugins ?? [];
437
+ if (!pluginInstalled || plugins.some((entry) => pluginName(entry) === PYTHON_GRAPH_PLUGIN)) {
438
+ return nxJson.plugins === void 0 ? nxJson : { ...nxJson, plugins };
439
+ }
440
+ return { ...nxJson, plugins: [...plugins, PYTHON_GRAPH_PLUGIN] };
441
+ }
418
442
  function withReleaseConfig(nxJson, ci) {
419
443
  return { ...nxJson, defaultBase: "main", release: releaseConfig(ci) };
420
444
  }
@@ -865,12 +889,27 @@ var NPM_AUDIT_STEP = `node -e "const cp=require('node:child_process');const r=cp
865
889
  var VERIFY_TARGETS = "lint,typecheck,test,build";
866
890
  var AFFECTED_OR_ALL_GUARD = `node -e "const cp=require('node:child_process');const T='${VERIFY_TARGETS}';const all=()=>process.exit(cp.spawnSync('npx nx run-many -t '+T,{stdio:'inherit',shell:true}).status ?? 1);const ref=process.env.GITHUB_BASE_REF||process.env.SYSTEM_PULLREQUEST_TARGETBRANCH||'';if(!ref){console.log('Not a pull request - verifying EVERY project.');all()}const target=ref.replace('refs/heads/','');const mergeBase=r=>{const o=cp.spawnSync('git',['merge-base',r,'HEAD'],{encoding:'utf8'});return o.status===0?o.stdout.trim():''};let base=mergeBase('origin/'+target);if(!base){console.log('No origin/'+target+' ref - fetching it to resolve a merge-base.');cp.spawnSync('git',['fetch','--no-tags','origin',target],{stdio:'inherit'});base=mergeBase('FETCH_HEAD')}if(!base){console.log('Could not resolve a merge-base with '+target+' - verifying EVERY project.');all()}console.log('Pull request against '+target+' - verifying projects affected since '+base);process.exit(cp.spawnSync('npx nx affected -t '+T+' --base='+base,{stdio:'inherit',shell:true}).status ?? 1)"`;
867
891
  var PACK_APPS_GUARD = `node -e "const fs=require('node:fs');fs.mkdirSync('dist/drop',{recursive:true});const hasProjectJson=fs.globSync('apps/*/project.json').length>0;const hasInlineNx=fs.globSync('apps/*/package.json').some((f)=>{try{return Boolean(JSON.parse(fs.readFileSync(f,'utf8')).nx)}catch{return false}});const hasCsproj=fs.globSync('apps/*/*.csproj').length>0;if(!hasProjectJson&&!hasInlineNx&&!hasCsproj){console.log('No apps to pack - skipping.');process.exit(0)}process.exit(require('node:child_process').spawnSync('npx nx run-many -t package',{stdio:'inherit',shell:true}).status ?? 1)"`;
892
+ var NPM_AUTH_PREFLIGHT = `node -e "const fs=require('node:fs'),cp=require('node:child_process');if(fs.globSync('packages/*/package.json').length===0){console.log('No npm packages to release - skipping.');process.exit(0)}if(!process.env.NODE_AUTH_TOKEN){console.error('NPM_TOKEN is empty or unset, so the publish would fail AFTER nx release has already tagged. Add it as a repository secret named exactly NPM_TOKEN - under Actions, not the Dependabot or Codespaces tab, and not an Environment secret. Use an npm Automation token - a Publish token is refused by 2FA in CI.');process.exit(1)}const r=cp.spawnSync('npm',['whoami','--registry=https://registry.npmjs.org/'],{encoding:'utf8',shell:process.platform==='win32'});if(r.status!==0){console.error('NPM_TOKEN is set but the registry rejected it - '+((r.stderr||'')+(r.stdout||'')).trim()+'. Check that it has not expired and that it grants publish rights on this scope.');process.exit(1)}console.log('npm auth OK as '+r.stdout.trim())"`;
893
+ function npmAuthPreflight(registryKind) {
894
+ return registryKind === "npm" ? NPM_AUTH_PREFLIGHT : "";
895
+ }
868
896
  var SHALLOW_CLONE_GUARD = `node -e "const r=require('node:child_process').spawnSync('git',['rev-parse','--is-shallow-repository'],{encoding:'utf8'});if(r.status!==0){console.error('Could not determine whether this checkout has full history (git rev-parse --is-shallow-repository failed) - refusing to release. '+(r.stderr||'').trim());process.exit(1)}if(r.stdout.trim()==='true'){console.error('This checkout is a shallow clone. nx release resolves the current version of each package from its git tag, and silently falls back to the permanently-stale on-disk version when a tag cannot be found - which can propose, and publish, a version DOWNGRADE. Fetch full history before releasing - set fetchDepth (Azure) or fetch-depth (GitHub) to 0.');process.exit(1)}"`;
869
897
  function releaseGuard(pythonPublishEnv, nugetPublishEnv) {
870
898
  return String.raw`node -e "const fs=require('node:fs'),cp=require('node:child_process');const npmCount=fs.globSync('packages/*/package.json').length;const csharpCount=fs.globSync('packages/*/*.csproj').length;const pythonCount=fs.globSync('python-packages/*/pyproject.toml').length;const dartCount=fs.globSync('packages/*/pubspec.yaml').length;const hasNpm=npmCount>0;const hasPython=pythonCount>0;const hasCsharp=csharpCount>0;const hasDart=dartCount>0;if(!hasNpm&&!hasPython&&!hasCsharp&&!hasDart){console.log('Nothing to release - skipping.');process.exit(0)}const specifier=process.env.RELEASE_SPECIFIER||'';let releaseCmd='npx nx release --yes';if(specifier){if(!/^(major|minor|patch|\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?)$/.test(specifier)){console.error('RELEASE_SPECIFIER value \''+specifier+'\' is invalid - use major, minor, patch, or an exact version like 1.2.3.');process.exit(1)}const releaseProjectCount=npmCount+csharpCount+pythonCount+dartCount;if(/^(major|minor|patch)$/.test(specifier)&&releaseProjectCount>1){console.error('RELEASE_SPECIFIER is a keyword (\''+specifier+'\') but this workspace has '+releaseProjectCount+' releasable packages - a keyword under-bumps interdependent packages, because nx computes the dependency-bump pass from a stale cached version. Set RELEASE_SPECIFIER to an exact version instead, or clear it.');process.exit(1)}releaseCmd='npx nx release '+specifier+' --yes'}const env={...process.env};${pythonPublishEnv}${nugetPublishEnv}process.exit(cp.spawnSync(releaseCmd,{stdio:'inherit',shell:true,env}).status ?? 1)"`;
871
899
  }
872
- function pythonPublishEnvFragment(pythonPublishUrl2) {
873
- return pythonPublishUrl2 ? `if(hasPython){env.TWINE_REPOSITORY_URL='${pythonPublishUrl2}';env.TWINE_USERNAME='AzureArtifacts';env.TWINE_PASSWORD=Buffer.from(process.env.PAT,'base64').toString()}` : "";
900
+ var PYPI_TOKEN_VARIABLE = "PYPI_TOKEN";
901
+ function pypiTokenEnvLine(registryKind, variableReference, indent) {
902
+ return registryKind === "npm" ? `
903
+ ${indent}${PYPI_TOKEN_VARIABLE}: ${variableReference(PYPI_TOKEN_VARIABLE)}` : "";
904
+ }
905
+ function pythonPublishEnvFragment(pythonPublishUrl2, registryKind) {
906
+ if (pythonPublishUrl2 !== void 0) {
907
+ return `if(hasPython){env.TWINE_REPOSITORY_URL='${pythonPublishUrl2}';env.TWINE_USERNAME='AzureArtifacts';env.TWINE_PASSWORD=Buffer.from(process.env.PAT,'base64').toString()}`;
908
+ }
909
+ if (registryKind !== "npm") {
910
+ return "";
911
+ }
912
+ return `if(hasPython){if(!process.env.${PYPI_TOKEN_VARIABLE}){console.error('This workspace has '+pythonCount+' Python package(s) to publish but ${PYPI_TOKEN_VARIABLE} is empty. Add a PyPI API token as a secret named ${PYPI_TOKEN_VARIABLE}, or remove the Python packages from release.projects.');process.exit(1)}env.TWINE_USERNAME='__token__';env.TWINE_PASSWORD=process.env.${PYPI_TOKEN_VARIABLE};env.TWINE_NON_INTERACTIVE='1'}`;
874
913
  }
875
914
  function nugetPublishEnvFragment(nugetFeedUrl2) {
876
915
  return nugetFeedUrl2 ? "if(hasCsharp){env.NUGET_PAT=Buffer.from(process.env.PAT,'base64').toString()}" : "";
@@ -1129,7 +1168,17 @@ steps:
1129
1168
  - script: ${SHALLOW_CLONE_GUARD}
1130
1169
  displayName: Verify this is a full checkout (nx release needs the real tag history)
1131
1170
  condition: ${onMain}
1132
-
1171
+ ${npmAuthPreflight(registryKind) ? `
1172
+ # Prove npm will accept us BEFORE anything irreversible happens. nx release
1173
+ # tags first and publishes last, so a rejected token leaves a version tagged
1174
+ # with nothing on the registry - and that number is then skipped forever,
1175
+ # because the next run resolves the current version from the newest tag.
1176
+ - script: ${npmAuthPreflight(registryKind)}
1177
+ displayName: Preflight \u2014 npm must accept the token before anything is tagged
1178
+ condition: ${onMain}
1179
+ env:
1180
+ NODE_AUTH_TOKEN: $(NPM_TOKEN)
1181
+ ` : ""}
1133
1182
  # Version + tag + publish, in one release, for npm (packages/*), Python
1134
1183
  # (python-packages/*) AND C# (packages/*/*.csproj) \u2014 conventional commits,
1135
1184
  # tag-only push. Portable guard: nx release errors on an empty scope, so
@@ -1148,11 +1197,11 @@ steps:
1148
1197
  # it whenever more than one package is releasable. The guard below fails
1149
1198
  # the run rather than under-bumping silently: clear the variable back to
1150
1199
  # '' once the override is no longer needed.
1151
- - script: ${releaseGuard(pythonPublishEnvFragment(pythonPublishUrl2), nugetPublishEnvFragment(nugetFeedUrl2))}
1200
+ - script: ${releaseGuard(pythonPublishEnvFragment(pythonPublishUrl2, registryKind), nugetPublishEnvFragment(nugetFeedUrl2))}
1152
1201
  displayName: Release \u2014 version, tag and publish (npm + Python + C#)
1153
1202
  condition: ${onMain}
1154
1203
  env:
1155
- ${npmAuthName}: ${npmAuthValue}
1204
+ ${npmAuthName}: ${npmAuthValue}${pypiTokenEnvLine(registryKind, (name) => `$(${name})`, " ".repeat(6))}
1156
1205
  RELEASE_SPECIFIER: $(RELEASE_SPECIFIER)
1157
1206
 
1158
1207
  # nx release's own git push (release.git.push) is deliberately left off: it
@@ -1355,7 +1404,18 @@ jobs:
1355
1404
  - run: ${SHALLOW_CLONE_GUARD}
1356
1405
  name: Verify this is a full checkout (nx release needs the real tag history)
1357
1406
  if: \${{ ${onMain} }}
1358
-
1407
+ ${npmAuthPreflight(registryKind) ? `
1408
+ # Prove npm will accept us BEFORE anything irreversible happens. nx
1409
+ # release tags first and publishes last, so a rejected token leaves a
1410
+ # version tagged with nothing on the registry - and that number is then
1411
+ # skipped forever, because the next run resolves the current version
1412
+ # from the newest tag.
1413
+ - run: ${npmAuthPreflight(registryKind)}
1414
+ name: Preflight \u2014 npm must accept the token before anything is tagged
1415
+ if: \${{ ${onMain} }}
1416
+ env:
1417
+ NODE_AUTH_TOKEN: \${{ secrets.NPM_TOKEN }}
1418
+ ` : ""}
1359
1419
  # Version + tag + publish, in one release, for npm (packages/*), Python
1360
1420
  # (python-packages/*) AND C# (packages/*/*.csproj) \u2014 conventional
1361
1421
  # commits, tag-only push. Portable guard: nx release errors on an empty
@@ -1381,11 +1441,11 @@ jobs:
1381
1441
  # which is why 'nx release' pushes the tag itself here \u2014 see
1382
1442
  # releaseConfig's remarks for why that is safe on this Nx version and
1383
1443
  # why every other provider combination keeps the explicit push step below.` : ""}
1384
- - run: ${releaseGuard(pythonPublishEnvFragment(pythonPublishUrl2), nugetPublishEnvFragment(nugetFeedUrl2))}
1444
+ - run: ${releaseGuard(pythonPublishEnvFragment(pythonPublishUrl2, registryKind), nugetPublishEnvFragment(nugetFeedUrl2))}
1385
1445
  name: Release \u2014 version, tag${githubReleases ? ", publish and GitHub Release" : " and publish"} (npm + Python + C#)
1386
1446
  if: \${{ ${onMain} }}
1387
1447
  env:
1388
- ${npmAuthName}: ${npmAuthValue}
1448
+ ${npmAuthName}: ${npmAuthValue}${pypiTokenEnvLine(registryKind, (name) => `\${{ secrets.${name} }}`, " ".repeat(10))}
1389
1449
  RELEASE_SPECIFIER: \${{ vars.RELEASE_SPECIFIER }}${githubReleases ? `
1390
1450
  GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}` : ""}
1391
1451
  ${githubReleases ? "" : `
@@ -1498,7 +1558,13 @@ function applyOverlay(workspaceRoot, options, onProgress = () => {
1498
1558
  };
1499
1559
  const sync = { ...nxJson.sync, ...SYNC_CONFIG };
1500
1560
  const mnci = { ...nxJson.mnci, ...mnciConfig(options) };
1501
- const patched = withSharedGlobals(withEslintPlugin(withReleaseConfig(nxJson, options.ci)));
1561
+ const rootManifest = readJson((0, import_node_path2.join)(workspaceRoot, "package.json"));
1562
+ const hasPythonPlugin = (rootManifest.devDependencies ?? {})["@mnci/nx-python-pip"] !== void 0 || (rootManifest.dependencies ?? {})["@mnci/nx-python-pip"] !== void 0;
1563
+ const withRelease = withReleaseConfig(nxJson, options.ci);
1564
+ const patched = withPythonGraphPlugin(
1565
+ withSharedGlobals(withEslintPlugin(withRelease)),
1566
+ hasPythonPlugin
1567
+ );
1502
1568
  onProgress("nx.json \u2014 release, sync, generators, shared inputs, mnci block");
1503
1569
  writeFileEnsured(nxJsonPath, toJson({ ...patched, generators, sync, mnci }));
1504
1570
  onProgress("package.json \u2014 root scripts, TS compilers, ESLint toolchain, overrides");
@@ -1892,14 +1958,22 @@ function pascalScope(scope) {
1892
1958
  var CSHARP_VERSION_ACTIONS_PATH = "tools/csharp-version-actions.cjs";
1893
1959
  var CSHARP_VERSION_ACTIONS = `'use strict'
1894
1960
 
1895
- const { join } = require('node:path')
1961
+ const { posix } = require('node:path')
1896
1962
  const { VersionActions } = require('nx/release')
1897
1963
 
1898
1964
  const VERSION_TAG = /<Version>([^<]*)<\\/Version>/
1899
1965
 
1966
+ /*
1967
+ * posix.join, never plain join. An Nx Tree path is workspace-relative and
1968
+ * forward-slashed on EVERY platform, while join emits a backslashed path on
1969
+ * Windows. This value is handed back to nx release for changelog and manifest
1970
+ * bookkeeping, and interpolated into the user-facing error below, so it was
1971
+ * wrong on every Windows machine. The same bug was already fixed in the
1972
+ * Python and Flutter plugins' release actions; this third copy was missed.
1973
+ */
1900
1974
  function findCsproj (tree, root) {
1901
1975
  const name = tree.children(root).find((entry) => entry.endsWith('.csproj'))
1902
- return name ? join(root, name) : null
1976
+ return name ? posix.join(root, name) : null
1903
1977
  }
1904
1978
 
1905
1979
  /**
@@ -2836,18 +2910,35 @@ function pythonPipPluginSpec() {
2836
2910
  return process.env.MNCI2_PYTHON_PIP_SPEC ?? "@mnci/nx-python-pip";
2837
2911
  }
2838
2912
  function ensurePythonPipPlugin(workspaceRoot) {
2839
- if (hasPlugin(workspaceRoot, "@mnci/nx-python-pip")) {
2913
+ if (!hasPlugin(workspaceRoot, "@mnci/nx-python-pip")) {
2914
+ const spec = pythonPipPluginSpec();
2915
+ logger.step(`Installing the Python toolchain plugin (${spec})`);
2916
+ if (runShell("npm", ["install", "--save-dev", spec, "--no-audit", "--no-fund"], workspaceRoot) !== 0) {
2917
+ throw new Error("npm install of @mnci/nx-python-pip failed");
2918
+ }
2919
+ }
2920
+ registerPythonGraphPlugin(workspaceRoot);
2921
+ }
2922
+ function registerPythonGraphPlugin(workspaceRoot) {
2923
+ const nxJsonPath = (0, import_node_path12.join)(workspaceRoot, "nx.json");
2924
+ if (!fileExists(nxJsonPath)) {
2840
2925
  return;
2841
2926
  }
2842
- const spec = pythonPipPluginSpec();
2843
- logger.step(`Installing the Python toolchain plugin (${spec})`);
2844
- if (runShell("npm", ["install", "--save-dev", spec, "--no-audit", "--no-fund"], workspaceRoot) !== 0) {
2845
- throw new Error("npm install of @mnci/nx-python-pip failed");
2927
+ const nxJson = readJson(nxJsonPath);
2928
+ const patched = withPythonGraphPlugin(nxJson, true);
2929
+ if (patched.plugins?.includes(PYTHON_GRAPH_PLUGIN) !== true) {
2930
+ return;
2931
+ }
2932
+ if (nxJson.plugins?.includes(PYTHON_GRAPH_PLUGIN) === true) {
2933
+ return;
2846
2934
  }
2935
+ logger.step(`Registering ${PYTHON_GRAPH_PLUGIN} so nx affected sees Python dependencies`);
2936
+ writeFileEnsured(nxJsonPath, toJson(patched));
2847
2937
  }
2848
2938
  var PYTHON_REQUIREMENTS_DEV = `build
2849
2939
  twine
2850
2940
  ruff
2941
+ mypy
2851
2942
  pytest
2852
2943
  pip-audit
2853
2944
  `;