@mnci/cli 1.6.0 → 1.8.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
@@ -15,20 +15,21 @@ config package, a custom CI engine, a dependency-injection step for published
15
15
  packages, a doctor/drift-sync system to keep it all consistent — already has a
16
16
  first-party (or established community) Nx equivalent:
17
17
 
18
- | Hand-rolled elsewhere | This CLI uses instead |
19
- | ------------------------------------------------ | ----------------------------------------------------------- |
20
- | Template engine + per-project config files | `create-nx-workspace --preset=ts` + `nx g` plugin generators |
21
- | A shared toolchain package for configs | The configs the Nx generators emit (one root ESLint/tsconfig) |
22
- | A custom multi-step CI engine | `nx affected -t lint,test,build` + `nx release` (~60-line pipeline) |
23
- | A dist-package dependency injector | `nx release` updates dependent versions natively |
24
- | Hand-written Azure Function templates | `@nx/node:application` (plain Node app) + a thin Azure Functions v4 overlay |
25
- | doctor/drift sync of tool-owned files | Nothing to drift: this CLI owns 5 small files, Nx owns the rest |
18
+ | Hand-rolled elsewhere | This CLI uses instead |
19
+ | ------------------------------------------ | --------------------------------------------------------------------------- |
20
+ | Template engine + per-project config files | `create-nx-workspace --preset=ts` + `nx g` plugin generators |
21
+ | A shared toolchain package for configs | The configs the Nx generators emit (one root ESLint/tsconfig) |
22
+ | A custom multi-step CI engine | `nx affected -t lint,test,build` + `nx release` (~60-line pipeline) |
23
+ | A dist-package dependency injector | `nx release` updates dependent versions natively |
24
+ | Hand-written Azure Function templates | `@nx/node:application` (plain Node app) + a thin Azure Functions v4 overlay |
25
+ | doctor/drift sync of tool-owned files | Nothing to drift: this CLI owns 5 small files, Nx owns the rest |
26
26
 
27
27
  ## Commands (deliberately just three)
28
28
 
29
29
  ```sh
30
30
  mnci new my-repo # create a monorepo (prompts scope + registry)
31
31
  mnci new my-repo --yes --registry npm --scope @my
32
+ mnci new my-repo --yes --registry npm --scope @my --nx-cloud # opt in to Nx Cloud
32
33
 
33
34
  cd my-repo
34
35
  mnci add react-app web # @nx/react (Vite + Jest)
@@ -43,6 +44,13 @@ mnci add python-app svc # app -> apps/ (wheel, zipped into the drop)
43
44
  mnci add python-function-app fn # Azure Functions (Python v2) -> apps/
44
45
  mnci add python-lib shared # publishable -> python-packages/ (twine upload)
45
46
  mnci add python-internal-lib core # private shared lib -> libs/
47
+ mnci add python-vendor shared --lib core # wire core's module into shared's built wheel
48
+
49
+ # Go (@nx-go/nx-go — one root go.mod, golangci-lint + go test)
50
+ mnci add go-app api # executable -> apps/ (binary, zipped into the drop)
51
+ mnci add go-function-app fn # serverless handler -> apps/
52
+ mnci add go-lib core # publishable (by git tag) -> packages/
53
+ mnci add go-internal-lib util # private shared package -> libs/
46
54
 
47
55
  mnci upgrade # re-apply the latest overlay (see below)
48
56
  mnci upgrade --agent windows-latest # ...with an explicit override
@@ -51,15 +59,16 @@ mnci upgrade --agent windows-latest # ...with an explicit override
51
59
  Everything else is plain Nx, surfaced as a small curated set of root scripts —
52
60
  each a single cross-platform command:
53
61
 
54
- | Script | Runs |
55
- | --------------------- | ------------------------------- |
56
- | `npm run build` | `nx run-many -t build` |
57
- | `npm run lint` | `nx run-many -t lint` |
58
- | `npm run test` | `nx run-many -t test` |
59
- | `npm run affected` | `nx affected -t lint,test,build` (vs `main`) |
60
- | `npm run graph` | `nx graph` |
61
- | `npm run release:preview` | `nx release --dry-run` |
62
- | `prepare` | `husky` (commit-msg lint hook) |
62
+ | Script | Runs |
63
+ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
64
+ | `npm run build` | `nx run-many -t build` |
65
+ | `npm run lint` | `nx run-many -t lint` |
66
+ | `npm run test` | `nx run-many -t test` |
67
+ | `npm run affected` | `nx affected -t lint,test,build` (vs `main`) |
68
+ | `npm run graph` | `nx graph` |
69
+ | `npm run release:preview` | `nx release --dry-run` |
70
+ | `npm run python:install` | fixed Python toolchain (ruff/pytest/build/twine) + editable-install every Python project — the same two guards CI runs, for local dev |
71
+ | `prepare` | `husky` (commit-msg lint hook) |
63
72
 
64
73
  ## What `new` actually does
65
74
 
@@ -75,14 +84,17 @@ each a single cross-platform command:
75
84
  `commit-msg` hook, the chosen CI provider's pipeline file(s)
76
85
  (`azure-pipelines.yml` and/or `.github/workflows/ci.yml`, `--ci`, default
77
86
  `azure`; `github`/`both` also gets `.github/dependabot.yml` — weekly
78
- dependency-update PRs), and the curated root scripts.
87
+ dependency-update PRs), a `<workspace-name>.code-workspace` file (VS Code
88
+ workspace configuration with folder structure, ESLint/Prettier settings, and
89
+ recommended extensions — open it in VS Code via `File > Open Workspace from
90
+ File`), and the curated root scripts.
79
91
  4. Installs the chosen **stack** (see below), `husky` + `@commitlint/*` for
80
92
  real, so versions resolve at generation time.
81
93
 
82
94
  ## `mnci upgrade`: re-applying the overlay to an existing workspace
83
95
 
84
96
  Every fix to `overlay.ts` — a release-config correction, a CI guard rewritten,
85
- a new Windows code path — only ever reached *future* `mnci new` calls until
97
+ a new Windows code path — only ever reached _future_ `mnci new` calls until
86
98
  this existed; nothing let an already-generated workspace pick one up.
87
99
  `mnci upgrade`, run from the workspace root, closes that gap: it resolves the
88
100
  same options `new` would have and calls the exact same `applyOverlay` `new`
@@ -117,16 +129,23 @@ will overwrite hand customizations to any of the files it owns (e.g. an extra
117
129
  CI job appended by hand to the pipeline file) — `git diff` is exactly how
118
130
  you'd notice and re-apply those on top.
119
131
 
120
- ## Stack: two choices asked up front
132
+ ## Stack: one choice asked up front
133
+
134
+ `mnci new` (run bare, or with flags) asks one question — the test runner. It is
135
+ stored where every later `mnci add` honours it, so the whole workspace stays one
136
+ stack:
121
137
 
122
- `mnci new` (run bare, or with flags) asks two questions — the linter and the
123
- test runner. Each is stored where every later `mnci add` honours it, so the
124
- whole workspace stays one stack:
138
+ | Question | Options | Default | Stored as / honoured via |
139
+ | --------------- | ------------------ | ------- | ---------------------------------------------------------------------------------------- |
140
+ | `--test-runner` | `jest` \| `vitest` | `jest` | `nx.json` generator `unitTestRunner` default; the hand-built function app follows it too |
125
141
 
126
- | Question | Options | Default | Stored as / honoured via |
127
- | -------------- | ------------------ | ------- | ------------------------ |
128
- | `--linter` | `eslint` \| `oxlint` | `eslint` | `nx.json` generator `linter` default (`none` for oxlint) + a typed `oxlint.config.mts` + `oxfmt.config.mts` (both from the [oxc-standard](https://github.com/JohnDeved/ox-standard) preset) + the `oxlint` / `oxfmt` root scripts |
129
- | `--test-runner`| `jest` \| `vitest` | `jest` | `nx.json` generator `unitTestRunner` default; the hand-built function app follows it too |
142
+ **Linting and formatting are unified across the workspace**: every project uses
143
+ **ESLint + Prettier** with the JavaScript Standard Style configuration (no
144
+ semicolons, single quotes, 2-space indents). ESLint is a per-project Nx target
145
+ handling code quality (correctness only), while Prettier handles all formatting.
146
+ Both are automatically configured; `npm run lint` checks code quality, `npm run
147
+ format` (write) and `npm run format:check` (CI-safe) handle formatting. The CI
148
+ runs lint/test/build only — formatting is left as a local/pre-commit step.
130
149
 
131
150
  TypeScript is not a question — every workspace runs the **dual compiler** from
132
151
  [Nx's TS 7 guide](https://nx.dev/docs/technologies/typescript/guides/typescript-7):
@@ -138,35 +157,17 @@ analysing config through the TS 6 API — no target rewiring, frozen per repo by
138
157
  the lockfile. (A plain `typescript@7` install would break Nx, since TS 7 ships
139
158
  no programmatic API yet; the two aliases are what make it work.)
140
159
 
141
- - **Linter**: ESLint is a per-project Nx target; oxlint is a single
142
- workspace-wide binary. Either way `npm run lint` (and the CI) is
143
- linter-agnostic, so nothing downstream branches. Under oxlint a publishable
144
- package's private-lib import needs no dependency-check override (that rule is
145
- ESLint-only), so none is written.
146
- - The oxlint stack installs **[oxc-standard](https://github.com/JohnDeved/ox-standard)**
147
- (which brings `oxlint` + `oxfmt`) and generates two `.mts` configs that use
148
- its **JavaScript Standard Style** preset: `oxlint.config.mts` *extends*
149
- `oxc-standard`'s rule set (unicorn + React + react-perf + TypeScript + oxc),
150
- and `oxfmt.config.mts` mirrors its formatting (no semicolons, single quotes,
151
- 2-space, `es5` trailing commas, avoid arrow parens). That gives the full
152
- Standard experience — **linting *and* formatting** — since oxlint (a linter)
153
- does not enforce layout.
154
- - Formatting is `npm run format` (write) and `npm run format:check` (CI-safe,
155
- no writes). Nx generators emit semicolon/double-quote code, so run
156
- `npm run format` once after scaffolding to normalise a new workspace to
157
- Standard Style. (CI stays lint/test/build only — formatting is left as a
158
- local/pre-commit step so the pipeline needs no linter-specific branch.)
159
160
  - **Test runner**: passed straight to the `@nx/*` generators; the function app
160
161
  gets a matching `jest.config.mjs` (+ ts-jest) or `vitest.config.ts`.
161
162
 
162
163
  ## Layout convention = release scoping
163
164
 
164
- | Directory | Contents | Released? |
165
- | ------------------ | ------------------------------------------------- | ---------------------------- |
166
- | `apps/` | React / Node / Python apps (plain or Azure Functions) | Never (packed into the drop) |
167
- | `packages/` | Publishable npm libraries (rollup-bundled) | Yes — `nx release`, per-package tags |
168
- | `python-packages/` | Publishable Python packages (hatchling wheels) | Yes — `twine upload` (Azure Artifacts) |
169
- | `libs/` | Internal libraries (TS or Python), never published | Never |
165
+ | Directory | Contents | Released? |
166
+ | ------------------ | ----------------------------------------------------- | -------------------------------------- |
167
+ | `apps/` | React / Node / Python apps (plain or Azure Functions) | Never (packed into the drop) |
168
+ | `packages/` | Publishable npm libraries (rollup-bundled) | Yes — `nx release`, per-package tags |
169
+ | `python-packages/` | Publishable Python packages (hatchling wheels) | Yes — `twine upload` (Azure Artifacts) |
170
+ | `libs/` | Internal libraries (TS or Python), never published | Never |
170
171
 
171
172
  No custom tags, no stamp file — the directory (and, for npm, the `private` flag)
172
173
  are the whole model. Publishable Python packages get their own
@@ -220,13 +221,13 @@ file to add a new cross-project import** later (nothing about that is an
220
221
  `mnci add`, so that step can't catch it). For that case every generated
221
222
  workspace sets `sync.applyChanges: true` in `nx.json`: `--preset=ts` already
222
223
  registers the `@nx/js:typescript-sync` generator on the `build`/`typecheck`
223
- targets, so instead of just *prompting* ("Would you like to sync the
224
+ targets, so instead of just _prompting_ ("Would you like to sync the
224
225
  identified changes?") on your next `nx build`/`typecheck`/`affected`, Nx fixes
225
226
  the references **automatically** — no prompt, no manual `npx nx sync`. A
226
227
  brand-new package may still need one VSCode window reload to be picked up by
227
228
  the TypeScript server.
228
229
 
229
- `applyChanges` only affects *interactive* runs, by design: CI always runs sync
230
+ `applyChanges` only affects _interactive_ runs, by design: CI always runs sync
230
231
  generators in dry-run mode and fails instead of silently patching an ephemeral
231
232
  checkout that never gets committed. That's what the pipeline's `nx sync:check`
232
233
  step (below) surfaces early — if it fails, run `npx nx sync` locally and
@@ -258,7 +259,7 @@ Every run (PR and main) installs dependencies, then runs `npm audit`
258
259
  (also non-blocking) — visibility, not enforcement: verified empirically that
259
260
  a real `npm audit` on this monorepo's own tree flagged nothing but
260
261
  already-latest upstream packages (`nx`, `verdaccio`) bundling their own not-
261
- yet-patched transitive dependencies, nothing an edit to *this* workspace's
262
+ yet-patched transitive dependencies, nothing an edit to _this_ workspace's
262
263
  manifest could fix. A hard-failing audit step would turn CI red for a
263
264
  problem with no user-actionable fix, for as long as upstream took to patch
264
265
  it — so both steps always exit 0 regardless of findings, surfacing results
@@ -276,7 +277,7 @@ lint,test,build`. Pushes to `main` then:
276
277
  `dist/drop/<type>-<name>.zip` (e.g. `node-function-app-api.zip`,
277
278
  `react-app-web.zip`); the whole `dist/drop` is published as the **`drop`**
278
279
  artifact.
279
- - **Tag the run per app** *(Azure only)* — one build tag per zip, **exactly**
280
+ - **Tag the run per app** _(Azure only)_ — one build tag per zip, **exactly**
280
281
  `<type>-<name>` (derived from the zip filenames, so the tag can never drift
281
282
  from the artifact). A classic Azure release/CD pipeline keys its trigger off
282
283
  these; GitHub Actions has no equivalent mechanism, so the `drop` artifact
@@ -303,7 +304,7 @@ the root `.npmrc`'s `_password` block — the PAT value never lands in a file.
303
304
  No `npmAuthenticate@0` task (it would overwrite the hand-set password).
304
305
 
305
306
  On Azure, two one-time grants are required (project admin): **Contribute** on
306
- the repo for the *Project Collection Build Service* account (tag push), and
307
+ the repo for the _Project Collection Build Service_ account (tag push), and
307
308
  **publish** rights on the feed for the PAT's owner. On GitHub, the workflow's
308
309
  `permissions: contents: write` is what lets its own checkout token push the
309
310
  release tag — no separate grant, but the job still needs that permission
@@ -318,7 +319,7 @@ to feed" instructions give you. npm's `.npmrc` `_password` field expects
318
319
  exactly that pre-encoded form, so it's used as-is. `twine`/pypi basic auth, by
319
320
  contrast, wants the **raw** token — so the shared `releaseGuard` fragment
320
321
  (`overlay.ts`, used by both `azurePipelinesYaml` and `githubActionsYaml`)
321
- explicitly *decodes* the same `PAT`
322
+ explicitly _decodes_ the same `PAT`
322
323
  (`Buffer.from(process.env.PAT, 'base64').toString()`) before handing it to
323
324
  `TWINE_PASSWORD`. Both are correct for their protocol today, but it's an easy
324
325
  trap to get backwards: if you ever wire a third registry protocol, check
@@ -340,27 +341,46 @@ only for `github`/`both` — an `azure`-only workspace gets no
340
341
  `.github/dependabot.yml`, matching every other GitHub-only file this CLI
341
342
  writes.
342
343
 
344
+ ### Nx Cloud (`--nx-cloud`, opt-in)
345
+
346
+ `mnci new` never connects to Nx Cloud unless asked — `--nx-cloud` (or
347
+ answering "yes" to the interactive prompt) opts in; the flagless/`--yes`
348
+ default stays fully disconnected, exactly as before this existed. When
349
+ opted in, `mnci` passes `create-nx-workspace` a **named** `--nxCloud`
350
+ provider value derived from the chosen `--ci` (`azure`→`azure`,
351
+ `github`/`both`→`github`) — never the bare `--nxCloud=yes`. Verified
352
+ empirically: bare `yes` prompts "Will you be using GitHub as your git
353
+ hosting provider?" even with `--no-interactive` set, and exits without
354
+ creating the workspace at all when stdin isn't a TTY — a real
355
+ `create-nx-workspace` inconsistency, not something `mnci` can configure
356
+ around. The named-provider value sidesteps it and completes non-interactively
357
+ every time. The only visible effect of _which_ named value is chosen is a
358
+ throwaway CI workflow file `create-nx-workspace` writes as a side effect of
359
+ Cloud setup — this CLI's own overlay unconditionally overwrites whatever
360
+ lands at that path immediately after, so the pipeline you actually get is
361
+ always the same one described above, Nx Cloud or not.
362
+
363
+ Connecting still requires finishing setup in a browser: `create-nx-workspace`
364
+ prints a `https://cloud.nx.app/connect/…` URL to complete linking the
365
+ workspace to an Nx Cloud account (remote caching, CI insights, `nx
366
+ fix-ci`) — `mnci` does not automate that step.
367
+
343
368
  ## Dependency & risk notes
344
369
 
345
370
  Being upfront about what mnci leans on, so it's a conscious trade-off rather
346
371
  than a surprise:
347
372
 
348
- - **One unofficial, small-team third-party Nx plugin carries real weight**:
349
- [`oxc-standard`](https://github.com/JohnDeved/ox-standard) (the oxlint/oxfmt
350
- StandardJS preset) — not `@nx`-scoped/officially maintained. Neither Azure
351
- Function generation nor any Python kind pulls in a *third-party* Nx plugin:
352
- Node apps use the **official** `@nx/node:application` plus a small
353
- hand-written Azure Functions v4 file overlay (see "How Node apps work"
354
- below), and Python uses **`@mnci/nx-python-pip`** — a real Nx plugin this
355
- project built and maintains itself (`libs/nx-python-pip` in this same
356
- monorepo), after `@nxlv/python` (the previous Python plugin) turned out to
357
- require `uv`, which the company standardizing on this tool does not use,
358
- and no maintained alternative supports pip. That trades third-party risk
359
- for a different, real one: **this project now owns a second package's
360
- maintenance surface** (generators, executors, its own release cycle) —
361
- worth being explicit about, since it did not exist before this migration.
362
- If `oxc-standard` stalls or breaks compatibility with a future Nx major,
363
- that surface needs a real maintenance response, not just a version bump.
373
+ - **One Nx plugin this project builds and maintains** carries the most weight:
374
+ **`@mnci/nx-python-pip`** — a real Nx plugin built into this monorepo itself
375
+ (`packages/nx-python-pip`), created because no maintained, Nx-23-compatible
376
+ Python plugin supports pip (the previous Python plugin, `@nxlv/python`,
377
+ requires `uv`, which the company standardizing on this tool does not use, and
378
+ no maintained alternative supports pip). This trades third-party risk for a
379
+ different, real one: **this project now owns a second package's maintenance
380
+ surface** (generators, executors, its own release cycle) — worth being
381
+ explicit about, since it did not exist before. Unlike official `@nx/*`
382
+ plugins, if this one needs fixes or updates, this project owns those directly.
383
+ That's the cost of having no maintained pip-native Nx plugin in the ecosystem.
364
384
  - **The TS7 dual-compiler aliases pin a very new, fast-moving dependency.**
365
385
  TypeScript 7's native compiler is recent; `TS_COMPILER_DEPENDENCIES` pins
366
386
  `npm:typescript@^7.0.2` / `npm:@typescript/typescript6@^6.0.2` specifically
@@ -375,7 +395,7 @@ than a surprise:
375
395
  - Azure Functions Core Tools is only needed for **local** `func start` — never
376
396
  for `mnci add node-function-app`/`python-function-app` generation, since
377
397
  neither shells out to the `func` CLI.
378
- - Function-app *deployment* (e.g. `AzureFunctionApp@2`) is not wired into the
398
+ - Function-app _deployment_ (e.g. `AzureFunctionApp@2`) is not wired into the
379
399
  pipeline; the `node-function-app-<name>.zip`/`python-function-app-<name>.zip`
380
400
  inside the published `drop` artifact is the deploy input. Deploying it means
381
401
  Azure's Oryx build installing real dependencies (`npm install`/`pip install`)
@@ -395,9 +415,10 @@ than a surprise:
395
415
  Python project workspace-wide (see "Workspace-wide install" above),
396
416
  straight into whatever `<python>` resolves to on the agent (`python3` on
397
417
  POSIX, `python` on Windows — see above); locally, create your own venv
398
- (`python3 -m venv` / `python -m venv` on Windows) and reproduce the same
399
- two installs by hand (`pip install -r requirements-dev.txt`, then
400
- `pip install -e <dir>` for each Python project).
418
+ (`python3 -m venv` / `python -m venv` on Windows) and run
419
+ `npm run python:install` to reproduce the same two installs — the root
420
+ script chains the identical two guards CI runs (see the scripts table
421
+ above).
401
422
 
402
423
  ## How Node apps work (plain `@nx/node:application`, no Azure Functions plugin)
403
424
 
@@ -520,32 +541,35 @@ Windows (the standard python.org Windows installer registers no
520
541
  hard-coded name, so a `windows-latest` (or self-hosted Windows) agent works
521
542
  identically to a Linux/macOS one.
522
543
 
523
- | Kind | Location | Build / deploy |
524
- | ---- | -------- | -------------- |
525
- | `python-app` | `apps/<name>` | `python -m build` wheel (the plugin's `build` executor), zipped by mnci into `dist/drop/python-app-<name>.zip` |
526
- | `python-function-app` | `apps/<name>` | Azure Functions **v2** (`function_app.py` + `host.json` + `requirements.txt`); no `pyproject.toml`/wheel — the **source** is zipped by mnci into `dist/drop/python-function-app-<name>.zip` (no `func` CLI needed to generate) |
527
- | `python-lib` | `python-packages/<name>` | publishable wheel; the plugin's `publish` executor (`twine upload --skip-existing`) |
528
- | `python-internal-lib` | `libs/<name>` | private shared code, lint + test only — no build/package target of its own |
544
+ | Kind | Location | Build / deploy |
545
+ | --------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
546
+ | `python-app` | `apps/<name>` | `python -m build` wheel (the plugin's `build` executor), zipped by mnci into `dist/drop/python-app-<name>.zip` |
547
+ | `python-function-app` | `apps/<name>` | Azure Functions **v2** (`function_app.py` + `host.json` + `requirements.txt`); no `pyproject.toml`/wheel — the **source** is zipped by mnci into `dist/drop/python-function-app-<name>.zip` (no `func` CLI needed to generate) |
548
+ | `python-lib` | `python-packages/<name>` | publishable wheel; the plugin's `publish` executor (`twine upload --skip-existing`) |
549
+ | `python-internal-lib` | `libs/<name>` | private shared code, lint + test only — no build/package target of its own |
529
550
 
530
551
  - **Apps** get a `package` target — mnci's own CI packaging convention, not
531
552
  a generic plugin concern — merged into the plugin-written `project.json`
532
553
  after generation, fitting the existing CI unchanged: the pipeline's
533
554
  `apps/*` pack step tags them `python-app-<name>` / `python-function-app-
534
- <name>` just like the TS apps.
555
+ <name>` just like the TS apps.
535
556
  - **Internal-lib vendoring** replaces `@nxlv/python`'s `bundleLocalDependencies`:
536
557
  plain pip has no bundled-local-dependency feature, so a project that imports
537
- a workspace-internal Python library needs a hand-added `vendor` entry (under
558
+ a workspace-internal Python library needs a `vendor` entry (under
538
559
  `[tool.mnci-python-pip]`) in its own `pyproject.toml` (the pip-world
539
- counterpart of hand-wiring a `dependencies = [...]` entry — neither mnci
540
- nor the plugin wires cross-project Python dependencies automatically). The
541
- plugin's `build` executor reads that entry, resolves the named project's
542
- root via the **Nx project graph**, copies its module into a staged copy of
543
- the consuming project, and builds from there — so the resulting wheel
544
- contains the vendored module as a real top-level package. Verified
545
- empirically that this does **not** reproduce the old `@nxlv/python` bug
546
- where combining a vendored internal lib and a real external dependency on
547
- the same project silently dropped the external one from the wheel's
548
- metadata — both survive correctly.
560
+ counterpart of a `dependencies = [...]` entry — neither mnci nor the plugin
561
+ wires cross-project Python dependencies automatically). `mnci add
562
+ python-vendor <consumer> --lib <name>` automates writing that entry —
563
+ idempotent (safe to run twice), and works on any consumer with a
564
+ `pyproject.toml` (app, publishable lib, or another internal lib), not just
565
+ apps. The plugin's `build` executor reads the entry, resolves the named
566
+ project's root via the **Nx project graph**, copies its module into a
567
+ staged copy of the consuming project, and builds from there — so the
568
+ resulting wheel contains the vendored module as a real top-level package.
569
+ Verified empirically that this does **not** reproduce the old
570
+ `@nxlv/python` bug where combining a vendored internal lib and a real
571
+ external dependency on the same project silently dropped the external one
572
+ from the wheel's metadata — both survive correctly.
549
573
  - **Workspace-wide install** (mnci's own CI step, not the plugin's) — pip has
550
574
  no npm-workspaces-style hoisting, so mnci writes one: a guarded step
551
575
  editable-installs every Python project (`apps/*`, `python-packages/*`,
@@ -569,7 +593,7 @@ identically to a Linux/macOS one.
569
593
  `release.version.versionActions` override pointing at
570
594
  `@mnci/nx-python-pip/release/version-actions` (a `VersionActions`
571
595
  implementation — six methods, verified empirically against a real `nx
572
- release version --dry-run`), which wins over the workspace's default
596
+ release version --dry-run`), which wins over the workspace's default
573
597
  (npm's) `versionActions` for that one project. **Publishing** reuses the
574
598
  registry: an Azure Artifacts feed is **multi-protocol**, so the same
575
599
  org/project/feed serves Python — the release step exports `TWINE_*` (URL +
@@ -578,8 +602,68 @@ identically to a Linux/macOS one.
578
602
  Python package is still versioned + tagged, but publishing it needs
579
603
  user-provided `TWINE_*` — e.g. a PyPI token.)
580
604
  - **CI** also runs `nx run-many -t lint,test,build`, so Python's ruff `lint`
581
- target runs alongside the JS build even on the oxlint stack (whose
582
- `npm run lint` only covers JS). One guarded pipeline step installs
605
+ target runs alongside the JS ESLint build. One guarded pipeline step installs
583
606
  `requirements-dev.txt` first (the fixed toolchain), then a second installs
584
607
  every Python project workspace-wide (the workspace-wide install above) —
585
608
  both skipped cleanly when the workspace has no Python projects.
609
+
610
+ ## Go (`@nx-go/nx-go` — one root `go.mod`, golangci-lint + `go test`)
611
+
612
+ Requires **Go 1.21+** on the machine and on the build agent; `mnci add go-*`
613
+ fails fast with an install link when `go` is not on the `PATH`. The generated
614
+ pipeline installs `golangci-lint` itself (see below).
615
+
616
+ | Kind | Location | Build / deploy |
617
+ | ----------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
618
+ | `go-app` | `apps/<name>` | `go build` binary into `dist/apps/<name>/`, zipped by mnci into `dist/drop/go-app-<name>.zip` |
619
+ | `go-function-app` | `apps/<name>` | same build; zipped into `dist/drop/go-function-app-<name>.zip`. The handler body is yours to write — AWS Lambda, Google Cloud Functions and Azure each want a different signature, and mnci does not pick one |
620
+ | `go-lib` | `packages/<name>` | publishable **by git tag** — see below; lint + test targets only |
621
+ | `go-internal-lib` | `libs/<name>` | private shared code, lint + test only — a non-`main` package produces no binary |
622
+
623
+ - **One root `go.mod`**, matching how TS uses one root `package.json` and
624
+ Python one root `requirements-dev.txt`. `add/go.ts` bootstraps it on the
625
+ first Go `add` by running the plugin's `init` then `convert-to-one-mod`
626
+ generators, in that order — `convert-to-one-mod` refuses once `go.work`
627
+ lists any module, so it has to happen before the first Go project exists.
628
+ Every Go project then shares that module and imports its siblings as
629
+ `<module>/libs/<name>`, with no per-project manifests and no `replace`
630
+ directives.
631
+ - **The `go.work` multi-module layout was rejected deliberately.** Besides
632
+ splitting dependencies across per-project manifests, it is brittle: a
633
+ single stale `use` entry — a project directory removed by hand — makes
634
+ `go list -m -json` fail, and that breaks the **entire** Nx project graph,
635
+ not just the Go projects. Verified empirically.
636
+ - **Targets are written explicitly** rather than inferred. `@nx-go/nx-go`
637
+ supplies `build`/`test`/`lint` by inference, but keys that inference on a
638
+ per-project `go.mod` — which single-module mode does not have, so nothing
639
+ is inferred. mnci writes them into `project.json` instead, as it already
640
+ does for most kinds.
641
+ - **Lint is `golangci-lint`, pinned deliberately.** The plugin's `lint`
642
+ executor defaults to `go fmt`, which only reformats — a green lint step
643
+ with that default would mean nothing. The generated target passes
644
+ `linter: golangci-lint`, and `mnci add` warns (without failing) when the
645
+ binary is missing locally, since CI installs its own.
646
+ - **Publishing a `go-lib` is a git tag, not a registry upload.** The whole
647
+ repository is one module, so consumers depend on a library by import path
648
+ at a repo-level version tag — `go get <module>/packages/<name>@v1.2.3`.
649
+ That tag is the one `nx release` already creates, so no
650
+ `nx-release-publish` target is written: there is nothing to push. The only
651
+ real difference between `go-lib` and `go-internal-lib` is intent, recorded
652
+ in the `type:go-lib` tag and the `packages/` location.
653
+ - **No publish-time dependency injection**, unlike Python's vendoring: `go
654
+ build` links statically, so the binary in the drop already contains
655
+ everything it needs.
656
+ - **Build output is a directory**, `dist/apps/<name>/`, with the binary
657
+ inside it. The executor's own default writes a bare file at
658
+ `dist/apps/<name>`, which cannot be declared as an Nx `outputs` entry —
659
+ Nx scans each declared output to cache it, and scanning a file fails with
660
+ `ENOTDIR`. Building one level deeper keeps the root-`dist` convention and
661
+ makes the target cacheable.
662
+ - **CI** runs Go through the same `nx run-many -t lint,test,build` as
663
+ everything else. Two guarded steps precede it: `go mod download` (so a
664
+ network failure reads as a dependency failure rather than a confusing
665
+ build error), and a `golangci-lint` install via `go install` — no package
666
+ manager, no sudo, same command on every agent OS — whose `GOPATH/bin` is
667
+ then added to `PATH` for later steps. All three skip cleanly when the
668
+ workspace has no root `go.mod`, and the linter install also skips when the
669
+ agent already provides it.