nx/scripts/nx-release.ts
Jason Jean 73c8c3343d
Revert publish (#26544)
<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->

The below commit caused the publish pipeline to publish a `minor`
version when it should have been a `next` version.

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

This reverts commit
3750366ebc.

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
2024-06-13 14:27:36 -04:00

404 lines
13 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,
});
}
const buildCommand = 'pnpm build';
console.log(`> ${buildCommand}`);
execSync(buildCommand, {
stdio: [0, 1, 2],
maxBuffer: LARGE_BUFFER,
});
// 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,
});
// 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,
});
execSync('pnpm nx run-many --target=artifacts', {
stdio: [0, 1, 2],
maxBuffer: LARGE_BUFFER,
});
}
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,
});
};
// 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');
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,
});
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,
});
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,
});
const distTag = determineDistTag(options.version);
if (options.dryRun) {
console.warn('Not Publishing because --dryRun was passed');
} else {
// 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,
});
let version;
if (['minor', 'major', 'patch'].includes(options.version)) {
version = execSync(`npm view nx@${distTag} version`).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 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) => {
if (version !== 'canary') {
return version;
}
/**
* Handle the special case of `canary`
*/
const currentLatestVersion = execSync('npm view nx@latest version')
.toString()
.trim();
const currentNextVersion = execSync('npm view nx@next version')
.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').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) => {
const registry = getRegistry();
const registryIsLocalhost = registry.hostname === 'localhost';
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').toString().trim());
}
function determineDistTag(
newVersion: string
): 'latest' | 'next' | 'previous' | 'canary' {
// Special case of canary
if (newVersion.includes('canary')) {
return 'canary';
}
// 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')
.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;
}