diff --git a/packages/nx/src/tasks-runner/init-tasks-runner.ts b/packages/nx/src/tasks-runner/init-tasks-runner.ts index 86b9e7838f272..d64e17d4953d1 100644 --- a/packages/nx/src/tasks-runner/init-tasks-runner.ts +++ b/packages/nx/src/tasks-runner/init-tasks-runner.ts @@ -24,7 +24,7 @@ export async function initTasksRunner(nxArgs: NxArgs) { tasks: Task[]; parallel: number; }): Promise<{ - status: number; + status: NodeJS.Process['exitCode']; taskGraph: TaskGraph; taskResults: Record; }> => { @@ -51,7 +51,7 @@ export async function initTasksRunner(nxArgs: NxArgs) { }, {} as any), }; - const status = await invokeTasksRunner({ + const taskResults = await invokeTasksRunner({ tasks: opts.tasks, projectGraph, taskGraph, @@ -63,9 +63,14 @@ export async function initTasksRunner(nxArgs: NxArgs) { }); return { - status, + status: Object.values(taskResults).some( + (taskResult) => + taskResult.status === 'failure' || taskResult.status === 'skipped' + ) + ? 1 + : 0, taskGraph, - taskResults: lifeCycle.getTaskResults(), + taskResults, }; }, }; diff --git a/packages/nx/src/tasks-runner/life-cycles/invoke-runner-terminal-output-life-cycle.ts b/packages/nx/src/tasks-runner/life-cycles/invoke-runner-terminal-output-life-cycle.ts index 6247ae39b628b..1aecf95d22e7d 100644 --- a/packages/nx/src/tasks-runner/life-cycles/invoke-runner-terminal-output-life-cycle.ts +++ b/packages/nx/src/tasks-runner/life-cycles/invoke-runner-terminal-output-life-cycle.ts @@ -7,7 +7,6 @@ import { Task } from '../../config/task-graph'; export class InvokeRunnerTerminalOutputLifeCycle implements LifeCycle { failedTasks = [] as Task[]; cachedTasks = [] as Task[]; - private taskResults = {} as Record; constructor(private readonly tasks: Task[]) {} @@ -56,7 +55,6 @@ export class InvokeRunnerTerminalOutputLifeCycle implements LifeCycle { endTasks(taskResults: TaskResult[]): void { for (let t of taskResults) { - this.taskResults[t.task.id] = t; if (t.status === 'failure') { this.failedTasks.push(t.task); } else if (t.status === 'local-cache') { @@ -77,8 +75,4 @@ export class InvokeRunnerTerminalOutputLifeCycle implements LifeCycle { const args = getPrintableCommandArgsForTask(task); output.logCommandOutput(args.join(' '), cacheStatus, terminalOutput); } - - getTaskResults() { - return this.taskResults; - } } diff --git a/packages/nx/src/tasks-runner/life-cycles/static-run-many-terminal-output-life-cycle.ts b/packages/nx/src/tasks-runner/life-cycles/static-run-many-terminal-output-life-cycle.ts index b5e1581fe42c0..3122157d9c5cc 100644 --- a/packages/nx/src/tasks-runner/life-cycles/static-run-many-terminal-output-life-cycle.ts +++ b/packages/nx/src/tasks-runner/life-cycles/static-run-many-terminal-output-life-cycle.ts @@ -1,7 +1,7 @@ import { output } from '../../utils/output'; import { TaskStatus } from '../tasks-runner'; import { getPrintableCommandArgsForTask } from '../utils'; -import type { LifeCycle } from '../life-cycle'; +import type { LifeCycle, TaskResult } from '../life-cycle'; import { Task } from '../../config/task-graph'; import { formatFlags, formatTargetsAndProjects } from './formatting-utils'; @@ -126,9 +126,7 @@ export class StaticRunManyTerminalOutputLifeCycle implements LifeCycle { return this.tasks.filter((t) => !this.allCompletedTasks.has(t.id)); } - endTasks( - taskResults: { task: Task; status: TaskStatus; code: number }[] - ): void { + endTasks(taskResults: TaskResult[]): void { for (let t of taskResults) { this.allCompletedTasks.set(t.task.id, t.task); if (t.status === 'failure') { diff --git a/packages/nx/src/tasks-runner/life-cycles/static-run-one-terminal-output-life-cycle.ts b/packages/nx/src/tasks-runner/life-cycles/static-run-one-terminal-output-life-cycle.ts index eed6db41742ea..b880b26197166 100644 --- a/packages/nx/src/tasks-runner/life-cycles/static-run-one-terminal-output-life-cycle.ts +++ b/packages/nx/src/tasks-runner/life-cycles/static-run-one-terminal-output-life-cycle.ts @@ -1,7 +1,7 @@ import { output } from '../../utils/output'; import { TaskStatus } from '../tasks-runner'; import { getPrintableCommandArgsForTask } from '../utils'; -import type { LifeCycle } from '../life-cycle'; +import type { LifeCycle, TaskResult } from '../life-cycle'; import { Task } from '../../config/task-graph'; import { formatTargetsAndProjects } from './formatting-utils'; @@ -90,9 +90,7 @@ export class StaticRunOneTerminalOutputLifeCycle implements LifeCycle { } } - endTasks( - taskResults: { task: Task; status: TaskStatus; code: number }[] - ): void { + endTasks(taskResults: TaskResult[]): void { for (let t of taskResults) { if (t.status === 'failure') { this.failedTasks.push(t.task); diff --git a/packages/nx/src/tasks-runner/life-cycles/store-run-information-life-cycle.ts b/packages/nx/src/tasks-runner/life-cycles/store-run-information-life-cycle.ts index c5bc53d19c102..c15a67a2fb63d 100644 --- a/packages/nx/src/tasks-runner/life-cycles/store-run-information-life-cycle.ts +++ b/packages/nx/src/tasks-runner/life-cycles/store-run-information-life-cycle.ts @@ -1,6 +1,6 @@ import { parse, join } from 'path'; import { writeFileSync } from 'fs'; -import { LifeCycle } from '../../tasks-runner/life-cycle'; +import { LifeCycle, TaskResult } from '../../tasks-runner/life-cycle'; import { Task } from '../../config/task-graph'; import { TaskStatus } from '../../tasks-runner/tasks-runner'; import { cacheDir } from '../../utils/cache-directory'; @@ -36,9 +36,7 @@ export class StoreRunInformationLifeCycle implements LifeCycle { } } - endTasks( - taskResults: Array<{ task: Task; status: TaskStatus; code: number }> - ): void { + endTasks(taskResults: TaskResult[]): void { for (let tr of taskResults) { if (tr.task.startTime) { this.timings[tr.task.id].start = new Date( diff --git a/packages/nx/src/tasks-runner/life-cycles/task-profiling-life-cycle.ts b/packages/nx/src/tasks-runner/life-cycles/task-profiling-life-cycle.ts index 2f777a97d48da..fb37cdd94eab7 100644 --- a/packages/nx/src/tasks-runner/life-cycles/task-profiling-life-cycle.ts +++ b/packages/nx/src/tasks-runner/life-cycles/task-profiling-life-cycle.ts @@ -1,4 +1,4 @@ -import { LifeCycle, TaskMetadata } from '../life-cycle'; +import { LifeCycle, TaskMetadata, TaskResult } from '../life-cycle'; import { TaskStatus } from '../tasks-runner'; import { performance } from 'perf_hooks'; @@ -32,10 +32,7 @@ export class TaskProfilingLifeCycle implements LifeCycle { } } - endTasks( - taskResults: Array<{ task: Task; status: TaskStatus; code: number }>, - metadata: TaskMetadata - ): void { + endTasks(taskResults: TaskResult[], metadata: TaskMetadata): void { for (let tr of taskResults) { if (tr.task.startTime) { this.timings[tr.task.id].perfStart = tr.task.startTime; diff --git a/packages/nx/src/tasks-runner/life-cycles/task-results-life-cycle.ts b/packages/nx/src/tasks-runner/life-cycles/task-results-life-cycle.ts new file mode 100644 index 0000000000000..5396667f0021f --- /dev/null +++ b/packages/nx/src/tasks-runner/life-cycles/task-results-life-cycle.ts @@ -0,0 +1,15 @@ +import type { LifeCycle, TaskResult } from '../life-cycle'; + +export class TaskResultsLifeCycle implements LifeCycle { + private taskResults = {} as Record; + + endTasks(taskResults: TaskResult[]): void { + for (let t of taskResults) { + this.taskResults[t.task.id] = t; + } + } + + getTaskResults(): Record { + return this.taskResults; + } +} diff --git a/packages/nx/src/tasks-runner/life-cycles/task-timings-life-cycle.ts b/packages/nx/src/tasks-runner/life-cycles/task-timings-life-cycle.ts index fd533901ac181..190c7d7c69543 100644 --- a/packages/nx/src/tasks-runner/life-cycles/task-timings-life-cycle.ts +++ b/packages/nx/src/tasks-runner/life-cycles/task-timings-life-cycle.ts @@ -1,5 +1,5 @@ import { Task } from '../../config/task-graph'; -import { LifeCycle } from '../life-cycle'; +import { LifeCycle, TaskResult } from '../life-cycle'; import { TaskStatus } from '../tasks-runner'; export class TaskTimingsLifeCycle implements LifeCycle { @@ -19,13 +19,7 @@ export class TaskTimingsLifeCycle implements LifeCycle { } } - endTasks( - taskResults: Array<{ - task: Task; - status: TaskStatus; - code: number; - }> - ): void { + endTasks(taskResults: TaskResult[]): void { for (let tr of taskResults) { if (tr.task.startTime) { this.timings[tr.task.id].start = tr.task.startTime; diff --git a/packages/nx/src/tasks-runner/run-command.ts b/packages/nx/src/tasks-runner/run-command.ts index b0502d9033ae5..8656ee105538d 100644 --- a/packages/nx/src/tasks-runner/run-command.ts +++ b/packages/nx/src/tasks-runner/run-command.ts @@ -4,7 +4,6 @@ import { join } from 'path'; import { NxJsonConfiguration, readNxJson, - TargetDefaults, TargetDependencies, } from '../config/nx-json'; import { ProjectGraph, ProjectGraphProjectNode } from '../config/project-graph'; @@ -32,7 +31,7 @@ import { } from '../utils/sync-generators'; import { workspaceRoot } from '../utils/workspace-root'; import { createTaskGraph } from './create-task-graph'; -import { CompositeLifeCycle, LifeCycle } from './life-cycle'; +import { CompositeLifeCycle, LifeCycle, TaskResult } 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'; @@ -42,6 +41,7 @@ import { TaskHistoryLifeCycle } from './life-cycles/task-history-life-cycle'; import { LegacyTaskHistoryLifeCycle } from './life-cycles/task-history-life-cycle-old'; import { TaskProfilingLifeCycle } from './life-cycles/task-profiling-life-cycle'; import { TaskTimingsLifeCycle } from './life-cycles/task-timings-life-cycle'; +import { TaskResultsLifeCycle } from './life-cycles/task-results-life-cycle'; import { findCycle, makeAcyclic, @@ -50,6 +50,7 @@ import { import { TasksRunner, TaskStatus } from './tasks-runner'; import { shouldStreamOutput } from './utils'; import chalk = require('chalk'); +import type { Observable } from 'rxjs'; async function getTerminalOutputLifeCycle( initiatingProject: string, @@ -172,49 +173,77 @@ export async function runCommand( const status = await handleErrors( process.env.NX_VERBOSE_LOGGING === 'true', async () => { - const projectNames = projectsToRun.map((t) => t.name); - - const { projectGraph, taskGraph } = - await ensureWorkspaceIsInSyncAndGetGraphs( - currentProjectGraph, - nxJson, - projectNames, - nxArgs, - overrides, - extraTargetDependencies, - extraOptions - ); - const tasks = Object.values(taskGraph.tasks); - - const { lifeCycle, renderIsDone } = await getTerminalOutputLifeCycle( - initiatingProject, - projectNames, - tasks, + const taskResults = await runCommandForTasks( + projectsToRun, + currentProjectGraph, + { nxJson }, nxArgs, - nxJson, - overrides - ); - - const status = await invokeTasksRunner({ - tasks, - projectGraph, - taskGraph, - lifeCycle, - nxJson, - nxArgs, - loadDotEnvFiles: extraOptions.loadDotEnvFiles, + overrides, initiatingProject, - }); - - await renderIsDone; + extraTargetDependencies, + extraOptions + ); - return status; + return Object.values(taskResults).some( + (taskResult) => + taskResult.status === 'failure' || taskResult.status === 'skipped' + ) + ? 1 + : 0; } ); return status; } +export async function runCommandForTasks( + projectsToRun: ProjectGraphProjectNode[], + currentProjectGraph: ProjectGraph, + { nxJson }: { nxJson: NxJsonConfiguration }, + nxArgs: NxArgs, + overrides: any, + initiatingProject: string | null, + extraTargetDependencies: Record, + extraOptions: { excludeTaskDependencies: boolean; loadDotEnvFiles: boolean } +): Promise<{ [id: string]: TaskResult }> { + const projectNames = projectsToRun.map((t) => t.name); + + const { projectGraph, taskGraph } = await ensureWorkspaceIsInSyncAndGetGraphs( + currentProjectGraph, + nxJson, + projectNames, + nxArgs, + overrides, + extraTargetDependencies, + extraOptions + ); + const tasks = Object.values(taskGraph.tasks); + + const { lifeCycle, renderIsDone } = await getTerminalOutputLifeCycle( + initiatingProject, + projectNames, + tasks, + nxArgs, + nxJson, + overrides + ); + + const taskResults = await invokeTasksRunner({ + tasks, + projectGraph, + taskGraph, + lifeCycle, + nxJson, + nxArgs, + loadDotEnvFiles: extraOptions.loadDotEnvFiles, + initiatingProject, + }); + + await renderIsDone; + + return taskResults; +} + async function ensureWorkspaceIsInSyncAndGetGraphs( projectGraph: ProjectGraph, nxJson: NxJsonConfiguration, @@ -554,7 +583,7 @@ export async function invokeTasksRunner({ nxArgs: NxArgs; loadDotEnvFiles: boolean; initiatingProject: string | null; -}) { +}): Promise<{ [id: string]: TaskResult }> { setEnvVarsBasedOnArgs(nxArgs, loadDotEnvFiles); const { tasksRunner, runnerOptions } = getRunner(nxArgs, nxJson); @@ -571,12 +600,19 @@ export async function invokeTasksRunner({ taskGraph, nxJson ); - - const promiseOrObservable = tasksRunner( + const taskResultsLifecycle = new TaskResultsLifeCycle(); + const compositedLifeCycle: LifeCycle = new CompositeLifeCycle([ + ...constructLifeCycles(lifeCycle), + taskResultsLifecycle, + ]); + + let promiseOrObservable: + | Observable<{ task: Task; success: boolean }> + | Promise<{ [id: string]: TaskStatus }> = tasksRunner( tasks, { ...runnerOptions, - lifeCycle: new CompositeLifeCycle(constructLifeCycles(lifeCycle)), + lifeCycle: compositedLifeCycle, }, { initiatingProject: @@ -641,17 +677,19 @@ export async function invokeTasksRunner({ daemon: daemonClient, } ); - let anyFailures; if ((promiseOrObservable as any).subscribe) { - anyFailures = await anyFailuresInObservable(promiseOrObservable); - } else { - // simply await the promise - anyFailures = await anyFailuresInPromise(promiseOrObservable as any); + promiseOrObservable = convertObservableToPromise( + promiseOrObservable as Observable<{ task: Task; success: boolean }> + ); } - return anyFailures ? 1 : 0; + + await (promiseOrObservable as Promise<{ + [id: string]: TaskStatus; + }>); + return taskResultsLifecycle.getTaskResults(); } -function constructLifeCycles(lifeCycle: LifeCycle) { +function constructLifeCycles(lifeCycle: LifeCycle): LifeCycle[] { const lifeCycles = [] as LifeCycle[]; lifeCycles.push(new StoreRunInformationLifeCycle()); lifeCycles.push(lifeCycle); @@ -671,55 +709,26 @@ function constructLifeCycles(lifeCycle: LifeCycle) { return lifeCycles; } -function mergeTargetDependencies( - defaults: TargetDefaults | undefined | null, - deps: TargetDependencies -): TargetDependencies { - const res = {}; - Object.keys(defaults ?? {}).forEach((k) => { - res[k] = defaults[k].dependsOn; - }); - if (deps) { - Object.keys(deps).forEach((k) => { - if (res[k]) { - res[k] = [...res[k], deps[k]]; - } else { - res[k] = deps[k]; - } - }); - - return res; - } -} - -async function anyFailuresInPromise( - promise: Promise<{ [id: string]: TaskStatus }> -) { - return Object.values(await promise).some( - (v) => v === 'failure' || v === 'skipped' - ); -} - -async function anyFailuresInObservable(obs: any) { +async function convertObservableToPromise( + obs: Observable<{ task: Task; success: boolean }> +): Promise<{ [id: string]: TaskStatus }> { return await new Promise((res) => { - let anyFailures = false; - obs.subscribe( - (t) => { - if (!t.success) { - anyFailures = true; - } + let tasksResults: { [id: string]: TaskStatus } = {}; + obs.subscribe({ + next: (t) => { + tasksResults[t.task.id] = t.success ? 'success' : 'failure'; }, - (error) => { + error: (error) => { output.error({ title: 'Unhandled error in task executor', }); console.error(error); - res(true); + res(tasksResults); }, - () => { - res(anyFailures); - } - ); + complete: () => { + res(tasksResults); + }, + }); }); } @@ -742,7 +751,7 @@ function shouldUseDynamicLifeCycle( return !tasks.find((t) => shouldStreamOutput(t, null)); } -function loadTasksRunner(modulePath: string) { +function loadTasksRunner(modulePath: string): TasksRunner { try { const maybeTasksRunner = require(modulePath) as | TasksRunner