Summary
I'm working on converting our Rush monorepo to shard our various test related processes. One issue I have run into is that now that I have added sharding for the test phases into our shared rig configs, EVERY PACKAGE must have every shard phase's script defined or Rush throws The project 'X' does not define a '_phase:name:shard' command in the 'scripts' section of its package.json for each package that doesn't define it. Which means I need to add dummy shard phase scripts to all packages that don't even participate in the phase, which is less than ideal.
Repro steps
- In a Rush monorepo, put
sharding: { count: N } on a phase operation (e.g. _phase:test) inside a rig's config/rush-project.json.
- Have two kinds of projects using that rig:
- Project A: defines both
_phase:test and _phase:test:shard in its package.json scripts.
- Project B: defines neither — it simply doesn't participate in the test phase.
- Run
rush test (or any phased command that includes _phase:test and selects both projects).
Expected result: Project A gets sharded. Project B's _phase:test op resolves to a no-op (same as it does today for a project that doesn't define a phase script), and sharding is skipped for it.
Actual result: Rush throws during operation graph construction:
The project '@kx/bulk-package-json-editor' does not define a
'_phase:test:shard' command in the 'scripts' section of its package.json
…even though @kx/bulk-package-json-editor also does not define _phase:test and would otherwise be treated as a no-op for that phase.
Details
This was generated by opus 4.7 with claude code, I don't know the details of the code but it seems accurate.
Root cause, tracing through rush-lib on main:
-
PhasedOperationPlugin (libraries/rush-lib/src/logic/operations/PhasedOperationPlugin.ts:44-48) creates an operation for every (phase, project) pair in the selection, regardless of whether the project defines a script for that phase. The "this project doesn't use this phase" decision is deferred to whichever runner plugin picks it up.
-
Each operation's settings is pulled from projectConfigurations.get(project)?.operationSettingsByOperationName.get(name) (same file, line 63–65). Because settings originate from the rig's rush-project.json, they apply to every project using that rig — including projects that don't implement the phase at all. So settings.sharding is attached to those ops.
-
Plugin registration order in PhasedScriptAction.ts:417-421:
new PhasedOperationPlugin().apply(hooks);
new ShardedPhasedOperationPlugin().apply(hooks);
new ShellOperationRunnerPlugin().apply(hooks);
ShardedPhasedOperationPlugin taps into createOperations before ShellOperationRunnerPlugin does.
-
The guard in ShardedPhaseOperationPlugin.ts:59 is:
if (operationSettings?.sharding && !operation.runner) { ... }
This appears to be trying to skip sharding for operations that have already been marked as no-ops — but because the sharding plugin runs before the shell plugin, operation.runner is always still undefined at this point, even for projects that have no _phase:test script and would be turned into a NullOperationRunner moments later.
-
The strict throw at ShardedPhaseOperationPlugin.ts:148-152 then fires, because the project doesn't define _phase:test:shard:
const baseCommand: string | undefined = scripts?.[shardOperationName];
if (baseCommand === undefined) {
throw new Error(
`The project '${project.packageName}' does not define a '${phase.name}:shard' command in the 'scripts' section of its package.json`
);
}
Nothing checks whether the base phase script (scripts[phase.name]) exists. If it had, we could cleanly distinguish "this project uses the phase but has no shard script" (keep throwing — that's a real config mistake) from "this project doesn't use the phase at all" (skip sharding, let ShellOperationRunnerPlugin NullOp it).
Suggested fix — either of:
-
(a) Skip sharding when the base phase script is not defined, in ShardedPhaseOperationPlugin.ts:
const { scripts } = project.packageJson;
const phaseCommand = phase.shellCommand ?? scripts?.[phase.name];
if (phaseCommand === undefined) {
continue; // no-op for this project; let ShellOperationRunnerPlugin handle it
}
Inserted before the current work at line 60. Preserves the strict error for projects that do define the base phase script but forgot the :shard variant.
-
(b) Swap plugin registration order so ShellOperationRunnerPlugin runs first and the existing !operation.runner guard actually does what it looks like it was intended to do. More invasive — likely has other ordering implications.
(a) is the minimal fix and preserves existing behaviour for correctly-configured projects.
Standard questions
Please answer these questions to help us investigate your issue more quickly:
| Question |
Answer |
@microsoft/rush globally installed version? |
5.165.0 |
rushVersion from rush.json? |
5.165.0 |
pnpmVersion, npmVersion, or yarnVersion from rush.json? |
pnpm@10.24.0 |
(if pnpm) useWorkspaces from pnpm-config.json? |
true |
| Operating system? |
Linux |
| Would you consider contributing a PR? |
Yes |
Node.js version (node -v)? |
22.14.0 |
Summary
I'm working on converting our Rush monorepo to shard our various test related processes. One issue I have run into is that now that I have added sharding for the test phases into our shared rig configs, EVERY PACKAGE must have every shard phase's script defined or Rush throws
The project 'X' does not define a '_phase:name:shard' command in the 'scripts' section of its package.jsonfor each package that doesn't define it. Which means I need to add dummy shard phase scripts to all packages that don't even participate in the phase, which is less than ideal.Repro steps
sharding: { count: N }on a phase operation (e.g._phase:test) inside a rig'sconfig/rush-project.json._phase:testand_phase:test:shardin itspackage.jsonscripts.rush test(or any phased command that includes_phase:testand selects both projects).Expected result: Project A gets sharded. Project B's
_phase:testop resolves to a no-op (same as it does today for a project that doesn't define a phase script), and sharding is skipped for it.Actual result: Rush throws during operation graph construction:
…even though
@kx/bulk-package-json-editoralso does not define_phase:testand would otherwise be treated as a no-op for that phase.Details
This was generated by opus 4.7 with claude code, I don't know the details of the code but it seems accurate.
Root cause, tracing through
rush-libonmain:PhasedOperationPlugin(libraries/rush-lib/src/logic/operations/PhasedOperationPlugin.ts:44-48) creates an operation for every(phase, project)pair in the selection, regardless of whether the project defines a script for that phase. The "this project doesn't use this phase" decision is deferred to whichever runner plugin picks it up.Each operation's
settingsis pulled fromprojectConfigurations.get(project)?.operationSettingsByOperationName.get(name)(same file, line 63–65). Because settings originate from the rig'srush-project.json, they apply to every project using that rig — including projects that don't implement the phase at all. Sosettings.shardingis attached to those ops.Plugin registration order in
PhasedScriptAction.ts:417-421:ShardedPhasedOperationPlugintaps intocreateOperationsbeforeShellOperationRunnerPlugindoes.The guard in
ShardedPhaseOperationPlugin.ts:59is:This appears to be trying to skip sharding for operations that have already been marked as no-ops — but because the sharding plugin runs before the shell plugin,
operation.runneris always stillundefinedat this point, even for projects that have no_phase:testscript and would be turned into aNullOperationRunnermoments later.The strict throw at
ShardedPhaseOperationPlugin.ts:148-152then fires, because the project doesn't define_phase:test:shard:Nothing checks whether the base phase script (
scripts[phase.name]) exists. If it had, we could cleanly distinguish "this project uses the phase but has no shard script" (keep throwing — that's a real config mistake) from "this project doesn't use the phase at all" (skip sharding, let ShellOperationRunnerPlugin NullOp it).Suggested fix — either of:
(a) Skip sharding when the base phase script is not defined, in
ShardedPhaseOperationPlugin.ts:Inserted before the current work at line 60. Preserves the strict error for projects that do define the base phase script but forgot the
:shardvariant.(b) Swap plugin registration order so
ShellOperationRunnerPluginruns first and the existing!operation.runnerguard actually does what it looks like it was intended to do. More invasive — likely has other ordering implications.(a) is the minimal fix and preserves existing behaviour for correctly-configured projects.
Standard questions
Please answer these questions to help us investigate your issue more quickly:
@microsoft/rushglobally installed version?rushVersionfrom rush.json?pnpmVersion,npmVersion, oryarnVersionfrom rush.json?useWorkspacesfrom pnpm-config.json?node -v)?