@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/README.md +1 -1
- package/dist/cli.js +135 -386
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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
|
|
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
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
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
|
-
|
|
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.
|
|
1481
|
-
#
|
|
1482
|
-
- script:
|
|
1483
|
-
displayName:
|
|
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
|
|
1555
|
-
#
|
|
1556
|
-
#
|
|
1557
|
-
#
|
|
1558
|
-
#
|
|
1559
|
-
|
|
1560
|
-
|
|
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
|
|
1576
|
-
- script:
|
|
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
|
-
#
|
|
1595
|
-
#
|
|
1596
|
-
#
|
|
1597
|
-
#
|
|
1598
|
-
#
|
|
1599
|
-
#
|
|
1600
|
-
#
|
|
1601
|
-
|
|
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
|
-
#
|
|
1634
|
-
# more than one releasable package
|
|
1635
|
-
# is unreliable
|
|
1636
|
-
#
|
|
1637
|
-
#
|
|
1638
|
-
#
|
|
1639
|
-
|
|
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
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
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:
|
|
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
|
-
#
|
|
1792
|
-
#
|
|
1793
|
-
#
|
|
1794
|
-
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
#
|
|
1798
|
-
#
|
|
1799
|
-
#
|
|
1800
|
-
#
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
#
|
|
1806
|
-
#
|
|
1807
|
-
#
|
|
1808
|
-
|
|
1809
|
-
|
|
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
|
|
1863
|
-
#
|
|
1864
|
-
#
|
|
1865
|
-
#
|
|
1866
|
-
#
|
|
1867
|
-
|
|
1868
|
-
|
|
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
|
|
1885
|
-
- run:
|
|
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
|
-
|
|
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
|
-
|
|
1983
|
-
|
|
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
|
-
...
|
|
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
|
|
2554
|
-
|
|
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
|