@mnci/cli 4.34.0 → 4.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -695,6 +695,10 @@ var ESLINT_CONFIG_VERSION = ">=0.1.0 <1.0.0";
695
695
  function eslintConfigSpec() {
696
696
  return process.env.MNCI_ESLINT_CONFIG_SPEC ?? ESLINT_CONFIG_VERSION;
697
697
  }
698
+ var CLI_VERSION = ">=4.0.0 <5.0.0";
699
+ function cliSpec() {
700
+ return process.env.MNCI_CLI_SPEC ?? CLI_VERSION;
701
+ }
698
702
  var ESLINT_VERSION = "^10.8.0";
699
703
  function devcontainerJson(workspaceName) {
700
704
  return `${toJson({
@@ -713,9 +717,9 @@ function devcontainerJson(workspaceName) {
713
717
  // precisely the drift this file exists to remove.
714
718
  `npm install -g npm@${NPM_VERSION}`,
715
719
  "npm ci",
716
- "npm run python:install",
717
- GOLANGCI_LINT_INSTALL_GUARD,
718
- FLUTTER_SDK_INSTALL_GUARD
720
+ // The same command the pipeline runs: the Python, Go and Flutter toolchains, each
721
+ // skipped when the workspace has no project in that language.
722
+ "npx mnci ci setup"
719
723
  ].join(" && "),
720
724
  // The same recommendations the `.code-workspace` file carries, so opening
721
725
  // the folder in a container suggests the identical toolset.
@@ -786,6 +790,9 @@ function eslintToolchainDependencies(nxVersion) {
786
790
  "@mnci/eslint-config": eslintConfigSpec()
787
791
  };
788
792
  }
793
+ function mnciToolchainDependencies(nxVersion) {
794
+ return { ...eslintToolchainDependencies(nxVersion), "@mnci/cli": cliSpec() };
795
+ }
789
796
  var RETIRED_FORMATTER_FILES = [
790
797
  ".prettierrc",
791
798
  ".prettierrc.json",
@@ -1151,13 +1158,7 @@ function nugetConfigContent(registry, scope) {
1151
1158
  }
1152
1159
  var PYTHON_INSTALL_GUARD = `node -e "if(!require('node:fs').existsSync('requirements-dev.txt')){console.log('No Python projects - skipping.');process.exit(0)}const py=process.platform==='win32'?'python':'python3';process.exit(require('node:child_process').spawnSync(py+' -m pip install -r requirements-dev.txt',{stdio:'inherit',shell:true}).status ?? 1)"`;
1153
1160
  var PYTHON_WORKSPACE_INSTALL_GUARD = `node -e "const fs=require('node:fs'),path=require('node:path');const editableDirs=[...fs.globSync('apps/*/pyproject.toml'),...fs.globSync('python-packages/*/pyproject.toml'),...fs.globSync('libs/*/pyproject.toml')].map((p)=>path.dirname(p));const requirementsFiles=fs.globSync('apps/*/requirements.txt');if(editableDirs.length===0&&requirementsFiles.length===0){console.log('No Python projects - skipping.');process.exit(0)}const args=['-m','pip','install','--quiet',...editableDirs.flatMap((d)=>['-e',d]),...requirementsFiles.flatMap((f)=>['-r',f])];const py=process.platform==='win32'?'python':'python3';process.exit(require('node:child_process').spawnSync(py,args,{stdio:'inherit'}).status ?? 1)"`;
1154
- var PIP_AUDIT_GUARD = `node -e "if(!require('node:fs').existsSync('requirements-dev.txt')){console.log('No Python projects - skipping.');process.exit(0)}const py=process.platform==='win32'?'python':'python3';require('node:child_process').spawnSync(py,['-m','pip_audit'],{stdio:'inherit'});process.exit(0)"`;
1155
- var GO_MODULE_DOWNLOAD_GUARD = `node -e "if(!require('node:fs').existsSync('go.mod')){console.log('No Go projects - skipping.');process.exit(0)}process.exit(require('node:child_process').spawnSync('go',['mod','download'],{stdio:'inherit'}).status ?? 1)"`;
1156
1161
  var GOLANGCI_LINT_VERSION = "2.14.0";
1157
- var GOLANGCI_LINT_INSTALL_GUARD = `node -e "const fs=require('node:fs'),cp=require('node:child_process'),path=require('node:path'),os=require('node:os'),crypto=require('node:crypto');if(!fs.existsSync('go.mod')){console.log('No Go projects - skipping.');process.exit(0)}if(cp.spawnSync('golangci-lint',['--version'],{stdio:'ignore'}).status===0){console.log('golangci-lint already installed - skipping.');process.exit(0)}const v='${GOLANGCI_LINT_VERSION}';const fallback=reason=>{console.log('Prebuilt golangci-lint '+v+' unavailable ('+reason+') - building it with go install instead.');process.exit(cp.spawnSync('go',['install','github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v'+v],{stdio:'inherit'}).status ?? 1)};const plat={linux:'linux',darwin:'darwin',win32:'windows'}[process.platform],arch={x64:'amd64',arm64:'arm64'}[process.arch];if(!plat||!arch)fallback('no prebuilt release for '+process.platform+'/'+process.arch);const gopath=cp.spawnSync('go',['env','GOPATH'],{encoding:'utf8'});if(gopath.status!==0)fallback('go env GOPATH failed');const name='golangci-lint-'+v+'-'+plat+'-'+arch,file=name+(plat==='windows'?'.zip':'.tar.gz'),base='https://github.com/golangci/golangci-lint/releases/download/v'+v+'/';const get=async u=>{const r=await fetch(u,{signal:AbortSignal.timeout(60000)});if(!r.ok)throw new Error('HTTP '+r.status+' for '+u);return Buffer.from(await r.arrayBuffer())};(async()=>{` + String.raw`const sums=(await get(base+'golangci-lint-'+v+'-checksums.txt')).toString('utf8').split('\n').map(l=>l.trim().split(' ').filter(Boolean));` + `const entry=sums.find(p=>p[1]===file);if(!entry)throw new Error(file+' is not in the release checksums');const archive=await get(base+file);if(crypto.createHash('sha256').update(archive).digest('hex')!==entry[0])throw new Error('checksum mismatch for '+file);const tmp=fs.mkdtempSync(path.join(os.tmpdir(),'golangci-lint-')),archivePath=path.join(tmp,file);fs.writeFileSync(archivePath,archive);const tar=plat==='windows'?path.join(process.env.SystemRoot||'','System32','tar.exe'):'tar';const x=cp.spawnSync(tar,['-xf',archivePath,'-C',tmp],{stdio:'inherit'});if(x.status!==0)throw new Error('could not extract '+file);const exe='golangci-lint'+(plat==='windows'?'.exe':''),bin=path.join(gopath.stdout.trim(),'bin');fs.mkdirSync(bin,{recursive:true});fs.copyFileSync(path.join(tmp,name,exe),path.join(bin,exe));fs.chmodSync(path.join(bin,exe),0o755);fs.rmSync(tmp,{recursive:true,force:true});console.log('golangci-lint '+v+' installed from its checksum-verified prebuilt release into '+bin)})().catch(error=>fallback(error.message))"`;
1158
- 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');";
1159
- var GO_TOOL_PATH_AZURE = `node -e "${GO_TOOL_PATH_PRELUDE}console.log('##vso[task.prependpath]'+bin)"`;
1160
- 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'`})"`;
1161
1162
  var FLUTTER_SDK_VERSION = "3.44.8";
1162
1163
  var NODE_VERSION = "24";
1163
1164
  var ACTION_VERSIONS = {
@@ -1170,24 +1171,10 @@ var ACTION_VERSIONS = {
1170
1171
  var NPM_VERSION = "11";
1171
1172
  var DOTNET_SDK_VERSION = "10.0.x";
1172
1173
  var DOTNET_DETECT_AZURE = `node -e "const fs=require('node:fs');const has=[...fs.globSync('apps/*/*.csproj'),...fs.globSync('packages/*/*.csproj'),...fs.globSync('libs/*/*.csproj')].length>0;console.log('##vso[task.setvariable variable=hasDotnetProjects]'+has)"`;
1173
- var FLUTTER_SDK_DIRECTORY_EXPRESSION = `require('node:path').join(require('node:os').homedir(),'.mnci-flutter-${FLUTTER_SDK_VERSION}')`;
1174
- 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)"`;
1175
- 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');`;
1176
- var FLUTTER_TOOL_PATH_AZURE = `node -e "${FLUTTER_TOOL_PATH_PRELUDE}console.log('##vso[task.prependpath]'+bin)"`;
1177
- 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'`})"`;
1178
- 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)"`;
1179
- var NPM_AUDIT_STEP = `node -e "const cp=require('node:child_process');const r=cp.spawnSync('npm',['audit','--json'],{encoding:'utf8',shell:process.platform==='win32',maxBuffer:33554432});let d;try{d=JSON.parse(r.stdout)}catch{console.log('npm audit produced no JSON (exit '+r.status+') - not blocking on a broken audit.');process.exit(0)}const all=Object.values(d.vulnerabilities||{});const BLOCK=['critical','high','moderate'];const major=v=>v.fixAvailable&&typeof v.fixAvailable==='object'&&v.fixAvailable.isSemVerMajor;const act=all.filter(v=>v.fixAvailable&&BLOCK.includes(v.severity)&&!major(v));const rest=all.filter(v=>!act.includes(v));for(const v of rest)console.log(' note ['+v.severity+'] '+v.name+(!v.fixAvailable?' - NO fix available upstream, nothing to do here':major(v)?' - only a semver-major change would remove it (often a parent downgrade), not applied automatically':' - fix available, below the blocking threshold'));if(act.length===0){console.log('npm audit - '+all.length+' advisory(ies), none actionable at moderate or above.');process.exit(0)}for(const v of act)console.log(' BLOCKING ['+v.severity+'] '+v.name+' - fix available');console.log('Each has a published fix. Add a targeted overrides entry in package.json rather than npm audit fix --force.');process.exit(1)"`;
1180
1174
  var VERIFY_TARGETS = "lint,typecheck,test,build";
1181
- function affectedOrAllGuard(exclude = "") {
1182
- const extra = exclude === "" ? "" : `+' ${exclude}'`;
1183
- return `node -e "const cp=require('node:child_process');const T='${VERIFY_TARGETS}';const all=()=>process.exit(cp.spawnSync('npx nx run-many -t '+T${extra},{stdio:'inherit',shell:true}).status ?? 1);const ref=process.env.GITHUB_BASE_REF||process.env.SYSTEM_PULLREQUEST_TARGETBRANCH||'';if(!ref){console.log('Not a pull request - verifying EVERY project.');all()}const target=ref.replace('refs/heads/','');const mergeBase=r=>{const o=cp.spawnSync('git',['merge-base',r,'HEAD'],{encoding:'utf8'});return o.status===0?o.stdout.trim():''};let base=mergeBase('origin/'+target);if(!base){console.log('No origin/'+target+' ref - fetching it to resolve a merge-base.');cp.spawnSync('git',['fetch','--no-tags','origin',target],{stdio:'inherit'});base=mergeBase('FETCH_HEAD')}if(!base){console.log('Could not resolve a merge-base with '+target+' - verifying EVERY project.');all()}console.log('Pull request against '+target+' - verifying projects affected since '+base);process.exit(cp.spawnSync('npx nx affected -t '+T+' --base='+base${extra},{stdio:'inherit',shell:true}).status ?? 1)"`;
1184
- }
1185
- var AFFECTED_OR_ALL_GUARD = affectedOrAllGuard();
1186
- var AFFECTED_OR_ALL_GUARD_WITHOUT_NATIVE = affectedOrAllGuard(`--exclude=tag:${GO_CGO_TAG}`);
1187
1175
  var NATIVE_TARGETS = "lint,test,build-native,package-native";
1188
- var NATIVE_BUILD_COMMAND = `npx nx run-many -t ${NATIVE_TARGETS} --projects=tag:${GO_CGO_TAG}`;
1189
1176
  var NATIVE_LINUX_PREREQUISITES = "sudo apt-get update && sudo apt-get install -y gcc pkg-config";
1190
- function githubNativeJob(npmAuthName, npmAuthValue, onMain) {
1177
+ function githubNativeJob(npmAuthName, npmAuthValue) {
1191
1178
  return `
1192
1179
  # Apps that need a C toolchain (mnci add go-app --cgo) cannot be cross-compiled
1193
1180
  # from the job above, so each is linted, tested, built and packaged here, on a
@@ -1222,14 +1209,10 @@ function githubNativeJob(npmAuthName, npmAuthValue, onMain) {
1222
1209
  env:
1223
1210
  ${npmAuthName}: ${npmAuthValue}
1224
1211
 
1225
- - run: ${GO_MODULE_DOWNLOAD_GUARD}
1226
- name: Download Go module dependencies
1227
-
1228
- - run: ${GOLANGCI_LINT_INSTALL_GUARD}
1229
- name: Install golangci-lint
1230
-
1231
- - run: ${GO_TOOL_PATH_GITHUB}
1232
- name: Add Go tool bin to PATH
1212
+ # The Go toolchain: the modules, and a checksum-verified golangci-lint put
1213
+ # on PATH. The other toolchains are skipped cleanly when absent.
1214
+ - run: npx mnci ci setup
1215
+ name: Set up the Go toolchain
1233
1216
 
1234
1217
  # A C compiler and pkg-config, which is all mnci can know a cgo app needs.
1235
1218
  # Add the -dev packages your app links to this line, for example
@@ -1239,22 +1222,20 @@ function githubNativeJob(npmAuthName, npmAuthValue, onMain) {
1239
1222
  name: Install native prerequisites (Linux)
1240
1223
  if: \${{ runner.os == 'Linux' }}
1241
1224
 
1242
- - run: ${NATIVE_BUILD_COMMAND}
1225
+ # Lints, tests, builds and packages the native apps on this OS. On a push to
1226
+ # main, a releasable native app (--cgo with --release) also gets this OS's zip
1227
+ # attached to the GitHub Release the ci job just created, stamped with the
1228
+ # tag's version.
1229
+ - run: npx mnci ci native
1243
1230
  name: Lint, test, build and package the native apps
1231
+ env:
1232
+ GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
1244
1233
 
1245
1234
  - uses: actions/upload-artifact@${ACTION_VERSIONS["actions/upload-artifact"]}
1246
1235
  with:
1247
1236
  name: native-\${{ matrix.os }}
1248
1237
  path: dist/drop
1249
1238
  if-no-files-found: ignore
1250
-
1251
- # A releasable native app (--cgo with --release) gets this OS's zip attached to
1252
- # the GitHub Release the ci job just created, stamped with the tag's version.
1253
- - run: node tools/go-app-release.cjs assets --native
1254
- name: Attach this OS's zip to the GitHub Release (releasable native apps)
1255
- if: \${{ ${onMain} && hashFiles('tools/go-app-release.cjs') != '' }}
1256
- env:
1257
- GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
1258
1239
  `;
1259
1240
  }
1260
1241
  function indentYaml(text, spaces) {
@@ -1299,40 +1280,12 @@ ${matrix}
1299
1280
  ${indentYaml(nativeSteps, 4)}
1300
1281
  `;
1301
1282
  }
1302
- var PACK_APPS_GUARD = `node -e "const fs=require('node:fs');fs.mkdirSync('dist/drop',{recursive:true});const hasProjectJson=fs.globSync('apps/*/project.json').length>0;const hasInlineNx=fs.globSync('apps/*/package.json').some((f)=>{try{return Boolean(JSON.parse(fs.readFileSync(f,'utf8')).nx)}catch{return false}});const hasCsproj=fs.globSync('apps/*/*.csproj').length>0;if(!hasProjectJson&&!hasInlineNx&&!hasCsproj){console.log('No apps to pack - skipping.');process.exit(0)}process.exit(require('node:child_process').spawnSync('npx nx run-many -t package',{stdio:'inherit',shell:true}).status ?? 1)"`;
1303
- var NPM_AUTH_PREFLIGHT = `node -e "const fs=require('node:fs'),cp=require('node:child_process');if(fs.globSync('packages/*/package.json').length===0){console.log('No npm packages to release - skipping.');process.exit(0)}if(!process.env.NODE_AUTH_TOKEN){console.error('NPM_TOKEN is empty or unset, so the publish would fail AFTER nx release has already tagged. Add it as a repository secret named exactly NPM_TOKEN - under Actions, not the Dependabot or Codespaces tab, and not an Environment secret. Use an npm Automation token - a Publish token is refused by 2FA in CI.');process.exit(1)}const r=cp.spawnSync('npm',['whoami','--registry=https://registry.npmjs.org/'],{encoding:'utf8',shell:process.platform==='win32'});if(r.status!==0){console.error('NPM_TOKEN is set but the registry rejected it - '+((r.stderr||'')+(r.stdout||'')).trim()+'. Check that it has not expired and that it grants publish rights on this scope.');process.exit(1)}console.log('npm auth OK as '+r.stdout.trim())"`;
1304
- function npmAuthPreflight(registryKind) {
1305
- return registryKind === "npm" ? NPM_AUTH_PREFLIGHT : "";
1306
- }
1307
- 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.')})()"`;
1308
- function pypiReleasePreflight(registryKind) {
1309
- return registryKind === "npm" ? PYPI_RELEASE_PREFLIGHT : "";
1310
- }
1311
- 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)}"`;
1312
- function releaseGuard(pythonPublishEnv, nugetPublishEnv) {
1313
- 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 vscodeCount=fs.globSync('apps/*/package.json').filter(p=>{try{return(JSON.parse(fs.readFileSync(p,'utf8')).nx?.tags||[]).includes('${VSCODE_EXTENSION_TAG}')}catch{return false}}).length;const goCount=fs.globSync('apps/*/project.json').filter(p=>{try{return(JSON.parse(fs.readFileSync(p,'utf8')).tags||[]).includes('${GO_RELEASE_TAG}')}catch{return false}}).length;const hasNpm=npmCount>0;const hasPython=pythonCount>0;const hasCsharp=csharpCount>0;const hasDart=dartCount>0;const hasVscode=vscodeCount>0;const hasGo=goCount>0;if(!hasNpm&&!hasPython&&!hasCsharp&&!hasDart&&!hasVscode&&!hasGo){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+vscodeCount+goCount;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)"`;
1314
- }
1315
1283
  var PYPI_TOKEN_VARIABLE = "PYPI_TOKEN";
1316
1284
  var AZURE_PAT_VARIABLE = "PAT";
1317
- function azurePatPreflight(countExpression, subject) {
1318
- return `if(!process.env.${AZURE_PAT_VARIABLE}){console.error('This workspace has '+${countExpression}+' ${subject} package(s) to publish to Azure Artifacts but ${AZURE_PAT_VARIABLE} is empty. Add your base64-encoded Azure DevOps PAT as a secret named ${AZURE_PAT_VARIABLE}, or remove the ${subject} packages from release.projects.');process.exit(1)}`;
1319
- }
1320
1285
  function pypiTokenEnvLine(registryKind, variableReference, indent) {
1321
1286
  return registryKind === "npm" ? `
1322
1287
  ${indent}${PYPI_TOKEN_VARIABLE}: ${variableReference(PYPI_TOKEN_VARIABLE)}` : "";
1323
1288
  }
1324
- function pythonPublishEnvFragment(pythonPublishUrl2, registryKind) {
1325
- if (pythonPublishUrl2 !== void 0) {
1326
- return `if(hasPython){${azurePatPreflight("pythonCount", "Python")}env.TWINE_REPOSITORY_URL='${pythonPublishUrl2}';env.TWINE_USERNAME='AzureArtifacts';env.TWINE_PASSWORD=Buffer.from(process.env.${AZURE_PAT_VARIABLE},'base64').toString()}`;
1327
- }
1328
- if (registryKind !== "npm") {
1329
- return "";
1330
- }
1331
- return `if(hasPython){if(!process.env.${PYPI_TOKEN_VARIABLE}){console.error('This workspace has '+pythonCount+' Python package(s) to publish but ${PYPI_TOKEN_VARIABLE} is empty. Add a PyPI API token as a secret named ${PYPI_TOKEN_VARIABLE}, or remove the Python packages from release.projects.');process.exit(1)}env.TWINE_USERNAME='__token__';env.TWINE_PASSWORD=process.env.${PYPI_TOKEN_VARIABLE};env.TWINE_NON_INTERACTIVE='1'}`;
1332
- }
1333
- function nugetPublishEnvFragment(nugetFeedUrl2) {
1334
- return nugetFeedUrl2 ? `if(hasCsharp){${azurePatPreflight("csharpCount", "C#")}env.NUGET_PAT=Buffer.from(process.env.${AZURE_PAT_VARIABLE},'base64').toString()}` : "";
1335
- }
1336
1289
  function npmAuthEnvVariable(registryKind, variableReference) {
1337
1290
  return registryKind === "npm" ? ["NODE_AUTH_TOKEN", variableReference("NPM_TOKEN")] : [AZURE_PAT_VARIABLE, variableReference(AZURE_PAT_VARIABLE)];
1338
1291
  }
@@ -1476,66 +1429,25 @@ steps:
1476
1429
  ${npmAuthenticateStep} - script: npm ci
1477
1430
  displayName: Install dependencies${npmCiEnv}
1478
1431
 
1432
+ # Everything from here to the release is a command of the mnci CLI, a
1433
+ # devDependency of this workspace, so the version is the lockfile's and a
1434
+ # developer reproduces CI by running the same line. The guards that used to be
1435
+ # inline here are tested TypeScript in the CLI's ci-pipeline slice. Node, the
1436
+ # checkout and 'npm ci' stay above: Node must exist before 'npx' can run.
1437
+
1438
+ # The language toolchains the workspace needs: Python (ruff, pytest, build,
1439
+ # twine, and every project installed into one shared environment), Go (the
1440
+ # modules and a checksum-verified golangci-lint, put on PATH) and Flutter (the
1441
+ # pinned SDK, put on PATH, and one pub get). Each is skipped cleanly when the
1442
+ # workspace has no project in that language.
1443
+ - script: npx mnci ci setup
1444
+ displayName: Set up the language toolchains (Python, Go, Flutter)
1445
+
1479
1446
  # Fails ONLY on an advisory that has a published fix, at moderate or above;
1480
- # anything upstream has not fixed is printed and passes. See NPM_AUDIT_STEP's
1481
- # remarks for why that split replaced a warn-only step.
1482
- - script: ${NPM_AUDIT_STEP}
1483
- displayName: npm audit (fails on an actionable advisory)
1484
-
1485
- # Installs the fixed Python toolchain (ruff, pytest, build, twine, pip-audit)
1486
- # \u2014 written by 'mnci add' to requirements-dev.txt on the first Python
1487
- # project. Plain pip, no uv/Poetry: portable guard skips cleanly on a
1488
- # workspace with none.
1489
- - script: ${PYTHON_INSTALL_GUARD}
1490
- displayName: Install Python dependencies (ruff, pytest, build, twine, pip-audit)
1491
-
1492
- # Editable-installs every Python project into one shared environment, the
1493
- # pip-world counterpart of 'npm install' hoisting every workspace package
1494
- # into one root node_modules \u2014 so a project that imports an internal lib
1495
- # (normally vendored only at build time) can resolve that import at
1496
- # lint/test time too. Portable guard skips cleanly on a workspace with none.
1497
- - script: ${PYTHON_WORKSPACE_INSTALL_GUARD}
1498
- displayName: Install Python project dependencies (editable, workspace-wide)
1499
-
1500
- # Non-blocking, same reasoning as the npm audit step above. Runs after the
1501
- # workspace-wide install so it scans the workspace's actual dependency set,
1502
- # not just the fixed toolchain.
1503
- - script: ${PIP_AUDIT_GUARD}
1504
- displayName: pip-audit (non-blocking)
1505
-
1506
- # Go, if the workspace has any: hosted agents ship the toolchain, so only
1507
- # the module cache and the linter need seeding. Both guards skip cleanly on
1508
- # a workspace with no root go.mod.
1509
- - script: ${GO_MODULE_DOWNLOAD_GUARD}
1510
- displayName: Download Go module dependencies
1511
-
1512
- # golangci-lint is what the generated Go lint target actually runs (the
1513
- # plugin's own default is 'go fmt', which only reformats). The pinned,
1514
- # checksum-verified prebuilt release lands in GOPATH/bin (go install at the
1515
- # same version is the fallback), which is not on PATH by default on a hosted
1516
- # agent \u2014 so prepend it for every later step in the job.
1517
- - script: ${GOLANGCI_LINT_INSTALL_GUARD}
1518
- displayName: Install golangci-lint
1519
-
1520
- - script: ${GO_TOOL_PATH_AZURE}
1521
- displayName: Add Go tool bin to PATH
1522
-
1523
- # Flutter, if the workspace has any. Unlike Python and Go, hosted agents do
1524
- # NOT ship the Flutter SDK, so this installs it (shallow git clone at a
1525
- # pinned tag) and puts it on PATH. All three guards skip cleanly on a
1526
- # workspace with no root pubspec.yaml, and the install also skips when an
1527
- # SDK is already on PATH.
1528
- - script: ${FLUTTER_SDK_INSTALL_GUARD}
1529
- displayName: Install the Flutter SDK (${FLUTTER_SDK_VERSION})
1530
-
1531
- - script: ${FLUTTER_TOOL_PATH_AZURE}
1532
- displayName: Add the Flutter SDK to PATH
1533
-
1534
- # One command resolves EVERY Dart dependency, internal and external, for the
1535
- # whole workspace: the projects are pub workspace members, so this writes a
1536
- # single root pubspec.lock they all share.
1537
- - script: ${FLUTTER_PUB_GET_GUARD}
1538
- displayName: Resolve Dart dependencies (one pub get for the whole workspace)
1447
+ # anything upstream has not fixed is printed and passes. pip-audit only
1448
+ # reports: its output has no "a fix exists" field to draw that line with.
1449
+ - script: npx mnci ci audit
1450
+ displayName: Audit dependencies (fails on an actionable npm advisory)
1539
1451
 
1540
1452
  # .NET, if the workspace has any. Azure's 'condition:' expression language
1541
1453
  # has no glob function, so this script step detects C# projects first and
@@ -1551,29 +1463,18 @@ ${npmAuthenticateStep} - script: npm ci
1551
1463
  inputs:
1552
1464
  version: ${DOTNET_SDK_VERSION}
1553
1465
 
1554
- # Fails fast, with an unambiguous message, when a stale TypeScript project
1555
- # reference (or another sync generator's drift) was never synced+committed
1556
- # locally \u2014 sync.applyChanges (nx.json) only auto-applies interactively, so
1557
- # CI still needs its own explicit, early check rather than surfacing this as
1558
- # a confusing failure buried inside the build step below.
1559
- - script: npx nx sync:check
1560
- displayName: Verify the workspace is synced (run 'npx nx sync' locally and commit if this fails)
1561
-
1562
- # The one verify step, and deliberately the only one: affected projects on a
1563
- # pull request, EVERY project on anything else (a push to main included, so a
1564
- # release is always verified in full). Every fallback \u2014 no target branch, an
1565
- # unresolvable merge-base \u2014 takes the full path, because a run that verifies
1566
- # too little still reports green.
1567
- #
1568
- # 'npm run lint' is 'nx run-many -t lint', a strict subset of the targets
1569
- # below, so adding it back as its own step would only duplicate work \u2014 and on
1570
- # a pull request it would re-lint every project, discarding the point of this.
1571
- - script: ${nativeApps ? AFFECTED_OR_ALL_GUARD_WITHOUT_NATIVE : AFFECTED_OR_ALL_GUARD}
1572
- displayName: Verify (affected on a PR, every project on main)
1466
+ # Fails fast on a stale TypeScript project reference, then the one verify
1467
+ # step, and deliberately the only one: affected projects on a pull request,
1468
+ # EVERY project on anything else (a push to main included, so a release is
1469
+ # always verified in full). Every fallback takes the full path, because a run
1470
+ # that verifies too little still reports green. Apps that need a C toolchain
1471
+ # are left to the native job.
1472
+ - script: npx mnci ci verify
1473
+ displayName: Verify (sync check, then affected on a PR, every project on main)
1573
1474
 
1574
1475
  # Pack every app into dist/drop/<type>-<name>.zip via each app's 'package'
1575
- # target. Portable guard: skip cleanly when the workspace has no apps yet.
1576
- - script: ${PACK_APPS_GUARD}
1476
+ # target; skipped cleanly when the workspace has no apps yet.
1477
+ - script: npx mnci ci pack
1577
1478
  displayName: Pack all apps (one zip per app -> dist/drop)
1578
1479
  condition: ${onMain}
1579
1480
 
@@ -1591,56 +1492,23 @@ ${npmAuthenticateStep} - script: npm ci
1591
1492
  displayName: Tag the run per app (type-name)
1592
1493
  condition: ${onMain}
1593
1494
 
1594
- # nx release resolves each package's current version from its git tag, and
1595
- # falls back to the (permanently stale, since git.commit is false) on-disk
1596
- # version when a tag cannot be resolved \u2014 which happens whenever this
1597
- # checkout lacks full history. This is currently guaranteed by fetchDepth 0
1598
- # above plus the explicit fetch below, but neither is expressed as a
1599
- # dependency anywhere else, so this makes it a hard, loud precondition
1600
- # instead of a silently wrong version discovered after release.
1601
- - script: ${SHALLOW_CLONE_GUARD}
1602
- displayName: Verify this is a full checkout (nx release needs the real tag history)
1603
- condition: ${onMain}
1604
- ${npmAuthPreflight(registryKind) ? `
1605
- # Prove npm will accept us BEFORE anything irreversible happens. nx release
1606
- # tags first and publishes last, so a rejected token leaves a version tagged
1607
- # with nothing on the registry - and that number is then skipped forever,
1608
- # because the next run resolves the current version from the newest tag.
1609
- - script: ${npmAuthPreflight(registryKind)}
1610
- displayName: Preflight \u2014 npm must accept the token before anything is tagged
1611
- condition: ${onMain}
1612
- env:
1613
- NODE_AUTH_TOKEN: $(NPM_TOKEN)
1614
- ` : ""}${pypiReleasePreflight(registryKind) ? `
1615
- # PyPI cannot be asked whether a token is good - the only endpoint that
1616
- # authenticates is the upload itself - so this checks the token's SHAPE and
1617
- # names the projects this release would have to CREATE. Project creation is
1618
- # rate limited per account, and a 429 arrives AFTER nx release has tagged.
1619
- - script: ${pypiReleasePreflight(registryKind)}
1620
- displayName: Preflight \u2014 name the PyPI projects this release would create
1621
- condition: ${onMain}
1622
- env:
1623
- PYPI_TOKEN: $(PYPI_TOKEN)
1624
- ` : ""}
1625
- # Version + tag + publish, in one release, for npm (packages/*), Python
1626
- # (python-packages/*) AND C# (packages/*/*.csproj) \u2014 conventional commits,
1627
- # tag-only push. Portable guard: nx release errors on an empty scope, so
1628
- # skip cleanly when there is nothing to release. When there are Python or
1629
- # C# packages and an Azure feed, twine/NuGet publish credentials are
1630
- # exported (raw PAT, decoded from the base64 variable).
1495
+ # The release, in the order that keeps a failure safe: refuse a shallow
1496
+ # clone, prove the npm token and name the PyPI projects it would create (all
1497
+ # BEFORE anything is tagged), then version + tag + publish in one 'nx release'
1498
+ # for npm, Python, C# and VS Code, then push the tags explicitly (nx release's
1499
+ # own push never runs without a remote GitHub/GitLab Release configured, which
1500
+ # this pipeline never does). Which registry is in use is read from the 'mnci'
1501
+ # block of nx.json, not baked into this file. The secrets are mapped here, in
1502
+ # the one place a command cannot reach.
1631
1503
  #
1632
- # RELEASE_SPECIFIER (the pipeline variable above) overrides the bump nx
1633
- # would compute from conventional commits for THIS run. In a workspace with
1634
- # more than one releasable package, a bare keyword ('major'/'minor'/'patch')
1635
- # is unreliable \u2014 nx versions interdependent packages in two passes (a
1636
- # dependency-bump pass, then the specifier pass), and the second pass
1637
- # computes from a version cached before the first pass ran, silently
1638
- # landing back on the same patch bump either way. An exact version ('1.2.3')
1639
- # does not have this problem, since both passes apply it verbatim \u2014 prefer
1640
- # it whenever more than one package is releasable. The guard below fails
1641
- # the run rather than under-bumping silently: clear the variable back to
1642
- # '' once the override is no longer needed.
1643
- - script: ${releaseGuard(pythonPublishEnvFragment(pythonPublishUrl2, registryKind), nugetPublishEnvFragment(nugetFeedUrl2))}
1504
+ # RELEASE_SPECIFIER (the pipeline variable above) overrides the bump nx would
1505
+ # compute from conventional commits for THIS run: 'major', 'minor', 'patch', or
1506
+ # an exact version ('1.2.3'). With more than one releasable package a bare
1507
+ # keyword is unreliable (nx versions interdependent packages in two passes, and
1508
+ # the second computes from a version cached before the first), so the command
1509
+ # fails the run rather than under-bumping silently: prefer an exact version,
1510
+ # and clear the variable back to '' once the override is no longer needed.
1511
+ - script: npx mnci ci release
1644
1512
  displayName: Release \u2014 version, tag and publish (npm + Python + C# + VS Code)
1645
1513
  condition: ${onMain}
1646
1514
  env:
@@ -1649,15 +1517,6 @@ ${npmAuthPreflight(registryKind) ? `
1649
1517
  # A VS Code extension publishes with it; tools/vscode-extension.cjs skips the
1650
1518
  # Marketplace when it is unset, including when Azure leaves it as '$(VSCE_PAT)'.
1651
1519
  VSCE_PAT: $(VSCE_PAT)
1652
-
1653
- # nx release's own git push (release.git.push) is deliberately left off: it
1654
- # only runs when a remote GitHub/GitLab Release is configured, which this
1655
- # pipeline never does, so it would never push the tag the step above just
1656
- # created. Pushed explicitly, unconditionally (a no-op when nothing released)
1657
- # once tagging is guaranteed to have already happened.
1658
- - script: git push origin --tags
1659
- displayName: Push release tags (nx release's own push never runs without a remote Release configured)
1660
- condition: ${onMain}
1661
1520
  `;
1662
1521
  if (!nativeApps) {
1663
1522
  return document;
@@ -1680,14 +1539,10 @@ ${npmAuthPreflight(registryKind) ? `
1680
1539
  ${npmAuthenticateStep} - script: npm ci
1681
1540
  displayName: Install dependencies${npmCiEnv}
1682
1541
 
1683
- - script: ${GO_MODULE_DOWNLOAD_GUARD}
1684
- displayName: Download Go module dependencies
1685
-
1686
- - script: ${GOLANGCI_LINT_INSTALL_GUARD}
1687
- displayName: Install golangci-lint
1688
-
1689
- - script: ${GO_TOOL_PATH_AZURE}
1690
- displayName: Add Go tool bin to PATH
1542
+ # The Go toolchain: the modules, and a checksum-verified golangci-lint put on
1543
+ # PATH. The other toolchains are skipped cleanly when absent.
1544
+ - script: npx mnci ci setup
1545
+ displayName: Set up the Go toolchain
1691
1546
 
1692
1547
  # A C compiler and pkg-config, which is all mnci can know a cgo app needs. Add the
1693
1548
  # -dev packages your app links to this line, for example libgtk-3-dev and
@@ -1697,7 +1552,7 @@ ${npmAuthenticateStep} - script: npm ci
1697
1552
  displayName: Install native prerequisites (Linux)
1698
1553
  condition: eq(variables['Agent.OS'], 'Linux')
1699
1554
 
1700
- - script: ${NATIVE_BUILD_COMMAND}
1555
+ - script: npx mnci ci native
1701
1556
  displayName: Lint, test, build and package the native apps
1702
1557
 
1703
1558
  - task: PublishBuildArtifacts@1
@@ -1788,66 +1643,25 @@ jobs:
1788
1643
  env:
1789
1644
  ${npmAuthName}: ${npmAuthValue}
1790
1645
 
1791
- # Fails ONLY on an advisory that has a published fix, at moderate or
1792
- # above; anything upstream has not fixed is printed and passes. See
1793
- # NPM_AUDIT_STEP's remarks for why that split replaced a warn-only step.
1794
- - run: ${NPM_AUDIT_STEP}
1795
- name: npm audit (fails on an actionable advisory)
1796
-
1797
- # Installs the fixed Python toolchain (ruff, pytest, build, twine,
1798
- # pip-audit) \u2014 written by 'mnci add' to requirements-dev.txt on the
1799
- # first Python project. Plain pip, no uv/Poetry: portable guard skips
1800
- # cleanly on a workspace with none.
1801
- - run: ${PYTHON_INSTALL_GUARD}
1802
- name: Install Python dependencies (ruff, pytest, build, twine, pip-audit)
1803
-
1804
- # Editable-installs every Python project into one shared environment, the
1805
- # pip-world counterpart of 'npm install' hoisting every workspace package
1806
- # into one root node_modules \u2014 so a project that imports an internal lib
1807
- # (normally vendored only at build time) can resolve that import at
1808
- # lint/test time too. Portable guard skips cleanly on a workspace with none.
1809
- - run: ${PYTHON_WORKSPACE_INSTALL_GUARD}
1810
- name: Install Python project dependencies (editable, workspace-wide)
1811
-
1812
- # Non-blocking, same reasoning as the npm audit step above. Runs after
1813
- # the workspace-wide install so it scans the workspace's actual
1814
- # dependency set, not just the fixed toolchain.
1815
- - run: ${PIP_AUDIT_GUARD}
1816
- name: pip-audit (non-blocking)
1817
-
1818
- # Go, if the workspace has any: hosted runners ship the toolchain, so
1819
- # only the module cache and the linter need seeding. Both guards skip
1820
- # cleanly on a workspace with no root go.mod.
1821
- - run: ${GO_MODULE_DOWNLOAD_GUARD}
1822
- name: Download Go module dependencies
1823
-
1824
- # golangci-lint is what the generated Go lint target actually runs (the
1825
- # plugin's own default is 'go fmt', which only reformats). The pinned,
1826
- # checksum-verified prebuilt release lands in GOPATH/bin (go install at the
1827
- # same version is the fallback), which is not on PATH by default on a
1828
- # hosted runner \u2014 so publish it for every later step in the job.
1829
- - run: ${GOLANGCI_LINT_INSTALL_GUARD}
1830
- name: Install golangci-lint
1831
-
1832
- - run: ${GO_TOOL_PATH_GITHUB}
1833
- name: Add Go tool bin to PATH
1834
-
1835
- # Flutter, if the workspace has any. Unlike Python and Go, hosted runners
1836
- # do NOT ship the Flutter SDK, so this installs it (shallow git clone at
1837
- # a pinned tag) and puts it on PATH. All three guards skip cleanly on a
1838
- # workspace with no root pubspec.yaml, and the install also skips when an
1839
- # SDK is already on PATH.
1840
- - run: ${FLUTTER_SDK_INSTALL_GUARD}
1841
- name: Install the Flutter SDK (${FLUTTER_SDK_VERSION})
1842
-
1843
- - run: ${FLUTTER_TOOL_PATH_GITHUB}
1844
- name: Add the Flutter SDK to PATH
1845
-
1846
- # One command resolves EVERY Dart dependency, internal and external, for
1847
- # the whole workspace: the projects are pub workspace members, so this
1848
- # writes a single root pubspec.lock they all share.
1849
- - run: ${FLUTTER_PUB_GET_GUARD}
1850
- name: Resolve Dart dependencies (one pub get for the whole workspace)
1646
+ # Everything from here to the release is a command of the mnci CLI, a
1647
+ # devDependency of this workspace, so the version is the lockfile's and a
1648
+ # developer reproduces CI by running the same line. The guards that used to be
1649
+ # inline here are tested TypeScript in the CLI's ci-pipeline slice. Node,
1650
+ # the checkout and 'npm ci' stay above: Node must exist before 'npx' can run.
1651
+
1652
+ # The language toolchains the workspace needs: Python (ruff, pytest, build,
1653
+ # twine, and every project installed into one shared environment), Go (the
1654
+ # modules and a checksum-verified golangci-lint, put on PATH) and Flutter (the
1655
+ # pinned SDK, put on PATH, and one pub get). Each is skipped cleanly when the
1656
+ # workspace has no project in that language.
1657
+ - run: npx mnci ci setup
1658
+ name: Set up the language toolchains (Python, Go, Flutter)
1659
+
1660
+ # Fails ONLY on an advisory that has a published fix, at moderate or above;
1661
+ # anything upstream has not fixed is printed and passes. pip-audit only
1662
+ # reports: its output has no "a fix exists" field to draw that line with.
1663
+ - run: npx mnci ci audit
1664
+ name: Audit dependencies (fails on an actionable npm advisory)
1851
1665
 
1852
1666
  # .NET, if the workspace has any. actions/setup-dotnet is the
1853
1667
  # maintained, cached installer GitHub itself ships, so this uses it
@@ -1859,30 +1673,18 @@ jobs:
1859
1673
  with:
1860
1674
  dotnet-version: ${DOTNET_SDK_VERSION}
1861
1675
 
1862
- # Fails fast, with an unambiguous message, when a stale TypeScript project
1863
- # reference (or another sync generator's drift) was never synced+committed
1864
- # locally \u2014 sync.applyChanges (nx.json) only auto-applies interactively, so
1865
- # CI still needs its own explicit, early check rather than surfacing this as
1866
- # a confusing failure buried inside the build step below.
1867
- - run: npx nx sync:check
1868
- name: Verify the workspace is synced (run 'npx nx sync' locally and commit if this fails)
1869
-
1870
- # The one verify step, and deliberately the only one: affected projects on
1871
- # a pull request, EVERY project on anything else (a push to main included,
1872
- # so a release is always verified in full). Every fallback \u2014 no target
1873
- # branch, an unresolvable merge-base \u2014 takes the full path, because a run
1874
- # that verifies too little still reports green.
1875
- #
1876
- # 'npm run lint' is 'nx run-many -t lint', a strict subset of the targets
1877
- # below, so adding it back as its own step would only duplicate work \u2014 and
1878
- # on a pull request it would re-lint every project, discarding the point
1879
- # of this.
1880
- - run: ${nativeApps ? AFFECTED_OR_ALL_GUARD_WITHOUT_NATIVE : AFFECTED_OR_ALL_GUARD}
1881
- name: Verify (affected on a PR, every project on main)
1676
+ # Fails fast on a stale TypeScript project reference, then the one verify
1677
+ # step, and deliberately the only one: affected projects on a pull request,
1678
+ # EVERY project on anything else (a push to main included, so a release is
1679
+ # always verified in full). Every fallback takes the full path, because a run
1680
+ # that verifies too little still reports green. Apps that need a C toolchain
1681
+ # are left to the native job.
1682
+ - run: npx mnci ci verify
1683
+ name: Verify (sync check, then affected on a PR, every project on main)
1882
1684
 
1883
1685
  # Pack every app into dist/drop/<type>-<name>.zip via each app's 'package'
1884
- # target. Portable guard: skip cleanly when the workspace has no apps yet.
1885
- - run: ${PACK_APPS_GUARD}
1686
+ # target; skipped cleanly when the workspace has no apps yet.
1687
+ - run: npx mnci ci pack
1886
1688
  name: Pack all apps (one zip per app -> dist/drop)
1887
1689
  if: \${{ ${onMain} }}
1888
1690
 
@@ -1893,65 +1695,6 @@ jobs:
1893
1695
  path: dist/drop
1894
1696
  if-no-files-found: ignore
1895
1697
 
1896
- # nx release resolves each package's current version from its git tag,
1897
- # and falls back to the (permanently stale, since git.commit is false)
1898
- # on-disk version when a tag cannot be resolved \u2014 which happens
1899
- # whenever this checkout lacks full history. This is currently
1900
- # guaranteed by fetch-depth 0 above plus the explicit fetch below, but
1901
- # neither is expressed as a dependency anywhere else, so this makes it
1902
- # a hard, loud precondition instead of a silently wrong version
1903
- # discovered after release.
1904
- - run: ${SHALLOW_CLONE_GUARD}
1905
- name: Verify this is a full checkout (nx release needs the real tag history)
1906
- if: \${{ ${onMain} }}
1907
- ${npmAuthPreflight(registryKind) ? `
1908
- # Prove npm will accept us BEFORE anything irreversible happens. nx
1909
- # release tags first and publishes last, so a rejected token leaves a
1910
- # version tagged with nothing on the registry - and that number is then
1911
- # skipped forever, because the next run resolves the current version
1912
- # from the newest tag.
1913
- - run: ${npmAuthPreflight(registryKind)}
1914
- name: Preflight \u2014 npm must accept the token before anything is tagged
1915
- if: \${{ ${onMain} }}
1916
- env:
1917
- NODE_AUTH_TOKEN: \${{ secrets.NPM_TOKEN }}
1918
- ` : ""}${pypiReleasePreflight(registryKind) ? `
1919
- # PyPI cannot be asked whether a token is good - the only endpoint that
1920
- # authenticates is the upload itself - so this checks the token's SHAPE
1921
- # and names the projects this release would have to CREATE. Project
1922
- # creation is rate limited per account, and a 429 arrives AFTER nx
1923
- # release has tagged.
1924
- - run: ${pypiReleasePreflight(registryKind)}
1925
- name: Preflight \u2014 name the PyPI projects this release would create
1926
- if: \${{ ${onMain} }}
1927
- env:
1928
- PYPI_TOKEN: \${{ secrets.PYPI_TOKEN }}
1929
- ` : ""}
1930
- # Version + tag + publish, in one release, for npm (packages/*), Python
1931
- # (python-packages/*) AND C# (packages/*/*.csproj) \u2014 conventional
1932
- # commits, tag-only push. Portable guard: nx release errors on an empty
1933
- # scope, so skip cleanly when there is nothing to release. When there
1934
- # are Python or C# packages and an Azure feed, twine/NuGet publish
1935
- # credentials are exported (raw PAT, decoded from the base64 secret).
1936
- #
1937
- # RELEASE_SPECIFIER (repository variable, Settings -> Secrets and
1938
- # variables -> Actions -> Variables) overrides the bump nx would compute
1939
- # from conventional commits for THIS run: 'major', 'minor', 'patch', or
1940
- # an exact version ('1.2.3'). In a workspace with more than one
1941
- # releasable package, a bare keyword is unreliable \u2014 nx versions
1942
- # interdependent packages in two passes (a dependency-bump pass, then
1943
- # the specifier pass), and the second pass computes from a version
1944
- # cached before the first pass ran, silently landing back on the same
1945
- # patch bump either way. An exact version does not have this problem,
1946
- # since both passes apply it verbatim \u2014 prefer it whenever more than one
1947
- # package is releasable. The guard below fails the run rather than
1948
- # under-bumping silently: unset or clear the variable once the override
1949
- # is no longer needed.${githubReleases ? `
1950
- # This provider also creates a per-project GitHub Release (changelog
1951
- # generated from conventional commits since that project's last tag),
1952
- # which is why 'nx release' pushes the tag itself here \u2014 see
1953
- # releaseConfig's remarks for why that is safe on this Nx version and
1954
- # why every other provider combination keeps the explicit push step below.` : ""}
1955
1698
  # Signs in to Microsoft Entra ID as the Marketplace publishing identity (an
1956
1699
  # app registration with a federated credential for this repository's main
1957
1700
  # branch), so the release step publishes VS Code extensions with
@@ -1967,7 +1710,23 @@ ${npmAuthPreflight(registryKind) ? `
1967
1710
  tenant-id: \${{ vars.AZURE_TENANT_ID }}
1968
1711
  allow-no-subscriptions: true
1969
1712
 
1970
- - run: ${releaseGuard(pythonPublishEnvFragment(pythonPublishUrl2, registryKind), nugetPublishEnvFragment(nugetFeedUrl2))}
1713
+ # The release, in the order that keeps a failure safe: refuse a shallow
1714
+ # clone, prove the npm token and name the PyPI projects it would create (all
1715
+ # BEFORE anything is tagged), then version + tag + publish in one 'nx release'
1716
+ # for npm, Python, C# and VS Code, then attach a releasable Go app's zips and
1717
+ # push the tags. Which registry, and whether GitHub Releases are on, are read
1718
+ # from the 'mnci' block of nx.json, not baked into this file. The secrets are
1719
+ # mapped here, in the one place a command cannot reach.
1720
+ #
1721
+ # RELEASE_SPECIFIER (repository variable, Settings -> Secrets and
1722
+ # variables -> Actions -> Variables) overrides the bump nx would compute
1723
+ # from conventional commits for THIS run: 'major', 'minor', 'patch', or an
1724
+ # exact version ('1.2.3'). With more than one releasable package a bare
1725
+ # keyword is unreliable (nx versions interdependent packages in two passes,
1726
+ # and the second computes from a version cached before the first), so the
1727
+ # command fails the run rather than under-bumping silently: prefer an exact
1728
+ # version, and clear the variable once the override is no longer needed.
1729
+ - run: npx mnci ci release
1971
1730
  name: Release \u2014 version, tag${githubReleases ? ", publish and GitHub Release" : " and publish"} (npm + Python + C# + VS Code)
1972
1731
  if: \${{ ${onMain} }}
1973
1732
  env:
@@ -1978,28 +1737,9 @@ ${npmAuthPreflight(registryKind) ? `
1978
1737
  # tools/vscode-extension.cjs skips the Marketplace with neither.
1979
1738
  VSCE_AUTH: \${{ vars.AZURE_CLIENT_ID != '' && 'entra' || '' }}
1980
1739
  VSCE_PAT: \${{ secrets.VSCE_PAT }}${githubReleases ? `
1981
- GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}` : ""}
1982
- ${githubReleases ? `
1983
- # A releasable Go app (mnci add go-app --release) is versioned by its git
1984
- # tag, and its per-platform zips are attached to the GitHub Release the step
1985
- # above just created. Built here, after tagging, with VERSION set to the tag's
1986
- # version so the binary reports it. A run that released no Go app uploads
1987
- # nothing, and a workspace without one skips the step.
1988
- - run: node tools/go-app-release.cjs assets
1989
- name: Attach the per-platform zips to the GitHub Release (releasable Go apps)
1990
- if: \${{ ${onMain} && hashFiles('tools/go-app-release.cjs') != '' }}
1991
- env:
1992
- GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
1993
- ` : `
1994
- # nx release's own git push (release.git.push) is deliberately left off: it
1995
- # only runs when a remote GitHub/GitLab Release is configured, which this
1996
- # workflow never does, so it would never push the tag the step above just
1997
- # created. Pushed explicitly, unconditionally (a no-op when nothing released)
1998
- # once tagging is guaranteed to have already happened.
1999
- - run: git push origin --tags
2000
- name: Push release tags (nx release's own push never runs without a remote Release configured)
2001
- if: \${{ ${onMain} }}
2002
- `}${nativeApps ? githubNativeJob(npmAuthName, npmAuthValue, onMain) : ""}`;
1740
+ GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
1741
+ GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}` : ""}
1742
+ ${nativeApps ? githubNativeJob(npmAuthName, npmAuthValue) : ""}`;
2003
1743
  }
2004
1744
  var PIP_PROJECT_GLOBS = ["apps", "python-packages", "libs"];
2005
1745
  var PUB_PROJECT_GLOBS = ["apps", "packages", "libs"];
@@ -2235,7 +1975,7 @@ function applyOverlay(workspaceRoot, options, onProgress = () => {
2235
1975
  ...existingDevDeps,
2236
1976
  ...TS_COMPILER_DEPENDENCIES,
2237
1977
  // The preset pins `nx` itself; the ESLint plugins must match it exactly.
2238
- ...eslintToolchainDependencies(existingDevDeps?.nx ?? "latest")
1978
+ ...mnciToolchainDependencies(existingDevDeps?.nx ?? "latest")
2239
1979
  }),
2240
1980
  LOCAL_REGISTRY_SCAFFOLDING.devDependency
2241
1981
  );
@@ -2366,7 +2106,7 @@ function runNative(workspaceRoot, dependencies = {}) {
2366
2106
  if (built !== 0) {
2367
2107
  return built;
2368
2108
  }
2369
- if (!isMainPush(environment) || !(0, import_node_fs5.existsSync)((0, import_node_path4.join)(workspaceRoot, "tools", "go-app-release.cjs"))) {
2109
+ if (host !== "github" || !isMainPush(environment) || !(0, import_node_fs5.existsSync)((0, import_node_path4.join)(workspaceRoot, "tools", "go-app-release.cjs"))) {
2370
2110
  return 0;
2371
2111
  }
2372
2112
  return inGroup("Attach this OS's zip to the GitHub Release", "node", ["tools/go-app-release.cjs", "assets", "--native"]);
@@ -2550,14 +2290,22 @@ function runReleaseCommand(workspaceRoot, registry, counts, environment, process
2550
2290
  }
2551
2291
  return processes.run("npx", ["nx", "release", ...extra, "--yes"]);
2552
2292
  }
2553
- function afterRelease(workspaceRoot, processes) {
2554
- if ((0, import_node_fs7.existsSync)((0, import_node_path6.join)(workspaceRoot, "tools", "go-app-release.cjs"))) {
2293
+ function nxPushesTags(workspaceRoot) {
2294
+ try {
2295
+ const nx = JSON.parse((0, import_node_fs7.readFileSync)((0, import_node_path6.join)(workspaceRoot, "nx.json"), "utf8"));
2296
+ return nx.release?.git?.push === true;
2297
+ } catch {
2298
+ return false;
2299
+ }
2300
+ }
2301
+ function afterRelease(workspaceRoot, processes, githubReleases) {
2302
+ if (githubReleases && (0, import_node_fs7.existsSync)((0, import_node_path6.join)(workspaceRoot, "tools", "go-app-release.cjs"))) {
2555
2303
  const assets = processes.run("node", ["tools/go-app-release.cjs", "assets"]);
2556
2304
  if (assets !== 0) {
2557
2305
  return assets;
2558
2306
  }
2559
2307
  }
2560
- return processes.run("git", ["push", "origin", "--tags"]);
2308
+ return githubReleases ? 0 : processes.run("git", ["push", "origin", "--tags"]);
2561
2309
  }
2562
2310
  async function runRelease(workspaceRoot, dependencies = {}) {
2563
2311
  const environment = dependencies.environment ?? process.env;
@@ -2598,7 +2346,7 @@ async function runRelease(workspaceRoot, dependencies = {}) {
2598
2346
  if (released !== 0) {
2599
2347
  return close(released);
2600
2348
  }
2601
- return close(afterRelease(workspaceRoot, processes));
2349
+ return close(afterRelease(workspaceRoot, processes, nxPushesTags(workspaceRoot)));
2602
2350
  }
2603
2351
 
2604
2352
  // src/ci-pipeline/setup.use-case.ts
@@ -8517,6 +8265,7 @@ function runUpgrade(workspaceRoot, options) {
8517
8265
  );
8518
8266
  runFormatter(workspaceRoot);
8519
8267
  logger.success("Done. Review the changes with `git diff` before committing.");
8268
+ logger.info("Run `npm install` and commit package-lock.json too: the overlay may have added or moved a dependency, and CI installs with `npm ci`, which refuses a stale lockfile.");
8520
8269
  }
8521
8270
 
8522
8271
  // src/workspace-creation/interactive-wizard.use-case.ts