feat(core): programmatic API for nx release (#20371)
This commit is contained in:
parent
905ef65136
commit
1a994c7f92
@ -121,6 +121,14 @@ Type: `string`
|
||||
|
||||
Exact version or semver keyword to apply to the selected release group.
|
||||
|
||||
##### stageChanges
|
||||
|
||||
Type: `boolean`
|
||||
|
||||
Default: `false`
|
||||
|
||||
Whether or not to stage the changes made by this command, irrespective of the git config in nx.json related to automated commits. Useful when combining this command with changelog generation.
|
||||
|
||||
##### version
|
||||
|
||||
Type: `boolean`
|
||||
|
||||
@ -121,6 +121,14 @@ Type: `string`
|
||||
|
||||
Exact version or semver keyword to apply to the selected release group.
|
||||
|
||||
##### stageChanges
|
||||
|
||||
Type: `boolean`
|
||||
|
||||
Default: `false`
|
||||
|
||||
Whether or not to stage the changes made by this command, irrespective of the git config in nx.json related to automated commits. Useful when combining this command with changelog generation.
|
||||
|
||||
##### version
|
||||
|
||||
Type: `boolean`
|
||||
|
||||
@ -24,7 +24,8 @@ describe('@nx/workspace:convert-to-monorepo', () => {
|
||||
|
||||
afterEach(() => cleanupProject());
|
||||
|
||||
it('should convert a standalone project to a monorepo', async () => {
|
||||
// TODO: troubleshoot and reenable this test
|
||||
xit('should convert a standalone project to a monorepo', async () => {
|
||||
const reactApp = uniq('reactapp');
|
||||
runCLI(
|
||||
`generate @nx/react:app ${reactApp} --rootProject=true --bundler=webpack --unitTestRunner=jest --e2eTestRunner=cypress --no-interactive`
|
||||
|
||||
@ -304,14 +304,7 @@ To fix this you will either need to add a package.json file at that location, or
|
||||
}
|
||||
}
|
||||
|
||||
if (!specifier) {
|
||||
log(
|
||||
`🚫 Skipping versioning "${projectPackageJson.name}" as no changes were detected.`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Resolve any local package dependencies for this project (before applying the new version)
|
||||
// Resolve any local package dependencies for this project (before applying the new version or updating the versionData)
|
||||
const localPackageDependencies = resolveLocalPackageDependencies(
|
||||
tree,
|
||||
options.projectGraph,
|
||||
@ -322,11 +315,31 @@ To fix this you will either need to add a package.json file at that location, or
|
||||
options.releaseGroup.projectsRelationship === 'independent'
|
||||
);
|
||||
|
||||
const dependentProjects = Object.values(localPackageDependencies)
|
||||
.flat()
|
||||
.filter((localPackageDependency) => {
|
||||
return localPackageDependency.target === project.name;
|
||||
});
|
||||
|
||||
versionData[projectName] = {
|
||||
currentVersion,
|
||||
dependentProjects,
|
||||
newVersion: null, // will stay as null in the final result the case that no changes are detected
|
||||
};
|
||||
|
||||
if (!specifier) {
|
||||
log(
|
||||
`🚫 Skipping versioning "${projectPackageJson.name}" as no changes were detected.`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const newVersion = deriveNewSemverVersion(
|
||||
currentVersion,
|
||||
specifier,
|
||||
options.preid
|
||||
);
|
||||
versionData[projectName].newVersion = newVersion;
|
||||
|
||||
writeJson(tree, packageJsonPath, {
|
||||
...projectPackageJson,
|
||||
@ -337,12 +350,6 @@ To fix this you will either need to add a package.json file at that location, or
|
||||
`✍️ New version ${newVersion} written to ${workspaceRelativePackageJsonPath}`
|
||||
);
|
||||
|
||||
const dependentProjects = Object.values(localPackageDependencies)
|
||||
.flat()
|
||||
.filter((localPackageDependency) => {
|
||||
return localPackageDependency.target === project.name;
|
||||
});
|
||||
|
||||
if (dependentProjects.length > 0) {
|
||||
log(
|
||||
`✍️ Applying new version ${newVersion} to ${
|
||||
@ -369,12 +376,6 @@ To fix this you will either need to add a package.json file at that location, or
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
versionData[projectName] = {
|
||||
currentVersion,
|
||||
newVersion,
|
||||
dependentProjects,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -56,17 +56,14 @@ import {
|
||||
|
||||
type PostGitTask = (latestCommit: string) => Promise<void>;
|
||||
|
||||
export async function changelogHandler(
|
||||
/**
|
||||
* NOTE: This function is also exported for programmatic usage and forms part of the public API
|
||||
* of Nx.
|
||||
*/
|
||||
export async function releaseChangelog(
|
||||
args: ChangelogOptions
|
||||
): Promise<number> {
|
||||
return handleErrors(args.verbose, async () => {
|
||||
// Right now, the given version must be valid semver in order to proceed
|
||||
if (!valid(args.version)) {
|
||||
throw new Error(
|
||||
`The given version "${args.version}" is not a valid semver version. Please provide your version in the format "1.0.0", "1.0.0-beta.1" etc`
|
||||
);
|
||||
}
|
||||
|
||||
const projectGraph = await createProjectGraphAsync({ exitOnError: true });
|
||||
const nxJson = readNxJson();
|
||||
|
||||
@ -83,21 +80,6 @@ export async function changelogHandler(
|
||||
return await handleNxReleaseConfigError(configError);
|
||||
}
|
||||
|
||||
const toSHA = await getCommitHash(args.to);
|
||||
const headSHA = args.to === 'HEAD' ? toSHA : await getCommitHash('HEAD');
|
||||
|
||||
/**
|
||||
* Protect the user against attempting to create a new commit when recreating an old release changelog,
|
||||
* this seems like it would always be unintentional.
|
||||
*/
|
||||
const autoCommitEnabled =
|
||||
args.gitCommit ?? nxReleaseConfig.changelog.git.commit;
|
||||
if (autoCommitEnabled && headSHA !== toSHA) {
|
||||
throw new Error(
|
||||
`You are attempting to recreate the changelog for an old release, but you have enabled auto-commit mode. Please disable auto-commit mode by updating your nx.json, or passing --git-commit=false`
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
error: filterError,
|
||||
releaseGroups,
|
||||
@ -113,6 +95,43 @@ export async function changelogHandler(
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* For determining the versions to use within changelog files, there are a few different possibilities:
|
||||
* - the user is using the nx CLI, and therefore passes a single --version argument which represents the version for any and all changelog
|
||||
* files which will be generated (i.e. both the workspace changelog, and all project changelogs, depending on which of those has been enabled)
|
||||
* - the user is using the nxReleaseChangelog API programmatically, and:
|
||||
* - passes only a version property
|
||||
* - this works in the same way as described above for the CLI
|
||||
* - passes only a versionData object
|
||||
* - this is a special case where the user is providing a version for each project, and therefore the version argument is not needed
|
||||
* - NOTE: it is not possible to generate a workspace level changelog with only a versionData object, and this will produce an error
|
||||
* - passes both a version and a versionData object
|
||||
* - in this case, the version property will be used as the reference for the workspace changelog, and the versionData object will be used
|
||||
* to generate project changelogs
|
||||
*/
|
||||
const { workspaceChangelogVersion, projectsVersionData } =
|
||||
resolveChangelogVersions(
|
||||
args,
|
||||
releaseGroups,
|
||||
releaseGroupToFilteredProjects
|
||||
);
|
||||
|
||||
const to = args.to || 'HEAD';
|
||||
const toSHA = await getCommitHash(to);
|
||||
const headSHA = to === 'HEAD' ? toSHA : await getCommitHash('HEAD');
|
||||
|
||||
/**
|
||||
* Protect the user against attempting to create a new commit when recreating an old release changelog,
|
||||
* this seems like it would always be unintentional.
|
||||
*/
|
||||
const autoCommitEnabled =
|
||||
args.gitCommit ?? nxReleaseConfig.changelog.git.commit;
|
||||
if (autoCommitEnabled && headSHA !== toSHA) {
|
||||
throw new Error(
|
||||
`You are attempting to recreate the changelog for an old release, but you have enabled auto-commit mode. Please disable auto-commit mode by updating your nx.json, or passing --git-commit=false`
|
||||
);
|
||||
}
|
||||
|
||||
const fromRef =
|
||||
args.from ||
|
||||
(await getLatestGitTagForPattern(nxReleaseConfig.releaseTagPattern))?.tag;
|
||||
@ -140,31 +159,13 @@ export async function changelogHandler(
|
||||
|
||||
const tree = new FsTree(workspaceRoot, args.verbose);
|
||||
|
||||
// Create a pseudo-versionData object using the version passed into the command so that we can share commit and tagging utils with version
|
||||
const versionData: VersionData = releaseGroups.reduce(
|
||||
(versionData, releaseGroup) => {
|
||||
const releaseGroupProjectNames = Array.from(
|
||||
releaseGroupToFilteredProjects.get(releaseGroup)
|
||||
);
|
||||
for (const projectName of releaseGroupProjectNames) {
|
||||
versionData[projectName] = {
|
||||
newVersion: args.version,
|
||||
currentVersion: '', // not needed within changelog/commit generation
|
||||
dependentProjects: [], // not needed within changelog/commit generation
|
||||
};
|
||||
}
|
||||
return versionData;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
const userCommitMessage: string | undefined =
|
||||
args.gitCommitMessage || nxReleaseConfig.changelog.git.commitMessage;
|
||||
|
||||
const commitMessageValues: string[] = createCommitMessageValues(
|
||||
releaseGroups,
|
||||
releaseGroupToFilteredProjects,
|
||||
versionData,
|
||||
projectsVersionData,
|
||||
userCommitMessage
|
||||
);
|
||||
|
||||
@ -174,7 +175,7 @@ export async function changelogHandler(
|
||||
? createGitTagValues(
|
||||
releaseGroups,
|
||||
releaseGroupToFilteredProjects,
|
||||
versionData
|
||||
projectsVersionData
|
||||
)
|
||||
: [];
|
||||
handleDuplicateGitTags(gitTagValues);
|
||||
@ -185,6 +186,7 @@ export async function changelogHandler(
|
||||
tree,
|
||||
args,
|
||||
nxReleaseConfig,
|
||||
workspaceChangelogVersion,
|
||||
commits,
|
||||
postGitTasks
|
||||
);
|
||||
@ -202,6 +204,7 @@ export async function changelogHandler(
|
||||
tree,
|
||||
args,
|
||||
commits,
|
||||
projectsVersionData,
|
||||
postGitTasks,
|
||||
releaseGroup,
|
||||
projectNodes
|
||||
@ -231,6 +234,7 @@ export async function changelogHandler(
|
||||
tree,
|
||||
args,
|
||||
commits,
|
||||
projectsVersionData,
|
||||
postGitTasks,
|
||||
releaseGroup,
|
||||
projectNodes
|
||||
@ -249,6 +253,67 @@ export async function changelogHandler(
|
||||
});
|
||||
}
|
||||
|
||||
function resolveChangelogVersions(
|
||||
args: ChangelogOptions,
|
||||
releaseGroups: ReleaseGroupWithName[],
|
||||
releaseGroupToFilteredProjects: Map<ReleaseGroupWithName, Set<string>>
|
||||
): {
|
||||
workspaceChangelogVersion: string | undefined;
|
||||
projectsVersionData: VersionData;
|
||||
} {
|
||||
if (!args.version && !args.versionData) {
|
||||
throw new Error(
|
||||
`You must provide a version string and/or a versionData object.`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: revaluate this assumption holistically in a dedicated PR when we add support for calver
|
||||
* (e.g. the Release class also uses semver utils to check if prerelease).
|
||||
*
|
||||
* Right now, the given version must be valid semver in order to proceed
|
||||
*/
|
||||
if (args.version && !valid(args.version)) {
|
||||
throw new Error(
|
||||
`The given version "${args.version}" is not a valid semver version. Please provide your version in the format "1.0.0", "1.0.0-beta.1" etc`
|
||||
);
|
||||
}
|
||||
|
||||
const versionData: VersionData = releaseGroups.reduce(
|
||||
(versionData, releaseGroup) => {
|
||||
const releaseGroupProjectNames = Array.from(
|
||||
releaseGroupToFilteredProjects.get(releaseGroup)
|
||||
);
|
||||
for (const projectName of releaseGroupProjectNames) {
|
||||
if (!args.versionData) {
|
||||
versionData[projectName] = {
|
||||
newVersion: args.version,
|
||||
currentVersion: '', // not relevant within changelog/commit generation
|
||||
dependentProjects: [], // not relevant within changelog/commit generation
|
||||
};
|
||||
continue;
|
||||
}
|
||||
/**
|
||||
* In the case where a versionData object was provided, we need to make sure all projects are present,
|
||||
* otherwise it suggests a filtering mismatch between the version and changelog command invocations.
|
||||
*/
|
||||
if (!args.versionData[projectName]) {
|
||||
throw new Error(
|
||||
`The provided versionData object does not contain a version for project "${projectName}". This suggests a filtering mismatch between the version and changelog command invocations.`
|
||||
);
|
||||
}
|
||||
}
|
||||
return versionData;
|
||||
},
|
||||
args.versionData || {}
|
||||
);
|
||||
|
||||
return {
|
||||
workspaceChangelogVersion: args.version,
|
||||
projectsVersionData: versionData,
|
||||
};
|
||||
}
|
||||
|
||||
async function applyChangesAndExit(
|
||||
args: ChangelogOptions,
|
||||
nxReleaseConfig: NxReleaseConfig,
|
||||
@ -260,10 +325,22 @@ async function applyChangesAndExit(
|
||||
) {
|
||||
let latestCommit = toSHA;
|
||||
|
||||
const changes = tree.listChanges();
|
||||
// This could happen we using conventional commits, for example
|
||||
if (!changes.length) {
|
||||
output.warn({
|
||||
title: `No changes detected for changelogs`,
|
||||
bodyLines: [
|
||||
`No changes were detected for any changelog files, so no changelog entries will be generated.`,
|
||||
],
|
||||
});
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Generate a new commit for the changes, if configured to do so
|
||||
if (args.gitCommit ?? nxReleaseConfig.changelog.git.commit) {
|
||||
await commitChanges(
|
||||
tree.listChanges().map((f) => f.path),
|
||||
changes.map((f) => f.path),
|
||||
!!args.dryRun,
|
||||
!!args.verbose,
|
||||
commitMessageValues,
|
||||
@ -326,6 +403,7 @@ async function generateChangelogForWorkspace(
|
||||
tree: Tree,
|
||||
args: ChangelogOptions,
|
||||
nxReleaseConfig: NxReleaseConfig,
|
||||
workspaceChangelogVersion: (string | null) | undefined,
|
||||
commits: GitCommit[],
|
||||
postGitTasks: PostGitTask[]
|
||||
) {
|
||||
@ -335,11 +413,21 @@ async function generateChangelogForWorkspace(
|
||||
return;
|
||||
}
|
||||
|
||||
// If explicitly null it must mean that no changes were detected (e.g. when using conventional commits), so do nothing
|
||||
if (workspaceChangelogVersion === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!workspaceChangelogVersion) {
|
||||
throw new Error(
|
||||
`Workspace changelog is enabled but no overall version was provided. Please provide an explicit version using --version`
|
||||
);
|
||||
}
|
||||
|
||||
// Only trigger interactive mode for the workspace changelog if the user explicitly requested it via "all" or "workspace"
|
||||
const interactive =
|
||||
args.interactive === 'all' || args.interactive === 'workspace';
|
||||
const dryRun = !!args.dryRun;
|
||||
const verbose = !!args.verbose;
|
||||
const gitRemote = args.gitRemote;
|
||||
|
||||
const changelogRenderer = resolveChangelogRenderer(config.renderer);
|
||||
@ -354,7 +442,7 @@ async function generateChangelogForWorkspace(
|
||||
}
|
||||
|
||||
const releaseVersion = new ReleaseVersion({
|
||||
version: args.version,
|
||||
version: workspaceChangelogVersion,
|
||||
releaseTagPattern: nxReleaseConfig.releaseTagPattern,
|
||||
});
|
||||
|
||||
@ -551,6 +639,7 @@ async function generateChangelogForProjects(
|
||||
tree: Tree,
|
||||
args: ChangelogOptions,
|
||||
commits: GitCommit[],
|
||||
projectsVersionData: VersionData,
|
||||
postGitTasks: PostGitTask[],
|
||||
releaseGroup: ReleaseGroupWithName,
|
||||
projects: ProjectGraphProjectNode[]
|
||||
@ -566,7 +655,6 @@ async function generateChangelogForProjects(
|
||||
args.interactive === 'all' || args.interactive === 'projects';
|
||||
const dryRun = !!args.dryRun;
|
||||
const gitRemote = args.gitRemote;
|
||||
const rawVersion = args.version;
|
||||
|
||||
const changelogRenderer = resolveChangelogRenderer(config.renderer);
|
||||
|
||||
@ -580,8 +668,16 @@ async function generateChangelogForProjects(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* newVersion will be null in the case that no changes were detected (e.g. in conventional commits mode),
|
||||
* no changelog entry is relevant in that case.
|
||||
*/
|
||||
if (projectsVersionData[project.name].newVersion === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const releaseVersion = new ReleaseVersion({
|
||||
version: rawVersion,
|
||||
version: projectsVersionData[project.name].newVersion,
|
||||
releaseTagPattern: releaseGroup.releaseTagPattern,
|
||||
projectName: project.name,
|
||||
});
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
import { Argv, CommandModule, showHelp } from 'yargs';
|
||||
import { readNxJson } from '../../project-graph/file-utils';
|
||||
import {
|
||||
OutputStyle,
|
||||
RunManyOptions,
|
||||
parseCSV,
|
||||
withOutputStyleOption,
|
||||
withOverrides,
|
||||
withRunManyOptions,
|
||||
} from '../yargs-utils/shared-options';
|
||||
import { VersionData } from './utils/shared';
|
||||
|
||||
export interface NxReleaseArgs {
|
||||
groups?: string[];
|
||||
@ -28,19 +30,22 @@ export type VersionOptions = NxReleaseArgs &
|
||||
GitCommitAndTagOptions & {
|
||||
specifier?: string;
|
||||
preid?: string;
|
||||
stageChanges?: boolean;
|
||||
};
|
||||
|
||||
export type ChangelogOptions = NxReleaseArgs &
|
||||
GitCommitAndTagOptions & {
|
||||
version: string;
|
||||
to: string;
|
||||
// version and/or versionData must be set
|
||||
version?: string | null;
|
||||
versionData?: VersionData;
|
||||
to?: string;
|
||||
from?: string;
|
||||
interactive?: string;
|
||||
gitRemote?: string;
|
||||
};
|
||||
|
||||
export type PublishOptions = NxReleaseArgs &
|
||||
RunManyOptions & {
|
||||
Partial<RunManyOptions> & { outputStyle?: OutputStyle } & {
|
||||
registry?: string;
|
||||
tag?: string;
|
||||
otp?: number;
|
||||
@ -130,8 +135,22 @@ const versionCommand: CommandModule<NxReleaseArgs, VersionOptions> = {
|
||||
'The optional prerelease identifier to apply to the version, in the case that specifier has been set to prerelease.',
|
||||
default: '',
|
||||
})
|
||||
.option('stageChanges', {
|
||||
type: 'boolean',
|
||||
describe:
|
||||
'Whether or not to stage the changes made by this command, irrespective of the git config in nx.json related to automated commits. Useful when combining this command with changelog generation.',
|
||||
default: false,
|
||||
})
|
||||
),
|
||||
handler: (args) => import('./version').then((m) => m.versionHandler(args)),
|
||||
handler: (args) =>
|
||||
import('./version')
|
||||
.then((m) => m.releaseVersion(args))
|
||||
.then((versionDataOrExitCode) => {
|
||||
if (typeof versionDataOrExitCode === 'number') {
|
||||
return process.exit(versionDataOrExitCode);
|
||||
}
|
||||
process.exit(0);
|
||||
}),
|
||||
};
|
||||
|
||||
const changelogCommand: CommandModule<NxReleaseArgs, ChangelogOptions> = {
|
||||
@ -182,7 +201,7 @@ const changelogCommand: CommandModule<NxReleaseArgs, ChangelogOptions> = {
|
||||
})
|
||||
),
|
||||
handler: async (args) => {
|
||||
const status = await (await import('./changelog')).changelogHandler(args);
|
||||
const status = await (await import('./changelog')).releaseChangelog(args);
|
||||
process.exit(status);
|
||||
},
|
||||
};
|
||||
@ -208,7 +227,7 @@ const publishCommand: CommandModule<NxReleaseArgs, PublishOptions> = {
|
||||
}),
|
||||
handler: (args) =>
|
||||
import('./publish').then((m) =>
|
||||
m.publishHandler(coerceParallelOption(withOverrides(args, 2)))
|
||||
m.releasePublish(coerceParallelOption(withOverrides(args, 2)))
|
||||
),
|
||||
};
|
||||
|
||||
|
||||
@ -340,7 +340,7 @@ export async function createNxReleaseConfig(
|
||||
|
||||
export async function handleNxReleaseConfigError(
|
||||
error: CreateNxReleaseConfigError
|
||||
) {
|
||||
): Promise<never> {
|
||||
switch (error.code) {
|
||||
case 'RELEASE_GROUP_MATCHES_NO_PROJECTS':
|
||||
{
|
||||
|
||||
12
packages/nx/src/command-line/release/index.ts
Normal file
12
packages/nx/src/command-line/release/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export { releaseChangelog } from './changelog';
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export { releasePublish } from './publish';
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export { releaseVersion } from './version';
|
||||
@ -19,13 +19,24 @@ import {
|
||||
} from './config/config';
|
||||
import { filterReleaseGroups } from './config/filter-release-groups';
|
||||
|
||||
export async function publishHandler(
|
||||
args: PublishOptions & { __overrides_unparsed__: string[] }
|
||||
): Promise<void> {
|
||||
/**
|
||||
* NOTE: This function is also exported for programmatic usage and forms part of the public API
|
||||
* of Nx.
|
||||
*/
|
||||
export async function releasePublish(args: PublishOptions): 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.
|
||||
*
|
||||
* We intentionally do not include that in the function signature, however, so as not to cause
|
||||
* confusing errors for programmatic consumers of this function.
|
||||
*/
|
||||
const _args = args as PublishOptions & { __overrides_unparsed__: string[] };
|
||||
|
||||
const projectGraph = await createProjectGraphAsync({ exitOnError: true });
|
||||
const nxJson = readNxJson();
|
||||
|
||||
if (args.verbose) {
|
||||
if (_args.verbose) {
|
||||
process.env.NX_VERBOSE_LOGGING = 'true';
|
||||
}
|
||||
|
||||
@ -46,8 +57,8 @@ export async function publishHandler(
|
||||
} = filterReleaseGroups(
|
||||
projectGraph,
|
||||
nxReleaseConfig,
|
||||
args.projects,
|
||||
args.groups
|
||||
_args.projects,
|
||||
_args.groups
|
||||
);
|
||||
if (filterError) {
|
||||
output.error(filterError);
|
||||
@ -60,7 +71,7 @@ export async function publishHandler(
|
||||
*/
|
||||
for (const releaseGroup of releaseGroups) {
|
||||
await runPublishOnProjects(
|
||||
args,
|
||||
_args,
|
||||
projectGraph,
|
||||
nxJson,
|
||||
Array.from(releaseGroupToFilteredProjects.get(releaseGroup))
|
||||
@ -75,14 +86,14 @@ export async function publishHandler(
|
||||
*/
|
||||
for (const releaseGroup of releaseGroups) {
|
||||
await runPublishOnProjects(
|
||||
args,
|
||||
_args,
|
||||
projectGraph,
|
||||
nxJson,
|
||||
releaseGroup.projects
|
||||
);
|
||||
}
|
||||
|
||||
if (args.dryRun) {
|
||||
if (_args.dryRun) {
|
||||
logger.warn(
|
||||
`\nNOTE: The "dryRun" flag means no projects were actually published.`
|
||||
);
|
||||
|
||||
@ -174,7 +174,7 @@ export async function gitCommit({
|
||||
if (verbose) {
|
||||
logFn(
|
||||
dryRun
|
||||
? `Would commit files in git with the following command, but --dry-run was set:`
|
||||
? `Would commit all previously staged files in git with the following command, but --dry-run was set:`
|
||||
: `Committing files in git with the following command:`
|
||||
);
|
||||
logFn(`git ${commandArgs.join(' ')}`);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { prompt } from 'enquirer';
|
||||
import { RELEASE_TYPES, valid } from 'semver';
|
||||
import { ProjectGraph } from '../../../config/project-graph';
|
||||
import { createProjectFileMapUsingProjectGraph } from '../../../project-graph/file-map-utils';
|
||||
import { createFileMapUsingProjectGraph } from '../../../project-graph/file-map-utils';
|
||||
import { getGitDiff, parseCommits } from './git';
|
||||
import { ConventionalCommitsConfig, determineSemverChange } from './semver';
|
||||
|
||||
@ -24,18 +24,28 @@ export async function resolveSemverSpecifierFromConventionalCommits(
|
||||
): Promise<string | null> {
|
||||
const commits = await getGitDiff(from);
|
||||
const parsedCommits = parseCommits(commits);
|
||||
const projectFileMap = await createProjectFileMapUsingProjectGraph(
|
||||
projectGraph
|
||||
);
|
||||
const { fileMap } = await createFileMapUsingProjectGraph(projectGraph);
|
||||
const filesInReleaseGroup = new Set<string>(
|
||||
projectNames.reduce(
|
||||
(files, p) => [...files, ...projectFileMap[p].map((f) => f.file)],
|
||||
(files, p) => [...files, ...fileMap.projectFileMap[p].map((f) => f.file)],
|
||||
[] as string[]
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* The relevant commits are those that either:
|
||||
* - touch project files which are contained within the release group directly
|
||||
* - touch non-project files and the commit is not scoped
|
||||
*/
|
||||
const relevantCommits = parsedCommits.filter((c) =>
|
||||
c.affectedFiles.some((f) => filesInReleaseGroup.has(f))
|
||||
c.affectedFiles.some(
|
||||
(f) =>
|
||||
filesInReleaseGroup.has(f) ||
|
||||
(!c.scope &&
|
||||
fileMap.nonProjectFiles.some(
|
||||
(nonProjectFile) => nonProjectFile.file === f
|
||||
))
|
||||
)
|
||||
);
|
||||
|
||||
return determineSemverChange(relevantCommits, CONVENTIONAL_COMMITS_CONFIG);
|
||||
|
||||
@ -19,7 +19,7 @@ import {
|
||||
createProjectGraphAsync,
|
||||
readProjectsConfigurationFromProjectGraph,
|
||||
} from '../../project-graph/project-graph';
|
||||
import { combineOptionsForGenerator } from '../../utils/params';
|
||||
import { combineOptionsForGenerator, handleErrors } from '../../utils/params';
|
||||
import { parseGeneratorString } from '../generate/generate';
|
||||
import { getGeneratorInformation } from '../generate/generator-utils';
|
||||
import { VersionOptions } from './command-object';
|
||||
@ -31,7 +31,7 @@ import {
|
||||
ReleaseGroupWithName,
|
||||
filterReleaseGroups,
|
||||
} from './config/filter-release-groups';
|
||||
import { gitTag } from './utils/git';
|
||||
import { gitAdd, gitTag } from './utils/git';
|
||||
import { printDiff } from './utils/print-changes';
|
||||
import {
|
||||
VersionData,
|
||||
@ -58,49 +58,158 @@ export interface ReleaseVersionGeneratorSchema {
|
||||
currentVersionResolverMetadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export async function versionHandler(args: VersionOptions): Promise<void> {
|
||||
const projectGraph = await createProjectGraphAsync({ exitOnError: true });
|
||||
const { projects } = readProjectsConfigurationFromProjectGraph(projectGraph);
|
||||
const nxJson = readNxJson();
|
||||
interface NxReleaseVersionResult {
|
||||
/**
|
||||
* In one specific (and very common) case, an overall workspace version is relevant, for example when there is
|
||||
* only a single release group in which all projects have a fixed relationship to each other. In this case, the
|
||||
* overall workspace version is the same as the version of the release group (and every project within it). This
|
||||
* version could be a `string`, or it could be `null` if using conventional commits and no changes were detected.
|
||||
*
|
||||
* In all other cases (independent versioning, multiple release groups etc), the overall workspace version is
|
||||
* not applicable and will be `undefined` here. If a user attempts to use this value later when it is `undefined`
|
||||
* (for example in the changelog command), we will throw an appropriate error.
|
||||
*/
|
||||
workspaceVersion: (string | null) | undefined;
|
||||
projectsVersionData: VersionData;
|
||||
}
|
||||
|
||||
if (args.verbose) {
|
||||
process.env.NX_VERBOSE_LOGGING = 'true';
|
||||
}
|
||||
/**
|
||||
* NOTE: This function is also exported for programmatic usage and forms part of the public API
|
||||
* of Nx.
|
||||
*/
|
||||
export async function releaseVersion(
|
||||
args: VersionOptions
|
||||
): Promise<NxReleaseVersionResult> {
|
||||
return handleErrors(args.verbose, async () => {
|
||||
const projectGraph = await createProjectGraphAsync({ exitOnError: true });
|
||||
const { projects } =
|
||||
readProjectsConfigurationFromProjectGraph(projectGraph);
|
||||
const nxJson = readNxJson();
|
||||
|
||||
// Apply default configuration to any optional user configuration
|
||||
const { error: configError, nxReleaseConfig } = await createNxReleaseConfig(
|
||||
projectGraph,
|
||||
nxJson.release,
|
||||
'nx-release-publish'
|
||||
);
|
||||
if (configError) {
|
||||
return await handleNxReleaseConfigError(configError);
|
||||
}
|
||||
if (args.verbose) {
|
||||
process.env.NX_VERBOSE_LOGGING = 'true';
|
||||
}
|
||||
|
||||
const {
|
||||
error: filterError,
|
||||
releaseGroups,
|
||||
releaseGroupToFilteredProjects,
|
||||
} = filterReleaseGroups(
|
||||
projectGraph,
|
||||
nxReleaseConfig,
|
||||
args.projects,
|
||||
args.groups
|
||||
);
|
||||
if (filterError) {
|
||||
output.error(filterError);
|
||||
process.exit(1);
|
||||
}
|
||||
// Apply default configuration to any optional user configuration
|
||||
const { error: configError, nxReleaseConfig } = await createNxReleaseConfig(
|
||||
projectGraph,
|
||||
nxJson.release,
|
||||
'nx-release-publish'
|
||||
);
|
||||
if (configError) {
|
||||
return await handleNxReleaseConfigError(configError);
|
||||
}
|
||||
|
||||
const tree = new FsTree(workspaceRoot, args.verbose);
|
||||
const {
|
||||
error: filterError,
|
||||
releaseGroups,
|
||||
releaseGroupToFilteredProjects,
|
||||
} = filterReleaseGroups(
|
||||
projectGraph,
|
||||
nxReleaseConfig,
|
||||
args.projects,
|
||||
args.groups
|
||||
);
|
||||
if (filterError) {
|
||||
output.error(filterError);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const versionData: VersionData = {};
|
||||
const userCommitMessage: string | undefined =
|
||||
args.gitCommitMessage || nxReleaseConfig.version.git.commitMessage;
|
||||
const tree = new FsTree(workspaceRoot, args.verbose);
|
||||
|
||||
const versionData: VersionData = {};
|
||||
const userCommitMessage: string | undefined =
|
||||
args.gitCommitMessage || nxReleaseConfig.version.git.commitMessage;
|
||||
|
||||
if (args.projects?.length) {
|
||||
/**
|
||||
* Run versioning for all remaining release groups and filtered projects within them
|
||||
*/
|
||||
for (const releaseGroup of releaseGroups) {
|
||||
const releaseGroupName = releaseGroup.name;
|
||||
|
||||
// Resolve the generator data for the current release group
|
||||
const generatorData = resolveGeneratorData({
|
||||
...extractGeneratorCollectionAndName(
|
||||
`release-group "${releaseGroupName}"`,
|
||||
releaseGroup.version.generator
|
||||
),
|
||||
configGeneratorOptions: releaseGroup.version.generatorOptions,
|
||||
projects,
|
||||
});
|
||||
|
||||
const releaseGroupProjectNames = Array.from(
|
||||
releaseGroupToFilteredProjects.get(releaseGroup)
|
||||
);
|
||||
|
||||
await runVersionOnProjects(
|
||||
projectGraph,
|
||||
nxJson,
|
||||
args,
|
||||
tree,
|
||||
generatorData,
|
||||
releaseGroupProjectNames,
|
||||
releaseGroup,
|
||||
versionData
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve any git tags as early as possible so that we can hard error in case of any duplicates before reaching the actual git command
|
||||
const gitTagValues: string[] =
|
||||
args.gitTag ?? nxReleaseConfig.version.git.tag
|
||||
? createGitTagValues(
|
||||
releaseGroups,
|
||||
releaseGroupToFilteredProjects,
|
||||
versionData
|
||||
)
|
||||
: [];
|
||||
handleDuplicateGitTags(gitTagValues);
|
||||
|
||||
printAndFlushChanges(tree, !!args.dryRun);
|
||||
|
||||
if (args.gitCommit ?? nxReleaseConfig.version.git.commit) {
|
||||
await commitChanges(
|
||||
tree.listChanges().map((f) => f.path),
|
||||
!!args.dryRun,
|
||||
!!args.verbose,
|
||||
createCommitMessageValues(
|
||||
releaseGroups,
|
||||
releaseGroupToFilteredProjects,
|
||||
versionData,
|
||||
userCommitMessage
|
||||
),
|
||||
args.gitCommitArgs || nxReleaseConfig.version.git.commitArgs
|
||||
);
|
||||
}
|
||||
|
||||
if (args.gitTag ?? nxReleaseConfig.version.git.tag) {
|
||||
output.logSingleLine(`Tagging commit with git`);
|
||||
for (const tag of gitTagValues) {
|
||||
await gitTag({
|
||||
tag,
|
||||
message:
|
||||
args.gitTagMessage || nxReleaseConfig.version.git.tagMessage,
|
||||
additionalArgs:
|
||||
args.gitTagArgs || nxReleaseConfig.version.git.tagArgs,
|
||||
dryRun: args.dryRun,
|
||||
verbose: args.verbose,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (args.dryRun) {
|
||||
logger.warn(`\nNOTE: The "dryRun" flag means no changes were made.`);
|
||||
}
|
||||
|
||||
return {
|
||||
// An overall workspace version cannot be relevant when filtering to independent projects
|
||||
workspaceVersion: undefined,
|
||||
projectsVersionData: versionData,
|
||||
};
|
||||
}
|
||||
|
||||
if (args.projects?.length) {
|
||||
/**
|
||||
* Run versioning for all remaining release groups and filtered projects within them
|
||||
* Run versioning for all remaining release groups
|
||||
*/
|
||||
for (const releaseGroup of releaseGroups) {
|
||||
const releaseGroupName = releaseGroup.name;
|
||||
@ -115,17 +224,13 @@ export async function versionHandler(args: VersionOptions): Promise<void> {
|
||||
projects,
|
||||
});
|
||||
|
||||
const releaseGroupProjectNames = Array.from(
|
||||
releaseGroupToFilteredProjects.get(releaseGroup)
|
||||
);
|
||||
|
||||
await runVersionOnProjects(
|
||||
projectGraph,
|
||||
nxJson,
|
||||
args,
|
||||
tree,
|
||||
generatorData,
|
||||
releaseGroupProjectNames,
|
||||
releaseGroup.projects,
|
||||
releaseGroup,
|
||||
versionData
|
||||
);
|
||||
@ -144,9 +249,42 @@ export async function versionHandler(args: VersionOptions): Promise<void> {
|
||||
|
||||
printAndFlushChanges(tree, !!args.dryRun);
|
||||
|
||||
// Only applicable when there is a single release group with a fixed relationship
|
||||
let workspaceVersion: string | null | undefined = undefined;
|
||||
if (releaseGroups.length === 1) {
|
||||
const releaseGroup = releaseGroups[0];
|
||||
if (releaseGroup.projectsRelationship === 'fixed') {
|
||||
const releaseGroupProjectNames = Array.from(
|
||||
releaseGroupToFilteredProjects.get(releaseGroup)
|
||||
);
|
||||
workspaceVersion = versionData[releaseGroupProjectNames[0]].newVersion; // all projects have the same version so we can just grab the first
|
||||
}
|
||||
}
|
||||
|
||||
const changedFiles = tree.listChanges().map((f) => f.path);
|
||||
|
||||
// No further actions are necessary in this scenario (e.g. if conventional commits detected no changes)
|
||||
if (!changedFiles.length) {
|
||||
return {
|
||||
workspaceVersion,
|
||||
projectsVersionData: versionData,
|
||||
};
|
||||
}
|
||||
|
||||
if (args.stageChanges) {
|
||||
output.logSingleLine(
|
||||
`Staging changed files with git because --stage-changes was set`
|
||||
);
|
||||
await gitAdd({
|
||||
changedFiles,
|
||||
dryRun: args.dryRun,
|
||||
verbose: args.verbose,
|
||||
});
|
||||
}
|
||||
|
||||
if (args.gitCommit ?? nxReleaseConfig.version.git.commit) {
|
||||
await commitChanges(
|
||||
tree.listChanges().map((f) => f.path),
|
||||
changedFiles,
|
||||
!!args.dryRun,
|
||||
!!args.verbose,
|
||||
createCommitMessageValues(
|
||||
@ -177,83 +315,11 @@ export async function versionHandler(args: VersionOptions): Promise<void> {
|
||||
logger.warn(`\nNOTE: The "dryRun" flag means no changes were made.`);
|
||||
}
|
||||
|
||||
return process.exit(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run versioning for all remaining release groups
|
||||
*/
|
||||
for (const releaseGroup of releaseGroups) {
|
||||
const releaseGroupName = releaseGroup.name;
|
||||
|
||||
// Resolve the generator data for the current release group
|
||||
const generatorData = resolveGeneratorData({
|
||||
...extractGeneratorCollectionAndName(
|
||||
`release-group "${releaseGroupName}"`,
|
||||
releaseGroup.version.generator
|
||||
),
|
||||
configGeneratorOptions: releaseGroup.version.generatorOptions,
|
||||
projects,
|
||||
});
|
||||
|
||||
await runVersionOnProjects(
|
||||
projectGraph,
|
||||
nxJson,
|
||||
args,
|
||||
tree,
|
||||
generatorData,
|
||||
releaseGroup.projects,
|
||||
releaseGroup,
|
||||
versionData
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve any git tags as early as possible so that we can hard error in case of any duplicates before reaching the actual git command
|
||||
const gitTagValues: string[] =
|
||||
args.gitTag ?? nxReleaseConfig.version.git.tag
|
||||
? createGitTagValues(
|
||||
releaseGroups,
|
||||
releaseGroupToFilteredProjects,
|
||||
versionData
|
||||
)
|
||||
: [];
|
||||
handleDuplicateGitTags(gitTagValues);
|
||||
|
||||
printAndFlushChanges(tree, !!args.dryRun);
|
||||
|
||||
if (args.gitCommit ?? nxReleaseConfig.version.git.commit) {
|
||||
await commitChanges(
|
||||
tree.listChanges().map((f) => f.path),
|
||||
!!args.dryRun,
|
||||
!!args.verbose,
|
||||
createCommitMessageValues(
|
||||
releaseGroups,
|
||||
releaseGroupToFilteredProjects,
|
||||
versionData,
|
||||
userCommitMessage
|
||||
),
|
||||
args.gitCommitArgs || nxReleaseConfig.version.git.commitArgs
|
||||
);
|
||||
}
|
||||
|
||||
if (args.gitTag ?? nxReleaseConfig.version.git.tag) {
|
||||
output.logSingleLine(`Tagging commit with git`);
|
||||
for (const tag of gitTagValues) {
|
||||
await gitTag({
|
||||
tag,
|
||||
message: args.gitTagMessage || nxReleaseConfig.version.git.tagMessage,
|
||||
additionalArgs: args.gitTagArgs || nxReleaseConfig.version.git.tagArgs,
|
||||
dryRun: args.dryRun,
|
||||
verbose: args.verbose,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (args.dryRun) {
|
||||
logger.warn(`\nNOTE: The "dryRun" flag means no changes were made.`);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
return {
|
||||
workspaceVersion,
|
||||
projectsVersionData: versionData,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function appendVersionData(
|
||||
@ -285,7 +351,7 @@ async function runVersionOnProjects(
|
||||
const generatorOptions: ReleaseVersionGeneratorSchema = {
|
||||
// Always ensure a string to avoid generator schema validation errors
|
||||
specifier: args.specifier ?? '',
|
||||
preid: args.preid,
|
||||
preid: args.preid ?? '',
|
||||
...generatorData.configGeneratorOptions,
|
||||
// The following are not overridable by user config
|
||||
projects: projectNames.map((p) => projectGraph.nodes[p]),
|
||||
|
||||
@ -224,9 +224,24 @@ export function withOverrides<T extends { _: Array<string | number> }>(
|
||||
};
|
||||
}
|
||||
|
||||
const allOutputStyles = [
|
||||
'dynamic',
|
||||
'static',
|
||||
'stream',
|
||||
'stream-without-prefixes',
|
||||
'compact',
|
||||
] as const;
|
||||
|
||||
export type OutputStyle = typeof allOutputStyles[number];
|
||||
|
||||
export function withOutputStyleOption(
|
||||
yargs: Argv,
|
||||
choices = ['dynamic', 'static', 'stream', 'stream-without-prefixes']
|
||||
choices: ReadonlyArray<OutputStyle> = [
|
||||
'dynamic',
|
||||
'static',
|
||||
'stream',
|
||||
'stream-without-prefixes',
|
||||
]
|
||||
) {
|
||||
return yargs.option('output-style', {
|
||||
describe: 'Defines how Nx emits outputs tasks logs',
|
||||
@ -295,13 +310,7 @@ export function withRunOneOptions(yargs: Argv) {
|
||||
);
|
||||
|
||||
const res = withRunOptions(
|
||||
withOutputStyleOption(withConfiguration(yargs), [
|
||||
'dynamic',
|
||||
'static',
|
||||
'stream',
|
||||
'stream-without-prefixes',
|
||||
'compact',
|
||||
])
|
||||
withOutputStyleOption(withConfiguration(yargs), allOutputStyles)
|
||||
)
|
||||
.parserConfiguration({
|
||||
'strip-dashed': true,
|
||||
|
||||
@ -4,46 +4,55 @@ import {
|
||||
ProjectFileMap,
|
||||
ProjectGraph,
|
||||
} from '../config/project-graph';
|
||||
import {
|
||||
createProjectRootMappingsFromProjectConfigurations,
|
||||
findProjectForPath,
|
||||
} from './utils/find-project-for-path';
|
||||
import {
|
||||
ProjectConfiguration,
|
||||
ProjectsConfigurations,
|
||||
} from '../config/workspace-json-project-json';
|
||||
import { daemonClient } from '../daemon/client/client';
|
||||
import { readProjectsConfigurationFromProjectGraph } from './project-graph';
|
||||
import { NxWorkspaceFilesExternals } from '../native';
|
||||
import {
|
||||
getAllFileDataInContext,
|
||||
updateProjectFiles,
|
||||
} from '../utils/workspace-context';
|
||||
import { workspaceRoot } from '../utils/workspace-root';
|
||||
import { ExternalObject, NxWorkspaceFilesExternals } from '../native';
|
||||
import { readProjectsConfigurationFromProjectGraph } from './project-graph';
|
||||
import { buildAllWorkspaceFiles } from './utils/build-all-workspace-files';
|
||||
import {
|
||||
createProjectRootMappingsFromProjectConfigurations,
|
||||
findProjectForPath,
|
||||
} from './utils/find-project-for-path';
|
||||
|
||||
export interface WorkspaceFileMap {
|
||||
allWorkspaceFiles: FileData[];
|
||||
fileMap: FileMap;
|
||||
}
|
||||
|
||||
export async function createProjectFileMapUsingProjectGraph(
|
||||
graph: ProjectGraph
|
||||
): Promise<ProjectFileMap> {
|
||||
return (await createFileMapUsingProjectGraph(graph)).fileMap.projectFileMap;
|
||||
}
|
||||
|
||||
// TODO: refactor this to pull straight from the rust context instead of creating the file map in JS
|
||||
export async function createFileMapUsingProjectGraph(
|
||||
graph: ProjectGraph
|
||||
): Promise<WorkspaceFileMap> {
|
||||
const configs = readProjectsConfigurationFromProjectGraph(graph);
|
||||
|
||||
let files;
|
||||
let files: FileData[];
|
||||
if (daemonClient.enabled()) {
|
||||
files = await daemonClient.getAllFileData();
|
||||
} else {
|
||||
files = getAllFileDataInContext(workspaceRoot);
|
||||
}
|
||||
|
||||
return createFileMap(configs, files).fileMap.projectFileMap;
|
||||
return createFileMap(configs, files);
|
||||
}
|
||||
|
||||
export function createFileMap(
|
||||
projectsConfigurations: ProjectsConfigurations,
|
||||
allWorkspaceFiles: FileData[]
|
||||
): {
|
||||
allWorkspaceFiles: FileData[];
|
||||
fileMap: FileMap;
|
||||
} {
|
||||
): WorkspaceFileMap {
|
||||
const projectFileMap: ProjectFileMap = {};
|
||||
const projectRootMappings =
|
||||
createProjectRootMappingsFromProjectConfigurations(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user