From b9ad9899db04e073c0b5534bcf72321c916cbe51 Mon Sep 17 00:00:00 2001 From: Jason Jean Date: Fri, 13 Mar 2026 10:58:23 -0400 Subject: [PATCH 1/5] chore(repo): remove populate-storage scripts, consolidate into nx-release Remove the now-redundant populate-storage.js and run-populate-storage.mjs scripts. The release logic is inlined directly in global-setup.ts, and the populate-local-registry-storage task is simplified to an orchestration point that delegates to nx-release via dependsOn. When running e2e tests outside of Nx (e.g. Jest directly), global-setup now auto-starts verdaccio if it's not already running. --- e2e/utils/global-setup.ts | 65 +++++++++++---- project.json | 11 +-- scripts/local-registry/populate-storage.js | 79 ------------------- .../local-registry/run-populate-storage.mjs | 11 --- scripts/nx-release.ts | 38 ++++++++- 5 files changed, 85 insertions(+), 119 deletions(-) delete mode 100644 scripts/local-registry/populate-storage.js delete mode 100644 scripts/local-registry/run-populate-storage.mjs diff --git a/e2e/utils/global-setup.ts b/e2e/utils/global-setup.ts index 1501e6afdf2c92..369aad9b52a814 100644 --- a/e2e/utils/global-setup.ts +++ b/e2e/utils/global-setup.ts @@ -1,10 +1,9 @@ import { Config } from '@jest/types'; import { existsSync, removeSync } from 'fs-extra'; import * as isCI from 'is-ci'; -import { exec, execSync } from 'node:child_process'; +import { ChildProcess, exec, execSync, spawn } from 'node:child_process'; import { join } from 'node:path'; import { registerTsConfigPaths } from '../../packages/nx/src/plugins/js/utils/register'; -import { runLocalRelease } from '../../scripts/local-registry/populate-storage'; export default async function (globalConfig: Config.ConfigGlobals) { try { @@ -25,14 +24,23 @@ export default async function (globalConfig: Config.ConfigGlobals) { const registry = `http://${listenAddress}:${port}`; const authToken = 'secretVerdaccioToken'; - while (true) { - await new Promise((resolve) => setTimeout(resolve, 250)); - try { - await assertLocalRegistryIsRunning(registry); - break; - } catch { - console.log(`Waiting for Local registry to start on ${registry}...`); - } + // When running outside of Nx (e.g. Jest directly), start verdaccio ourselves + let verdaccioProcess: ChildProcess | undefined; + if (requiresLocalRelease && !(await isLocalRegistryRunning(registry))) { + console.log( + `Local registry not detected at ${registry}, starting verdaccio...` + ); + verdaccioProcess = spawn( + 'npx', + [ + 'verdaccio', + '--config', + '.verdaccio/config.yml', + '--listen', + `${listenAddress}:${port}`, + ], + { stdio: 'ignore', detached: true } + ); } process.env.npm_config_registry = registry; @@ -54,6 +62,11 @@ export default async function (globalConfig: Config.ConfigGlobals) { global.e2eTeardown = () => { // Clean up environment variable instead of npm config command delete process.env[`npm_config_//${listenAddress}:${port}/:_authToken`]; + // Kill verdaccio if we started it + if (verdaccioProcess) { + verdaccioProcess.kill(); + verdaccioProcess = undefined; + } }; /** @@ -77,8 +90,26 @@ export default async function (globalConfig: Config.ConfigGlobals) { if (requiresLocalRelease) { console.log('Publishing packages to local registry'); const publishVersion = process.env.PUBLISHED_VERSION ?? 'major'; - // Always show full release logs on CI, they should only happen once via e2e-ci - await runLocalRelease(publishVersion, isCI || isVerbose); + const verbose = isCI || isVerbose; + const releaseCommand = `pnpm nx-release --local ${publishVersion}`; + console.log(`> ${releaseCommand}`); + await new Promise((resolve, reject) => { + const child = exec(releaseCommand, { + maxBuffer: 1024 * 1000000, + windowsHide: false, + }); + if (verbose) { + child.stdout?.pipe(process.stdout); + child.stderr?.pipe(process.stderr); + } + child.on('exit', (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`Local release failed with exit code ${code}`)); + } + }); + }); } } } catch (err) { @@ -112,9 +143,11 @@ function getPublishedVersion(): Promise { }); } -async function assertLocalRegistryIsRunning(url) { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); +async function isLocalRegistryRunning(url: string): Promise { + try { + const response = await fetch(url); + return response.ok; + } catch { + return false; } } diff --git a/project.json b/project.json index 43c0e1899affe3..ef72aa8eb845d4 100644 --- a/project.json +++ b/project.json @@ -22,17 +22,10 @@ "input": "production", "projects": ["tag:maven:dev.nx.maven"] }, - "{workspaceRoot}/scripts/local-registry", "native" ], - "dependsOn": [ - "local-registry", - { - "target": "build", - "projects": ["tag:npm:public"] - } - ], - "command": "node ./scripts/local-registry/run-populate-storage.mjs", + "dependsOn": ["local-registry", "nx-release"], + "command": "echo 'Registry storage populated via nx-release dependency'", "outputs": ["{workspaceRoot}/dist/local-registry/storage"] }, "nx-release": { diff --git a/scripts/local-registry/populate-storage.js b/scripts/local-registry/populate-storage.js deleted file mode 100644 index 5dd90812fe5c91..00000000000000 --- a/scripts/local-registry/populate-storage.js +++ /dev/null @@ -1,79 +0,0 @@ -// @ts-check -const { exec, execSync } = require('node:child_process'); -const { - LARGE_BUFFER, -} = require('nx/src/executors/run-commands/run-commands.impl'); - -async function populateLocalRegistryStorage() { - const listenAddress = 'localhost'; - const port = process.env.NX_LOCAL_REGISTRY_PORT ?? '4873'; - const registry = `http://${listenAddress}:${port}`; - const authToken = 'secretVerdaccioToken'; - - while (true) { - await new Promise((resolve) => setTimeout(resolve, 250)); - try { - await assertLocalRegistryIsRunning(registry); - break; - } catch { - console.log(`Waiting for Local registry to start on ${registry}...`); - } - } - - process.env.npm_config_registry = registry; - - // bun - process.env.BUN_CONFIG_REGISTRY = registry; - process.env.BUN_CONFIG_TOKEN = authToken; - // yarnv1 - process.env.YARN_REGISTRY = registry; - // yarnv2 - process.env.YARN_NPM_REGISTRY_SERVER = registry; - process.env.YARN_UNSAFE_HTTP_WHITELIST = listenAddress; - - try { - const publishVersion = process.env.PUBLISHED_VERSION ?? 'major'; - const isVerbose = process.env.NX_VERBOSE_LOGGING === 'true'; - - console.log('Publishing packages to local registry to populate storage'); - await runLocalRelease(publishVersion, isVerbose); - } catch (err) { - console.error('Error:', err); - process.exit(1); - } -} -exports.populateLocalRegistryStorage = populateLocalRegistryStorage; - -function runLocalRelease(publishVersion, isVerbose) { - return new Promise((res, rej) => { - const publishProcess = exec(`pnpm nx-release --local ${publishVersion}`, { - env: process.env, - maxBuffer: LARGE_BUFFER, - }); - let logs = Buffer.from(''); - if (isVerbose) { - publishProcess?.stdout?.pipe(process.stdout); - publishProcess?.stderr?.pipe(process.stderr); - } else { - publishProcess?.stdout?.on('data', (data) => (logs += data)); - publishProcess?.stderr?.on('data', (data) => (logs += data)); - } - publishProcess.on('exit', (code) => { - if (code && code > 0) { - if (!isVerbose) { - console.log(logs.toString()); - } - rej(code); - } - res(undefined); - }); - }); -} -exports.runLocalRelease = runLocalRelease; - -async function assertLocalRegistryIsRunning(url) { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } -} diff --git a/scripts/local-registry/run-populate-storage.mjs b/scripts/local-registry/run-populate-storage.mjs deleted file mode 100644 index 40faf0b8e12030..00000000000000 --- a/scripts/local-registry/run-populate-storage.mjs +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-check - -import { populateLocalRegistryStorage } from './populate-storage.js'; - -/** - * This script is primarily intended to run as part of e2e-ci, - * so we want to capture the full logs of the local release. - */ -process.env.NX_VERBOSE_LOGGING = 'true'; - -await populateLocalRegistryStorage(); diff --git a/scripts/nx-release.ts b/scripts/nx-release.ts index 3a28311ef0088e..294ac44bc044a9 100644 --- a/scripts/nx-release.ts +++ b/scripts/nx-release.ts @@ -218,6 +218,13 @@ const VALID_AUTHORS_FOR_LATEST = [ hackFixForDevkitPeerDependencies(); + if (options.local) { + const port = process.env.NX_LOCAL_REGISTRY_PORT ?? '4873'; + const localRegistryUrl = `http://localhost:${port}`; + await waitForLocalRegistry(localRegistryUrl); + process.env.npm_config_registry = localRegistryUrl; + } + // Run with dynamic output-style so that we have more minimal logs by default but still always see errors let publishCommand = `pnpm nx release publish --registry=${getRegistry()} --tag=${distTag} --output-style=dynamic --parallel=8`; if (options.dryRun) { @@ -400,10 +407,6 @@ function parseArgs() { 'Registry is still set to localhost! Run "pnpm local-registry disable" or pass --force' ); } - } else { - if (!args.force && !registryIsLocalhost) { - throw new Error('--local was passed and registry is not localhost'); - } } return true; @@ -476,6 +479,33 @@ function determineDistTag( return distTag; } +function waitForLocalRegistry(registryUrl: string): Promise { + console.log(`Waiting for local registry at ${registryUrl}...`); + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + clearInterval(interval); + reject( + new Error( + `Local registry at ${registryUrl} did not become available within 60 seconds` + ) + ); + }, 60_000); + const interval = setInterval(async () => { + try { + const response = await fetch(registryUrl); + if (response.ok) { + clearInterval(interval); + clearTimeout(timeout); + console.log('Local registry is ready.'); + resolve(); + } + } catch { + // Registry not up yet + } + }, 50); + }); +} + //TODO(@Coly010): Remove this after fixing up the release peer dep handling function hackFixForDevkitPeerDependencies() { const { readFileSync, writeFileSync } = require('fs'); From 74622cf71e07a2a205fac094059baef1f44e9a5a Mon Sep 17 00:00:00 2001 From: Jason Jean Date: Fri, 13 Mar 2026 11:57:59 -0400 Subject: [PATCH 2/5] chore(repo): bust cache for populate-local-registry-storage --- nx.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nx.json b/nx.json index 00d81c930a286a..8582bf6df6e17b 100644 --- a/nx.json +++ b/nx.json @@ -343,7 +343,7 @@ "nxCloudId": "62d013ea0852fe0a2df74438", "nxCloudUrl": "https://staging.nx.app", "parallel": 1, - "bust": 2, + "bust": 3, "defaultBase": "master", "sync": { "applyChanges": true From 592265bea3fb47f512b994cbc43069cec0cfdbd7 Mon Sep 17 00:00:00 2001 From: Jason Jean Date: Fri, 13 Mar 2026 12:37:59 -0400 Subject: [PATCH 3/5] chore(repo): split nx-release into local and npm targets Add nx-release-npm target for real npm publishes (--local false). Keep nx-release for local verdaccio releases via populate-local-registry-storage. Update publish.yml to use the new target. --- .github/workflows/publish.yml | 2 +- project.json | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b65d6c218b1f31..8a0c8543a290a5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -596,7 +596,7 @@ jobs: echo "Version set to: $VERSION" echo "DRY_RUN set to: $DRY_RUN" echo "" - pnpm nx-release --local=false $VERSION $DRY_RUN + pnpm nx nx-release-npm @nx/nx-source -- $VERSION $DRY_RUN - name: (Stable Release Only) Trigger Docs Release # Publish docs only on a full release diff --git a/project.json b/project.json index ef72aa8eb845d4..96832e8916be59 100644 --- a/project.json +++ b/project.json @@ -30,6 +30,7 @@ }, "nx-release": { "dependsOn": [ + "local-registry", { "target": "build", "projects": ["tag:npm:public"] @@ -37,6 +38,15 @@ ], "command": "ts-node -P ./scripts/tsconfig.release.json ./scripts/nx-release.ts" }, + "nx-release-npm": { + "dependsOn": [ + { + "target": "build", + "projects": ["tag:npm:public"] + } + ], + "command": "ts-node -P ./scripts/tsconfig.release.json ./scripts/nx-release.ts --local false" + }, "start-docker-registry": { "continuous": true, "command": "docker run --rm -p 5000:5000 --name local-docker-registry registry:2" From 94a64642bba99ab8b1f59e5a66dff4230b7d7762 Mon Sep 17 00:00:00 2001 From: Jason Jean Date: Fri, 13 Mar 2026 13:52:52 -0400 Subject: [PATCH 4/5] chore(repo): assign astro-docs build to extra-large agents --- .nx/workflows/dynamic-changesets.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.nx/workflows/dynamic-changesets.yaml b/.nx/workflows/dynamic-changesets.yaml index 6fe1754aea731d..086781047074f0 100644 --- a/.nx/workflows/dynamic-changesets.yaml +++ b/.nx/workflows/dynamic-changesets.yaml @@ -78,6 +78,7 @@ assignment-rules: # These projects should not need to be isolated. - projects: - nx-dev + - astro-docs targets: - build* run-on: From cd7c0e56c2d27263d494eb41f80be5435e3f8c13 Mon Sep 17 00:00:00 2001 From: Jason Jean Date: Fri, 13 Mar 2026 18:46:05 -0400 Subject: [PATCH 5/5] fix(repo): remove detached flag from verdaccio spawn in global-setup --- e2e/utils/global-setup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/utils/global-setup.ts b/e2e/utils/global-setup.ts index 369aad9b52a814..c2fd6d3b1f6fc0 100644 --- a/e2e/utils/global-setup.ts +++ b/e2e/utils/global-setup.ts @@ -39,7 +39,7 @@ export default async function (globalConfig: Config.ConfigGlobals) { '--listen', `${listenAddress}:${port}`, ], - { stdio: 'ignore', detached: true } + { stdio: 'ignore' } ); }