fix(release): release plan command should take filters and touched projects into account (#27706)

This commit is contained in:
Austin Fahsl 2024-08-30 07:38:02 -06:00 committed by GitHub
parent 83a387a105
commit 4c39ad76bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 677 additions and 176 deletions

View File

@ -144,7 +144,9 @@ describe('nx release version plans check command', () => {
}); });
// Create a version plan // Create a version plan
runCLI(`release plan minor --message "This is an awesome change"`); runCLI(
`release plan minor --message "This is an awesome change" --only-touched=false`
);
// it should show information about the pending bumps and print a success message // it should show information about the pending bumps and print a success message
expect(runCLI('release plan:check')).toMatchInlineSnapshot(` expect(runCLI('release plan:check')).toMatchInlineSnapshot(`
@ -318,7 +320,7 @@ describe('nx release version plans check command', () => {
// create a version plan which references fixed-group directly by name // create a version plan which references fixed-group directly by name
runCLI( runCLI(
`release plan patch --message "A change for fixed-group" -g fixed-group` `release plan patch --message "A change for fixed-group" -g fixed-group --only-touched=false`
); );
// it should provide logs about the pending bump for fixed-group // it should provide logs about the pending bump for fixed-group

View File

@ -0,0 +1,301 @@
import { NxJsonConfiguration } from '@nx/devkit';
import {
cleanupProject,
newProject,
runCLI,
runCommandAsync,
uniq,
updateJson,
} from '@nx/e2e/utils';
expect.addSnapshotSerializer({
serialize(str: string) {
return (
str
// Remove all output unique to specific projects to ensure deterministic snapshots
.replaceAll(/my-pkg-\d+/g, '{project-name}')
.replaceAll(
/integrity:\s*.*/g,
'integrity: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
)
.replaceAll(/version-plan-\d*.md/g, 'version-plan-XXX.md')
.replaceAll(/\b[0-9a-f]{40}\b/g, '{SHASUM}')
.replaceAll(/\d*B index\.js/g, 'XXB index.js')
.replaceAll(/\d*B project\.json/g, 'XXB project.json')
.replaceAll(/\d*B package\.json/g, 'XXXB package.json')
.replaceAll(/size:\s*\d*\s?B/g, 'size: XXXB')
.replaceAll(/\d*\.\d*\s?kB/g, 'XXX.XXX kb')
.replaceAll(/[a-fA-F0-9]{7}/g, '{COMMIT_SHA}')
.replaceAll(/Test @[\w\d]+/g, 'Test @{COMMIT_AUTHOR}')
// Normalize the version title date.
.replaceAll(/\(\d{4}-\d{2}-\d{2}\)/g, '(YYYY-MM-DD)')
// We trim each line to reduce the chances of snapshot flakiness
.split('\n')
.map((r) => r.trim())
.join('\n')
);
},
test(val: string) {
return val != null && typeof val === 'string';
},
});
describe('nx release version plans only touched', () => {
let pkg1: string;
let pkg2: string;
let pkg3: string;
let pkg4: string;
let pkg5: string;
beforeEach(async () => {
newProject({
unsetProjectNameAndRootFormat: false,
packages: ['@nx/js'],
});
pkg1 = uniq('my-pkg-1');
runCLI(`generate @nx/workspace:npm-package ${pkg1}`);
pkg2 = uniq('my-pkg-2');
runCLI(`generate @nx/workspace:npm-package ${pkg2}`);
pkg3 = uniq('my-pkg-3');
runCLI(`generate @nx/workspace:npm-package ${pkg3}`);
pkg4 = uniq('my-pkg-4');
runCLI(`generate @nx/workspace:npm-package ${pkg4}`);
pkg5 = uniq('my-pkg-5');
runCLI(`generate @nx/workspace:npm-package ${pkg5}`);
await runCommandAsync(`git add .`);
await runCommandAsync(`git commit -m "chore: initial commit"`);
await runCommandAsync(`git tag -a v0.0.0 -m "v0.0.0"`);
await runCommandAsync(`git tag -a ${pkg3}@0.0.0 -m "${pkg3}@0.0.0"`);
await runCommandAsync(`git tag -a ${pkg4}@0.0.0 -m "${pkg4}@0.0.0"`);
await runCommandAsync(`git tag -a ${pkg5}@0.0.0 -m "${pkg5}@0.0.0"`);
}, 60000);
afterEach(() => cleanupProject());
it('should pick new versions based on version plans', async () => {
updateJson<NxJsonConfiguration>('nx.json', (nxJson) => {
nxJson.release = {
groups: {
'fixed-group': {
projects: [pkg1, pkg2],
releaseTagPattern: 'v{version}',
},
'independent-group': {
projects: [pkg3, pkg4, pkg5],
projectsRelationship: 'independent',
releaseTagPattern: '{projectName}@{version}',
},
},
version: {
generatorOptions: {
specifierSource: 'version-plans',
},
},
changelog: {
projectChangelogs: true,
},
versionPlans: true,
};
return nxJson;
});
const noChangedProjectsResult = runCLI(
'release plan minor -m "Should not happen due to no changed projects" --verbose',
{
silenceError: true,
}
);
expect(noChangedProjectsResult).toMatchInlineSnapshot(`
NX Affected criteria defaulted to --base=main --head=HEAD
NX Changed files based on resolved "base" ({SHASUM}) and "head" (HEAD)
- nx.json
NX No touched projects found based on changed files under release group "fixed-group"
NX No touched projects found based on changed files under release group "independent-group"
NX No version bumps were selected so no version plan file was created.
This might be because no projects have been changed, or projects you expected to release have not been touched
To include all projects, not just those that have been changed, pass --only-touched=false
Alternatively, you can specify alternate --base and --head refs to include only changes from certain commits
`);
await runCommandAsync(`touch ${pkg1}/test.txt`);
const changedProjectsResult = runCLI(
'release plan minor -m "Should happen due to changed project 1" --verbose'
);
expect(changedProjectsResult).toMatchInlineSnapshot(`
NX Affected criteria defaulted to --base=main --head=HEAD
NX Changed files based on resolved "base" ({SHASUM}) and "head" (HEAD)
- nx.json
- {project-name}/test.txt
NX Touched projects based on changed files under release group "fixed-group"
- {project-name}
NOTE: You can adjust your "versionPlans.ignorePatternsForPlanCheck" config to stop certain files from resulting in projects being classed as touched for the purposes of this command.
NX No touched projects found based on changed files under release group "independent-group"
NX Creating version plan file "version-plan-XXX.md"
+ ---
+ fixed-group: minor
+ ---
+
+ Should happen due to changed project 1
+
`);
await runCommandAsync(`git checkout -b branch1`);
await runCommandAsync(`git checkout -b branch2`);
await runCommandAsync(`git add ${pkg1}/test.txt`);
await runCommandAsync(`git commit -m "chore: update pkg1"`);
await runCommandAsync(`git checkout -b branch3`);
await runCommandAsync(`touch ${pkg3}/test.txt`);
await runCommandAsync(`git add ${pkg3}/test.txt`);
await runCommandAsync(`git commit -m "chore: update pkg3"`);
const changedProjectsResult2 = runCLI(
'release plan minor -m "Should happen due to changed project 1 and 3" --verbose'
);
expect(changedProjectsResult2).toMatchInlineSnapshot(`
NX Affected criteria defaulted to --base=main --head=HEAD
NX Changed files based on resolved "base" ({SHASUM}) and "head" (HEAD)
- {project-name}/test.txt
- {project-name}/test.txt
- nx.json
- .nx/version-plans/version-plan-XXX.md
NX Touched projects based on changed files under release group "fixed-group"
- {project-name}
NOTE: You can adjust your "versionPlans.ignorePatternsForPlanCheck" config to stop certain files from resulting in projects being classed as touched for the purposes of this command.
NX Touched projects based on changed files under release group "independent-group"
- {project-name}
NOTE: You can adjust your "versionPlans.ignorePatternsForPlanCheck" config to stop certain files from resulting in projects being classed as touched for the purposes of this command.
NX Creating version plan file "version-plan-XXX.md"
+ ---
+ fixed-group: minor
+ {project-name}: minor
+ ---
+
+ Should happen due to changed project 1 and 3
+
`);
const changedProjectsResult3 = runCLI(
`release plan minor -m "Should happen due to changed project 3 only" --verbose --base=branch2`
);
expect(changedProjectsResult3).toMatchInlineSnapshot(`
NX Changed files based on resolved "base" ({SHASUM}) and "head" (HEAD)
- {project-name}/test.txt
- nx.json
- .nx/version-plans/version-plan-XXX.md
- .nx/version-plans/version-plan-XXX.md
NX No touched projects found based on changed files under release group "fixed-group"
NX Touched projects based on changed files under release group "independent-group"
- {project-name}
NOTE: You can adjust your "versionPlans.ignorePatternsForPlanCheck" config to stop certain files from resulting in projects being classed as touched for the purposes of this command.
NX Creating version plan file "version-plan-XXX.md"
+ ---
+ {project-name}: minor
+ ---
+
+ Should happen due to changed project 3 only
+
`);
const changedProjectsResult4 = runCLI(
`release plan minor -m "Should happen due to changed project 1 only" --verbose --base=branch1 --head=branch2`
);
expect(changedProjectsResult4).toMatchInlineSnapshot(`
NX Changed files based on resolved "base" ({SHASUM}) and "head" (branch2)
- {project-name}/test.txt
NX Touched projects based on changed files under release group "fixed-group"
- {project-name}
NOTE: You can adjust your "versionPlans.ignorePatternsForPlanCheck" config to stop certain files from resulting in projects being classed as touched for the purposes of this command.
NX No touched projects found based on changed files under release group "independent-group"
NX Creating version plan file "version-plan-XXX.md"
+ ---
+ fixed-group: minor
+ ---
+
+ Should happen due to changed project 1 only
+
`);
});
});

View File

@ -111,10 +111,9 @@ describe('nx release version plans', () => {
}); });
const versionPlansDir = tmpProjPath('.nx/version-plans'); const versionPlansDir = tmpProjPath('.nx/version-plans');
await ensureDir(versionPlansDir);
runCLI( runCLI(
'release plan minor -g fixed-group -m "Update the fixed packages with a minor release." --verbose', 'release plan minor -g fixed-group -m "Update the fixed packages with a minor release." --verbose --only-touched=false',
{ {
silenceError: true, silenceError: true,
} }
@ -713,7 +712,7 @@ Update packages in both groups with a mix #2
await ensureDir(versionPlansDir); await ensureDir(versionPlansDir);
runCLI( runCLI(
'release plan minor -m "Update the fixed packages with a minor release." --verbose', 'release plan minor -m "Update the fixed packages with a minor release." --verbose --only-touched=false',
{ {
silenceError: true, silenceError: true,
} }
@ -796,14 +795,14 @@ Update packages in both groups with a mix #2
}); });
runCLI( runCLI(
'release plan minor -g fixed-group -m "Update the fixed packages with another minor release." --verbose', 'release plan minor -g fixed-group -m "Update the fixed packages with another minor release." --verbose --only-touched=false',
{ {
silenceError: true, silenceError: true,
} }
); );
runCLI( runCLI(
'release plan minor -g independent-group -m "Update the independent packages with another minor release." --verbose', 'release plan minor -g independent-group -m "Update the independent packages with another minor release." --verbose --only-touched=false',
{ {
silenceError: true, silenceError: true,
} }

View File

@ -919,7 +919,18 @@ async function applyChangesAndExit(
releaseGroups.forEach((group) => { releaseGroups.forEach((group) => {
if (group.resolvedVersionPlans) { if (group.resolvedVersionPlans) {
group.resolvedVersionPlans.forEach((plan) => { group.resolvedVersionPlans.forEach((plan) => {
if (!args.dryRun) {
removeSync(plan.absolutePath); removeSync(plan.absolutePath);
if (args.verbose) {
console.log(`Removing ${plan.relativePath}`);
}
} else {
if (args.verbose) {
console.log(
`Would remove ${plan.relativePath}, but --dry-run was set`
);
}
}
planFiles.add(plan.relativePath); planFiles.add(plan.relativePath);
}); });
} }

View File

@ -70,6 +70,7 @@ export type PublishOptions = NxReleaseArgs &
export type PlanOptions = NxReleaseArgs & { export type PlanOptions = NxReleaseArgs & {
bump?: string; bump?: string;
message?: string; message?: string;
onlyTouched?: boolean;
}; };
export type PlanCheckOptions = BaseNxReleaseArgs & { export type PlanCheckOptions = BaseNxReleaseArgs & {
@ -353,7 +354,7 @@ const planCommand: CommandModule<NxReleaseArgs, PlanOptions> = {
// Hidden for now until the feature is more stable // Hidden for now until the feature is more stable
describe: false, describe: false,
builder: (yargs) => builder: (yargs) =>
yargs withAffectedOptions(yargs)
.positional('bump', { .positional('bump', {
type: 'string', type: 'string',
describe: 'Semver keyword to use for the selected release group.', describe: 'Semver keyword to use for the selected release group.',
@ -371,6 +372,12 @@ const planCommand: CommandModule<NxReleaseArgs, PlanOptions> = {
type: 'string', type: 'string',
alias: 'm', alias: 'm',
describe: 'Custom message to use for the changelog entry', describe: 'Custom message to use for the changelog entry',
})
.option('onlyTouched', {
type: 'boolean',
describe:
'Only include projects that have been affected by the current changes',
default: true,
}), }),
handler: async (args) => { handler: async (args) => {
const release = await import('./plan'); const release = await import('./plan');

View File

@ -1,14 +1,11 @@
import { NxReleaseConfiguration, readNxJson } from '../../config/nx-json'; import { NxReleaseConfiguration, readNxJson } from '../../config/nx-json';
import { getTouchedProjects } from '../../project-graph/affected/locators/workspace-projects';
import { createProjectFileMapUsingProjectGraph } from '../../project-graph/file-map-utils'; import { createProjectFileMapUsingProjectGraph } from '../../project-graph/file-map-utils';
import { calculateFileChanges } from '../../project-graph/file-utils';
import { createProjectGraphAsync } from '../../project-graph/project-graph'; import { createProjectGraphAsync } from '../../project-graph/project-graph';
import { allFileData } from '../../utils/all-file-data'; import { allFileData } from '../../utils/all-file-data';
import { import {
parseFiles, parseFiles,
splitArgsIntoNxArgsAndOverrides, splitArgsIntoNxArgsAndOverrides,
} from '../../utils/command-line-utils'; } from '../../utils/command-line-utils';
import { getIgnoreObject } from '../../utils/ignore';
import { output } from '../../utils/output'; import { output } from '../../utils/output';
import { handleErrors } from '../../utils/params'; import { handleErrors } from '../../utils/params';
import { PlanCheckOptions, PlanOptions } from './command-object'; import { PlanCheckOptions, PlanOptions } from './command-object';
@ -23,6 +20,7 @@ import {
readRawVersionPlans, readRawVersionPlans,
setResolvedVersionPlansOnGroups, setResolvedVersionPlansOnGroups,
} from './config/version-plans'; } from './config/version-plans';
import { createGetTouchedProjectsForGroup } from './utils/get-touched-projects-for-group';
import { printConfigAndExit } from './utils/print-config'; import { printConfigAndExit } from './utils/print-config';
export const releasePlanCheckCLIHandler = (args: PlanCheckOptions) => export const releasePlanCheckCLIHandler = (args: PlanCheckOptions) =>
@ -55,15 +53,10 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
}); });
} }
const { // No filtering is applied here, as we want to consider all release groups for plan:check
error: filterError, const { error: filterError, releaseGroups } = filterReleaseGroups(
releaseGroups,
releaseGroupToFilteredProjects,
} = filterReleaseGroups(
projectGraph, projectGraph,
nxReleaseConfig, nxReleaseConfig
args.projects,
args.groups
); );
if (filterError) { if (filterError) {
output.error(filterError); output.error(filterError);
@ -116,14 +109,12 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
} }
const resolvedAllFileData = await allFileData(); const resolvedAllFileData = await allFileData();
/** const getTouchedProjectsForGroup = createGetTouchedProjectsForGroup(
* Create a minimal subset of touched projects based on the configured ignore patterns, we only need nxArgs,
* to recompute when the ignorePatternsForPlanCheck differs between release groups. projectGraph,
*/ changedFiles,
const serializedIgnorePatternsToTouchedProjects = new Map< resolvedAllFileData
string, );
Record<string, true> // project names -> true for O(N) lookup later
>();
const NOTE_ABOUT_VERBOSE_LOGGING = const NOTE_ABOUT_VERBOSE_LOGGING =
'Run with --verbose to see the full list of changed files used for the touched projects logic.'; 'Run with --verbose to see the full list of changed files used for the touched projects logic.';
@ -158,97 +149,12 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
continue; continue;
} }
// Exclude patterns from .nxignore, .gitignore and explicit version plan config const touchedProjectsUnderReleaseGroup = await getTouchedProjectsForGroup(
let serializedIgnorePatterns = '[]'; releaseGroup,
const ignore = getIgnoreObject(); // We do not take any --projects or --groups filtering into account for plan:check
releaseGroup.projects,
if ( false
typeof releaseGroup.versionPlans !== 'boolean' &&
Array.isArray(releaseGroup.versionPlans.ignorePatternsForPlanCheck) &&
releaseGroup.versionPlans.ignorePatternsForPlanCheck.length
) {
output.note({
title: `Applying configured ignore patterns to changed files${
releaseGroup.name !== IMPLICIT_DEFAULT_RELEASE_GROUP
? ` for release group "${releaseGroup.name}"`
: ''
}`,
bodyLines: [
...releaseGroup.versionPlans.ignorePatternsForPlanCheck.map(
(pattern) => ` - ${pattern}`
),
],
});
ignore.add(releaseGroup.versionPlans.ignorePatternsForPlanCheck);
serializedIgnorePatterns = JSON.stringify(
releaseGroup.versionPlans.ignorePatternsForPlanCheck
); );
}
let touchedProjects = {};
if (
serializedIgnorePatternsToTouchedProjects.has(serializedIgnorePatterns)
) {
touchedProjects = serializedIgnorePatternsToTouchedProjects.get(
serializedIgnorePatterns
);
} else {
// We only care about directly touched projects, not implicitly affected ones etc
const touchedProjectsArr = await getTouchedProjects(
calculateFileChanges(
changedFiles,
resolvedAllFileData,
nxArgs,
undefined,
ignore
),
projectGraph.nodes
);
touchedProjects = touchedProjectsArr.reduce(
(acc, project) => ({ ...acc, [project]: true }),
{}
);
serializedIgnorePatternsToTouchedProjects.set(
serializedIgnorePatterns,
touchedProjects
);
}
const touchedProjectsUnderReleaseGroup = releaseGroup.projects.filter(
(project) => touchedProjects[project]
);
if (touchedProjectsUnderReleaseGroup.length) {
output.log({
title: `Touched projects based on changed files${
releaseGroup.name !== IMPLICIT_DEFAULT_RELEASE_GROUP
? ` under release group "${releaseGroup.name}"`
: ''
}`,
bodyLines: [
...touchedProjectsUnderReleaseGroup.map(
(project) => ` - ${project}`
),
'',
'NOTE: You can adjust your "versionPlans.ignorePatternsForPlanCheck" config to stop certain files from resulting in projects being classed as touched for the purposes of this command.',
],
});
} else {
output.log({
title: `No touched projects found based on changed files${
typeof releaseGroup.versionPlans !== 'boolean' &&
Array.isArray(
releaseGroup.versionPlans.ignorePatternsForPlanCheck
) &&
releaseGroup.versionPlans.ignorePatternsForPlanCheck.length
? ' combined with configured ignore patterns'
: ''
}${
releaseGroup.name !== IMPLICIT_DEFAULT_RELEASE_GROUP
? ` under release group "${releaseGroup.name}"`
: ''
}`,
});
}
const projectsInResolvedVersionPlans: Record< const projectsInResolvedVersionPlans: Record<
string, string,

View File

@ -6,18 +6,24 @@ import { dirSync } from 'tmp';
import { NxReleaseConfiguration, readNxJson } from '../../config/nx-json'; import { NxReleaseConfiguration, readNxJson } from '../../config/nx-json';
import { createProjectFileMapUsingProjectGraph } from '../../project-graph/file-map-utils'; import { createProjectFileMapUsingProjectGraph } from '../../project-graph/file-map-utils';
import { createProjectGraphAsync } from '../../project-graph/project-graph'; import { createProjectGraphAsync } from '../../project-graph/project-graph';
import { allFileData } from '../../utils/all-file-data';
import {
parseFiles,
splitArgsIntoNxArgsAndOverrides,
} from '../../utils/command-line-utils';
import { output } from '../../utils/output'; import { output } from '../../utils/output';
import { handleErrors } from '../../utils/params'; import { handleErrors } from '../../utils/params';
import { PlanOptions } from './command-object'; import { PlanOptions } from './command-object';
import { import {
IMPLICIT_DEFAULT_RELEASE_GROUP,
createNxReleaseConfig, createNxReleaseConfig,
handleNxReleaseConfigError, handleNxReleaseConfigError,
IMPLICIT_DEFAULT_RELEASE_GROUP,
} from './config/config'; } from './config/config';
import { deepMergeJson } from './config/deep-merge-json'; import { deepMergeJson } from './config/deep-merge-json';
import { filterReleaseGroups } from './config/filter-release-groups'; import { filterReleaseGroups } from './config/filter-release-groups';
import { getVersionPlansAbsolutePath } from './config/version-plans'; import { getVersionPlansAbsolutePath } from './config/version-plans';
import { generateVersionPlanContent } from './utils/generate-version-plan-content'; import { generateVersionPlanContent } from './utils/generate-version-plan-content';
import { createGetTouchedProjectsForGroup } from './utils/get-touched-projects-for-group';
import { launchEditor } from './utils/launch-editor'; import { launchEditor } from './utils/launch-editor';
import { printDiff } from './utils/print-changes'; import { printDiff } from './utils/print-changes';
import { printConfigAndExit } from './utils/print-config'; import { printConfigAndExit } from './utils/print-config';
@ -69,6 +75,36 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
process.exit(1); process.exit(1);
} }
// If no release groups have version plans enabled, it doesn't make sense to use the plan command only to set yourself up for an error at release time
if (!releaseGroups.some((group) => group.versionPlans === true)) {
if (releaseGroups.length === 1) {
output.warn({
title: `Version plans are not enabled in your release configuration`,
bodyLines: [
'To enable version plans, set `"versionPlans": true` at the top level of your `"release"` configuration',
],
});
return 0;
}
output.warn({
title: 'No release groups have version plans enabled',
bodyLines: [
'To enable version plans, set `"versionPlans": true` at the top level of your `"release"` configuration to apply it to all groups, otherwise set it at the release group level',
],
});
return 0;
}
// Resolve the final values for base, head etc to use when resolving the changes to consider
const { nxArgs } = splitArgsIntoNxArgsAndOverrides(
args,
'affected',
{
printWarnings: args.verbose,
},
nxJson
);
const versionPlanBumps: Record<string, string> = {}; const versionPlanBumps: Record<string, string> = {};
const setBumpIfNotNone = (projectOrGroup: string, version: string) => { const setBumpIfNotNone = (projectOrGroup: string, version: string) => {
if (version !== 'none') { if (version !== 'none') {
@ -76,56 +112,170 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
} }
}; };
if (releaseGroups[0].name === IMPLICIT_DEFAULT_RELEASE_GROUP) { // Changed files are only relevant if considering touched projects
const group = releaseGroups[0]; let changedFiles: string[] = [];
if (group.projectsRelationship === 'independent') { let getProjectsToVersionForGroup:
for (const project of group.projects) { | ReturnType<typeof createGetTouchedProjectsForGroup>
| undefined;
if (args.onlyTouched) {
changedFiles = parseFiles(nxArgs).files;
if (nxArgs.verbose) {
if (changedFiles.length) {
output.log({
title: `Changed files based on resolved "base" (${
nxArgs.base
}) and "head" (${nxArgs.head ?? 'HEAD'})`,
bodyLines: changedFiles.map((file) => ` - ${file}`),
});
} else {
output.warn({
title: 'No changed files found based on resolved "base" and "head"',
});
}
}
const resolvedAllFileData = await allFileData();
getProjectsToVersionForGroup = createGetTouchedProjectsForGroup(
nxArgs,
projectGraph,
changedFiles,
resolvedAllFileData
);
}
if (args.projects?.length) {
/**
* Run plan for all remaining release groups and filtered projects within them
*/
for (const releaseGroup of releaseGroups) {
const releaseGroupName = releaseGroup.name;
const releaseGroupProjectNames = Array.from(
releaseGroupToFilteredProjects.get(releaseGroup)
);
let applicableProjects = releaseGroupProjectNames;
if (
args.onlyTouched &&
typeof getProjectsToVersionForGroup === 'function'
) {
applicableProjects = await getProjectsToVersionForGroup(
releaseGroup,
releaseGroupProjectNames,
true
);
}
if (!applicableProjects.length) {
continue;
}
if (releaseGroup.projectsRelationship === 'independent') {
for (const project of applicableProjects) {
setBumpIfNotNone( setBumpIfNotNone(
project, project,
args.bump || args.bump ||
(await promptForVersion( (await promptForVersion(
`How do you want to bump the version of the project "${project}"?` `How do you want to bump the version of the project "${project}"${
)) releaseGroupName === IMPLICIT_DEFAULT_RELEASE_GROUP
); ? ''
} : ` within group "${releaseGroupName}"`
} else { }?`
// TODO: use project names instead of the implicit default release group name? (though this might be confusing, as users might think they can just delete one of the project bumps to change the behavior to independent versioning)
setBumpIfNotNone(
group.name,
args.bump ||
(await promptForVersion(
`How do you want to bump the versions of all projects?`
))
);
}
} else {
for (const group of releaseGroups) {
if (group.projectsRelationship === 'independent') {
for (const project of releaseGroupToFilteredProjects.get(group)) {
setBumpIfNotNone(
project,
args.bump ||
(await promptForVersion(
`How do you want to bump the version of the project "${project}" within group "${group.name}"?`
)) ))
); );
} }
} else { } else {
setBumpIfNotNone( setBumpIfNotNone(
group.name, releaseGroupName,
args.bump || args.bump ||
(await promptForVersion( (await promptForVersion(
`How do you want to bump the versions of the projects in the group "${group.name}"?` `How do you want to bump the versions of ${
releaseGroupName === IMPLICIT_DEFAULT_RELEASE_GROUP
? 'all projects'
: `the projects in the group "${releaseGroupName}"`
}?`
)) ))
); );
} }
} }
}
// Create a version plan file if applicable
await createVersionPlanFileForBumps(args, versionPlanBumps);
return 0;
}
/**
* Run plan for all remaining release groups
*/
for (const releaseGroup of releaseGroups) {
const releaseGroupName = releaseGroup.name;
let applicableProjects = releaseGroup.projects;
if (
args.onlyTouched &&
typeof getProjectsToVersionForGroup === 'function'
) {
applicableProjects = await getProjectsToVersionForGroup(
releaseGroup,
releaseGroup.projects,
false
);
}
if (!applicableProjects.length) {
continue;
}
if (releaseGroup.projectsRelationship === 'independent') {
for (const project of applicableProjects) {
setBumpIfNotNone(
project,
args.bump ||
(await promptForVersion(
`How do you want to bump the version of the project "${project}"${
releaseGroupName === IMPLICIT_DEFAULT_RELEASE_GROUP
? ''
: ` within group "${releaseGroupName}"`
}?`
))
);
}
} else {
setBumpIfNotNone(
releaseGroupName,
args.bump ||
(await promptForVersion(
`How do you want to bump the versions of ${
releaseGroupName === IMPLICIT_DEFAULT_RELEASE_GROUP
? 'all projects'
: `the projects in the group "${releaseGroupName}"`
}?`
))
);
}
}
// Create a version plan file if applicable
await createVersionPlanFileForBumps(args, versionPlanBumps);
return 0;
};
}
async function createVersionPlanFileForBumps(
args: PlanOptions,
versionPlanBumps: Record<string, string>
) {
if (!Object.keys(versionPlanBumps).length) { if (!Object.keys(versionPlanBumps).length) {
let bodyLines: string[] = [];
if (args.onlyTouched) {
bodyLines = [
'This might be because no projects have been changed, or projects you expected to release have not been touched',
'To include all projects, not just those that have been changed, pass --only-touched=false',
'Alternatively, you can specify alternate --base and --head refs to include only changes from certain commits',
];
}
output.warn({ output.warn({
title: title:
'No version bumps were selected so no version plan file was created.', 'No version bumps were selected so no version plan file was created.',
bodyLines,
}); });
return 0; return 0;
} }
@ -145,9 +295,7 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
); );
printDiff('', versionPlanFileContent, 1); printDiff('', versionPlanFileContent, 1);
} else { } else {
output.logSingleLine( output.logSingleLine(`Creating version plan file "${versionPlanFileName}"`);
`Creating version plan file "${versionPlanFileName}"`
);
printDiff('', versionPlanFileContent, 1); printDiff('', versionPlanFileContent, 1);
const versionPlansAbsolutePath = getVersionPlansAbsolutePath(); const versionPlansAbsolutePath = getVersionPlansAbsolutePath();
@ -157,9 +305,6 @@ export function createAPI(overrideReleaseConfig: NxReleaseConfiguration) {
versionPlanFileContent versionPlanFileContent
); );
} }
return 0;
};
} }
async function promptForVersion(message: string): Promise<string> { async function promptForVersion(message: string): Promise<string> {

View File

@ -0,0 +1,130 @@
import { FileData, ProjectGraph } from '../../../config/project-graph';
import { getTouchedProjects } from '../../../project-graph/affected/locators/workspace-projects';
import { calculateFileChanges } from '../../../project-graph/file-utils';
import { NxArgs } from '../../../utils/command-line-utils';
import { getIgnoreObject } from '../../../utils/ignore';
import { output } from '../../../utils/output';
import { IMPLICIT_DEFAULT_RELEASE_GROUP } from '../config/config';
import { ReleaseGroupWithName } from '../config/filter-release-groups';
/**
* Create a function that returns the touched projects for a given release group. Only relevant when version plans are enabled.
*/
export function createGetTouchedProjectsForGroup(
nxArgs: NxArgs,
projectGraph: ProjectGraph,
changedFiles: string[],
fileData: FileData[]
) {
/**
* Create a minimal subset of touched projects based on the configured ignore patterns, we only need
* to recompute when the ignorePatternsForPlanCheck differs between release groups.
*/
const serializedIgnorePatternsToTouchedProjects = new Map<
string,
Record<string, true> // project names -> true for O(N) lookup later
>();
return async function getTouchedProjectsForGroup(
releaseGroup: ReleaseGroupWithName,
// We don't access releaseGroups.projects directly, because we need to take the --projects filter into account
releaseGroupFilteredProjectNames: string[],
hasProjectsFilter: boolean
): Promise<string[]> {
// The current release group doesn't leverage version plans
if (!releaseGroup.versionPlans) {
return [];
}
// Exclude patterns from .nxignore, .gitignore and explicit version plan config
let serializedIgnorePatterns = '[]';
const ignore = getIgnoreObject();
if (
typeof releaseGroup.versionPlans !== 'boolean' &&
Array.isArray(releaseGroup.versionPlans.ignorePatternsForPlanCheck) &&
releaseGroup.versionPlans.ignorePatternsForPlanCheck.length
) {
output.note({
title: `Applying configured ignore patterns to changed files${
releaseGroup.name !== IMPLICIT_DEFAULT_RELEASE_GROUP
? ` for release group "${releaseGroup.name}"`
: ''
}`,
bodyLines: [
...releaseGroup.versionPlans.ignorePatternsForPlanCheck.map(
(pattern) => ` - ${pattern}`
),
],
});
ignore.add(releaseGroup.versionPlans.ignorePatternsForPlanCheck);
serializedIgnorePatterns = JSON.stringify(
releaseGroup.versionPlans.ignorePatternsForPlanCheck
);
}
let touchedProjects = {};
if (
serializedIgnorePatternsToTouchedProjects.has(serializedIgnorePatterns)
) {
touchedProjects = serializedIgnorePatternsToTouchedProjects.get(
serializedIgnorePatterns
);
} else {
// We only care about directly touched projects, not implicitly affected ones etc
const touchedProjectsArr = await getTouchedProjects(
calculateFileChanges(changedFiles, fileData, nxArgs, undefined, ignore),
projectGraph.nodes
);
touchedProjects = touchedProjectsArr.reduce(
(acc, project) => ({ ...acc, [project]: true }),
{}
);
serializedIgnorePatternsToTouchedProjects.set(
serializedIgnorePatterns,
touchedProjects
);
}
const touchedProjectsUnderReleaseGroup =
releaseGroupFilteredProjectNames.filter(
(project) => touchedProjects[project]
);
if (touchedProjectsUnderReleaseGroup.length) {
output.log({
title: `Touched projects${
hasProjectsFilter ? ` (after --projects filter applied)` : ''
} based on changed files${
releaseGroup.name !== IMPLICIT_DEFAULT_RELEASE_GROUP
? ` under release group "${releaseGroup.name}"`
: ''
}`,
bodyLines: [
...touchedProjectsUnderReleaseGroup.map(
(project) => ` - ${project}`
),
'',
'NOTE: You can adjust your "versionPlans.ignorePatternsForPlanCheck" config to stop certain files from resulting in projects being classed as touched for the purposes of this command.',
],
});
} else {
output.log({
title: `No touched projects${
hasProjectsFilter ? ` (after --projects filter applied)` : ''
} found based on changed files${
typeof releaseGroup.versionPlans !== 'boolean' &&
Array.isArray(releaseGroup.versionPlans.ignorePatternsForPlanCheck) &&
releaseGroup.versionPlans.ignorePatternsForPlanCheck.length
? ' combined with configured ignore patterns'
: ''
}${
releaseGroup.name !== IMPLICIT_DEFAULT_RELEASE_GROUP
? ` under release group "${releaseGroup.name}"`
: ''
}`,
});
}
return touchedProjectsUnderReleaseGroup;
};
}