Skip to content

Commit 520ba42

Browse files
committed
chore(release): move getCommitsRelevantToProjects calculation and cache to release-graph
1 parent 67e849d commit 520ba42

File tree

8 files changed

+109
-64
lines changed

8 files changed

+109
-64
lines changed

packages/nx/src/command-line/release/utils/git.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,8 @@ export class RepoGitTags {
153153
async resolveTags(
154154
checkAllBranchesWhen?: GetLatestGitTagForPatternOptions['checkAllBranchesWhen']
155155
) {
156-
const alwaysCheckAllBranches = await this.#alwaysCheckAllBranches(
157-
checkAllBranchesWhen
158-
);
156+
const alwaysCheckAllBranches =
157+
await this.#alwaysCheckAllBranches(checkAllBranchesWhen);
159158

160159
let tags = this.#tagsMap.get(alwaysCheckAllBranches);
161160
if (!tags) {

packages/nx/src/command-line/release/utils/release-graph.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@ import type {
66
ProjectGraph,
77
ProjectGraphProjectNode,
88
} from '../../../config/project-graph';
9+
import { NxArgs } from '../../../utils/command-line-utils';
910
import type { Tree } from '../../../generators/tree';
1011
import {
1112
IMPLICIT_DEFAULT_RELEASE_GROUP,
1213
type NxReleaseConfig,
1314
} from '../config/config';
1415
import type { ReleaseGroupWithName } from '../config/filter-release-groups';
1516
import { ProjectLogger } from '../version/project-logger';
17+
import { calculateFileChanges } from '../../../project-graph/file-utils';
18+
import { filterAffected } from '../../../project-graph/affected/affected-project-graph';
1619
import { resolveCurrentVersion } from '../version/resolve-current-version';
1720
import { topologicalSort } from '../version/topological-sort';
1821
import {
@@ -21,7 +24,11 @@ import {
2124
type AfterAllProjectsVersioned,
2225
type VersionActions,
2326
} from '../version/version-actions';
24-
import { getLatestGitTagForPattern, sanitizeProjectNameForGitTag } from './git';
27+
import {
28+
getLatestGitTagForPattern,
29+
GitCommit,
30+
sanitizeProjectNameForGitTag,
31+
} from './git';
2532
import { shouldSkipVersionActions, type VersionDataEntry } from './shared';
2633

2734
/**
@@ -124,6 +131,13 @@ export class ReleaseGraph {
124131
>();
125132
private originalFilteredProjects = new Set<string>();
126133

134+
/**
135+
* Store the affected graph per commit per project
136+
* to avoid recomputation of the graph on workspace
137+
* with multiple projects
138+
*/
139+
private affectedGraphPerCommit = new Map<string, ProjectGraph>();
140+
127141
/**
128142
* User-friendly log describing what the filter matched.
129143
* Null if no filters were applied.
@@ -982,6 +996,32 @@ Valid values are: ${validReleaseVersionPrefixes
982996
return this.allProjectsToProcess.has(projectName);
983997
}
984998

999+
async resolveAffectedFilesPerCommitInProjectGraph(
1000+
commit: GitCommit,
1001+
projectGraph: ProjectGraph
1002+
) {
1003+
// Try to get the graph associated with the commit shortHash
1004+
// if not available, calculate it and store it in the cache
1005+
const { shortHash } = commit;
1006+
let affectedGraph = this.affectedGraphPerCommit.get(shortHash);
1007+
1008+
if (affectedGraph) {
1009+
return affectedGraph;
1010+
}
1011+
1012+
// Convert affectedFiles to FileChange[] format with proper diff computation
1013+
const touchedFiles = calculateFileChanges(commit.affectedFiles, {
1014+
base: `${commit.shortHash}^`,
1015+
head: commit.shortHash,
1016+
} as NxArgs);
1017+
1018+
// Use the same affected detection logic as `nx affected`
1019+
affectedGraph = await filterAffected(projectGraph, touchedFiles);
1020+
this.affectedGraphPerCommit.set(shortHash, affectedGraph);
1021+
1022+
return affectedGraph;
1023+
}
1024+
9851025
/**
9861026
* Runs validation on resolved VersionActions instances. E.g. check that manifest files exist for all projects that will be processed.
9871027
* This should be called after preVersionCommand has run, as those commands may create manifest files that are needed for versioning.

packages/nx/src/command-line/release/utils/resolve-semver-specifier.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@ import { ProjectGraph } from '../../../config/project-graph';
44
import { NxReleaseConfig } from '../config/config';
55
import { SemverBumpType } from '../version/version-actions';
66
import { getGitDiff, parseCommits } from './git';
7+
import { ReleaseGraph } from './release-graph';
78
import { determineSemverChange, SemverSpecifier } from './semver';
89
import { getCommitsRelevantToProjects } from './shared';
910

1011
export async function resolveSemverSpecifierFromConventionalCommits(
1112
from: string,
1213
projectGraph: ProjectGraph,
1314
projectNames: string[],
14-
releaseConfig: NxReleaseConfig
15+
releaseConfig: NxReleaseConfig,
16+
releaseGraph: ReleaseGraph
1517
): // Map of projectName to semver bump type
1618
Promise<Map<string, SemverSpecifier | null>> {
1719
const commits = await getGitDiff(from);
@@ -20,7 +22,8 @@ Promise<Map<string, SemverSpecifier | null>> {
2022
projectGraph,
2123
parsedCommits,
2224
projectNames,
23-
releaseConfig
25+
releaseConfig,
26+
releaseGraph
2427
);
2528
return determineSemverChange(
2629
relevantCommits,

packages/nx/src/command-line/release/utils/shared.spec.ts

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import {
77
import { ProjectGraph } from '../../../config/project-graph';
88
import { GitCommit } from './git';
99
import { readNxJson } from '../../../config/nx-json';
10+
import { ReleaseGraph } from './release-graph';
11+
import { filterAffected } from '../../../project-graph/affected/affected-project-graph';
12+
import { calculateFileChanges } from '../../../project-graph/file-utils';
13+
import { NxArgs } from '../../../utils/command-line-utils';
1014

1115
jest.mock('../../../config/nx-json', () => ({
1216
...jest.requireActual('../../../config/nx-json'),
@@ -661,6 +665,7 @@ describe('shared', () => {
661665
describe(`getCommitsRelevantToProjects()`, () => {
662666
let mockProjectGraph: ProjectGraph;
663667
let mockReleaseConfig: NxReleaseConfig | null;
668+
let mockReleaseGraph: ReleaseGraph;
664669

665670
beforeEach(async () => {
666671
(readNxJson as jest.Mock).mockReturnValue({});
@@ -729,6 +734,19 @@ describe('shared', () => {
729734
},
730735
}
731736
));
737+
738+
// Create a mock ReleaseGraph with the required method
739+
mockReleaseGraph = {
740+
resolveAffectedFilesPerCommitInProjectGraph: jest.fn(
741+
async (commit: GitCommit, projectGraph: ProjectGraph) => {
742+
const touchedFiles = calculateFileChanges(commit.affectedFiles, {
743+
base: `${commit.shortHash}^`,
744+
head: commit.shortHash,
745+
} as NxArgs);
746+
return filterAffected(projectGraph, touchedFiles);
747+
}
748+
),
749+
} as unknown as ReleaseGraph;
732750
});
733751

734752
it('should include commits that directly touch target projects', async () => {
@@ -742,7 +760,8 @@ describe('shared', () => {
742760
mockProjectGraph,
743761
commits,
744762
['lib-a', 'lib-b'],
745-
mockReleaseConfig!
763+
mockReleaseConfig!,
764+
mockReleaseGraph
746765
);
747766

748767
expect(result.size).toBe(2);
@@ -763,7 +782,8 @@ describe('shared', () => {
763782
mockProjectGraph,
764783
commits,
765784
['lib-a'],
766-
mockReleaseConfig!
785+
mockReleaseConfig!,
786+
mockReleaseGraph
767787
);
768788

769789
// Both commits should be included - nx.json affects all, and lib-a is directly touched
@@ -784,7 +804,8 @@ describe('shared', () => {
784804
mockProjectGraph,
785805
commits,
786806
['lib-a'],
787-
mockReleaseConfig!
807+
mockReleaseConfig!,
808+
mockReleaseGraph
788809
);
789810

790811
expect(result.size).toBe(1);
@@ -811,7 +832,8 @@ describe('shared', () => {
811832
mockProjectGraph,
812833
commits,
813834
['lib-a'],
814-
mockReleaseConfig!
835+
mockReleaseConfig!,
836+
mockReleaseGraph
815837
);
816838

817839
// lib-a depends on lib-c, so commit touching lib-c should affect lib-a
@@ -833,7 +855,8 @@ describe('shared', () => {
833855
mockProjectGraph,
834856
commits,
835857
['lib-a'],
836-
mockReleaseConfig!
858+
mockReleaseConfig!,
859+
mockReleaseGraph
837860
);
838861

839862
expect(result.size).toBe(1);
@@ -853,7 +876,8 @@ describe('shared', () => {
853876
mockProjectGraph,
854877
commits,
855878
['lib-a', 'lib-b'],
856-
mockReleaseConfig!
879+
mockReleaseConfig!,
880+
mockReleaseGraph
857881
);
858882

859883
// Same commit should appear for both projects
@@ -871,7 +895,8 @@ describe('shared', () => {
871895
mockProjectGraph,
872896
commits,
873897
['lib-a', 'lib-b'],
874-
mockReleaseConfig!
898+
mockReleaseConfig!,
899+
mockReleaseGraph
875900
);
876901

877902
// Global file should appear for all requested projects
@@ -891,7 +916,8 @@ describe('shared', () => {
891916
mockProjectGraph,
892917
commits,
893918
['lib-a', 'lib-b'],
894-
mockReleaseConfig!
919+
mockReleaseConfig!,
920+
mockReleaseGraph
895921
);
896922

897923
expect(result.has('lib-a')).toBe(false);
@@ -904,7 +930,8 @@ describe('shared', () => {
904930
mockProjectGraph,
905931
[],
906932
['lib-a'],
907-
mockReleaseConfig!
933+
mockReleaseConfig!,
934+
mockReleaseGraph
908935
);
909936

910937
expect(result.size).toBe(0);
@@ -919,7 +946,8 @@ describe('shared', () => {
919946
mockProjectGraph,
920947
commits,
921948
[],
922-
mockReleaseConfig!
949+
mockReleaseConfig!,
950+
mockReleaseGraph
923951
);
924952

925953
expect(result.size).toBe(0);
@@ -935,7 +963,8 @@ describe('shared', () => {
935963
mockProjectGraph,
936964
commits,
937965
['lib-a'],
938-
mockReleaseConfig!
966+
mockReleaseConfig!,
967+
mockReleaseGraph
939968
);
940969

941970
// Lock file changes typically affect all or many projects
@@ -952,7 +981,8 @@ describe('shared', () => {
952981
mockProjectGraph,
953982
commits,
954983
['lib-a'],
955-
mockReleaseConfig!
984+
mockReleaseConfig!,
985+
mockReleaseGraph
956986
);
957987

958988
// package.json changes typically affect projects
@@ -977,7 +1007,8 @@ describe('shared', () => {
9771007
mockProjectGraph,
9781008
commits,
9791009
['lib-a'],
980-
mockReleaseConfig!
1010+
mockReleaseConfig!,
1011+
mockReleaseGraph
9811012
);
9821013

9831014
expect(result.size).toBe(0);

packages/nx/src/command-line/release/utils/shared.ts

Lines changed: 11 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,19 @@ import * as chalk from 'chalk';
22
import { prerelease } from 'semver';
33
import { ProjectGraph } from '../../../config/project-graph';
44
import { filterAffected } from '../../../project-graph/affected/affected-project-graph';
5-
import {
6-
calculateFileChanges,
7-
Change,
8-
DeletedFileChange,
9-
WholeFileChange,
10-
} from '../../../project-graph/file-utils';
5+
import { calculateFileChanges } from '../../../project-graph/file-utils';
116
import { interpolate } from '../../../tasks-runner/utils';
127
import type { NxArgs } from '../../../utils/command-line-utils';
138
import { output } from '../../../utils/output';
9+
import { NxReleaseConfig } from '../config/config';
1410
import type { ReleaseGroupWithName } from '../config/filter-release-groups';
1511
import {
16-
GitCommit,
1712
gitAdd,
13+
GitCommit,
1814
gitCommit,
1915
sanitizeProjectNameForGitTag,
2016
} from './git';
21-
import { NxReleaseConfig } from '../config/config';
17+
import { ReleaseGraph } from './release-graph';
2218

2319
export const noDiffInChangelogMessage = chalk.yellow(
2420
`NOTE: There was no diff detected for the changelog entry. Maybe you intended to pass alternative git references via --from and --to?`
@@ -432,28 +428,12 @@ function isAutomatedReleaseCommit(
432428
return false;
433429
}
434430

435-
/**
436-
* Store the affected graph per commit per project
437-
* to avoid recomputation of the graph on workspace
438-
* with multiple projects
439-
*/
440-
const affectedGraphPerCommit = new Map<string, ProjectGraph>();
441-
442-
/**
443-
* Clear the affected graph cache. Should be called after each release operation
444-
* to prevent memory leaks in long-running processes.
445-
*
446-
* @internal
447-
*/
448-
export function clearAffectedGraphCache(): void {
449-
affectedGraphPerCommit.clear();
450-
}
451-
452431
export async function getCommitsRelevantToProjects(
453432
projectGraph: ProjectGraph,
454433
commits: GitCommit[],
455434
projects: string[],
456-
nxReleaseConfig: NxReleaseConfig
435+
nxReleaseConfig: NxReleaseConfig,
436+
releaseGraph: ReleaseGraph
457437
): // Map of projectName to GitCommit[]
458438
Promise<Map<string, { commit: GitCommit; isProjectScopedCommit: boolean }[]>> {
459439
const projectSet = new Set(projects);
@@ -470,20 +450,11 @@ Promise<Map<string, { commit: GitCommit; isProjectScopedCommit: boolean }[]>> {
470450

471451
// Try to get the graph associated with the commit shortHash
472452
// if not available, calculate it and store it in the cache
473-
const { shortHash } = commit;
474-
let affectedGraph = affectedGraphPerCommit.get(shortHash);
475-
476-
if (!affectedGraph) {
477-
// Convert affectedFiles to FileChange[] format with proper diff computation
478-
const touchedFiles = calculateFileChanges(commit.affectedFiles, {
479-
base: `${commit.shortHash}^`,
480-
head: commit.shortHash,
481-
} as NxArgs);
482-
483-
// Use the same affected detection logic as `nx affected`
484-
affectedGraph = await filterAffected(projectGraph, touchedFiles);
485-
affectedGraphPerCommit.set(shortHash, affectedGraph);
486-
}
453+
let affectedGraph =
454+
await releaseGraph.resolveAffectedFilesPerCommitInProjectGraph(
455+
commit,
456+
projectGraph
457+
);
487458

488459
for (const projectName of Object.keys(affectedGraph.nodes)) {
489460
if (projectSet.has(projectName)) {

packages/nx/src/command-line/release/version.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ import { ReleaseGraph, createReleaseGraph } from './utils/release-graph';
3333
import { resolveNxJsonConfigErrorMessage } from './utils/resolve-nx-json-error-message';
3434
import {
3535
VersionData,
36-
clearAffectedGraphCache,
3736
commitChanges,
3837
createCommitMessageValues,
3938
createGitTagValues,
@@ -262,8 +261,6 @@ export function createAPI(
262261
processor.flushAllProjectLoggers();
263262
// Bubble up the error so that the CLI can print the error and exit, or the programmatic API can handle it
264263
throw err;
265-
} finally {
266-
clearAffectedGraphCache();
267264
}
268265

269266
/**

0 commit comments

Comments
 (0)