From fe80faf5954ed9e89f78f7fd9cc112258315be19 Mon Sep 17 00:00:00 2001 From: Max Kless Date: Thu, 11 Jul 2024 13:26:13 +0200 Subject: [PATCH 1/2] feat(core): error when running atomized tasks without cloud --- docs/nx-cloud/features/split-e2e-tasks.md | 18 ++- e2e/jest/src/jest.test.ts | 8 +- e2e/next/src/next.test.ts | 5 + packages/nx/src/tasks-runner/run-command.ts | 58 ++++--- .../src/tasks-runner/task-graph-utils.spec.ts | 141 +++++++++++++++++- .../nx/src/tasks-runner/task-graph-utils.ts | 53 +++++++ .../nx/src/tasks-runner/task-orchestrator.ts | 53 +++---- 7 files changed, 283 insertions(+), 53 deletions(-) diff --git a/docs/nx-cloud/features/split-e2e-tasks.md b/docs/nx-cloud/features/split-e2e-tasks.md index 2a7960539b3d4..487bb2dcd1d31 100644 --- a/docs/nx-cloud/features/split-e2e-tasks.md +++ b/docs/nx-cloud/features/split-e2e-tasks.md @@ -7,11 +7,11 @@ width="100%" /%} In almost every codebase, e2e tests are the largest portion of the CI pipeline. Typically, e2e tests are grouped by application so that whenever an application's code changes, all the e2e tests for that application are run. These large groupings of e2e tests make caching and distribution less effective. Also, because e2e tests deal with a lot of integration code, they are at a much higher risk to be flaky. -You could manually address these problems by splitting your e2e tests into smaller tasks, but this requires developer time to maintain and adds additional configuration overhead to your codebase. Or, you could allow Nx to automatically split your Cypress or Playwright e2e tests by file. +You could manually address these problems by splitting your e2e tests into smaller tasks, but this requires developer time to maintain and adds additional configuration overhead to your codebase. Or, you could allow Nx to automatically split your e2e tests by file. Doing this will split your large e2e tasks into smaller, atomized tasks. ## Set up -To enable automatically split e2e tasks, you need to turn on [inferred tasks](/concepts/inferred-tasks#existing-nx-workspaces) for the [@nx/cypress](/nx-api/cypress), [@nx/playwright](/nx-api/playwright), or [@nx/jest](/nx-api/jest) plugins. Run this command to set up inferred tasks: +To enable atomized tasks, you need to turn on [inferred tasks](/concepts/inferred-tasks#existing-nx-workspaces) for the [@nx/cypress](/nx-api/cypress), [@nx/playwright](/nx-api/playwright), [@nx/jest](/nx-api/jest) or [@nx/gradle](/nx-api/gradle) plugins. Run this command to set up inferred tasks: {% tabs %} {% tab label="Cypress" %} @@ -34,6 +34,13 @@ nx add @nx/playwright nx add @nx/jest ``` +{% /tab %} +{% tab label="Gradle" %} + +```shell {% skipRescope=true %} +nx add @nx/gradle +``` + {% /tab %} {% /tabs %} @@ -394,3 +401,10 @@ With more granular e2e tasks, all the other features of Nx become more powerful. ### More Precise Flaky Task Identification Nx Agents [automatically re-run failed flaky e2e tests](/ci/features/flaky-tasks) on a separate agent without a developer needing to manually re-run the CI pipeline. Leveraging e2e task splitting, Nx identifies the specific flaky test file - this way you can quickly fix the offending test file. Without e2e splitting, Nx identifies that at least one of the e2e tests are flaky - requiring you to find the flaky test on your own. + +## Nx Cloud is Required to Run Atomized Tasks! + +Running a group of atomized tasks on a single machine takes longer than running the large e2e task. +Nx Cloud [distributes](/ci/features/distribute-task-execution) the atomized tasks across multiple Nx agents in parallel. + +When running locally or if you cannot use Nx Agents, it's better to use the non-atomized target instead (`e2e` in the example above). diff --git a/e2e/jest/src/jest.test.ts b/e2e/jest/src/jest.test.ts index 5d2f1bce73e43..cb9672cc4bcbb 100644 --- a/e2e/jest/src/jest.test.ts +++ b/e2e/jest/src/jest.test.ts @@ -2,6 +2,7 @@ import { stripIndents } from '@angular-devkit/core/src/utils/literals'; import { cleanupProject, expectJestTestsToPass, + getStrippedEnvironmentVariables, newProject, runCLI, runCLIAsync, @@ -161,6 +162,11 @@ describe('Jest', () => { return json; }); - await runCLIAsync(`e2e-ci ${libName}`); + await runCLIAsync(`e2e-ci ${libName}`, { + env: { + ...getStrippedEnvironmentVariables(), + NX_SKIP_ATOMIZER_VALIDATION: 'true', + }, + }); }, 90000); }); diff --git a/e2e/next/src/next.test.ts b/e2e/next/src/next.test.ts index 1e550d396d416..8a1877d0361c8 100644 --- a/e2e/next/src/next.test.ts +++ b/e2e/next/src/next.test.ts @@ -2,6 +2,7 @@ import { checkFilesDoNotExist, checkFilesExist, cleanupProject, + getStrippedEnvironmentVariables, newProject, readFile, runCLI, @@ -195,6 +196,10 @@ describe('Next.js Applications', () => { if (runE2ETests('cypress')) { const e2eResults = runCLI(`e2e-ci ${appName}-e2e --verbose`, { verbose: true, + env: { + ...getStrippedEnvironmentVariables(), + NX_SKIP_ATOMIZER_VALIDATION: 'true', + }, }); expect(e2eResults).toContain( 'Successfully ran target e2e-ci for project' diff --git a/packages/nx/src/tasks-runner/run-command.ts b/packages/nx/src/tasks-runner/run-command.ts index 06a22716e8ab7..8fc5f7a98bbed 100644 --- a/packages/nx/src/tasks-runner/run-command.ts +++ b/packages/nx/src/tasks-runner/run-command.ts @@ -1,36 +1,40 @@ -import { TasksRunner, TaskStatus } from './tasks-runner'; import { join } from 'path'; -import { workspaceRoot } from '../utils/workspace-root'; -import { NxArgs } from '../utils/command-line-utils'; -import { isRelativePath } from '../utils/fileutils'; -import { output } from '../utils/output'; -import { shouldStreamOutput } from './utils'; -import { CompositeLifeCycle, LifeCycle } from './life-cycle'; -import { StaticRunManyTerminalOutputLifeCycle } from './life-cycles/static-run-many-terminal-output-life-cycle'; -import { StaticRunOneTerminalOutputLifeCycle } from './life-cycles/static-run-one-terminal-output-life-cycle'; -import { TaskTimingsLifeCycle } from './life-cycles/task-timings-life-cycle'; -import { createRunManyDynamicOutputRenderer } from './life-cycles/dynamic-run-many-terminal-output-life-cycle'; -import { TaskProfilingLifeCycle } from './life-cycles/task-profiling-life-cycle'; -import { isCI } from '../utils/is-ci'; -import { createRunOneDynamicOutputRenderer } from './life-cycles/dynamic-run-one-terminal-output-life-cycle'; -import { ProjectGraph, ProjectGraphProjectNode } from '../config/project-graph'; import { NxJsonConfiguration, readNxJson, TargetDefaults, TargetDependencies, } from '../config/nx-json'; +import { ProjectGraph, ProjectGraphProjectNode } from '../config/project-graph'; import { Task, TaskGraph } from '../config/task-graph'; -import { createTaskGraph } from './create-task-graph'; -import { findCycle, makeAcyclic } from './task-graph-utils'; import { TargetDependencyConfig } from '../config/workspace-json-project-json'; -import { handleErrors } from '../utils/params'; -import { hashTasksThatDoNotDependOnOutputsOfOtherTasks } from '../hasher/hash-task'; import { daemonClient } from '../daemon/client/client'; -import { StoreRunInformationLifeCycle } from './life-cycles/store-run-information-life-cycle'; import { createTaskHasher } from '../hasher/create-task-hasher'; -import { TaskHistoryLifeCycle } from './life-cycles/task-history-life-cycle'; +import { hashTasksThatDoNotDependOnOutputsOfOtherTasks } from '../hasher/hash-task'; +import { NxArgs } from '../utils/command-line-utils'; +import { isRelativePath } from '../utils/fileutils'; +import { isCI } from '../utils/is-ci'; import { isNxCloudUsed } from '../utils/nx-cloud-utils'; +import { output } from '../utils/output'; +import { handleErrors } from '../utils/params'; +import { workspaceRoot } from '../utils/workspace-root'; +import { createTaskGraph } from './create-task-graph'; +import { CompositeLifeCycle, LifeCycle } from './life-cycle'; +import { createRunManyDynamicOutputRenderer } from './life-cycles/dynamic-run-many-terminal-output-life-cycle'; +import { createRunOneDynamicOutputRenderer } from './life-cycles/dynamic-run-one-terminal-output-life-cycle'; +import { StaticRunManyTerminalOutputLifeCycle } from './life-cycles/static-run-many-terminal-output-life-cycle'; +import { StaticRunOneTerminalOutputLifeCycle } from './life-cycles/static-run-one-terminal-output-life-cycle'; +import { StoreRunInformationLifeCycle } from './life-cycles/store-run-information-life-cycle'; +import { TaskHistoryLifeCycle } from './life-cycles/task-history-life-cycle'; +import { TaskProfilingLifeCycle } from './life-cycles/task-profiling-life-cycle'; +import { TaskTimingsLifeCycle } from './life-cycles/task-timings-life-cycle'; +import { + findCycle, + makeAcyclic, + validateNoAtomizedTasks, +} from './task-graph-utils'; +import { TasksRunner, TaskStatus } from './tasks-runner'; +import { shouldStreamOutput } from './utils'; async function getTerminalOutputLifeCycle( initiatingProject: string, @@ -91,7 +95,7 @@ async function getTerminalOutputLifeCycle( } } -function createTaskGraphAndValidateCycles( +function createTaskGraphAndRunValidations( projectGraph: ProjectGraph, extraTargetDependencies: TargetDependencies, projectNames: string[], @@ -129,6 +133,14 @@ function createTaskGraphAndValidateCycles( } } + // validate that no atomized tasks like e2e-ci are used without Nx Cloud + if ( + !isNxCloudUsed(readNxJson()) && + !process.env['NX_SKIP_ATOMIZER_VALIDATION'] + ) { + validateNoAtomizedTasks(taskGraph, projectGraph); + } + return taskGraph; } @@ -147,7 +159,7 @@ export async function runCommand( async () => { const projectNames = projectsToRun.map((t) => t.name); - const taskGraph = createTaskGraphAndValidateCycles( + const taskGraph = createTaskGraphAndRunValidations( projectGraph, extraTargetDependencies ?? {}, projectNames, diff --git a/packages/nx/src/tasks-runner/task-graph-utils.spec.ts b/packages/nx/src/tasks-runner/task-graph-utils.spec.ts index 2d413e8929c70..ac5ef4e3b44b2 100644 --- a/packages/nx/src/tasks-runner/task-graph-utils.spec.ts +++ b/packages/nx/src/tasks-runner/task-graph-utils.spec.ts @@ -1,4 +1,19 @@ -import { findCycle, makeAcyclic } from './task-graph-utils'; +import '../internal-testing-utils/mock-fs'; + +import { vol } from 'memfs'; + +import { join } from 'path'; +import { + findCycle, + makeAcyclic, + validateNoAtomizedTasks, +} from './task-graph-utils'; +import { ensureDirSync, removeSync, writeFileSync } from 'fs-extra'; +import { tmpdir } from 'os'; +import { workspaceRoot } from '../utils/workspace-root'; +import { cacheDir } from '../utils/cache-directory'; +import { Task } from '../config/task-graph'; +import { ProjectGraph } from '../config/project-graph'; describe('task graph utils', () => { describe('findCycles', () => { @@ -57,4 +72,128 @@ describe('task graph utils', () => { expect(graph.roots).toEqual(['d', 'e']); }); }); + + describe('validateNoAtomizedTasks', () => { + let mockProcessExit: jest.SpyInstance; + let env: NodeJS.ProcessEnv; + + beforeEach(() => { + env = process.env; + process.env = {}; + + mockProcessExit = jest + .spyOn(process, 'exit') + .mockImplementation((code: number) => { + return undefined as any as never; + }); + }); + + afterEach(() => { + process.env = env; + vol.reset(); + mockProcessExit.mockRestore(); + }); + + it('should do nothing if no tasks are atomized', () => { + const taskGraph = { + tasks: { + 'e2e:e2e': { + target: { + project: 'e2e', + target: 'e2e', + }, + }, + }, + }; + const projectGraph = { + nodes: { + e2e: { + data: { + targets: { + e2e: {}, + }, + }, + }, + }, + }; + validateNoAtomizedTasks(taskGraph as any, projectGraph as any); + expect(mockProcessExit).not.toHaveBeenCalled(); + }); + + it('should exit if atomized task is present', () => { + const taskGraph = { + tasks: { + 'e2e:e2e-ci': { + target: { + project: 'e2e', + target: 'e2e-ci', + }, + }, + }, + }; + const projectGraph = { + nodes: { + e2e: { + data: { + targets: { + 'e2e-ci': { + metadata: { + nonAtomizedTarget: 'e2e', + }, + }, + }, + }, + }, + }, + }; + validateNoAtomizedTasks(taskGraph as any, projectGraph as any); + expect(mockProcessExit).toHaveBeenCalledWith(1); + }); + it('should exit if multiple atomized tasks are present', () => { + const taskGraph = { + tasks: { + 'e2e:e2e-ci': { + target: { + project: 'e2e', + target: 'e2e-ci', + }, + }, + 'gradle:test-ci': { + target: { + project: 'gradle', + target: 'test-ci', + }, + }, + }, + }; + const projectGraph = { + nodes: { + e2e: { + data: { + targets: { + 'e2e-ci': { + metadata: { + nonAtomizedTarget: 'e2e', + }, + }, + }, + }, + }, + gradle: { + data: { + targets: { + 'test-ci': { + metadata: { + nonAtomizedTarget: 'test', + }, + }, + }, + }, + }, + }, + }; + validateNoAtomizedTasks(taskGraph as any, projectGraph as any); + expect(mockProcessExit).toHaveBeenCalledWith(1); + }); + }); }); diff --git a/packages/nx/src/tasks-runner/task-graph-utils.ts b/packages/nx/src/tasks-runner/task-graph-utils.ts index c2f6fe4f43c86..02c03c4a8d9f2 100644 --- a/packages/nx/src/tasks-runner/task-graph-utils.ts +++ b/packages/nx/src/tasks-runner/task-graph-utils.ts @@ -1,3 +1,11 @@ +import { readNxJson } from '../config/configuration'; +import { ProjectGraph } from '../config/project-graph'; +import { Task, TaskGraph } from '../config/task-graph'; +import { isNxCloudUsed } from '../utils/nx-cloud-utils'; +import { output } from '../utils/output'; +import { serializeTarget } from '../utils/serialize-target'; +import chalk = require('chalk'); + function _findCycle( graph: { dependencies: Record }, id: string, @@ -66,3 +74,48 @@ export function makeAcyclic(graph: { (t) => graph.dependencies[t].length === 0 ); } + +export function validateNoAtomizedTasks( + taskGraph: TaskGraph, + projectGraph: ProjectGraph +) { + const getNonAtomizedTargetForTask = (task) => + projectGraph.nodes[task.target.project]?.data?.targets?.[task.target.target] + ?.metadata?.nonAtomizedTarget; + + const atomizedRootTasks = Object.values(taskGraph.tasks).filter( + (task) => getNonAtomizedTargetForTask(task) !== undefined + ); + + if (atomizedRootTasks.length === 0) { + return; + } + + const nonAtomizedTasks = atomizedRootTasks + .map((t) => `"${getNonAtomizedTargetForTask(t)}"`) + .filter((item, index, arr) => arr.indexOf(item) === index); + + const moreInfoLines = [ + `Please enable Nx Cloud or use the slower ${nonAtomizedTasks.join( + ',' + )} task${nonAtomizedTasks.length > 1 ? 's' : ''}.`, + 'Learn more at https://nx.dev/ci/features/split-e2e-tasks#use-atomizer-only-with-nx-cloud-distribution', + ]; + + if (atomizedRootTasks.length === 1) { + output.error({ + title: `The ${atomizedRootTasks[0].id} task should only be run with Nx Cloud.`, + bodyLines: [...moreInfoLines], + }); + } else { + output.error({ + title: `The following tasks should only be run with Nx Cloud:`, + bodyLines: [ + ...atomizedRootTasks.map((task) => ` - ${task.id}`), + '', + ...moreInfoLines, + ], + }); + } + process.exit(1); +} diff --git a/packages/nx/src/tasks-runner/task-orchestrator.ts b/packages/nx/src/tasks-runner/task-orchestrator.ts index cb9cb73e33324..f71771e71e2bb 100644 --- a/packages/nx/src/tasks-runner/task-orchestrator.ts +++ b/packages/nx/src/tasks-runner/task-orchestrator.ts @@ -1,13 +1,27 @@ import { defaultMaxListeners } from 'events'; -import { performance } from 'perf_hooks'; -import { relative } from 'path'; import { writeFileSync } from 'fs'; -import { TaskHasher } from '../hasher/task-hasher'; +import { relative } from 'path'; +import { performance } from 'perf_hooks'; +import { ProjectGraph } from '../config/project-graph'; +import { Task, TaskGraph } from '../config/task-graph'; +import { DaemonClient } from '../daemon/client/client'; import runCommandsImpl from '../executors/run-commands/run-commands.impl'; -import { ForkedProcessTaskRunner } from './forked-process-task-runner'; +import { hashTask } from '../hasher/hash-task'; +import { TaskHasher } from '../hasher/task-hasher'; +import { output } from '../utils/output'; +import { combineOptionsForExecutor } from '../utils/params'; +import { workspaceRoot } from '../utils/workspace-root'; import { Cache } from './cache'; import { DefaultTasksRunnerOptions } from './default-tasks-runner'; +import { ForkedProcessTaskRunner } from './forked-process-task-runner'; +import { TaskMetadata } from './life-cycle'; +import { + getEnvVariablesForBatchProcess, + getEnvVariablesForTask, + getTaskSpecificEnv, +} from './task-env'; import { TaskStatus } from './tasks-runner'; +import { Batch, TasksSchedule } from './tasks-schedule'; import { calculateReverseDeps, getExecutorForTask, @@ -17,20 +31,6 @@ import { removeTasksFromTaskGraph, shouldStreamOutput, } from './utils'; -import { Batch, TasksSchedule } from './tasks-schedule'; -import { TaskMetadata } from './life-cycle'; -import { ProjectGraph } from '../config/project-graph'; -import { Task, TaskGraph } from '../config/task-graph'; -import { DaemonClient } from '../daemon/client/client'; -import { hashTask } from '../hasher/hash-task'; -import { - getEnvVariablesForBatchProcess, - getEnvVariablesForTask, - getTaskSpecificEnv, -} from './task-env'; -import { workspaceRoot } from '../utils/workspace-root'; -import { output } from '../utils/output'; -import { combineOptionsForExecutor } from '../utils/params'; export class TaskOrchestrator { private cache = new Cache(this.options); @@ -388,7 +388,15 @@ export class TaskOrchestrator { task, this.projectGraph ); - if ( + + if (targetConfiguration.executor === 'nx:noop') { + writeFileSync(temporaryOutputPath, ''); + results.push({ + task, + status: 'success', + terminalOutput: '', + }); + } else if ( process.env.NX_RUN_COMMANDS_DIRECTLY !== 'false' && targetConfiguration.executor === 'nx:run-commands' && !shouldPrefix @@ -456,13 +464,6 @@ export class TaskOrchestrator { terminalOutput, }); } - } else if (targetConfiguration.executor === 'nx:noop') { - writeFileSync(temporaryOutputPath, ''); - results.push({ - task, - status: 'success', - terminalOutput: '', - }); } else { // cache prep const { code, terminalOutput } = await this.runTaskInForkedProcess( From d6a791a305082388d8bc7414eba3e5b15c717ead Mon Sep 17 00:00:00 2001 From: Max Kless Date: Tue, 23 Jul 2024 10:33:50 +0200 Subject: [PATCH 2/2] fix(core): review fixes --- docs/nx-cloud/features/split-e2e-tasks.md | 7 --- .../src/tasks-runner/task-graph-utils.spec.ts | 4 ++ .../nx/src/tasks-runner/task-orchestrator.ts | 53 +++++++++---------- 3 files changed, 30 insertions(+), 34 deletions(-) diff --git a/docs/nx-cloud/features/split-e2e-tasks.md b/docs/nx-cloud/features/split-e2e-tasks.md index 487bb2dcd1d31..d85c00933949a 100644 --- a/docs/nx-cloud/features/split-e2e-tasks.md +++ b/docs/nx-cloud/features/split-e2e-tasks.md @@ -34,13 +34,6 @@ nx add @nx/playwright nx add @nx/jest ``` -{% /tab %} -{% tab label="Gradle" %} - -```shell {% skipRescope=true %} -nx add @nx/gradle -``` - {% /tab %} {% /tabs %} diff --git a/packages/nx/src/tasks-runner/task-graph-utils.spec.ts b/packages/nx/src/tasks-runner/task-graph-utils.spec.ts index ac5ef4e3b44b2..182ed5d1a5442 100644 --- a/packages/nx/src/tasks-runner/task-graph-utils.spec.ts +++ b/packages/nx/src/tasks-runner/task-graph-utils.spec.ts @@ -98,6 +98,7 @@ describe('task graph utils', () => { const taskGraph = { tasks: { 'e2e:e2e': { + id: 'e2e:e2e', target: { project: 'e2e', target: 'e2e', @@ -124,6 +125,7 @@ describe('task graph utils', () => { const taskGraph = { tasks: { 'e2e:e2e-ci': { + id: 'e2e:e2e-ci', target: { project: 'e2e', target: 'e2e-ci', @@ -153,12 +155,14 @@ describe('task graph utils', () => { const taskGraph = { tasks: { 'e2e:e2e-ci': { + id: 'e2e:e2e-ci', target: { project: 'e2e', target: 'e2e-ci', }, }, 'gradle:test-ci': { + id: 'gradle:test-ci', target: { project: 'gradle', target: 'test-ci', diff --git a/packages/nx/src/tasks-runner/task-orchestrator.ts b/packages/nx/src/tasks-runner/task-orchestrator.ts index f71771e71e2bb..cb9cb73e33324 100644 --- a/packages/nx/src/tasks-runner/task-orchestrator.ts +++ b/packages/nx/src/tasks-runner/task-orchestrator.ts @@ -1,27 +1,13 @@ import { defaultMaxListeners } from 'events'; -import { writeFileSync } from 'fs'; -import { relative } from 'path'; import { performance } from 'perf_hooks'; -import { ProjectGraph } from '../config/project-graph'; -import { Task, TaskGraph } from '../config/task-graph'; -import { DaemonClient } from '../daemon/client/client'; -import runCommandsImpl from '../executors/run-commands/run-commands.impl'; -import { hashTask } from '../hasher/hash-task'; +import { relative } from 'path'; +import { writeFileSync } from 'fs'; import { TaskHasher } from '../hasher/task-hasher'; -import { output } from '../utils/output'; -import { combineOptionsForExecutor } from '../utils/params'; -import { workspaceRoot } from '../utils/workspace-root'; +import runCommandsImpl from '../executors/run-commands/run-commands.impl'; +import { ForkedProcessTaskRunner } from './forked-process-task-runner'; import { Cache } from './cache'; import { DefaultTasksRunnerOptions } from './default-tasks-runner'; -import { ForkedProcessTaskRunner } from './forked-process-task-runner'; -import { TaskMetadata } from './life-cycle'; -import { - getEnvVariablesForBatchProcess, - getEnvVariablesForTask, - getTaskSpecificEnv, -} from './task-env'; import { TaskStatus } from './tasks-runner'; -import { Batch, TasksSchedule } from './tasks-schedule'; import { calculateReverseDeps, getExecutorForTask, @@ -31,6 +17,20 @@ import { removeTasksFromTaskGraph, shouldStreamOutput, } from './utils'; +import { Batch, TasksSchedule } from './tasks-schedule'; +import { TaskMetadata } from './life-cycle'; +import { ProjectGraph } from '../config/project-graph'; +import { Task, TaskGraph } from '../config/task-graph'; +import { DaemonClient } from '../daemon/client/client'; +import { hashTask } from '../hasher/hash-task'; +import { + getEnvVariablesForBatchProcess, + getEnvVariablesForTask, + getTaskSpecificEnv, +} from './task-env'; +import { workspaceRoot } from '../utils/workspace-root'; +import { output } from '../utils/output'; +import { combineOptionsForExecutor } from '../utils/params'; export class TaskOrchestrator { private cache = new Cache(this.options); @@ -388,15 +388,7 @@ export class TaskOrchestrator { task, this.projectGraph ); - - if (targetConfiguration.executor === 'nx:noop') { - writeFileSync(temporaryOutputPath, ''); - results.push({ - task, - status: 'success', - terminalOutput: '', - }); - } else if ( + if ( process.env.NX_RUN_COMMANDS_DIRECTLY !== 'false' && targetConfiguration.executor === 'nx:run-commands' && !shouldPrefix @@ -464,6 +456,13 @@ export class TaskOrchestrator { terminalOutput, }); } + } else if (targetConfiguration.executor === 'nx:noop') { + writeFileSync(temporaryOutputPath, ''); + results.push({ + task, + status: 'success', + terminalOutput: '', + }); } else { // cache prep const { code, terminalOutput } = await this.runTaskInForkedProcess(