@mnci/cli 4.8.2 → 4.10.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/README.md +34 -13
- package/dist/cli.js +171 -13
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -909,7 +909,9 @@ as `${{ secrets.PAT }}` — GitHub has no "variable group" concept, so unlike
|
|
|
909
909
|
Azure this needs no CLI-collected name, just a secret you create once in the
|
|
910
910
|
repo settings. Either way it's mapped as `env` on the npm steps and read by
|
|
911
911
|
the root `.npmrc`'s `_password` block — the PAT value never lands in a file.
|
|
912
|
-
No `npmAuthenticate@0` task (it would overwrite the hand-set password).
|
|
912
|
+
No `npmAuthenticate@0` task in this mode (it would overwrite the hand-set password).
|
|
913
|
+
For an Azure feed there is a second mode that needs no PAT at all — see
|
|
914
|
+
[the alternative on Azure](#the-alternative-on-azure-build-identity-auth-no-pat-at-all).
|
|
913
915
|
|
|
914
916
|
#### Why `_password` and never `_authToken` — read this before "fixing" it
|
|
915
917
|
|
|
@@ -945,18 +947,23 @@ credentials by URL prefix and walks only *up* a path, so an entry on
|
|
|
945
947
|
|
|
946
948
|
#### The alternative on Azure: build-identity auth, no PAT at all
|
|
947
949
|
|
|
948
|
-
|
|
949
|
-
in the **same organisation** as the pipeline
|
|
950
|
-
|
|
950
|
+
`--npm-auth build-identity` (on `mnci new` and `mnci upgrade`) is the better setup for
|
|
951
|
+
an Azure Artifacts feed in the **same organisation** as the pipeline.
|
|
952
|
+
`npmAuthenticate@0` injects the build service identity's token — which *is*
|
|
951
953
|
Entra-issued, so it satisfies the Bearer scheme the feed advertises — and there is no
|
|
952
954
|
secret to store, encode, rotate or let expire.
|
|
953
955
|
|
|
954
|
-
|
|
956
|
+
```bash
|
|
957
|
+
mnci upgrade --npm-auth build-identity # switch an existing workspace
|
|
958
|
+
mnci upgrade --npm-auth pat # and back
|
|
959
|
+
```
|
|
960
|
+
|
|
961
|
+
It changes two files together, which is the point of doing it through mnci rather than
|
|
962
|
+
by hand:
|
|
955
963
|
|
|
956
|
-
1.
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
2. Add this step **before** `npm ci` in `azure-pipelines.yml`:
|
|
964
|
+
1. `.npmrc` keeps only the `@scope:registry=` routing line. No credentials.
|
|
965
|
+
2. `azure-pipelines.yml` runs this step **before** `npm ci`, and `npm ci` no longer
|
|
966
|
+
gets a `PAT` mapping it would never read:
|
|
960
967
|
|
|
961
968
|
```yaml
|
|
962
969
|
- task: npmAuthenticate@0
|
|
@@ -965,6 +972,17 @@ To switch a generated workspace over by hand:
|
|
|
965
972
|
workingFile: .npmrc
|
|
966
973
|
```
|
|
967
974
|
|
|
975
|
+
The choice is persisted in `nx.json`'s `mnci` block, so a plain `mnci upgrade` keeps it.
|
|
976
|
+
A workspace that added the task by hand before this option existed needs no flag either:
|
|
977
|
+
`mnci upgrade` sees `npmAuthenticate@0` in its pipeline and keeps that mode, rather than
|
|
978
|
+
regenerating the pipeline without it and writing a PAT block the feed rejects. An explicit
|
|
979
|
+
`--npm-auth pat` always wins over that detection. `mnci doctor` fails a workspace whose
|
|
980
|
+
two halves disagree — a credential-free `.npmrc` with no task publishes nothing (installs
|
|
981
|
+
still pass, so CI stays green until the release step's 401), and a PAT block *plus* the
|
|
982
|
+
task appends a second credential to a file that already has one.
|
|
983
|
+
|
|
984
|
+
The remaining manual step is the grant:
|
|
985
|
+
|
|
968
986
|
3. Grant the build identity **Feed Publisher (Contributor)** on the feed (Artifacts →
|
|
969
987
|
Feed Settings → Permissions). Which identity depends on the job authorization
|
|
970
988
|
scope: `<Project> Build Service (<org>)` when scoped to the project (the default),
|
|
@@ -972,11 +990,14 @@ To switch a generated workspace over by hand:
|
|
|
972
990
|
401 is the tell that the wrong identity is in play.
|
|
973
991
|
|
|
974
992
|
Caveats, stated rather than glossed. This is **Azure-only** — GitHub Actions has no
|
|
975
|
-
equivalent task, so
|
|
993
|
+
equivalent task, so `--ci github` and `--ci both` are **refused** with `build-identity`
|
|
994
|
+
(the credential-free `.npmrc` would authenticate nowhere on the GitHub side), and so is
|
|
995
|
+
public npm, which has no identity to borrow. Those workspaces keep the PAT.
|
|
976
996
|
Local development then needs `npx vsts-npm-auth -config .npmrc` (Windows) or a
|
|
977
|
-
hand-added credential, since developers no longer inherit one from the file
|
|
978
|
-
|
|
979
|
-
|
|
997
|
+
hand-added credential, since developers no longer inherit one from the file — the
|
|
998
|
+
generated `.npmrc` says so in its own comments. Python and NuGet publishing are
|
|
999
|
+
unchanged: they read the raw PAT from the variable group, so that group is still needed
|
|
1000
|
+
for a workspace that publishes either.
|
|
980
1001
|
|
|
981
1002
|
#### Diagnosing an auth failure
|
|
982
1003
|
|
package/dist/cli.js
CHANGED
|
@@ -303,13 +303,34 @@ var TS_COMPILER_DEPENDENCIES = {
|
|
|
303
303
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
304
304
|
"typescript": "npm:@typescript/typescript6@^6.0.2"
|
|
305
305
|
};
|
|
306
|
+
var NPM_AUTH_MODES = ["pat", "build-identity"];
|
|
307
|
+
function resolveNpmAuth(requested, registry, ci, fallback) {
|
|
308
|
+
if (requested !== void 0 && !NPM_AUTH_MODES.includes(requested)) {
|
|
309
|
+
throw new Error(`--npm-auth must be one of: ${NPM_AUTH_MODES.join(", ")} (got '${requested}').`);
|
|
310
|
+
}
|
|
311
|
+
const mode = requested ?? fallback;
|
|
312
|
+
if (mode !== "build-identity") {
|
|
313
|
+
return mode;
|
|
314
|
+
}
|
|
315
|
+
if (registry.kind !== "azure-artifacts") {
|
|
316
|
+
throw new Error(
|
|
317
|
+
"--npm-auth build-identity needs an Azure Artifacts feed: public npm has no build identity to borrow. Use --registry azure-artifacts, or drop the flag."
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
if (ci !== "azure") {
|
|
321
|
+
throw new Error(
|
|
322
|
+
`--npm-auth build-identity needs --ci azure (this workspace is '${ci}'): it is npmAuthenticate@0, an Azure Pipelines task, and GitHub Actions has no equivalent. Use --npm-auth pat, or drop the GitHub pipeline.`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
return mode;
|
|
326
|
+
}
|
|
306
327
|
function registryUrl(registry) {
|
|
307
328
|
if (registry.kind === "azure-artifacts") {
|
|
308
329
|
return `https://pkgs.dev.azure.com/${registry.organization}/${registry.project}/_packaging/${registry.artifactsFeed}/npm/registry/`;
|
|
309
330
|
}
|
|
310
331
|
return void 0;
|
|
311
332
|
}
|
|
312
|
-
function npmrcContent(registry, scope) {
|
|
333
|
+
function npmrcContent(registry, scope, npmAuth = "pat") {
|
|
313
334
|
if (registry.kind === "npm") {
|
|
314
335
|
return `; Publish authentication for the public npm registry.
|
|
315
336
|
;
|
|
@@ -343,6 +364,38 @@ function npmrcContent(registry, scope) {
|
|
|
343
364
|
`;
|
|
344
365
|
}
|
|
345
366
|
const feedUrl = registryUrl(registry);
|
|
367
|
+
if (npmAuth === "build-identity") {
|
|
368
|
+
return `; Publish + resolution routing for this workspace's own scope.
|
|
369
|
+
;
|
|
370
|
+
; '${scope}:registry' sends BOTH resolution and 'npm publish' of ${scope}/* to the
|
|
371
|
+
; feed: npm prefers a scope's registry over the global one when publishing a
|
|
372
|
+
; scoped package, so a ${scope}/* package cannot reach npmjs.org by accident.
|
|
373
|
+
;
|
|
374
|
+
; Only the scope is routed, on purpose. A global 'registry=' would send every
|
|
375
|
+
; install through the feed as well, so 'npm ci' would need feed auth just to fetch
|
|
376
|
+
; public packages.
|
|
377
|
+
${scope}:registry=${feedUrl}
|
|
378
|
+
|
|
379
|
+
; NO credentials are written here. The 'npmAuthenticate@0' task in
|
|
380
|
+
; azure-pipelines.yml injects them into this file at build time, using the build
|
|
381
|
+
; service identity - so there is no PAT to store, rotate or encode.
|
|
382
|
+
;
|
|
383
|
+
; That is forced by what the feed accepts. An unauthenticated PUT to the publish
|
|
384
|
+
; endpoint answers with:
|
|
385
|
+
; www-authenticate: Bearer authorization_uri=https://login.windows.net/<tenant>,
|
|
386
|
+
; Basic realm="...", TFS-Federated
|
|
387
|
+
; so its Bearer scheme wants an ENTRA ID access token, which is what the task
|
|
388
|
+
; supplies. A PAT is not one: npm sends _authToken verbatim as a Bearer header, so
|
|
389
|
+
; a PAT there is rejected with "Unable to authenticate, your authentication token
|
|
390
|
+
; seems to be invalid". A PAT can only authenticate through the Basic scheme
|
|
391
|
+
; (username + base64 _password), which is what 'mnci upgrade --npm-auth pat' writes.
|
|
392
|
+
;
|
|
393
|
+
; The cost is local: nothing in this file authenticates a developer's machine, so
|
|
394
|
+
; publishing or installing a private package by hand needs
|
|
395
|
+
; 'npx vsts-npm-auth -config .npmrc' (Windows) or a credential you add yourself.
|
|
396
|
+
; Building and testing never authenticate.
|
|
397
|
+
`;
|
|
398
|
+
}
|
|
346
399
|
const feedKey = feedUrl.replace(/^https:/, "");
|
|
347
400
|
const feedShortKey = feedKey.replace("npm/registry/", "npm/");
|
|
348
401
|
return `; Publish + resolution routing for this workspace's own scope.
|
|
@@ -606,6 +659,8 @@ var ESLINT_BLOCK_INVENTORY = `// WHAT IS IN HERE. Each line is one config block,
|
|
|
606
659
|
// mnci/html @html-eslint/eslint-plugin
|
|
607
660
|
// mnci/tests *.spec/*.test relaxations \u2014 eslint-plugin-jest
|
|
608
661
|
// (Vitest's globals too; the two stacks share them)
|
|
662
|
+
// mnci/tests/mock-aware specs in a project: eslint-plugin-jest's unbound-method,
|
|
663
|
+
// which knows expect(mock.method) has no this to lose
|
|
609
664
|
// mnci/nx-dependency-checks @nx/eslint-plugin, on publishable packages' manifests
|
|
610
665
|
// mnci/standard JavaScript Standard Style as ESLint rules \u2014 the
|
|
611
666
|
// whole formatting opinion, a faithful port of
|
|
@@ -852,7 +907,10 @@ function mnciConfig(options) {
|
|
|
852
907
|
agent: options.agent,
|
|
853
908
|
variableGroup: options.variableGroup,
|
|
854
909
|
ci: options.ci,
|
|
855
|
-
stack: { testRunner: options.stack.testRunner }
|
|
910
|
+
stack: { testRunner: options.stack.testRunner },
|
|
911
|
+
// Only when chosen: a workspace on the default keeps the block it always had,
|
|
912
|
+
// so an upgrade does not add a field nobody asked for.
|
|
913
|
+
...options.npmAuth !== void 0 && { npmAuth: options.npmAuth }
|
|
856
914
|
};
|
|
857
915
|
}
|
|
858
916
|
function readMnciConfig(workspaceRoot) {
|
|
@@ -957,6 +1015,10 @@ var NPM_AUTH_PREFLIGHT = `node -e "const fs=require('node:fs'),cp=require('node:
|
|
|
957
1015
|
function npmAuthPreflight(registryKind) {
|
|
958
1016
|
return registryKind === "npm" ? NPM_AUTH_PREFLIGHT : "";
|
|
959
1017
|
}
|
|
1018
|
+
var PYPI_RELEASE_PREFLIGHT = String.raw`node -e "const fs=require('node:fs');const files=fs.globSync('python-packages/*/pyproject.toml');if(files.length===0){console.log('No Python packages to release - skipping.');process.exit(0)}if(!process.env.PYPI_TOKEN){console.error('PYPI_TOKEN is empty or unset, so the publish would fail AFTER nx release has already tagged. Add it as a repository secret named exactly PYPI_TOKEN - under Actions, not the Dependabot or Codespaces tab, and not an Environment secret.');process.exit(1)}if(!process.env.PYPI_TOKEN.startsWith('pypi-')){console.error('PYPI_TOKEN does not look like a PyPI API token - every one of them begins with pypi-. A password or a truncated paste is rejected only at upload time, which is AFTER nx release has tagged.');process.exit(1)}const names=files.map(f=>{const t=fs.readFileSync(f,'utf8');const b=t.slice(t.indexOf('[project]'));const l=b.split('\n').find(x=>x.trim().startsWith('name'));return l?l.split('=')[1].replace(/[^a-zA-Z0-9._-]/g,''):''}).filter(Boolean);(async()=>{const fresh=[];for(const n of names){try{const r=await fetch('https://pypi.org/pypi/'+n.toLowerCase().replace(/[-_.]+/g,'-')+'/json',{signal:AbortSignal.timeout(10000)});if(r.status===404)fresh.push(n)}catch{console.log('Could not reach PyPI to check '+n+' - continuing.')}}if(fresh.length===0){console.log('Every Python package already exists on PyPI - this release creates none.');return}console.log('NOTE - this release may CREATE '+fresh.length+' new PyPI project(s) - '+fresh.join(', ')+'. Project creation is rate limited per account, and a 429 there arrives AFTER nx release has tagged - leaving those versions tagged with nothing published, and skipped forever, because the next run resolves the current version from the newest tag. If that happens, delete the tags for the versions that did not publish before releasing again. PyPI offers no way to check the limit, or the token, in advance.')})()"`;
|
|
1019
|
+
function pypiReleasePreflight(registryKind) {
|
|
1020
|
+
return registryKind === "npm" ? PYPI_RELEASE_PREFLIGHT : "";
|
|
1021
|
+
}
|
|
960
1022
|
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)}"`;
|
|
961
1023
|
function releaseGuard(pythonPublishEnv, nugetPublishEnv) {
|
|
962
1024
|
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)"`;
|
|
@@ -988,9 +1050,24 @@ function npmAuthEnvVariable(registryKind, variableReference) {
|
|
|
988
1050
|
function poolBlock(agent) {
|
|
989
1051
|
return /^(?:ubuntu|windows|macos)-/i.test(agent) ? ` vmImage: ${agent}` : ` name: ${agent}`;
|
|
990
1052
|
}
|
|
991
|
-
function azurePipelinesYaml(agent, variableGroup, pythonPublishUrl2, registryKind = "azure-artifacts", nugetFeedUrl2) {
|
|
1053
|
+
function azurePipelinesYaml(agent, variableGroup, pythonPublishUrl2, registryKind = "azure-artifacts", nugetFeedUrl2, npmAuth = "pat") {
|
|
992
1054
|
const onMain = "and(succeeded(), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI'), eq(variables['Build.SourceBranchName'], 'main'))";
|
|
993
1055
|
const [npmAuthName, npmAuthValue] = npmAuthEnvVariable(registryKind, (name) => `$(${name})`);
|
|
1056
|
+
const buildIdentity = npmAuth === "build-identity" && registryKind === "azure-artifacts";
|
|
1057
|
+
const npmAuthenticateStep = buildIdentity ? ` # Injects feed credentials into .npmrc using the BUILD SERVICE IDENTITY, so no
|
|
1058
|
+
# PAT is stored, rotated or encoded anywhere. This is what the feed accepts: its
|
|
1059
|
+
# publish endpoint advertises a Bearer scheme bound to login.windows.net, which
|
|
1060
|
+
# wants an Entra ID access token, and a PAT is not one. Must run before 'npm ci'.
|
|
1061
|
+
# Needs the build identity to be a Feed Publisher (Contributor) on the feed.
|
|
1062
|
+
- task: npmAuthenticate@0
|
|
1063
|
+
displayName: Authenticate npm against the feed (build identity, no PAT)
|
|
1064
|
+
inputs:
|
|
1065
|
+
workingFile: .npmrc
|
|
1066
|
+
|
|
1067
|
+
` : "";
|
|
1068
|
+
const npmCiEnv = buildIdentity ? "" : `
|
|
1069
|
+
env:
|
|
1070
|
+
${npmAuthName}: ${npmAuthValue}`;
|
|
994
1071
|
return `name: monorepo-ci-$(Date:yyyyMMdd)$(Rev:.r)
|
|
995
1072
|
|
|
996
1073
|
# Generated by MoNecromanCI. Deliberately thin: Nx builds, 'nx release'
|
|
@@ -1107,10 +1184,8 @@ steps:
|
|
|
1107
1184
|
npm | "$(Agent.OS)"
|
|
1108
1185
|
path: $(npm_config_cache)
|
|
1109
1186
|
|
|
1110
|
-
- script: npm ci
|
|
1111
|
-
displayName: Install dependencies
|
|
1112
|
-
env:
|
|
1113
|
-
${npmAuthName}: ${npmAuthValue}
|
|
1187
|
+
${npmAuthenticateStep} - script: npm ci
|
|
1188
|
+
displayName: Install dependencies${npmCiEnv}
|
|
1114
1189
|
|
|
1115
1190
|
# Fails ONLY on an advisory that has a published fix, at moderate or above;
|
|
1116
1191
|
# anything upstream has not fixed is printed and passes. See NPM_AUDIT_STEP's
|
|
@@ -1246,6 +1321,16 @@ ${npmAuthPreflight(registryKind) ? `
|
|
|
1246
1321
|
condition: ${onMain}
|
|
1247
1322
|
env:
|
|
1248
1323
|
NODE_AUTH_TOKEN: $(NPM_TOKEN)
|
|
1324
|
+
` : ""}${pypiReleasePreflight(registryKind) ? `
|
|
1325
|
+
# PyPI cannot be asked whether a token is good - the only endpoint that
|
|
1326
|
+
# authenticates is the upload itself - so this checks the token's SHAPE and
|
|
1327
|
+
# names the projects this release would have to CREATE. Project creation is
|
|
1328
|
+
# rate limited per account, and a 429 arrives AFTER nx release has tagged.
|
|
1329
|
+
- script: ${pypiReleasePreflight(registryKind)}
|
|
1330
|
+
displayName: Preflight \u2014 name the PyPI projects this release would create
|
|
1331
|
+
condition: ${onMain}
|
|
1332
|
+
env:
|
|
1333
|
+
PYPI_TOKEN: $(PYPI_TOKEN)
|
|
1249
1334
|
` : ""}
|
|
1250
1335
|
# Version + tag + publish, in one release, for npm (packages/*), Python
|
|
1251
1336
|
# (python-packages/*) AND C# (packages/*/*.csproj) \u2014 conventional commits,
|
|
@@ -1483,6 +1568,17 @@ ${npmAuthPreflight(registryKind) ? `
|
|
|
1483
1568
|
if: \${{ ${onMain} }}
|
|
1484
1569
|
env:
|
|
1485
1570
|
NODE_AUTH_TOKEN: \${{ secrets.NPM_TOKEN }}
|
|
1571
|
+
` : ""}${pypiReleasePreflight(registryKind) ? `
|
|
1572
|
+
# PyPI cannot be asked whether a token is good - the only endpoint that
|
|
1573
|
+
# authenticates is the upload itself - so this checks the token's SHAPE
|
|
1574
|
+
# and names the projects this release would have to CREATE. Project
|
|
1575
|
+
# creation is rate limited per account, and a 429 arrives AFTER nx
|
|
1576
|
+
# release has tagged.
|
|
1577
|
+
- run: ${pypiReleasePreflight(registryKind)}
|
|
1578
|
+
name: Preflight \u2014 name the PyPI projects this release would create
|
|
1579
|
+
if: \${{ ${onMain} }}
|
|
1580
|
+
env:
|
|
1581
|
+
PYPI_TOKEN: \${{ secrets.PYPI_TOKEN }}
|
|
1486
1582
|
` : ""}
|
|
1487
1583
|
# Version + tag + publish, in one release, for npm (packages/*), Python
|
|
1488
1584
|
# (python-packages/*) AND C# (packages/*/*.csproj) \u2014 conventional
|
|
@@ -1772,7 +1868,10 @@ function applyOverlay(workspaceRoot, options, onProgress = () => {
|
|
|
1772
1868
|
onProgress(
|
|
1773
1869
|
`.npmrc \u2014 ${options.registry.kind === "azure-artifacts" ? "Azure Artifacts feed routing and credentials" : "public npm registry auth"}`
|
|
1774
1870
|
);
|
|
1775
|
-
writeFileEnsured(
|
|
1871
|
+
writeFileEnsured(
|
|
1872
|
+
(0, import_node_path2.join)(workspaceRoot, ".npmrc"),
|
|
1873
|
+
npmrcContent(options.registry, options.scope, options.npmAuth)
|
|
1874
|
+
);
|
|
1776
1875
|
onProgress("commitlint.config.mjs and .husky/commit-msg \u2014 conventional commit enforcement");
|
|
1777
1876
|
writeFileEnsured((0, import_node_path2.join)(workspaceRoot, "commitlint.config.mjs"), COMMITLINT_CONFIG);
|
|
1778
1877
|
const hookPath = (0, import_node_path2.join)(workspaceRoot, ".husky/commit-msg");
|
|
@@ -1815,7 +1914,14 @@ function applyOverlay(workspaceRoot, options, onProgress = () => {
|
|
|
1815
1914
|
onProgress("azure-pipelines.yml \u2014 build, verify, pack and release");
|
|
1816
1915
|
writeFileEnsured(
|
|
1817
1916
|
(0, import_node_path2.join)(workspaceRoot, "azure-pipelines.yml"),
|
|
1818
|
-
azurePipelinesYaml(
|
|
1917
|
+
azurePipelinesYaml(
|
|
1918
|
+
options.agent,
|
|
1919
|
+
options.variableGroup,
|
|
1920
|
+
publishUrl,
|
|
1921
|
+
options.registry.kind,
|
|
1922
|
+
nugetUrl,
|
|
1923
|
+
options.npmAuth
|
|
1924
|
+
)
|
|
1819
1925
|
);
|
|
1820
1926
|
}
|
|
1821
1927
|
if (options.ci === "github" || options.ci === "both") {
|
|
@@ -4768,6 +4874,37 @@ function checkNpmrcCredentialResolves(workspaceRoot, environment = process.env)
|
|
|
4768
4874
|
remedy: "nothing, for installing or building \u2014 this only affects authenticated commands. To publish or deprecate by hand, run it from outside this workspace, or export a real token first. If it still fails there, `npm whoami` \u2014 an expired token gives the same 404"
|
|
4769
4875
|
};
|
|
4770
4876
|
}
|
|
4877
|
+
function checkNpmAuthMatchesPipeline(workspaceRoot, registry) {
|
|
4878
|
+
const npmrcPath = (0, import_node_path19.join)(workspaceRoot, ".npmrc");
|
|
4879
|
+
const pipelinePath = (0, import_node_path19.join)(workspaceRoot, "azure-pipelines.yml");
|
|
4880
|
+
if (registry?.kind !== "azure-artifacts" || !fileExists(npmrcPath) || !fileExists(pipelinePath)) {
|
|
4881
|
+
return void 0;
|
|
4882
|
+
}
|
|
4883
|
+
const holdsCredentials = (0, import_node_fs12.readFileSync)(npmrcPath, "utf8").split("\n").filter((line) => !line.trimStart().startsWith(";") && !line.trimStart().startsWith("#")).some((line) => /(?:_authToken|_password|_auth)\s*=/u.test(line));
|
|
4884
|
+
const runsTask = (0, import_node_fs12.readFileSync)(pipelinePath, "utf8").includes("npmAuthenticate@0");
|
|
4885
|
+
if (!holdsCredentials && !runsTask) {
|
|
4886
|
+
return {
|
|
4887
|
+
check: "npm auth: .npmrc and azure-pipelines.yml agree",
|
|
4888
|
+
ok: false,
|
|
4889
|
+
detail: ".npmrc carries no credentials and azure-pipelines.yml does not run npmAuthenticate@0, so nothing authenticates the feed \u2014 installs still pass, and the release step fails with a 401",
|
|
4890
|
+
remedy: "run `mnci upgrade --npm-auth build-identity` to add the task, or `mnci upgrade --npm-auth pat` to write the PAT block back"
|
|
4891
|
+
};
|
|
4892
|
+
}
|
|
4893
|
+
if (holdsCredentials && runsTask) {
|
|
4894
|
+
return {
|
|
4895
|
+
check: "npm auth: .npmrc and azure-pipelines.yml agree",
|
|
4896
|
+
ok: false,
|
|
4897
|
+
detail: ".npmrc holds a credential block AND azure-pipelines.yml runs npmAuthenticate@0, which appends a second credential to a file that already has one",
|
|
4898
|
+
remedy: "run `mnci upgrade --npm-auth build-identity` to drop the PAT block, or `mnci upgrade --npm-auth pat` to drop the task"
|
|
4899
|
+
};
|
|
4900
|
+
}
|
|
4901
|
+
return {
|
|
4902
|
+
check: "npm auth: .npmrc and azure-pipelines.yml agree",
|
|
4903
|
+
ok: true,
|
|
4904
|
+
detail: "",
|
|
4905
|
+
remedy: ""
|
|
4906
|
+
};
|
|
4907
|
+
}
|
|
4771
4908
|
var NPM_TOKEN_PREFIX = "npm_";
|
|
4772
4909
|
var LEGACY_NPM_TOKEN = /^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu;
|
|
4773
4910
|
function npmjsCredentialShape(credentials, environment) {
|
|
@@ -4972,6 +5109,7 @@ function collectFindings(workspaceRoot) {
|
|
|
4972
5109
|
checkResolvedEslint(workspaceRoot),
|
|
4973
5110
|
checkNpmrc(workspaceRoot, nxJson.mnci?.registry, nxJson.mnci?.scope),
|
|
4974
5111
|
checkNpmrcCredentialResolves(workspaceRoot),
|
|
5112
|
+
checkNpmAuthMatchesPipeline(workspaceRoot, nxJson.mnci?.registry),
|
|
4975
5113
|
checkNoRootRuntimeDependencies(workspaceRoot),
|
|
4976
5114
|
...checkRollupSourceMaps(workspaceRoot),
|
|
4977
5115
|
...checkDeclarationSpecifiers(workspaceRoot),
|
|
@@ -5128,6 +5266,7 @@ async function runNew(name, options) {
|
|
|
5128
5266
|
"ubuntu-latest"
|
|
5129
5267
|
));
|
|
5130
5268
|
const variableGroup = ci === "github" ? options.variableGroup ?? "Build" : options.variableGroup ?? (options.yes ? "Build" : await promptText("Azure DevOps variable group holding the npm PAT", "Build"));
|
|
5269
|
+
const npmAuth = resolveNpmAuth(options.npmAuth, registry, ci);
|
|
5131
5270
|
const stack = await resolveStack(options);
|
|
5132
5271
|
const nxCloud = options.nxCloud ?? (options.yes ? false : await promptNxCloud());
|
|
5133
5272
|
const stagingParent = adoptTarget === void 0 ? process.cwd() : (0, import_node_fs14.mkdtempSync)((0, import_node_path21.join)((0, import_node_os.tmpdir)(), "mnci-new-"));
|
|
@@ -5158,7 +5297,7 @@ async function runNew(name, options) {
|
|
|
5158
5297
|
);
|
|
5159
5298
|
applyOverlay(
|
|
5160
5299
|
workspaceRoot,
|
|
5161
|
-
{ workspaceName, scope, registry, agent, variableGroup, ci, stack },
|
|
5300
|
+
{ workspaceName, scope, registry, agent, variableGroup, ci, stack, ...npmAuth && { npmAuth } },
|
|
5162
5301
|
logger.detail
|
|
5163
5302
|
);
|
|
5164
5303
|
(0, import_node_fs14.rmSync)((0, import_node_path21.join)(workspaceRoot, "node_modules"), { recursive: true, force: true });
|
|
@@ -5236,6 +5375,13 @@ function resolveWorkspaceName(workspaceRoot, persisted) {
|
|
|
5236
5375
|
}
|
|
5237
5376
|
return (0, import_node_path22.basename)(workspaceRoot);
|
|
5238
5377
|
}
|
|
5378
|
+
function detectNpmAuth(workspaceRoot) {
|
|
5379
|
+
try {
|
|
5380
|
+
return (0, import_node_fs15.readFileSync)((0, import_node_path22.join)(workspaceRoot, "azure-pipelines.yml"), "utf8").includes("npmAuthenticate@0") ? "build-identity" : void 0;
|
|
5381
|
+
} catch {
|
|
5382
|
+
return void 0;
|
|
5383
|
+
}
|
|
5384
|
+
}
|
|
5239
5385
|
function resolveOverlayOptions(workspaceRoot, options, persisted) {
|
|
5240
5386
|
const scope = options.scope ?? persisted.scope;
|
|
5241
5387
|
if (!scope) {
|
|
@@ -5255,6 +5401,11 @@ function resolveOverlayOptions(workspaceRoot, options, persisted) {
|
|
|
5255
5401
|
);
|
|
5256
5402
|
}
|
|
5257
5403
|
const variableGroup = options.variableGroup ?? persisted.variableGroup ?? "Build";
|
|
5404
|
+
const detected = ci === "azure" && persisted.npmAuth === void 0 && options.npmAuth === void 0 ? detectNpmAuth(workspaceRoot) : void 0;
|
|
5405
|
+
if (detected) {
|
|
5406
|
+
logger.detail("keeping build-identity npm auth: azure-pipelines.yml already runs npmAuthenticate@0");
|
|
5407
|
+
}
|
|
5408
|
+
const npmAuth = resolveNpmAuth(options.npmAuth, registry, ci, persisted.npmAuth ?? detected);
|
|
5258
5409
|
const testRunner = options.testRunner ?? persisted.stack?.testRunner;
|
|
5259
5410
|
if (!testRunner) {
|
|
5260
5411
|
throw new Error(
|
|
@@ -5268,7 +5419,8 @@ function resolveOverlayOptions(workspaceRoot, options, persisted) {
|
|
|
5268
5419
|
agent,
|
|
5269
5420
|
variableGroup,
|
|
5270
5421
|
ci,
|
|
5271
|
-
stack: { testRunner }
|
|
5422
|
+
stack: { testRunner },
|
|
5423
|
+
...npmAuth && { npmAuth }
|
|
5272
5424
|
};
|
|
5273
5425
|
}
|
|
5274
5426
|
function runUpgrade(workspaceRoot, options) {
|
|
@@ -5404,7 +5556,10 @@ function buildProgram(cliVersion) {
|
|
|
5404
5556
|
program.command("new").argument("[name]", "workspace name").description("Create a new monorepo (Nx TS preset + release/pipeline overlay)").option("-y, --yes", "accept defaults for anything not passed as a flag").option("--scope <scope>", "npm scope for publishable packages (e.g. @demo)").option("--registry <kind>", "azure-artifacts | npm").option("--organization <name>", "Azure DevOps organization").option("--project <name>", "Azure DevOps project").option("--artifacts-feed <name>", "Azure Artifacts feed").option(
|
|
5405
5557
|
"--agent <pool>",
|
|
5406
5558
|
"CI build agent: a vmImage (e.g. ubuntu-latest) or self-hosted pool name"
|
|
5407
|
-
).option("--variable-group <name>", "Azure DevOps variable group holding the npm PAT").option(
|
|
5559
|
+
).option("--variable-group <name>", "Azure DevOps variable group holding the npm PAT").option(
|
|
5560
|
+
"--npm-auth <mode>",
|
|
5561
|
+
"how npm authenticates to an Azure Artifacts feed: pat (default) | build-identity (npmAuthenticate@0, no PAT; --ci azure only)"
|
|
5562
|
+
).option("--ci <provider>", "CI provider: azure | github | both").option("--test-runner <runner>", "unit-test runner: jest | vitest").option("--nx-cloud", "connect the workspace to Nx Cloud (remote caching + CI insights)").option(
|
|
5408
5563
|
"--into <dir>",
|
|
5409
5564
|
"bootstrap into an existing directory (a fresh clone holding only .git); the directory name is the workspace name unless one is given"
|
|
5410
5565
|
).action(async (name, options) => {
|
|
@@ -5412,7 +5567,10 @@ function buildProgram(cliVersion) {
|
|
|
5412
5567
|
});
|
|
5413
5568
|
program.command("upgrade").description(
|
|
5414
5569
|
"Re-apply the latest MoNecromanCI overlay to this workspace (release config, pipeline, npmrc, commitlint, husky hook, curated scripts)"
|
|
5415
|
-
).option("--scope <scope>", "npm scope for publishable packages (overrides the persisted value)").option("--registry <kind>", "azure-artifacts | npm (overrides the persisted value)").option("--organization <name>", "Azure DevOps organization").option("--project <name>", "Azure DevOps project").option("--artifacts-feed <name>", "Azure Artifacts feed").option("--agent <pool>", "CI build agent (overrides the persisted value)").option("--variable-group <name>", "Azure DevOps variable group holding the npm PAT").option(
|
|
5570
|
+
).option("--scope <scope>", "npm scope for publishable packages (overrides the persisted value)").option("--registry <kind>", "azure-artifacts | npm (overrides the persisted value)").option("--organization <name>", "Azure DevOps organization").option("--project <name>", "Azure DevOps project").option("--artifacts-feed <name>", "Azure Artifacts feed").option("--agent <pool>", "CI build agent (overrides the persisted value)").option("--variable-group <name>", "Azure DevOps variable group holding the npm PAT").option(
|
|
5571
|
+
"--npm-auth <mode>",
|
|
5572
|
+
"how npm authenticates to an Azure Artifacts feed: pat (default) | build-identity (npmAuthenticate@0, no PAT; --ci azure only)"
|
|
5573
|
+
).option("--ci <provider>", "CI provider: azure | github | both (overrides the persisted value)").option(
|
|
5416
5574
|
"--test-runner <runner>",
|
|
5417
5575
|
"unit-test runner: jest | vitest (overrides the persisted value)"
|
|
5418
5576
|
).action((options) => {
|