nx/scripts/nx-release.ts
MaxKless 499300fd76
fix(core): repair SIGINT signals on windows (#28496)
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.
2024-10-17 15:03:37 -04:00

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;
}