Skip to content

[rush] package using rig with sharding defined is required to define _phase:name:shard script, even if it doesn't have the phase #5789

@UberMouse

Description

@UberMouse

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

  1. In a Rush monorepo, put sharding: { count: N } on a phase operation (e.g. _phase:test) inside a rig's config/rush-project.json.
  2. 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.
  3. 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status

    Needs triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions