feat(core): programmatic API for nx release (#20371)

This commit is contained in:
James Henry 2023-11-24 00:50:18 +04:00 committed by GitHub
parent 905ef65136
commit 1a994c7f92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 484 additions and 234 deletions

View File

@ -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`

View File

@ -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`

View File

@ -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`

View File

@ -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,
};
}
/**

View File

@ -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,
});

View File

@ -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)))
),
};

View File

@ -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':
{

View File

@ -0,0 +1,12 @@
/**
* @public
*/
export { releaseChangelog } from './changelog';
/**
* @public
*/
export { releasePublish } from './publish';
/**
* @public
*/
export { releaseVersion } from './version';

View File

@ -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.`
);

View File

@ -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(' ')}`);

View File

@ -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);

View File

@ -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]),

View File

@ -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,

View File

@ -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(