Skip to content

Commit a60cceb

Browse files
authored
[rush] Fix implicit phase expansion (#5209)
Co-authored-by: David Michon <[email protected]>
1 parent 3209573 commit a60cceb

File tree

4 files changed

+673
-3299
lines changed

4 files changed

+673
-3299
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@microsoft/rush",
5+
"comment": "Fix an issue with implicit phase expansion when `--include-phase-deps` is not specified.",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@microsoft/rush"
10+
}

libraries/rush-lib/src/logic/operations/PhasedOperationPlugin.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,13 @@ function configureOperations(operations: Set<Operation>, context: ICreateOperati
105105
isInitial
106106
} = context;
107107

108+
const basePhases: ReadonlySet<IPhase> = includePhaseDeps ? phaseOriginal : phaseSelection;
109+
108110
// Grab all operations that were explicitly requested.
109111
const operationsWithWork: Set<Operation> = new Set();
110112
for (const operation of operations) {
111113
const { associatedPhase, associatedProject } = operation;
112-
if (phaseOriginal.has(associatedPhase) && changedProjects.has(associatedProject)) {
114+
if (basePhases.has(associatedPhase) && changedProjects.has(associatedProject)) {
113115
operationsWithWork.add(operation);
114116
}
115117
}

libraries/rush-lib/src/logic/operations/test/PhasedOperationPlugin.test.ts

+154-83
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,7 @@ import path from 'path';
55
import { JsonFile } from '@rushstack/node-core-library';
66

77
import { RushConfiguration } from '../../../api/RushConfiguration';
8-
import type { RushConfigurationProject } from '../../../api/RushConfigurationProject';
9-
import {
10-
CommandLineConfiguration,
11-
type IPhase,
12-
type IPhasedCommandConfig
13-
} from '../../../api/CommandLineConfiguration';
8+
import { CommandLineConfiguration, type IPhasedCommandConfig } from '../../../api/CommandLineConfiguration';
149
import { PhasedOperationPlugin } from '../PhasedOperationPlugin';
1510
import type { Operation } from '../Operation';
1611
import type { ICommandLineJson } from '../../../api/CommandLineJson';
@@ -21,18 +16,30 @@ import {
2116
PhasedCommandHooks
2217
} from '../../../pluginFramework/PhasedCommandHooks';
2318

24-
interface ISerializedOperation {
25-
name: string;
26-
silent: boolean;
27-
dependencies: string[];
19+
function serializeOperation(operation: Operation): string {
20+
return `${operation.name} (${operation.enabled ? 'enabled' : 'disabled'}${operation.runner!.silent ? ', silent' : ''}) -> [${Array.from(
21+
operation.dependencies,
22+
(dep: Operation) => dep.name
23+
)
24+
.sort()
25+
.join(', ')}]`;
26+
}
27+
28+
function compareOperation(a: Operation, b: Operation): number {
29+
if (a.enabled && !b.enabled) {
30+
return -1;
31+
}
32+
if (!a.enabled && b.enabled) {
33+
return 1;
34+
}
35+
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
2836
}
2937

30-
function serializeOperation(operation: Operation): ISerializedOperation {
31-
return {
32-
name: operation.name,
33-
silent: !operation.enabled || operation.runner!.silent,
34-
dependencies: Array.from(operation.dependencies, (dep: Operation) => dep.name)
35-
};
38+
function expectOperationsToMatchSnapshot(operations: Set<Operation>, name: string): void {
39+
const serializedOperations: string[] = Array.from(operations)
40+
.sort(compareOperation)
41+
.map(serializeOperation);
42+
expect(serializedOperations).toMatchSnapshot(name);
3643
}
3744

3845
describe(PhasedOperationPlugin.name, () => {
@@ -58,11 +65,22 @@ describe(PhasedOperationPlugin.name, () => {
5865
return operations;
5966
}
6067

61-
async function testCreateOperationsAsync(
62-
phaseSelection: Set<IPhase>,
63-
projectSelection: Set<RushConfigurationProject>,
64-
changedProjects: Set<RushConfigurationProject>
65-
): Promise<Set<Operation>> {
68+
interface ITestCreateOperationsContext {
69+
phaseOriginal?: ICreateOperationsContext['phaseOriginal'];
70+
phaseSelection: ICreateOperationsContext['phaseSelection'];
71+
projectSelection: ICreateOperationsContext['projectSelection'];
72+
projectsInUnknownState: ICreateOperationsContext['projectsInUnknownState'];
73+
includePhaseDeps?: ICreateOperationsContext['includePhaseDeps'];
74+
}
75+
76+
async function testCreateOperationsAsync(options: ITestCreateOperationsContext): Promise<Set<Operation>> {
77+
const {
78+
phaseSelection,
79+
projectSelection,
80+
projectsInUnknownState,
81+
phaseOriginal = phaseSelection,
82+
includePhaseDeps = false
83+
} = options;
6684
const hooks: PhasedCommandHooks = new PhasedCommandHooks();
6785
// Apply the plugin being tested
6886
new PhasedOperationPlugin().apply(hooks);
@@ -71,16 +89,18 @@ describe(PhasedOperationPlugin.name, () => {
7189

7290
const context: Pick<
7391
ICreateOperationsContext,
92+
| 'includePhaseDeps'
7493
| 'phaseOriginal'
7594
| 'phaseSelection'
7695
| 'projectSelection'
7796
| 'projectsInUnknownState'
7897
| 'projectConfigurations'
7998
> = {
80-
phaseOriginal: phaseSelection,
99+
includePhaseDeps,
100+
phaseOriginal,
81101
phaseSelection,
82102
projectSelection,
83-
projectsInUnknownState: changedProjects,
103+
projectsInUnknownState,
84104
projectConfigurations: new Map()
85105
};
86106
const operations: Set<Operation> = await hooks.createOperations.promise(
@@ -106,154 +126,205 @@ describe(PhasedOperationPlugin.name, () => {
106126
'build'
107127
)! as IPhasedCommandConfig;
108128

109-
const operations: Set<Operation> = await testCreateOperationsAsync(
110-
buildCommand.phases,
111-
new Set(rushConfiguration.projects),
112-
new Set(rushConfiguration.projects)
113-
);
129+
const operations: Set<Operation> = await testCreateOperationsAsync({
130+
phaseSelection: buildCommand.phases,
131+
projectSelection: new Set(rushConfiguration.projects),
132+
projectsInUnknownState: new Set(rushConfiguration.projects)
133+
});
114134

115135
// All projects
116-
expect(Array.from(operations, serializeOperation)).toMatchSnapshot();
136+
expectOperationsToMatchSnapshot(operations, 'full');
117137
});
118138

119139
it('handles filtered projects', async () => {
120140
const buildCommand: IPhasedCommandConfig = commandLineConfiguration.commands.get(
121141
'build'
122142
)! as IPhasedCommandConfig;
123143

124-
let operations: Set<Operation> = await testCreateOperationsAsync(
125-
buildCommand.phases,
126-
new Set([rushConfiguration.getProjectByName('g')!]),
127-
new Set([rushConfiguration.getProjectByName('g')!])
128-
);
144+
let operations: Set<Operation> = await testCreateOperationsAsync({
145+
phaseSelection: buildCommand.phases,
146+
projectSelection: new Set([rushConfiguration.getProjectByName('g')!]),
147+
projectsInUnknownState: new Set([rushConfiguration.getProjectByName('g')!])
148+
});
129149

130150
// Single project
131-
expect(Array.from(operations, serializeOperation)).toMatchSnapshot();
151+
expectOperationsToMatchSnapshot(operations, 'single');
132152

133-
operations = await testCreateOperationsAsync(
134-
buildCommand.phases,
135-
new Set([
153+
operations = await testCreateOperationsAsync({
154+
phaseSelection: buildCommand.phases,
155+
projectSelection: new Set([
136156
rushConfiguration.getProjectByName('f')!,
137157
rushConfiguration.getProjectByName('a')!,
138158
rushConfiguration.getProjectByName('c')!
139159
]),
140-
new Set([
160+
projectsInUnknownState: new Set([
141161
rushConfiguration.getProjectByName('f')!,
142162
rushConfiguration.getProjectByName('a')!,
143163
rushConfiguration.getProjectByName('c')!
144164
])
145-
);
165+
});
146166

147167
// Filtered projects
148-
expect(Array.from(operations, serializeOperation)).toMatchSnapshot();
168+
expectOperationsToMatchSnapshot(operations, 'filtered');
149169
});
150170

151171
it('handles some changed projects', async () => {
152172
const buildCommand: IPhasedCommandConfig = commandLineConfiguration.commands.get(
153173
'build'
154174
)! as IPhasedCommandConfig;
155175

156-
let operations: Set<Operation> = await testCreateOperationsAsync(
157-
buildCommand.phases,
158-
new Set(rushConfiguration.projects),
159-
new Set([rushConfiguration.getProjectByName('g')!])
160-
);
176+
let operations: Set<Operation> = await testCreateOperationsAsync({
177+
phaseSelection: buildCommand.phases,
178+
projectSelection: new Set(rushConfiguration.projects),
179+
projectsInUnknownState: new Set([rushConfiguration.getProjectByName('g')!])
180+
});
161181

162182
// Single project
163-
expect(Array.from(operations, serializeOperation)).toMatchSnapshot();
183+
expectOperationsToMatchSnapshot(operations, 'single');
164184

165-
operations = await testCreateOperationsAsync(
166-
buildCommand.phases,
167-
new Set(rushConfiguration.projects),
168-
new Set([
185+
operations = await testCreateOperationsAsync({
186+
phaseSelection: buildCommand.phases,
187+
projectSelection: new Set(rushConfiguration.projects),
188+
projectsInUnknownState: new Set([
169189
rushConfiguration.getProjectByName('f')!,
170190
rushConfiguration.getProjectByName('a')!,
171191
rushConfiguration.getProjectByName('c')!
172192
])
173-
);
193+
});
174194

175195
// Filtered projects
176-
expect(Array.from(operations, serializeOperation)).toMatchSnapshot();
196+
expectOperationsToMatchSnapshot(operations, 'multiple');
177197
});
178198

179199
it('handles some changed projects within filtered projects', async () => {
180200
const buildCommand: IPhasedCommandConfig = commandLineConfiguration.commands.get(
181201
'build'
182202
)! as IPhasedCommandConfig;
183203

184-
const operations: Set<Operation> = await testCreateOperationsAsync(
185-
buildCommand.phases,
186-
new Set([
204+
const operations: Set<Operation> = await testCreateOperationsAsync({
205+
phaseSelection: buildCommand.phases,
206+
projectSelection: new Set([
187207
rushConfiguration.getProjectByName('f')!,
188208
rushConfiguration.getProjectByName('a')!,
189209
rushConfiguration.getProjectByName('c')!
190210
]),
191-
new Set([rushConfiguration.getProjectByName('a')!, rushConfiguration.getProjectByName('c')!])
192-
);
211+
projectsInUnknownState: new Set([
212+
rushConfiguration.getProjectByName('a')!,
213+
rushConfiguration.getProjectByName('c')!
214+
])
215+
});
193216

194217
// Single project
195-
expect(Array.from(operations, serializeOperation)).toMatchSnapshot();
218+
expectOperationsToMatchSnapshot(operations, 'multiple');
219+
});
220+
221+
it('handles different phaseOriginal vs phaseSelection without --include-phase-deps', async () => {
222+
const operations: Set<Operation> = await testCreateOperationsAsync({
223+
includePhaseDeps: false,
224+
phaseSelection: new Set([
225+
commandLineConfiguration.phases.get('_phase:no-deps')!,
226+
commandLineConfiguration.phases.get('_phase:upstream-self')!
227+
]),
228+
phaseOriginal: new Set([commandLineConfiguration.phases.get('_phase:upstream-self')!]),
229+
projectSelection: new Set([rushConfiguration.getProjectByName('a')!]),
230+
projectsInUnknownState: new Set([rushConfiguration.getProjectByName('a')!])
231+
});
232+
233+
expectOperationsToMatchSnapshot(operations, 'single-project');
234+
});
235+
236+
it('handles different phaseOriginal vs phaseSelection with --include-phase-deps', async () => {
237+
const operations: Set<Operation> = await testCreateOperationsAsync({
238+
includePhaseDeps: true,
239+
phaseSelection: new Set([
240+
commandLineConfiguration.phases.get('_phase:no-deps')!,
241+
commandLineConfiguration.phases.get('_phase:upstream-self')!
242+
]),
243+
phaseOriginal: new Set([commandLineConfiguration.phases.get('_phase:upstream-self')!]),
244+
projectSelection: new Set([rushConfiguration.getProjectByName('a')!]),
245+
projectsInUnknownState: new Set([rushConfiguration.getProjectByName('a')!])
246+
});
247+
248+
expectOperationsToMatchSnapshot(operations, 'single-project');
249+
});
250+
251+
it('handles different phaseOriginal vs phaseSelection cross-project with --include-phase-deps', async () => {
252+
const operations: Set<Operation> = await testCreateOperationsAsync({
253+
includePhaseDeps: true,
254+
phaseSelection: new Set([
255+
commandLineConfiguration.phases.get('_phase:no-deps')!,
256+
commandLineConfiguration.phases.get('_phase:upstream-1')!
257+
]),
258+
phaseOriginal: new Set([commandLineConfiguration.phases.get('_phase:upstream-1')!]),
259+
projectSelection: new Set([
260+
rushConfiguration.getProjectByName('a')!,
261+
rushConfiguration.getProjectByName('h')!
262+
]),
263+
projectsInUnknownState: new Set([rushConfiguration.getProjectByName('h')!])
264+
});
265+
266+
expectOperationsToMatchSnapshot(operations, 'multiple-project');
196267
});
197268

198269
it('handles filtered phases', async () => {
199270
// Single phase with a missing dependency
200-
let operations: Set<Operation> = await testCreateOperationsAsync(
201-
new Set([commandLineConfiguration.phases.get('_phase:upstream-self')!]),
202-
new Set(rushConfiguration.projects),
203-
new Set(rushConfiguration.projects)
204-
);
205-
expect(Array.from(operations, serializeOperation)).toMatchSnapshot();
271+
let operations: Set<Operation> = await testCreateOperationsAsync({
272+
phaseSelection: new Set([commandLineConfiguration.phases.get('_phase:upstream-self')!]),
273+
projectSelection: new Set(rushConfiguration.projects),
274+
projectsInUnknownState: new Set(rushConfiguration.projects)
275+
});
276+
expectOperationsToMatchSnapshot(operations, 'single-phase');
206277

207278
// Two phases with a missing link
208-
operations = await testCreateOperationsAsync(
209-
new Set([
279+
operations = await testCreateOperationsAsync({
280+
phaseSelection: new Set([
210281
commandLineConfiguration.phases.get('_phase:complex')!,
211282
commandLineConfiguration.phases.get('_phase:upstream-3')!,
212283
commandLineConfiguration.phases.get('_phase:upstream-1')!,
213284
commandLineConfiguration.phases.get('_phase:no-deps')!
214285
]),
215-
new Set(rushConfiguration.projects),
216-
new Set(rushConfiguration.projects)
217-
);
218-
expect(Array.from(operations, serializeOperation)).toMatchSnapshot();
286+
projectSelection: new Set(rushConfiguration.projects),
287+
projectsInUnknownState: new Set(rushConfiguration.projects)
288+
});
289+
expectOperationsToMatchSnapshot(operations, 'two-phases');
219290
});
220291

221292
it('handles filtered phases on filtered projects', async () => {
222293
// Single phase with a missing dependency
223-
let operations: Set<Operation> = await testCreateOperationsAsync(
224-
new Set([commandLineConfiguration.phases.get('_phase:upstream-2')!]),
225-
new Set([
294+
let operations: Set<Operation> = await testCreateOperationsAsync({
295+
phaseSelection: new Set([commandLineConfiguration.phases.get('_phase:upstream-2')!]),
296+
projectSelection: new Set([
226297
rushConfiguration.getProjectByName('f')!,
227298
rushConfiguration.getProjectByName('a')!,
228299
rushConfiguration.getProjectByName('c')!
229300
]),
230-
new Set([
301+
projectsInUnknownState: new Set([
231302
rushConfiguration.getProjectByName('f')!,
232303
rushConfiguration.getProjectByName('a')!,
233304
rushConfiguration.getProjectByName('c')!
234305
])
235-
);
236-
expect(Array.from(operations, serializeOperation)).toMatchSnapshot();
306+
});
307+
expectOperationsToMatchSnapshot(operations, 'single-phase');
237308

238309
// Phases with missing links
239-
operations = await testCreateOperationsAsync(
240-
new Set([
310+
operations = await testCreateOperationsAsync({
311+
phaseSelection: new Set([
241312
commandLineConfiguration.phases.get('_phase:complex')!,
242313
commandLineConfiguration.phases.get('_phase:upstream-3')!,
243314
commandLineConfiguration.phases.get('_phase:upstream-1')!,
244315
commandLineConfiguration.phases.get('_phase:no-deps')!
245316
]),
246-
new Set([
317+
projectSelection: new Set([
247318
rushConfiguration.getProjectByName('f')!,
248319
rushConfiguration.getProjectByName('a')!,
249320
rushConfiguration.getProjectByName('c')!
250321
]),
251-
new Set([
322+
projectsInUnknownState: new Set([
252323
rushConfiguration.getProjectByName('f')!,
253324
rushConfiguration.getProjectByName('a')!,
254325
rushConfiguration.getProjectByName('c')!
255326
])
256-
);
257-
expect(Array.from(operations, serializeOperation)).toMatchSnapshot();
327+
});
328+
expectOperationsToMatchSnapshot(operations, 'missing-links');
258329
});
259330
});

0 commit comments

Comments
 (0)