@mnci/cli 1.8.0 → 1.9.1

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
@@ -52,6 +52,11 @@ mnci add go-function-app fn # serverless handler -> apps/
52
52
  mnci add go-lib core # publishable (by git tag) -> packages/
53
53
  mnci add go-internal-lib util # private shared package -> libs/
54
54
 
55
+ # Flutter (@mnci/nx-flutter — one root pubspec.yaml pub workspace, analyze + test)
56
+ mnci add flutter-app hello # Flutter web app -> apps/ (bundle, zipped into the drop)
57
+ mnci add flutter-lib shared # publishable (by git tag) -> packages/
58
+ mnci add flutter-internal-lib core # private shared package -> libs/
59
+
55
60
  mnci upgrade # re-apply the latest overlay (see below)
56
61
  mnci upgrade --agent windows-latest # ...with an explicit override
57
62
  ```
@@ -162,15 +167,24 @@ no programmatic API yet; the two aliases are what make it work.)
162
167
 
163
168
  ## Layout convention = release scoping
164
169
 
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 |
171
-
172
- No custom tags, no stamp file — the directory (and, for npm, the `private` flag)
173
- are the whole model. Publishable Python packages get their own
170
+ | Directory | Contents | Released? |
171
+ | ------------------ | -------------------------------------------------------------- | -------------------------------------- |
172
+ | `apps/` | React / Node / Python / Go / Flutter apps (plain or Functions) | Never (packed into the drop) |
173
+ | `packages/` | Publishable npm libraries, plus Go and Dart packages | Yes — `nx release`, per-package tags |
174
+ | `python-packages/` | Publishable Python packages (hatchling wheels) | Yes — `twine upload` (Azure Artifacts) |
175
+ | `libs/` | Internal libraries (TS, Python, Go or Dart), never published | Never |
176
+
177
+ The directory is very nearly the whole model — one exception, and it is a bug
178
+ fix rather than a nicety. `go-lib` also lives in `packages/`, but a Go package
179
+ has **no per-project manifest** (mnci puts every Go project in one root
180
+ `go.mod`), so Nx's default `versionActions` looks for a `package.json` that is
181
+ not there and aborts _while building the release graph_ — killing `nx release`
182
+ for the whole workspace, not just the Go project. It is therefore excluded with
183
+ `!tag:type:go-lib`, which is also the semantically correct call: one module
184
+ means its packages have no independent versions to bump. A publishable **Dart**
185
+ package in `packages/` needs no such exclusion — `pubspec.yaml` has a real
186
+ `version:` field, and `@mnci/nx-flutter` stamps a `versionActions` override that
187
+ reads it. Publishable Python packages get their own
174
188
  `python-packages/` dir so the npm `nx release` (`packages/*`) is never entangled
175
189
  with Python publishing.
176
190
 
@@ -291,7 +305,16 @@ lint,test,build`. Pushes to `main` then:
291
305
  the raw token twine needs for the Python publish. Skipped cleanly when there
292
306
  is nothing to release. A guarded step installs the fixed Python toolchain
293
307
  (`ruff`/`pytest`/`build`/`twine`/`pip-audit`) before any Python target runs,
294
- skipped cleanly on a workspace with no Python projects.
308
+ skipped cleanly on a workspace with no Python projects. On a `--ci=github`
309
+ workspace this same step also creates a **GitHub Release per project**, with
310
+ a changelog Nx generates from conventional commits — `nx release` pushes the
311
+ tag itself here (needs `GITHUB_TOKEN`, which GitHub Actions provides for
312
+ free under the workflow's own `contents: write` permission), so there's no
313
+ separate explicit `git push origin --tags` step on this provider. `--ci=azure`
314
+ and `--ci=both` keep today's behaviour (no GitHub Release, explicit tag
315
+ push) — GitHub Release creation only turns on when GitHub Actions is the
316
+ _only_ configured provider, since that's the one case a `GITHUB_TOKEN` is
317
+ guaranteed to exist.
295
318
 
296
319
  **npm auth** is the base64 `PAT`, read the same way on both providers but from
297
320
  a different place: on Azure Pipelines, a **variable group**
@@ -370,17 +393,31 @@ fix-ci`) — `mnci` does not automate that step.
370
393
  Being upfront about what mnci leans on, so it's a conscious trade-off rather
371
394
  than a surprise:
372
395
 
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.
396
+ - **Two Nx plugins this project builds and maintains** carry the most weight,
397
+ both for the same reason — the ecosystem has no maintained, Nx-23-compatible
398
+ option:
399
+ - **`@mnci/nx-python-pip`** (`packages/nx-python-pip`): no maintained plugin
400
+ supports pip. The obvious candidate, `@nxlv/python`, requires `uv`, which
401
+ the company standardizing on this tool does not use.
402
+ - **`@mnci/nx-flutter`** (`packages/nx-flutter`): `@nxrocks/nx-flutter`
403
+ cannot even load on Nx 23 — it imports
404
+ `@nx/workspace/src/utilities/fileutils`, removed in 23 — and there is no
405
+ alternative. Its exposure is smaller than the Python plugin's, because
406
+ scaffolding is delegated to the official `flutter create` rather than
407
+ hand-maintained templates; what this project owns is the pub-workspace
408
+ wiring, the targets and the release integration.
409
+
410
+ Both trade third-party risk for a different, real one: **this project owns
411
+ two extra packages' maintenance surface** (generators, executors, their own
412
+ release cycles). Unlike official `@nx/*` plugins, if either needs fixes, this
413
+ project owns them directly. That is the cost of the gaps in the ecosystem.
414
+
415
+ - **`@nx-go/nx-go` is a third-party plugin on a declared-incompatible range.**
416
+ It declares `@nx/devkit ">= 20 < 23"` while this workspace runs Nx 23. That
417
+ range is a plain dependency rather than a peer, so npm nests its own devkit
418
+ copy and everything works — validated empirically against a real Nx 23.1.0
419
+ workspace (generators, build, test, lint). It is still a version trap worth
420
+ re-checking on Nx upgrades.
384
421
  - **The TS7 dual-compiler aliases pin a very new, fast-moving dependency.**
385
422
  TypeScript 7's native compiler is recent; `TS_COMPILER_DEPENDENCIES` pins
386
423
  `npm:typescript@^7.0.2` / `npm:@typescript/typescript6@^6.0.2` specifically
@@ -392,6 +429,15 @@ than a surprise:
392
429
  ## Known gaps (accepted for the experiment)
393
430
 
394
431
  - No `doctor`/`resurrect`/`spell` — out of scope until the model is proven.
432
+ - **Flutter apps build for web only.** Android would require the Android SDK and
433
+ NDK on every build agent; iOS is impossible on a Linux agent regardless. Add
434
+ other platforms per-app with `flutter create --platforms=...` — the generated
435
+ `build` target only knows about web.
436
+ - **Go has no e2e coverage.** The `go-*` kinds have real unit tests and real CI
437
+ wiring, but the e2e suite never drives them, because it needs Go on the
438
+ machine. Flutter took the opposite approach — its e2e section runs when the
439
+ SDK is present and reports **SKIPPED** (loudly) when it is not — so the same
440
+ pattern could be applied to Go, but has not been.
395
441
  - Azure Functions Core Tools is only needed for **local** `func start` — never
396
442
  for `mnci add node-function-app`/`python-function-app` generation, since
397
443
  neither shells out to the `func` CLI.
@@ -400,8 +446,13 @@ than a surprise:
400
446
  inside the published `drop` artifact is the deploy input. Deploying it means
401
447
  Azure's Oryx build installing real dependencies (`npm install`/`pip install`)
402
448
  from the zipped manifest — no `node_modules`/venv is bundled.
403
- - Changelog files are off (unpushable under the tag-only model); the git tag
404
- history is the changelog for now.
449
+ - Changelog **files** are off everywhere (unpushable under the tag-only
450
+ model — `git.commit` stays `false`, so a written `CHANGELOG.md` would just
451
+ be discarded at the end of every CI run). On a `--ci=github` workspace (and
452
+ only that one — see below) each release still gets a real changelog: Nx
453
+ generates it from conventional commits and posts it straight to a GitHub
454
+ Release, with no file ever touched. `--ci=azure` and `--ci=both` fall back
455
+ to the git tag history as the changelog, same as before.
405
456
  - **No lock file for Python** — plain pip has none, matching the company
406
457
  standard this migration was for. A published wheel's `Requires-Dist` mirrors
407
458
  whatever specifier the `pyproject.toml` declares (e.g. `tomli>=2.0.0`)
@@ -667,3 +718,69 @@ build` links statically, so the binary in the drop already contains
667
718
  then added to `PATH` for later steps. All three skip cleanly when the
668
719
  workspace has no root `go.mod`, and the linter install also skips when the
669
720
  agent already provides it.
721
+
722
+ ## Flutter (`@mnci/nx-flutter` — one root `pubspec.yaml` pub workspace)
723
+
724
+ Requires the **Flutter SDK** (3.27+, for Dart 3.6+ pub workspaces) on the
725
+ machine; `mnci add flutter-*` fails fast with an install link when `flutter` is
726
+ not on the `PATH`. Unlike Python and Go, the SDK is **not** present on hosted
727
+ build agents, so the generated pipeline installs it itself (see below).
728
+
729
+ | Kind | Location | Build / deploy |
730
+ | ---------------------- | ----------------- | ----------------------------------------------------------------------------------------------------- |
731
+ | `flutter-app` | `apps/<name>` | `flutter build web` bundle into `dist/apps/<name>/`, zipped into `dist/drop/flutter-app-<name>.zip` |
732
+ | `flutter-lib` | `packages/<name>` | publishable **by git tag** — see below; analyze + test targets only |
733
+ | `flutter-internal-lib` | `libs/<name>` | private shared code, analyze + test only — a Dart package is compiled into whatever app depends on it |
734
+
735
+ - **Dependencies are central, via a Dart pub workspace.** One root
736
+ `pubspec.yaml` lists every project under `workspace:`, and each project
737
+ carries `resolution: workspace`. A single `flutter pub get` at the root then
738
+ resolves the whole graph into **one** `pubspec.lock` and **one**
739
+ `.dart_tool/package_config.json` — pub actively deletes any per-package
740
+ copies. This is the Flutter half of the same root-manifest model as the root
741
+ `package.json` for TS, `requirements-dev.txt` for Python and `go.mod` for Go.
742
+ - **An internal library is consumed with a plain version constraint — no
743
+ `path:`.** `dependencies: { core: ^0.0.1 }` resolves to the local package
744
+ because it is a workspace member. That is also why Flutter needs **no
745
+ vendoring step**: contrast `mnci add python-vendor`, which exists only
746
+ because pip cannot bundle an unpublished sibling into a wheel. Flutter is in
747
+ the Go camp here — nothing to weave in at build time.
748
+ - **Lint configuration is central too.** The workspace root owns one
749
+ `analysis_options.yaml` (including `package:flutter_lints/flutter.yaml`), and
750
+ each project's own file is a one-line relative `include:` of it, so a rule
751
+ change lands in one place.
752
+ - **`flutter analyze --fatal-infos`, pinned explicitly.** `flutter analyze`
753
+ already defaults `--fatal-infos` on — verified against 3.44.8, where
754
+ `--no-fatal-infos` turns a failing lint run green. It is passed anyway
755
+ because that default is the only thing making this a real gate: nearly every
756
+ `flutter_lints` rule reports at _info_ severity. Worth knowing that plain
757
+ `dart analyze` defaults the opposite way (it fails on errors and warnings but
758
+ not infos), so swapping the command without carrying the flag across would
759
+ silently stop enforcing anything.
760
+ - **Publishing a `flutter-lib` is a git tag, not a registry upload.** Azure
761
+ Artifacts has no pub/Dart feed type, so a private pub registry is not
762
+ available on this stack, and these packages are deliberately not pushed to
763
+ pub.dev. `nx release` versions and tags them; no `nx-release-publish` target
764
+ is written.
765
+ - **The publishable lib carries a `versionActions` override, and it is
766
+ load-bearing.** Nx's default reads a `package.json`, which a Dart package
767
+ does not have. Without the override `nx release` aborts while building the
768
+ release graph — taking down the release of **every** project in the
769
+ workspace, not just the Dart one. The plugin's `library` generator stamps
770
+ `@mnci/nx-flutter/release/version-actions` on, which reads and writes
771
+ `pubspec.yaml`'s `version:`.
772
+ - **Apps build for web only.** Web needs nothing beyond the Flutter SDK,
773
+ whereas an Android build would drag the whole Android SDK and NDK onto every
774
+ build agent. Other platforms can be added per-app later with
775
+ `flutter create --platforms=...`.
776
+ - **CI** installs the SDK by shallow `git clone` at a pinned tag — Flutter's own
777
+ documented install method, and the only one uniform across agents (the
778
+ release archives differ by platform). It is cloned **outside** the workspace,
779
+ under the agent's home directory: the SDK ships dozens of its own
780
+ `pubspec.yaml` files, which inside the tree would pollute pub's resolution and
781
+ give Nx thousands of extra files to glob. The SDK version is **pinned**
782
+ (unlike `golangci-lint`'s `@latest`) because it determines the Dart version,
783
+ and pub workspaces need Dart 3.6+. Three guarded steps precede the build —
784
+ install, add to `PATH`, and one root `flutter pub get` — and all three skip
785
+ cleanly when the workspace has no root `pubspec.yaml`; the install also skips
786
+ when an SDK is already available.
package/dist/cli.js CHANGED
@@ -164,10 +164,6 @@ function assertValidProjectName(name, label) {
164
164
  }
165
165
  }
166
166
 
167
- // src/commands/add/go.ts
168
- var import_node_fs2 = require("fs");
169
- var import_node_path3 = require("path");
170
-
171
167
  // src/commands/add/shared.ts
172
168
  var import_node_path2 = require("path");
173
169
  function hasPlugin(workspaceRoot, packageName) {
@@ -203,7 +199,48 @@ function addNxTargets(manifestPath, newTargets) {
203
199
  );
204
200
  }
205
201
 
202
+ // src/commands/add/flutter.ts
203
+ function ensureFlutter(workspaceRoot) {
204
+ if (runShell("flutter", ["--version"], workspaceRoot) !== 0) {
205
+ throw new Error(
206
+ "Flutter not found. Install the Flutter SDK first (3.27+, for Dart 3.6+ pub workspaces): https://docs.flutter.dev/get-started/install"
207
+ );
208
+ }
209
+ }
210
+ function nxFlutterPluginSpec() {
211
+ return process.env.MNCI_NX_FLUTTER_SPEC ?? "@mnci/nx-flutter";
212
+ }
213
+ function ensureNxFlutterPlugin(workspaceRoot) {
214
+ if (hasPlugin(workspaceRoot, "@mnci/nx-flutter")) {
215
+ return;
216
+ }
217
+ const spec = nxFlutterPluginSpec();
218
+ logger.step(`Installing the Flutter toolchain plugin (${spec})`);
219
+ if (runShell("npm", ["install", "--save-dev", spec, "--no-audit", "--no-fund"], workspaceRoot) !== 0) {
220
+ throw new Error("npm install of @mnci/nx-flutter failed");
221
+ }
222
+ }
223
+ function prepareFlutter(workspaceRoot) {
224
+ ensureFlutter(workspaceRoot);
225
+ ensureNxFlutterPlugin(workspaceRoot);
226
+ }
227
+ function addFlutterApp(workspaceRoot, name) {
228
+ prepareFlutter(workspaceRoot);
229
+ ensureAdmZip(workspaceRoot);
230
+ runNx(["g", "@mnci/nx-flutter:application", name, "--no-interactive"], workspaceRoot);
231
+ }
232
+ function addFlutterLib(workspaceRoot, name) {
233
+ prepareFlutter(workspaceRoot);
234
+ runNx(["g", "@mnci/nx-flutter:library", name, "--no-interactive"], workspaceRoot);
235
+ }
236
+ function addFlutterInternalLib(workspaceRoot, name) {
237
+ prepareFlutter(workspaceRoot);
238
+ runNx(["g", "@mnci/nx-flutter:internal-library", name, "--no-interactive"], workspaceRoot);
239
+ }
240
+
206
241
  // src/commands/add/go.ts
242
+ var import_node_fs2 = require("fs");
243
+ var import_node_path3 = require("path");
207
244
  function ensureGo(workspaceRoot) {
208
245
  if (runShell("go", ["version"], workspaceRoot) !== 0) {
209
246
  throw new Error("Go not found. Install Go 1.21+ first: https://go.dev/dl/");
@@ -539,6 +576,17 @@ export default [
539
576
  ignoredFiles: [
540
577
  '{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}',
541
578
  '{projectRoot}/rollup.config.{js,ts,mjs,mts,cjs,cts}',
579
+ // Same reasoning as the two config files above, extended to the test
580
+ // toolchain: rollup bundles from the entry point only, so neither the
581
+ // Vitest config nor the spec files reach the published package \u2014 their
582
+ // imports must not drive the published manifest.
583
+ //
584
+ // Without this, a vitest-stack workspace fails \`npm run lint\` on a
585
+ // freshly generated npm-lib: Nx's own spec imports \`vitest\`, and
586
+ // @nx/dependency-checks then demands it be declared as a runtime
587
+ // dependency of a package that never uses it at runtime.
588
+ '{projectRoot}/vitest.config.{js,ts,mjs,mts,cjs,cts}',
589
+ '{projectRoot}/**/*.spec.{js,ts,jsx,tsx}',
542
590
  ],
543
591
  },
544
592
  ],
@@ -873,7 +921,10 @@ var PROJECT_KINDS = [
873
921
  "go-app",
874
922
  "go-function-app",
875
923
  "go-lib",
876
- "go-internal-lib"
924
+ "go-internal-lib",
925
+ "flutter-app",
926
+ "flutter-lib",
927
+ "flutter-internal-lib"
877
928
  ];
878
929
  async function runAdd(kind, name, options) {
879
930
  const workspaceRoot = process.cwd();
@@ -953,6 +1004,18 @@ async function runAdd(kind, name, options) {
953
1004
  addGoInternalLib(workspaceRoot, resolvedName);
954
1005
  break;
955
1006
  }
1007
+ case "flutter-app": {
1008
+ addFlutterApp(workspaceRoot, resolvedName);
1009
+ break;
1010
+ }
1011
+ case "flutter-lib": {
1012
+ addFlutterLib(workspaceRoot, resolvedName);
1013
+ break;
1014
+ }
1015
+ case "flutter-internal-lib": {
1016
+ addFlutterInternalLib(workspaceRoot, resolvedName);
1017
+ break;
1018
+ }
956
1019
  case "python-vendor": {
957
1020
  await addPythonVendor(workspaceRoot, resolvedName, options, kindProvided);
958
1021
  syncProjectReferences(workspaceRoot);
@@ -1034,27 +1097,33 @@ function npmrcContent(registry, scope) {
1034
1097
  lines.push("");
1035
1098
  return lines.join("\n");
1036
1099
  }
1037
- var RELEASE_CONFIG = {
1038
- projectsRelationship: "independent",
1039
- projects: ["packages/*", "python-packages/*"],
1040
- releaseTag: { pattern: "{projectName}@{version}" },
1041
- git: { commit: false, tag: true, push: false },
1042
- version: {
1043
- conventionalCommits: true,
1044
- fallbackCurrentVersionResolver: "disk",
1045
- // Build only what is being released. Without this, @nx/js:lib's generator
1046
- // defaults the pre-version command to building EVERY project, so a broken
1047
- // (or merely slow) app build would block releasing unrelated packages.
1048
- // Set here at `new` time it wins: the generator only fills this in when
1049
- // absent (it spreads the existing release.version over its default). Both
1050
- // globs are listed; `nx run-many` no-ops cleanly when one matches nothing.
1051
- preVersionCommand: "npx nx run-many -t build --projects=packages/*,python-packages/*"
1052
- },
1053
- changelog: { workspaceChangelog: false }
1054
- };
1100
+ function releaseConfig(ci) {
1101
+ const githubReleases = ci === "github";
1102
+ return {
1103
+ projectsRelationship: "independent",
1104
+ projects: ["packages/*", "python-packages/*", "!tag:type:go-lib"],
1105
+ releaseTag: { pattern: "{projectName}@{version}" },
1106
+ git: { commit: false, tag: true, push: githubReleases },
1107
+ version: {
1108
+ conventionalCommits: true,
1109
+ fallbackCurrentVersionResolver: "disk",
1110
+ // Build only what is being released. Without this, @nx/js:lib's generator
1111
+ // defaults the pre-version command to building EVERY project, so a broken
1112
+ // (or merely slow) app build would block releasing unrelated packages.
1113
+ // Set here at `new` time it wins: the generator only fills this in when
1114
+ // absent (it spreads the existing release.version over its default). Both
1115
+ // globs are listed; `nx run-many` no-ops cleanly when one matches nothing.
1116
+ preVersionCommand: "npx nx run-many -t build --projects=packages/*,python-packages/*"
1117
+ },
1118
+ changelog: githubReleases ? {
1119
+ workspaceChangelog: false,
1120
+ projectChangelogs: { createRelease: "github", file: false }
1121
+ } : { workspaceChangelog: false }
1122
+ };
1123
+ }
1055
1124
  var SYNC_CONFIG = { applyChanges: true };
1056
- function withReleaseConfig(nxJson) {
1057
- return { ...nxJson, defaultBase: "main", release: RELEASE_CONFIG };
1125
+ function withReleaseConfig(nxJson, ci) {
1126
+ return { ...nxJson, defaultBase: "main", release: releaseConfig(ci) };
1058
1127
  }
1059
1128
  var COMMITLINT_CONFIG = `export default { extends: ['@commitlint/config-conventional'] }
1060
1129
  `;
@@ -1187,6 +1256,13 @@ var GOLANGCI_LINT_INSTALL_GUARD = `node -e "const fs=require('node:fs'),cp=requi
1187
1256
  var GO_TOOL_PATH_PRELUDE = `const fs=require('node:fs'),cp=require('node:child_process');if(!fs.existsSync('go.mod')){console.log('No Go projects - skipping.');process.exit(0)}const r=cp.spawnSync('go',['env','GOPATH'],{encoding:'utf8'});if(r.status!==0){console.log('Could not resolve GOPATH - skipping.');process.exit(0)}const bin=require('node:path').join(r.stdout.trim(),'bin');`;
1188
1257
  var GO_TOOL_PATH_AZURE = `node -e "${GO_TOOL_PATH_PRELUDE}console.log('##vso[task.prependpath]'+bin)"`;
1189
1258
  var GO_TOOL_PATH_GITHUB = `node -e "${GO_TOOL_PATH_PRELUDE}if(!process.env.GITHUB_PATH){console.log('Not running in GitHub Actions - skipping.');process.exit(0)}fs.appendFileSync(process.env.GITHUB_PATH,bin+${String.raw`'\n'`})"`;
1259
+ var FLUTTER_SDK_VERSION = "3.44.8";
1260
+ var FLUTTER_SDK_DIRECTORY_EXPRESSION = `require('node:path').join(require('node:os').homedir(),'.mnci-flutter-${FLUTTER_SDK_VERSION}')`;
1261
+ var FLUTTER_SDK_INSTALL_GUARD = `node -e "const fs=require('node:fs'),cp=require('node:child_process');if(!fs.existsSync('pubspec.yaml')){console.log('No Flutter projects - skipping.');process.exit(0)}if(cp.spawnSync('flutter',['--version'],{stdio:'ignore',shell:process.platform==='win32'}).status===0){console.log('Flutter SDK already on PATH - skipping.');process.exit(0)}const sdk=${FLUTTER_SDK_DIRECTORY_EXPRESSION};if(fs.existsSync(sdk)){console.log('Flutter SDK already installed at '+sdk+' - skipping.');process.exit(0)}process.exit(cp.spawnSync('git',['clone','--depth','1','--branch','${FLUTTER_SDK_VERSION}','https://github.com/flutter/flutter.git',sdk],{stdio:'inherit'}).status ?? 1)"`;
1262
+ var FLUTTER_TOOL_PATH_PRELUDE = `const fs=require('node:fs'),path=require('node:path');if(!fs.existsSync('pubspec.yaml')){console.log('No Flutter projects - skipping.');process.exit(0)}const sdk=${FLUTTER_SDK_DIRECTORY_EXPRESSION};if(!fs.existsSync(sdk)){console.log('Flutter SDK was not installed by mnci (already on PATH) - skipping.');process.exit(0)}const bin=path.join(sdk,'bin');`;
1263
+ var FLUTTER_TOOL_PATH_AZURE = `node -e "${FLUTTER_TOOL_PATH_PRELUDE}console.log('##vso[task.prependpath]'+bin)"`;
1264
+ var FLUTTER_TOOL_PATH_GITHUB = `node -e "${FLUTTER_TOOL_PATH_PRELUDE}if(!process.env.GITHUB_PATH){console.log('Not running in GitHub Actions - skipping.');process.exit(0)}fs.appendFileSync(process.env.GITHUB_PATH,bin+${String.raw`'\n'`})"`;
1265
+ var FLUTTER_PUB_GET_GUARD = `node -e "const fs=require('node:fs');if(!fs.existsSync('pubspec.yaml')){console.log('No Flutter projects - skipping.');process.exit(0)}process.exit(require('node:child_process').spawnSync('flutter',['pub','get'],{stdio:'inherit',shell:process.platform==='win32'}).status ?? 1)"`;
1190
1266
  var NPM_AUDIT_STEP = "npm audit --audit-level=high || echo npm audit found vulnerabilities, see log above - non-blocking, run npm audit locally to inspect";
1191
1267
  var PACK_APPS_GUARD = `node -e "const fs=require('node:fs');fs.mkdirSync('dist/drop',{recursive:true});if(fs.globSync('apps/*/project.json').length===0){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)"`;
1192
1268
  function releaseGuard(pythonPublishEnv) {
@@ -1302,6 +1378,23 @@ steps:
1302
1378
  - script: ${GO_TOOL_PATH_AZURE}
1303
1379
  displayName: Add Go tool bin to PATH
1304
1380
 
1381
+ # Flutter, if the workspace has any. Unlike Python and Go, hosted agents do
1382
+ # NOT ship the Flutter SDK, so this installs it (shallow git clone at a
1383
+ # pinned tag) and puts it on PATH. All three guards skip cleanly on a
1384
+ # workspace with no root pubspec.yaml, and the install also skips when an
1385
+ # SDK is already on PATH.
1386
+ - script: ${FLUTTER_SDK_INSTALL_GUARD}
1387
+ displayName: Install the Flutter SDK (${FLUTTER_SDK_VERSION})
1388
+
1389
+ - script: ${FLUTTER_TOOL_PATH_AZURE}
1390
+ displayName: Add the Flutter SDK to PATH
1391
+
1392
+ # One command resolves EVERY Dart dependency, internal and external, for the
1393
+ # whole workspace: the projects are pub workspace members, so this writes a
1394
+ # single root pubspec.lock they all share.
1395
+ - script: ${FLUTTER_PUB_GET_GUARD}
1396
+ displayName: Resolve Dart dependencies (one pub get for the whole workspace)
1397
+
1305
1398
  # Fails fast, with an unambiguous message, when a stale TypeScript project
1306
1399
  # reference (or another sync generator's drift) was never synced+committed
1307
1400
  # locally \u2014 sync.applyChanges (nx.json) only auto-applies interactively, so
@@ -1363,12 +1456,13 @@ steps:
1363
1456
  condition: ${onMain}
1364
1457
  `;
1365
1458
  }
1366
- function githubActionsYaml(agent, pythonPublishUrl2, registryKind = "azure-artifacts") {
1459
+ function githubActionsYaml(agent, pythonPublishUrl2, registryKind = "azure-artifacts", ci = "github") {
1367
1460
  const onMain = `github.event_name != 'pull_request' && github.ref_name == 'main'`;
1368
1461
  const [npmAuthName, npmAuthValue] = npmAuthEnvVariable(
1369
1462
  registryKind,
1370
1463
  (name) => `\${{ secrets.${name} }}`
1371
1464
  );
1465
+ const githubReleases = ci === "github";
1372
1466
  return `name: CI
1373
1467
 
1374
1468
  # Generated by MoNecromanCI. Deliberately thin: Nx builds, 'nx release'
@@ -1384,7 +1478,8 @@ on:
1384
1478
  branches: [main]
1385
1479
 
1386
1480
  permissions:
1387
- # Lets the release step push the version tag nx release creates back to main.
1481
+ # Lets the release step push the version tag nx release creates back to main,
1482
+ # and (github-only provider) create the GitHub Release itself.
1388
1483
  contents: write
1389
1484
 
1390
1485
  jobs:
@@ -1454,6 +1549,23 @@ jobs:
1454
1549
  - run: ${GO_TOOL_PATH_GITHUB}
1455
1550
  name: Add Go tool bin to PATH
1456
1551
 
1552
+ # Flutter, if the workspace has any. Unlike Python and Go, hosted runners
1553
+ # do NOT ship the Flutter SDK, so this installs it (shallow git clone at
1554
+ # a pinned tag) and puts it on PATH. All three guards skip cleanly on a
1555
+ # workspace with no root pubspec.yaml, and the install also skips when an
1556
+ # SDK is already on PATH.
1557
+ - run: ${FLUTTER_SDK_INSTALL_GUARD}
1558
+ name: Install the Flutter SDK (${FLUTTER_SDK_VERSION})
1559
+
1560
+ - run: ${FLUTTER_TOOL_PATH_GITHUB}
1561
+ name: Add the Flutter SDK to PATH
1562
+
1563
+ # One command resolves EVERY Dart dependency, internal and external, for
1564
+ # the whole workspace: the projects are pub workspace members, so this
1565
+ # writes a single root pubspec.lock they all share.
1566
+ - run: ${FLUTTER_PUB_GET_GUARD}
1567
+ name: Resolve Dart dependencies (one pub get for the whole workspace)
1568
+
1457
1569
  # Fails fast, with an unambiguous message, when a stale TypeScript project
1458
1570
  # reference (or another sync generator's drift) was never synced+committed
1459
1571
  # locally \u2014 sync.applyChanges (nx.json) only auto-applies interactively, so
@@ -1491,13 +1603,19 @@ jobs:
1491
1603
  # (python-packages/*) \u2014 conventional commits, tag-only push. Portable guard:
1492
1604
  # nx release errors on an empty scope, so skip cleanly when there is nothing
1493
1605
  # to release. When there are Python packages and an Azure feed, twine
1494
- # publish credentials are exported (raw PAT, decoded from the base64 secret).
1606
+ # publish credentials are exported (raw PAT, decoded from the base64 secret).${githubReleases ? `
1607
+ # This provider also creates a per-project GitHub Release (changelog
1608
+ # generated from conventional commits since that project's last tag),
1609
+ # which is why 'nx release' pushes the tag itself here \u2014 see
1610
+ # releaseConfig's remarks for why that is safe on this Nx version and
1611
+ # why every other provider combination keeps the explicit push step below.` : ""}
1495
1612
  - run: ${releaseGuard(pythonPublishEnvFragment(pythonPublishUrl2))}
1496
- name: Release \u2014 version, tag and publish (npm + Python)
1613
+ name: Release \u2014 version, tag${githubReleases ? ", publish and GitHub Release" : " and publish"} (npm + Python)
1497
1614
  if: \${{ ${onMain} }}
1498
1615
  env:
1499
- ${npmAuthName}: ${npmAuthValue}
1500
-
1616
+ ${npmAuthName}: ${npmAuthValue}${githubReleases ? `
1617
+ GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}` : ""}
1618
+ ${githubReleases ? "" : `
1501
1619
  # nx release's own git push (release.git.push) is deliberately left off: it
1502
1620
  # only runs when a remote GitHub/GitLab Release is configured, which this
1503
1621
  # workflow never does, so it would never push the tag the step above just
@@ -1506,7 +1624,7 @@ jobs:
1506
1624
  - run: git push origin --tags
1507
1625
  name: Push release tags (nx release's own push never runs without a remote Release configured)
1508
1626
  if: \${{ ${onMain} }}
1509
- `;
1627
+ `}`;
1510
1628
  }
1511
1629
  var DEPENDABOT_CONFIG = `# Generated by MoNecromanCI. Weekly dependency-update PRs so vulnerable or
1512
1630
  # stale dependencies surface as a reviewable PR instead of only a push-time
@@ -1533,6 +1651,18 @@ updates:
1533
1651
  - "/libs/*"
1534
1652
  schedule:
1535
1653
  interval: weekly
1654
+
1655
+ # Same glob reasoning as pip above. Dart projects are pub workspace members,
1656
+ # so each one declares its own dependencies even though they all resolve
1657
+ # through the single root pubspec.lock \u2014 hence the per-project directories
1658
+ # rather than just "/".
1659
+ - package-ecosystem: pub
1660
+ directories:
1661
+ - "/apps/*"
1662
+ - "/packages/*"
1663
+ - "/libs/*"
1664
+ schedule:
1665
+ interval: weekly
1536
1666
  `;
1537
1667
  function applyOverlay(workspaceRoot, options) {
1538
1668
  const nxJsonPath = (0, import_node_path9.join)(workspaceRoot, "nx.json");
@@ -1543,7 +1673,10 @@ function applyOverlay(workspaceRoot, options) {
1543
1673
  };
1544
1674
  const sync = { ...nxJson.sync, ...SYNC_CONFIG };
1545
1675
  const mnci = { ...nxJson.mnci, ...mnciConfig(options) };
1546
- writeFileEnsured(nxJsonPath, toJson({ ...withReleaseConfig(nxJson), generators, sync, mnci }));
1676
+ writeFileEnsured(
1677
+ nxJsonPath,
1678
+ toJson({ ...withReleaseConfig(nxJson, options.ci), generators, sync, mnci })
1679
+ );
1547
1680
  const manifestPath = (0, import_node_path9.join)(workspaceRoot, "package.json");
1548
1681
  const manifest = readJson(manifestPath);
1549
1682
  const scripts = {
@@ -1580,7 +1713,7 @@ function applyOverlay(workspaceRoot, options) {
1580
1713
  if (options.ci === "github" || options.ci === "both") {
1581
1714
  writeFileEnsured(
1582
1715
  (0, import_node_path9.join)(workspaceRoot, ".github/workflows/ci.yml"),
1583
- githubActionsYaml(options.agent, publishUrl, options.registry.kind)
1716
+ githubActionsYaml(options.agent, publishUrl, options.registry.kind, options.ci)
1584
1717
  );
1585
1718
  writeFileEnsured((0, import_node_path9.join)(workspaceRoot, ".github/dependabot.yml"), DEPENDABOT_CONFIG);
1586
1719
  }
@@ -1672,7 +1805,13 @@ async function runNew(name, options) {
1672
1805
  " mnci add react-app web # or: node-app, node-function-app, npm-lib, internal-lib,"
1673
1806
  );
1674
1807
  logger.info(
1675
- " # python-app, python-function-app, python-lib, python-internal-lib"
1808
+ " # python-app, python-function-app, python-lib, python-internal-lib,"
1809
+ );
1810
+ logger.info(
1811
+ " # go-app, go-function-app, go-lib, go-internal-lib,"
1812
+ );
1813
+ logger.info(
1814
+ " # flutter-app, flutter-lib, flutter-internal-lib"
1676
1815
  );
1677
1816
  logger.info(' git add -A && git commit -m "feat: initial workspace"');
1678
1817
  }