using `windowsHide: true` is causing an issue on windows: Ctrl + C handling isn't enabled and no `SIGINT` is sent to the child process when users exit the process. See https://github.com/nodejs/node/issues/29837 and https://github.com/nodejs/node-v0.x-archive/issues/5054 for reference. This will cause leftover processes throughout nx. This PR sets `windowsHide: false` everywhere except for the plugin workers and some short-lived utils. They `spawn` child processes but have explicit handling to make sure they kill themselves when the parent process dies, so the missing Ctrl + C handling doesn't cause issues. We will follow up to make sure any other culprits that still cause windows popups (especially when used through Nx Console) are handled. Leaving no leftover processes running is more important for now, though. Keep in mind the underlying tooling (like vite) might have some windows popups themselves that Nx will inherit.
444 lines
14 KiB
JavaScript
Executable File
444 lines
14 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
import { createProjectGraphAsync, workspaceRoot } from '@nx/devkit';
|
|
import * as chalk from 'chalk';
|
|
import { execSync } from 'node:child_process';
|
|
import { rmSync, writeFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { URL } from 'node:url';
|
|
import { isRelativeVersionKeyword } from 'nx/src/command-line/release/utils/semver';
|
|
import { ReleaseType, inc, major, parse } from 'semver';
|
|
import * as yargs from 'yargs';
|
|
|
|
const LARGE_BUFFER = 1024 * 1000000;
|
|
|
|
// DO NOT MODIFY, even for testing. This only gates releases to latest.
|
|
const VALID_AUTHORS_FOR_LATEST = [
|
|
'jaysoo',
|
|
'JamesHenry',
|
|
'FrozenPandaz',
|
|
'vsavkin',
|
|
];
|
|
|
|
(async () => {
|
|
const options = parseArgs();
|
|
// Perform minimal logging by default
|
|
let isVerboseLogging = process.env.NX_VERBOSE_LOGGING === 'true';
|
|
|
|
if (options.clearLocalRegistry) {
|
|
rmSync(join(__dirname, '../build/local-registry/storage'), {
|
|
recursive: true,
|
|
force: true,
|
|
});
|
|
}
|
|
|
|
// Ensure all the native-packages directories are available at the top level of the build directory, enabling consistent packageRoot structure
|
|
execSync(`pnpm nx copy-native-package-directories nx`, {
|
|
stdio: isVerboseLogging ? [0, 1, 2] : 'ignore',
|
|
maxBuffer: LARGE_BUFFER,
|
|
windowsHide: false,
|
|
});
|
|
|
|
// Expected to run as part of the Github `publish` workflow
|
|
if (!options.local && process.env.NODE_AUTH_TOKEN) {
|
|
// Delete all .node files that were built during the previous steps
|
|
// Always run before the artifacts step because we still need the .node files for native-packages
|
|
execSync('find ./build -name "*.node" -delete', {
|
|
stdio: [0, 1, 2],
|
|
maxBuffer: LARGE_BUFFER,
|
|
windowsHide: false,
|
|
});
|
|
|
|
execSync('pnpm nx run-many --target=artifacts', {
|
|
stdio: [0, 1, 2],
|
|
maxBuffer: LARGE_BUFFER,
|
|
windowsHide: false,
|
|
});
|
|
}
|
|
|
|
const runNxReleaseVersion = () => {
|
|
let versionCommand = `pnpm nx release version${
|
|
options.version ? ` --specifier ${options.version}` : ''
|
|
}`;
|
|
if (options.dryRun) {
|
|
versionCommand += ' --dry-run';
|
|
}
|
|
if (isVerboseLogging) {
|
|
versionCommand += ' --verbose';
|
|
}
|
|
console.log(`> ${versionCommand}`);
|
|
execSync(versionCommand, {
|
|
stdio: isVerboseLogging ? [0, 1, 2] : 'ignore',
|
|
maxBuffer: LARGE_BUFFER,
|
|
windowsHide: false,
|
|
});
|
|
};
|
|
|
|
// Intended for creating a github release which triggers the publishing workflow
|
|
if (!options.local && !process.env.NODE_AUTH_TOKEN) {
|
|
// For this important use-case it makes sense to always have full logs
|
|
isVerboseLogging = true;
|
|
|
|
execSync('git status --ahead-behind', {
|
|
windowsHide: false,
|
|
});
|
|
|
|
if (isRelativeVersionKeyword(options.version)) {
|
|
throw new Error(
|
|
'When creating actual releases, you must use an exact semver version'
|
|
);
|
|
}
|
|
|
|
runNxReleaseVersion();
|
|
|
|
execSync(`pnpm nx run-many -t add-extra-dependencies --parallel 8`, {
|
|
stdio: isVerboseLogging ? [0, 1, 2] : 'ignore',
|
|
maxBuffer: LARGE_BUFFER,
|
|
windowsHide: false,
|
|
});
|
|
|
|
let changelogCommand = `pnpm nx release changelog ${options.version} --interactive workspace`;
|
|
if (options.from) {
|
|
changelogCommand += ` --from ${options.from}`;
|
|
}
|
|
if (options.gitRemote) {
|
|
changelogCommand += ` --git-remote ${options.gitRemote}`;
|
|
}
|
|
if (options.dryRun) {
|
|
changelogCommand += ' --dry-run';
|
|
}
|
|
if (isVerboseLogging) {
|
|
changelogCommand += ' --verbose';
|
|
}
|
|
console.log(`> ${changelogCommand}`);
|
|
execSync(changelogCommand, {
|
|
stdio: isVerboseLogging ? [0, 1, 2] : 'ignore',
|
|
maxBuffer: LARGE_BUFFER,
|
|
windowsHide: false,
|
|
});
|
|
|
|
console.log(
|
|
'Check github: https://github.com/nrwl/nx/actions/workflows/publish.yml'
|
|
);
|
|
process.exit(0);
|
|
}
|
|
|
|
runNxReleaseVersion();
|
|
|
|
execSync(`pnpm nx run-many -t add-extra-dependencies --parallel 8`, {
|
|
stdio: isVerboseLogging ? [0, 1, 2] : 'ignore',
|
|
maxBuffer: LARGE_BUFFER,
|
|
windowsHide: false,
|
|
});
|
|
|
|
const distTag = determineDistTag(options.version);
|
|
|
|
// If publishing locally, force all projects to not be private first
|
|
if (options.local) {
|
|
console.log(
|
|
chalk.dim`\n Publishing locally, so setting all packages with existing nx-release-publish targets to not be private. If you have created a new private package and you want it to be published, you will need to manually configure the "nx-release-publish" target using executor "@nx/js:release-publish"`
|
|
);
|
|
const projectGraph = await createProjectGraphAsync();
|
|
for (const proj of Object.values(projectGraph.nodes)) {
|
|
if (proj.data.targets?.['nx-release-publish']) {
|
|
const packageJsonPath = join(
|
|
workspaceRoot,
|
|
proj.data.targets?.['nx-release-publish']?.options.packageRoot,
|
|
'package.json'
|
|
);
|
|
try {
|
|
const packageJson = require(packageJsonPath);
|
|
if (packageJson.private) {
|
|
console.log(
|
|
'- Publishing private package locally:',
|
|
packageJson.name
|
|
);
|
|
writeFileSync(
|
|
packageJsonPath,
|
|
JSON.stringify({ ...packageJson, private: false })
|
|
);
|
|
}
|
|
} catch {}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!options.local && (!distTag || distTag === 'latest')) {
|
|
// We are only expecting non-local latest releases to be performed within publish.yml on GitHub
|
|
const author = process.env.GITHUB_ACTOR ?? '';
|
|
if (!VALID_AUTHORS_FOR_LATEST.includes(author)) {
|
|
throw new Error(
|
|
`The GitHub user "${author}" is not allowed to publish to "latest". Please request one of the following users to carry out the release: ${VALID_AUTHORS_FOR_LATEST.join(
|
|
', '
|
|
)}`
|
|
);
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
publishCommand += ' --dry-run';
|
|
}
|
|
console.log(`\n> ${publishCommand}`);
|
|
execSync(publishCommand, {
|
|
stdio: [0, 1, 2],
|
|
maxBuffer: LARGE_BUFFER,
|
|
windowsHide: false,
|
|
});
|
|
|
|
if (!options.dryRun) {
|
|
let version;
|
|
if (['minor', 'major', 'patch'].includes(options.version)) {
|
|
version = execSync(`npm view nx@${distTag} version`, {
|
|
windowsHide: false,
|
|
})
|
|
.toString()
|
|
.trim();
|
|
} else {
|
|
version = options.version;
|
|
}
|
|
|
|
console.log(chalk.green` > Published version: ` + version);
|
|
console.log(chalk.dim` Use: npx create-nx-workspace@${version}\n`);
|
|
}
|
|
|
|
process.exit(0);
|
|
})();
|
|
|
|
function parseArgs() {
|
|
const registry = getRegistry();
|
|
const registryIsLocalhost = registry.hostname === 'localhost';
|
|
|
|
const parsedArgs = yargs
|
|
.scriptName('pnpm nx-release')
|
|
.wrap(144)
|
|
.strictOptions()
|
|
.version(false)
|
|
.command(
|
|
'$0 [version]',
|
|
'This script is for publishing Nx both locally and publically'
|
|
)
|
|
.option('dryRun', {
|
|
type: 'boolean',
|
|
description: 'Dry-run flag to be passed to all `nx release` commands',
|
|
})
|
|
.option('clearLocalRegistry', {
|
|
type: 'boolean',
|
|
description:
|
|
'Clear existing versions in the local registry so that you can republish the same version',
|
|
default: true,
|
|
})
|
|
.option('local', {
|
|
type: 'boolean',
|
|
description: 'Publish Nx locally, not to actual NPM',
|
|
alias: 'l',
|
|
default: true,
|
|
})
|
|
.option('force', {
|
|
type: 'boolean',
|
|
description: "Don't use this unless you really know what it does",
|
|
hidden: true,
|
|
})
|
|
.option('from', {
|
|
type: 'string',
|
|
description:
|
|
'Git ref to pass to `nx release changelog`. Not applicable for local publishing or e2e tests.',
|
|
})
|
|
.positional('version', {
|
|
type: 'string',
|
|
description:
|
|
'The version to publish. This does not need to be passed and can be inferred.',
|
|
default: 'minor',
|
|
coerce: (version: string) => {
|
|
const isGithubActions = !!process.env.GITHUB_ACTIONS;
|
|
if (
|
|
isGithubActions &&
|
|
!registryIsLocalhost &&
|
|
isRelativeVersionKeyword(version)
|
|
) {
|
|
// Print error rather than throw to avoid yargs noise in this specifically handled case
|
|
console.error(
|
|
'Error: The release script was triggered in a GitHub Actions workflow, to a non-local registry, but a relative version keyword was provided. This is an unexpected combination.'
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (version !== 'canary') {
|
|
return version;
|
|
}
|
|
/**
|
|
* Handle the special case of `canary`
|
|
*/
|
|
|
|
const currentLatestVersion = execSync('npm view nx@latest version', {
|
|
windowsHide: false,
|
|
})
|
|
.toString()
|
|
.trim();
|
|
const currentNextVersion = execSync('npm view nx@next version', {
|
|
windowsHide: false,
|
|
})
|
|
.toString()
|
|
.trim();
|
|
|
|
let canaryBaseVersion: string | null = null;
|
|
|
|
// If the latest and next are not on the same major version, then we need to publish a canary version of the next major
|
|
if (major(currentLatestVersion) !== major(currentNextVersion)) {
|
|
canaryBaseVersion = `${major(currentNextVersion)}.0.0`;
|
|
} else {
|
|
// Determine next minor version above the currentLatestVersion
|
|
const nextMinorRelease = inc(
|
|
currentLatestVersion,
|
|
'minor',
|
|
undefined
|
|
);
|
|
canaryBaseVersion = nextMinorRelease;
|
|
}
|
|
|
|
if (!canaryBaseVersion) {
|
|
throw new Error(`Unable to determine a base for the canary version.`);
|
|
}
|
|
|
|
// Create YYYYMMDD string
|
|
const date = new Date();
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0'); // Months are 0-indexed
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
const YYYYMMDD = `${year}${month}${day}`;
|
|
|
|
// Get the current short git sha
|
|
const gitSha = execSync('git rev-parse --short HEAD', {
|
|
windowsHide: false,
|
|
})
|
|
.toString()
|
|
.trim();
|
|
|
|
const canaryVersion = `${canaryBaseVersion}-canary.${YYYYMMDD}-${gitSha}`;
|
|
|
|
console.log(`\nDerived canary version dynamically`, {
|
|
currentLatestVersion,
|
|
currentNextVersion,
|
|
canaryVersion,
|
|
});
|
|
|
|
return canaryVersion;
|
|
},
|
|
})
|
|
.option('gitRemote', {
|
|
type: 'string',
|
|
description:
|
|
'Alternate git remote name to publish tags to (useful for testing changelog)',
|
|
default: 'origin',
|
|
})
|
|
.example(
|
|
'$0',
|
|
`By default, this will locally publish a minor version bump as latest. Great for local development. Most developers should only need this.`
|
|
)
|
|
.example(
|
|
'$0 --local false 2.3.4-beta.0',
|
|
`This will really publish a new version to npm as next.`
|
|
)
|
|
.example(
|
|
'$0 --local false 2.3.4',
|
|
`Given the current latest major version on npm is 2, this will really publish a new version to npm as latest.`
|
|
)
|
|
.example(
|
|
'$0 --local false 1.3.4-beta.0',
|
|
`Given the current latest major version on npm is 2, this will really publish a new version to npm as previous.`
|
|
)
|
|
.group(
|
|
['local', 'clearLocalRegistry'],
|
|
'Local Publishing Options for most developers'
|
|
)
|
|
.group(
|
|
['gitRemote', 'force'],
|
|
'Real Publishing Options for actually publishing to NPM'
|
|
)
|
|
.demandOption('version')
|
|
.check((args) => {
|
|
if (!args.local) {
|
|
if (!process.env.GH_TOKEN) {
|
|
throw new Error('process.env.GH_TOKEN is not set');
|
|
}
|
|
if (!args.force && registryIsLocalhost) {
|
|
throw new Error(
|
|
'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;
|
|
})
|
|
.parseSync();
|
|
|
|
return parsedArgs;
|
|
}
|
|
|
|
function getRegistry() {
|
|
return new URL(
|
|
execSync('npm config get registry', {
|
|
windowsHide: false,
|
|
})
|
|
.toString()
|
|
.trim()
|
|
);
|
|
}
|
|
|
|
function determineDistTag(
|
|
newVersion: string
|
|
): 'latest' | 'next' | 'previous' | 'canary' | 'pull-request' {
|
|
// Special case of canary
|
|
if (newVersion.includes('canary')) {
|
|
return 'canary';
|
|
}
|
|
|
|
// Special case of PR release
|
|
if (newVersion.startsWith('0.0.0-pr-')) {
|
|
return 'pull-request';
|
|
}
|
|
|
|
// For a relative version keyword, it cannot be previous
|
|
if (isRelativeVersionKeyword(newVersion)) {
|
|
const prereleaseKeywords: ReleaseType[] = [
|
|
'premajor',
|
|
'preminor',
|
|
'prepatch',
|
|
'prerelease',
|
|
];
|
|
return prereleaseKeywords.includes(newVersion) ? 'next' : 'latest';
|
|
}
|
|
|
|
const parsedGivenVersion = parse(newVersion);
|
|
if (!parsedGivenVersion) {
|
|
throw new Error(
|
|
`Unable to parse the given version: "${newVersion}". Is it valid semver?`
|
|
);
|
|
}
|
|
|
|
const currentLatestVersion = execSync('npm view nx version', {
|
|
windowsHide: false,
|
|
})
|
|
.toString()
|
|
.trim();
|
|
const parsedCurrentLatestVersion = parse(currentLatestVersion);
|
|
if (!parsedCurrentLatestVersion) {
|
|
throw new Error(
|
|
`The current version resolved from the npm registry could not be parsed (resolved "${currentLatestVersion}")`
|
|
);
|
|
}
|
|
|
|
const distTag =
|
|
parsedGivenVersion.prerelease.length > 0
|
|
? 'next'
|
|
: parsedGivenVersion.major < parsedCurrentLatestVersion.major
|
|
? 'previous'
|
|
: 'latest';
|
|
|
|
return distTag;
|
|
}
|