@mnci/cli 1.13.0 → 1.14.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 CHANGED
@@ -126,7 +126,7 @@ that would just fail felt worse than being upfront that it doesn't exist yet.
126
126
  is ever pushed to `main`; future runs resolve versions from tag names.
127
127
  3. Writes `eslint.config.mjs` (three lines importing `@mnci/eslint-config` —
128
128
  the whole linting opinion, in one root config), `.prettierrc.json` +
129
- `.prettierignore`, `.npmrc` (comment-only — see **Known gaps** below),
129
+ `.prettierignore`, `.npmrc` (publish auth — see **Publish auth** below),
130
130
  `commitlint.config.mjs`, a husky `commit-msg` hook, the chosen CI provider's
131
131
  pipeline file(s)
132
132
  (`azure-pipelines.yml` and/or `.github/workflows/ci.yml`, `--ci`, default
@@ -404,6 +404,42 @@ lint,test,build`. Pushes to `main` then:
404
404
  _only_ configured provider, since that's the one case a `GITHUB_TOKEN` is
405
405
  guaranteed to exist.
406
406
 
407
+ ### Publish auth
408
+
409
+ The generated `.npmrc` differs by registry kind, because the honest answer does.
410
+
411
+ **`--registry azure-artifacts`** routes the workspace's own scope to the feed and
412
+ supplies the feed's credentials:
413
+
414
+ ```ini
415
+ @my:registry=https://pkgs.dev.azure.com/<org>/<proj>/_packaging/<feed>/npm/registry/
416
+ //pkgs.dev.azure.com/.../npm/registry/:username=AzureArtifacts
417
+ //pkgs.dev.azure.com/.../npm/registry/:_password=${PAT}
418
+ ```
419
+
420
+ Scope routing is **real protection** here, not decoration: npm prefers a scope's
421
+ registry over the global one when publishing a scoped package, so `@my/*` cannot
422
+ reach npmjs.org by accident. Verified against a real registry — with only the
423
+ scope line set, npm reports `Publishing to <feed>` — and again from a generated
424
+ workspace, whose `npm publish --dry-run` targets the feed.
425
+
426
+ Only the scope is routed, deliberately. A global `registry=` would push every
427
+ install through the feed as well, so `npm ci` would need feed auth just to fetch
428
+ public packages; as generated, public dependencies still come from npmjs.org and
429
+ a developer with no `PAT` set can install normally.
430
+
431
+ **`--registry npm`** gets the auth line and nothing else:
432
+
433
+ ```ini
434
+ //registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}
435
+ ```
436
+
437
+ There is no `@scope:registry` line, and the generated file explains why: npmjs.org
438
+ is already the default, so routing the scope there changes nothing — and calling
439
+ it protection against an accidental public publish would be false, because the
440
+ public registry is the intended target. Worth stating plainly because this file
441
+ previously claimed exactly that protection while emitting no routing line at all.
442
+
407
443
  **npm auth** is the base64 `PAT`, read the same way on both providers but from
408
444
  a different place: on Azure Pipelines, a **variable group**
409
445
  (`--variable-group`, default `Build`) exposes it as `$(PAT)`; on GitHub
@@ -516,13 +552,10 @@ than a surprise:
516
552
 
517
553
  ## Known gaps (accepted for the experiment)
518
554
 
519
- - **The generated `.npmrc` is empty (comments only), so `npm publish` will not
520
- authenticate.** This is a deliberate deferral, not an oversight: the previous
521
- file claimed here that scope routing made accidental public publishes
522
- impossible, when in fact no `@scope:registry` line was ever emitted. Rather
523
- than ship half-wired auth, the CI token export (`NODE_AUTH_TOKEN`/`PAT`) is
524
- left in place so wiring it later is one line, and the generated `.npmrc`
525
- carries a comment saying so. Publish-path e2e assertions are relaxed to match.
555
+ - **A scoped package on public npm has no accidental-publish protection**, and
556
+ cannot: npmjs.org is the intended target there, so no `.npmrc` line could
557
+ prevent it. Generate with `--registry azure-artifacts` if you need a scope kept
558
+ off the public registry — that variant routes it (see **Publish auth** above).
526
559
  - No `doctor`/`resurrect`/`spell` — out of scope until the model is proven.
527
560
  - **Flutter apps build for web only.** Android would require the Android SDK and
528
561
  NDK on every build agent; iOS is impossible on a Linux agent regardless. Add
package/dist/cli.js CHANGED
@@ -1250,15 +1250,49 @@ var TS_COMPILER_DEPENDENCIES = {
1250
1250
  "@typescript/native": "npm:typescript@^7.0.2",
1251
1251
  typescript: "npm:@typescript/typescript6@^6.0.2"
1252
1252
  };
1253
- function npmrcContent(_registry, _scope) {
1254
- return `; Intentionally empty.
1253
+ function registryUrl(registry) {
1254
+ if (registry.kind === "azure-artifacts") {
1255
+ return `https://pkgs.dev.azure.com/${registry.organization}/${registry.project}/_packaging/${registry.artifactsFeed}/npm/registry/`;
1256
+ }
1257
+ return void 0;
1258
+ }
1259
+ function npmrcContent(registry, scope) {
1260
+ if (registry.kind === "npm") {
1261
+ return `; Publish authentication for the public npm registry.
1255
1262
  ;
1256
- ; Publish authentication is not configured yet. The generated CI exports a
1257
- ; token for the release step (NODE_AUTH_TOKEN for public npm, PAT for Azure
1258
- ; Artifacts), but nothing here consumes it, so 'npm publish' will not
1259
- ; authenticate until this is wired up.
1263
+ ; NODE_AUTH_TOKEN is exported by the generated CI's release step; an unset value
1264
+ ; is harmless locally, so no token is needed for day-to-day work.
1265
+ //registry.npmjs.org/:_authToken=\${NODE_AUTH_TOKEN}
1266
+
1267
+ ; There is deliberately NO '${scope}:registry' line here. npmjs.org is already
1268
+ ; the default registry, so routing the scope to it would change nothing \u2014 and
1269
+ ; presenting that as protection against an accidental public publish would be
1270
+ ; false, because the public registry is the intended target. (An earlier version
1271
+ ; of this file made exactly that claim while emitting no routing line at all.)
1272
+ ; To keep a scoped package off npmjs.org, use a private feed: generate with
1273
+ ; --registry azure-artifacts, which does route the scope.
1274
+ `;
1275
+ }
1276
+ const feedUrl = registryUrl(registry);
1277
+ const feedKey = feedUrl.replace(/^https:/, "");
1278
+ return `; Publish + resolution routing for this workspace's own scope.
1260
1279
  ;
1261
- ; See "Known gaps" in the @mnci/cli README.
1280
+ ; '${scope}:registry' sends BOTH resolution and 'npm publish' of ${scope}/* to the
1281
+ ; feed: npm prefers a scope's registry over the global one when publishing a
1282
+ ; scoped package, so a ${scope}/* package cannot reach npmjs.org by accident.
1283
+ ;
1284
+ ; Only the scope is routed, on purpose. A global 'registry=' would send every
1285
+ ; install through the feed as well, so 'npm ci' would need feed auth just to fetch
1286
+ ; public packages.
1287
+ ${scope}:registry=${feedUrl}
1288
+
1289
+ ; Feed credentials. PAT is the BASE64 value Azure Artifacts' "Connect to feed"
1290
+ ; instructions give you, which is exactly what _password expects, so it is used
1291
+ ; as-is. (twine wants the RAW token; the CI release step decodes it there.)
1292
+ ; Azure ignores the username, and npm requires an email it never uses.
1293
+ ${feedKey}:username=AzureArtifacts
1294
+ ${feedKey}:_password=\${PAT}
1295
+ ${feedKey}:email=npm-requires-this-and-never-uses-it
1262
1296
  `;
1263
1297
  }
1264
1298
  function releaseConfig(ci) {
@@ -1502,9 +1536,18 @@ function azurePipelinesYaml(agent, variableGroup, pythonPublishUrl2, registryKin
1502
1536
  # runs unchanged on Linux, macOS and Windows agents.
1503
1537
 
1504
1538
  trigger:
1539
+ # While a run is in progress, queue further pushes and run them together rather
1540
+ # than concurrently. This is the closest thing Azure Pipelines YAML has to
1541
+ # GitHub's concurrency group, and it is load-bearing on main for the same
1542
+ # reason: two concurrent 'nx release' runs would race to create the same tag.
1543
+ batch: true
1505
1544
  branches:
1506
1545
  include: [main]
1507
1546
 
1547
+ # Note: cancelling a superseded PR validation run is NOT a YAML setting on Azure
1548
+ # \u2014 it lives in the branch policy ("Build validation -> automatically cancel").
1549
+ # Nothing here can express it, so it is left to whoever configures the policy
1550
+ # rather than faked with a batch setting that would only delay PR runs.
1508
1551
  pr:
1509
1552
  branches:
1510
1553
  include: [main]
@@ -1517,6 +1560,12 @@ variables:
1517
1560
  # for an Azure Artifacts feed, or a raw npm automation token as \`NPM_TOKEN\`
1518
1561
  # for public npm. Mark it secret in Library. Add app build vars here too if needed.
1519
1562
  - group: ${variableGroup}
1563
+ # Relocates npm's cache inside the pipeline workspace so Cache@2 can restore it
1564
+ # (the default ~/.npm is outside the cacheable area on hosted agents). npm reads
1565
+ # this as an ordinary env var, so it needs no config file and works on every
1566
+ # agent OS.
1567
+ - name: npm_config_cache
1568
+ value: $(Pipeline.Workspace)/.npm
1520
1569
 
1521
1570
  steps:
1522
1571
  - checkout: self
@@ -1541,6 +1590,20 @@ steps:
1541
1590
  inputs:
1542
1591
  version: 24.x
1543
1592
 
1593
+ # The Azure counterpart of \`cache: npm\` on actions/setup-node: restores npm's
1594
+ # download cache so \`npm ci\` does not re-fetch every tarball on every run.
1595
+ # Keyed on the lockfile, so a dependency change misses and anything else hits;
1596
+ # restoreKeys falls back to the newest cache for this OS on a miss, which still
1597
+ # avoids a fully cold install. Agent.OS is in the key because a cached native
1598
+ # module built for one OS is not reusable on another.
1599
+ - task: Cache@2
1600
+ displayName: Cache npm packages
1601
+ inputs:
1602
+ key: 'npm | "$(Agent.OS)" | package-lock.json'
1603
+ restoreKeys: |
1604
+ npm | "$(Agent.OS)"
1605
+ path: $(npm_config_cache)
1606
+
1544
1607
  - script: npm ci
1545
1608
  displayName: Install dependencies
1546
1609
  env:
@@ -1698,6 +1761,19 @@ on:
1698
1761
  pull_request:
1699
1762
  branches: [main]
1700
1763
 
1764
+ concurrency:
1765
+ # One run at a time per ref, but \`cancel-in-progress\` is an **expression** on
1766
+ # purpose rather than a flat \`true\`.
1767
+ #
1768
+ # A superseded PR run is pure waste, so cancel it. A run on \`main\` must never
1769
+ # be cancelled: it publishes packages and pushes release tags, and killing it
1770
+ # part-way can leave a tag pushed with the publish only half done \u2014 a state no
1771
+ # rerun repairs cleanly, because the version is then already tagged. Those runs
1772
+ # queue instead, which also stops two \`nx release\` invocations racing to create
1773
+ # the same tag when two commits land close together.
1774
+ group: \${{ github.workflow }}-\${{ github.ref }}
1775
+ cancel-in-progress: \${{ github.event_name == 'pull_request' }}
1776
+
1701
1777
  permissions:
1702
1778
  # Lets the release step push the version tag nx release creates back to main,
1703
1779
  # and (github-only provider) create the GitHub Release itself.
@@ -1721,6 +1797,11 @@ jobs:
1721
1797
  - uses: actions/setup-node@v4
1722
1798
  with:
1723
1799
  node-version: 24
1800
+ # Caches ~/.npm keyed on package-lock.json, so \`npm ci\` restores from
1801
+ # the local cache instead of re-downloading every tarball on every run.
1802
+ # Nothing to invalidate by hand: the action keys on the lockfile, so a
1803
+ # dependency change misses the cache and a no-op change hits it.
1804
+ cache: npm
1724
1805
 
1725
1806
  - run: npm ci
1726
1807
  name: Install dependencies