fix(release): only add nx-release-publish to public packages (#21338)

This commit is contained in:
James Henry 2024-01-26 20:56:54 +04:00 committed by GitHub
parent f7f745f87f
commit c577f48cea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 169 additions and 290 deletions

View File

@ -75,10 +75,6 @@ describe('nx release - independent projects', () => {
pkg3 = uniq('my-pkg-3');
runCLI(`generate @nx/workspace:npm-package ${pkg3}`);
updateJson(`${pkg3}/package.json`, (json) => {
json.private = true;
return json;
});
/**
* Update pkg2 to depend on pkg3.
@ -205,9 +201,6 @@ describe('nx release - independent projects', () => {
+ "version": "999.9.9-package.3",
"scripts": {
}
+
"dependencies": {
- "@proj/{project-name}": "0.0.0"
@ -424,7 +417,7 @@ describe('nx release - independent projects', () => {
release: {
projectsRelationship: 'independent',
changelog: {
projectChangelogs: {}, // enable project changelogs with default options
projectChangelogs: true, // enable project changelogs with default options
workspaceChangelog: false, // disable workspace changelog
},
},
@ -746,7 +739,25 @@ describe('nx release - independent projects', () => {
> nx run {project-name}:nx-release-publish
Skipped package "@proj/{project-name}" from project "{project-name}", because it has \`"private": true\` in {project-name}/package.json
📦 @proj/{project-name}@999.9.9-version-git-operations-test.3
=== Tarball Contents ===
XXXB CHANGELOG.md
XXB index.js
XXXB package.json
XXB project.json
=== Tarball Details ===
name: @proj/{project-name}
version: 999.9.9-version-git-operations-test.3
filename: proj-{project-name}-999.9.9-version-git-operations-test.3.tgz
package size: XXXB
unpacked size: XXXB
shasum: {SHASUM}
integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
total files: 4
Would publish to http://localhost:4873 with tag "latest", but [dry-run] was set
@ -837,7 +848,25 @@ describe('nx release - independent projects', () => {
> nx run {project-name}:nx-release-publish
Skipped package "@proj/{project-name}" from project "{project-name}", because it has \`"private": true\` in {project-name}/package.json
📦 @proj/{project-name}@999.9.9-version-git-operations-test.3
=== Tarball Contents ===
XXXB CHANGELOG.md
XXB index.js
XXXB package.json
XXB project.json
=== Tarball Details ===
name: @proj/{project-name}
version: 999.9.9-version-git-operations-test.3
filename: proj-{project-name}-999.9.9-version-git-operations-test.3.tgz
package size: XXXB
unpacked size: XXXB
shasum: {SHASUM}
integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
total files: 4
Would publish to http://localhost:4873 with tag "latest", but [dry-run] was set
@ -862,7 +891,7 @@ describe('nx release - independent projects', () => {
},
},
changelog: {
projectChangelogs: {},
projectChangelogs: true,
},
},
};
@ -920,7 +949,7 @@ describe('nx release - independent projects', () => {
},
},
changelog: {
projectChangelogs: {},
projectChangelogs: true,
},
},
};

View File

@ -188,7 +188,9 @@ describe('nx release - private JS packages', () => {
`);
const privatePkgPublishOutput = runCLI(`release publish -p ${privatePkg}`);
const privatePkgPublishOutput = runCLI(`release publish -p ${privatePkg}`, {
silenceError: true,
});
expect(privatePkgPublishOutput).toMatchInlineSnapshot(`
> NX Your filter "{private-project-name}" matched the following projects:
@ -196,20 +198,13 @@ describe('nx release - private JS packages', () => {
- {private-project-name}
> NX Running target nx-release-publish for project {private-project-name}:
> NX Based on your config, the following projects were matched for publishing but do not have the "nx-release-publish" target specified:
- {private-project-name}
There are a few possible reasons for this: (1) The projects may be private (2) You may not have an appropriate plugin (such as \`@nx/js\`) installed which adds the target automatically to public projects (3) You intended to configure the target manually, or exclude those projects via config in nx.json
> nx run {private-project-name}:nx-release-publish
Skipped package "@proj/{private-project-name}" from project "{private-project-name}", because it has \`"private": true\` in {private-project-name}/package.json
> NX Successfully ran target nx-release-publish for project {private-project-name}
Pass --verbose to see the stacktrace.
`);

View File

@ -281,10 +281,12 @@ const publishCommand: CommandModule<NxReleaseArgs, PublishOptions> = {
description:
'A one-time password for publishing to a registry that requires 2FA',
}),
handler: (args) =>
import('./publish').then((m) =>
m.releasePublishCLIHandler(coerceParallelOption(withOverrides(args, 2)))
),
handler: async (args) => {
const status = await (
await import('./publish')
).releasePublishCLIHandler(coerceParallelOption(withOverrides(args, 2)));
process.exit(status);
},
};
function coerceParallelOption(args: any) {

View File

@ -1967,80 +1967,6 @@ describe('createNxReleaseConfig()', () => {
}
`);
});
it('should return an error if any matched projects do not have the required target specified', async () => {
const res = await createNxReleaseConfig(
{
...projectGraph,
nodes: {
...projectGraph.nodes,
'project-without-target': {
name: 'project-without-target',
type: 'lib',
data: {
root: 'libs/project-without-target',
targets: {},
} as any,
},
},
},
{
groups: {
'group-1': {
projects: '*', // using string form to ensure that is supported in addition to array form
},
},
},
'nx-release-publish'
);
expect(res).toMatchInlineSnapshot(`
{
"error": {
"code": "PROJECTS_MISSING_TARGET",
"data": {
"projects": [
"project-without-target",
],
"targetName": "nx-release-publish",
},
},
"nxReleaseConfig": null,
}
`);
const res2 = await createNxReleaseConfig(
{
...projectGraph,
nodes: {
...projectGraph.nodes,
'another-project-without-target': {
name: 'another-project-without-target',
type: 'lib',
data: {
root: 'libs/another-project-without-target',
targets: {},
} as any,
},
},
},
{},
'nx-release-publish'
);
expect(res2).toMatchInlineSnapshot(`
{
"error": {
"code": "PROJECTS_MISSING_TARGET",
"data": {
"projects": [
"another-project-without-target",
],
"targetName": "nx-release-publish",
},
},
"nxReleaseConfig": null,
}
`);
});
});
describe('user config -> mixed top level and granular git', () => {
@ -2172,80 +2098,6 @@ describe('createNxReleaseConfig()', () => {
`);
});
it('should return an error if any matched projects do not have the required target specified', async () => {
const res = await createNxReleaseConfig(
{
...projectGraph,
nodes: {
...projectGraph.nodes,
'project-without-target': {
name: 'project-without-target',
type: 'lib',
data: {
root: 'libs/project-without-target',
targets: {},
} as any,
},
},
},
{
groups: {
'group-1': {
projects: '*', // using string form to ensure that is supported in addition to array form
},
},
},
'nx-release-publish'
);
expect(res).toMatchInlineSnapshot(`
{
"error": {
"code": "PROJECTS_MISSING_TARGET",
"data": {
"projects": [
"project-without-target",
],
"targetName": "nx-release-publish",
},
},
"nxReleaseConfig": null,
}
`);
const res2 = await createNxReleaseConfig(
{
...projectGraph,
nodes: {
...projectGraph.nodes,
'another-project-without-target': {
name: 'another-project-without-target',
type: 'lib',
data: {
root: 'libs/another-project-without-target',
targets: {},
} as any,
},
},
},
{},
'nx-release-publish'
);
expect(res2).toMatchInlineSnapshot(`
{
"error": {
"code": "PROJECTS_MISSING_TARGET",
"data": {
"projects": [
"another-project-without-target",
],
"targetName": "nx-release-publish",
},
},
"nxReleaseConfig": null,
}
`);
});
it("should return an error if a group's releaseTagPattern has no {version} placeholder", async () => {
const res = await createNxReleaseConfig(projectGraph, {
groups: {

View File

@ -14,7 +14,6 @@
import { NxJsonConfiguration } from '../../../config/nx-json';
import { output, type ProjectGraph } from '../../../devkit-exports';
import { findMatchingProjects } from '../../../utils/find-matching-projects';
import { projectHasTarget } from '../../../utils/project-graph-utils';
import { resolveNxJsonConfigErrorMessage } from '../utils/resolve-nx-json-error-message';
type DeepRequired<T> = Required<{
@ -73,7 +72,6 @@ export interface CreateNxReleaseConfigError {
| 'RELEASE_GROUP_MATCHES_NO_PROJECTS'
| 'RELEASE_GROUP_RELEASE_TAG_PATTERN_VERSION_PLACEHOLDER_MISSING_OR_EXCESSIVE'
| 'PROJECT_MATCHES_MULTIPLE_GROUPS'
| 'PROJECTS_MISSING_TARGET'
| 'CONVENTIONAL_COMMITS_SHORTHAND_MIXED_WITH_OVERLAPPING_GENERATOR_OPTIONS'
| 'GLOBAL_GIT_CONFIG_MIXED_WITH_GRANULAR_GIT_CONFIG';
data: Record<string, string | string[]>;
@ -82,9 +80,7 @@ export interface CreateNxReleaseConfigError {
// Apply default configuration to any optional user configuration and handle known errors
export async function createNxReleaseConfig(
projectGraph: ProjectGraph,
userConfig: NxJsonConfiguration['release'] = {},
// Optionally ensure that all configured projects have implemented a certain target
requiredTargetName?: 'nx-release-publish'
userConfig: NxJsonConfiguration['release'] = {}
): Promise<{
error: null | CreateNxReleaseConfigError;
nxReleaseConfig: NxReleaseConfig | null;
@ -353,21 +349,6 @@ export async function createNxReleaseConfig(
};
}
// Ensure all matching projects have the relevant target available, if applicable
if (requiredTargetName) {
const error = ensureProjectsHaveTarget(
matchingProjects,
projectGraph,
requiredTargetName
);
if (error) {
return {
error,
nxReleaseConfig: null,
};
}
}
// If provided, ensure release tag pattern is valid
if (releaseGroup.releaseTagPattern) {
const error = ensureReleaseGroupReleaseTagPatternIsValid(
@ -526,14 +507,6 @@ export async function handleNxReleaseConfigError(
});
}
break;
case 'PROJECTS_MISSING_TARGET':
{
output.error({
title: `Based on your config, the following projects were matched for release but do not have a "${error.data.targetName}" target specified. Please ensure you have an appropriate plugin such as @nx/js installed, or have configured the target manually, or exclude the projects using release groups config in nx.json:`,
bodyLines: Array.from(error.data.projects).map((name) => `- ${name}`),
});
}
break;
case 'RELEASE_GROUP_RELEASE_TAG_PATTERN_VERSION_PLACEHOLDER_MISSING_OR_EXCESSIVE':
{
const nxJsonMessage = await resolveNxJsonConfigErrorMessage([
@ -610,27 +583,6 @@ function ensureArray(value: string | string[]): string[] {
return Array.isArray(value) ? value : [value];
}
function ensureProjectsHaveTarget(
projects: string[],
projectGraph: ProjectGraph,
requiredTargetName: string
): null | CreateNxReleaseConfigError {
const missingTargetProjects = projects.filter(
(project) =>
!projectHasTarget(projectGraph.nodes[project], requiredTargetName)
);
if (missingTargetProjects.length) {
return {
code: 'PROJECTS_MISSING_TARGET',
data: {
targetName: requiredTargetName,
projects: missingTargetProjects,
},
};
}
return null;
}
function isObject(value: any): value is Record<string, any> {
return value && typeof value === 'object' && !Array.isArray(value);
}

View File

@ -11,6 +11,8 @@ import {
readGraphFileFromGraphArg,
} from '../../utils/command-line-utils';
import { logger } from '../../utils/logger';
import { handleErrors } from '../../utils/params';
import { projectHasTarget } from '../../utils/project-graph-utils';
import { generateGraph } from '../graph/graph';
import { PublishOptions } from './command-object';
import {
@ -20,14 +22,17 @@ import {
import { filterReleaseGroups } from './config/filter-release-groups';
export const releasePublishCLIHandler = (args: PublishOptions) =>
releasePublish(args);
handleErrors(args.verbose, () => releasePublish(args, true));
/**
* NOTE: This function is also exported for programmatic usage and forms part of the public API
* of Nx. We intentionally do not wrap the implementation with handleErrors because users need
* to have control over their own error handling when using the API.
*/
export async function releasePublish(args: PublishOptions): Promise<void> {
export async function releasePublish(
args: PublishOptions,
isCLI = false
): Promise<void> {
/**
* When used via the CLI, the args object will contain a __overrides_unparsed__ property that is
* important for invoking the relevant executor behind the scenes.
@ -47,8 +52,7 @@ export async function releasePublish(args: PublishOptions): Promise<void> {
// Apply default configuration to any optional user configuration
const { error: configError, nxReleaseConfig } = await createNxReleaseConfig(
projectGraph,
nxJson.release,
'nx-release-publish'
nxJson.release
);
if (configError) {
return await handleNxReleaseConfigError(configError);
@ -86,7 +90,8 @@ export async function releasePublish(args: PublishOptions): Promise<void> {
projectGraph,
nxJson,
Array.from(releaseGroupToFilteredProjects.get(releaseGroup)),
shouldExcludeTaskDependencies
shouldExcludeTaskDependencies,
isCLI
);
}
@ -102,7 +107,8 @@ export async function releasePublish(args: PublishOptions): Promise<void> {
projectGraph,
nxJson,
releaseGroup.projects,
shouldExcludeTaskDependencies
shouldExcludeTaskDependencies,
isCLI
);
}
@ -120,7 +126,8 @@ async function runPublishOnProjects(
projectGraph: ProjectGraph,
nxJson: NxJsonConfiguration,
projectNames: string[],
shouldExcludeTaskDependencies: boolean
shouldExcludeTaskDependencies: boolean,
isCLI: boolean
) {
const projectsToRun: ProjectGraphProjectNode[] = projectNames.map(
(projectName) => projectGraph.nodes[projectName]
@ -171,7 +178,9 @@ async function runPublishOnProjects(
},
projectNames
);
} else {
}
ensureAllProjectsHaveTarget(projectsToRun);
/**
* Run the relevant nx-release-publish executor on each of the selected projects.
*/
@ -194,7 +203,30 @@ async function runPublishOnProjects(
);
if (status !== 0) {
process.exit(status);
// In order to not add noise to the overall CLI output, do not throw an additional error
if (isCLI) {
return status;
}
// Throw an additional error for programmatic API usage
throw new Error(
'One or more of the selected projects could not be published'
);
}
}
function ensureAllProjectsHaveTarget(projectsToRun: ProjectGraphProjectNode[]) {
const requiredTargetName = 'nx-release-publish';
const projectsMissingTarget = projectsToRun.filter(
(project) => !projectHasTarget(project, requiredTargetName)
);
if (projectsMissingTarget.length === 0) {
return;
}
throw new Error(
`Based on your config, the following projects were matched for publishing but do not have the "${requiredTargetName}" target specified:\n${[
...projectsMissingTarget.map((p) => `- ${p.name}`),
'',
'There are a few possible reasons for this: (1) The projects may be private (2) You may not have an appropriate plugin (such as `@nx/js`) installed which adds the target automatically to public projects (3) You intended to configure the target manually, or exclude those projects via config in nx.json',
].join('\n')}\n`
);
}

View File

@ -54,8 +54,7 @@ export async function release(
// Apply default configuration to any optional user configuration
const { error: configError, nxReleaseConfig } = await createNxReleaseConfig(
projectGraph,
nxJson.release,
'nx-release-publish'
nxJson.release
);
if (configError) {
return await handleNxReleaseConfigError(configError);

View File

@ -107,8 +107,7 @@ export async function releaseVersion(
// Apply default configuration to any optional user configuration
const { error: configError, nxReleaseConfig } = await createNxReleaseConfig(
projectGraph,
nxJson.release,
'nx-release-publish'
nxJson.release
);
if (configError) {
return await handleNxReleaseConfigError(configError);
@ -575,6 +574,7 @@ function resolveGeneratorData({
configGeneratorOptions,
projects,
}): GeneratorData {
try {
const { normalizedGeneratorName, schema, implementationFactory } =
getGeneratorInformation(
collectionName,
@ -591,4 +591,31 @@ function resolveGeneratorData({
schema,
implementationFactory,
};
} catch (err) {
if (err.message.startsWith('Unable to resolve')) {
// See if it is because the plugin is not installed
try {
require.resolve(collectionName);
// is installed
throw new Error(
`Unable to resolve the generator called "${generatorName}" within the "${collectionName}" package`
);
} catch {
/**
* Special messaging for the most common case (especially as the user is unlikely to explicitly have
* the @nx/js generator config in their nx.json so we need to be clear about what the problem is)
*/
if (collectionName === '@nx/js') {
throw new Error(
'The @nx/js plugin is required in order to version your JavaScript packages. Please install it and try again.'
);
}
throw new Error(
`Unable to resolve the package ${collectionName} in order to load the generator called ${generatorName}. Is the package installed?`
);
}
}
// Unexpected error, rethrow
throw err;
}
}

View File

@ -18,20 +18,6 @@ const libConfig = (root, name?: string) => ({
},
});
const packageLibConfig = (root, name?: string) => ({
name: name ?? toProjectName(`${root}/some-file`),
root,
sourceRoot: root,
projectType: 'library',
targets: {
'nx-release-publish': {
dependsOn: ['^nx-release-publish'],
executor: '@nx/js:release-publish',
options: {},
},
},
});
describe('Workspaces', () => {
let fs: TempFs;
beforeEach(() => {

View File

@ -134,7 +134,8 @@ export function buildTargetFromScript(
};
}
export function readTargetsFromPackageJson({ scripts, nx }: PackageJson) {
export function readTargetsFromPackageJson(packageJson: PackageJson) {
const { scripts, nx } = packageJson;
const res: Record<string, TargetConfiguration> = {};
Object.keys(scripts || {}).forEach((script) => {
if (!nx?.includedScripts || nx?.includedScripts.includes(script)) {
@ -142,8 +143,12 @@ export function readTargetsFromPackageJson({ scripts, nx }: PackageJson) {
}
});
// Add implicit nx-release-publish target for all package.json files to allow for lightweight configuration for package based repos
if (!res['nx-release-publish']) {
/**
* Add implicit nx-release-publish target for all package.json files that are
* not marked as `"private": true` to allow for lightweight configuration for
* package based repos.
*/
if (!packageJson.private && !res['nx-release-publish']) {
res['nx-release-publish'] = {
dependsOn: ['^nx-release-publish'],
executor: '@nx/js:release-publish',

View File

@ -120,7 +120,7 @@ const LARGE_BUFFER = 1024 * 1000000;
// If publishing locally, force all projects to not be private first
if (options.local) {
console.log(
chalk.dim`\n Publishing locally, so setting all resolved packages to not be private`
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)) {