From d1c3c91151972946e9858d7e5b3510c802bf4b06 Mon Sep 17 00:00:00 2001 From: Max Kless Date: Tue, 18 Jun 2024 10:22:27 +0200 Subject: [PATCH 1/3] feat(core): add lifecycle to record task history & retrieve via daemon --- packages/nx/src/daemon/client/client.ts | 24 ++++ .../src/daemon/message-types/task-history.ts | 38 ++++++ .../daemon/server/handle-get-task-history.ts | 9 ++ .../handle-write-task-runs-to-history.ts | 9 ++ packages/nx/src/daemon/server/server.ts | 14 ++ .../src/tasks-runner/default-tasks-runner.ts | 4 +- packages/nx/src/tasks-runner/life-cycle.ts | 36 ++--- .../life-cycles/task-history-life-cycle.ts | 56 ++++++++ packages/nx/src/tasks-runner/run-command.ts | 2 + .../nx/src/tasks-runner/task-orchestrator.ts | 8 +- packages/nx/src/utils/task-history.ts | 126 ++++++++++++++++++ 11 files changed, 305 insertions(+), 21 deletions(-) create mode 100644 packages/nx/src/daemon/message-types/task-history.ts create mode 100644 packages/nx/src/daemon/server/handle-get-task-history.ts create mode 100644 packages/nx/src/daemon/server/handle-write-task-runs-to-history.ts create mode 100644 packages/nx/src/tasks-runner/life-cycles/task-history-life-cycle.ts create mode 100644 packages/nx/src/utils/task-history.ts diff --git a/packages/nx/src/daemon/client/client.ts b/packages/nx/src/daemon/client/client.ts index 0c5c68516124d..baaf30605dffa 100644 --- a/packages/nx/src/daemon/client/client.ts +++ b/packages/nx/src/daemon/client/client.ts @@ -45,6 +45,11 @@ import { } from '../message-types/get-files-in-directory'; import { HASH_GLOB, HandleHashGlobMessage } from '../message-types/hash-glob'; import { NxWorkspaceFiles } from '../../native'; +import { TaskRun } from '../../utils/task-history'; +import { + HandleGetTaskHistoryForHashesMessage, + HandleWriteTaskRunsToHistoryMessage, +} from '../message-types/task-history'; const DAEMON_ENV_SETTINGS = { NX_PROJECT_GLOB_CACHE: 'false', @@ -312,6 +317,25 @@ export class DaemonClient { return this.sendToDaemonViaQueue(message); } + getTaskHistoryForHashes(hashes: string[]): Promise<{ + [hash: string]: TaskRun[]; + }> { + const message: HandleGetTaskHistoryForHashesMessage = { + type: 'GET_TASK_HISTORY_FOR_HASHES', + hashes, + }; + + return this.sendToDaemonViaQueue(message); + } + + writeTaskRunsToHistory(taskRuns: TaskRun[]): Promise { + const message: HandleWriteTaskRunsToHistoryMessage = { + type: 'WRITE_TASK_RUNS_TO_HISTORY', + taskRuns, + }; + return this.sendMessageToDaemon(message); + } + async isServerAvailable(): Promise { return new Promise((resolve) => { try { diff --git a/packages/nx/src/daemon/message-types/task-history.ts b/packages/nx/src/daemon/message-types/task-history.ts new file mode 100644 index 0000000000000..d940445a4c269 --- /dev/null +++ b/packages/nx/src/daemon/message-types/task-history.ts @@ -0,0 +1,38 @@ +import { TaskRun } from '../../utils/task-history'; + +export const GET_TASK_HISTORY_FOR_HASHES = + 'GET_TASK_HISTORY_FOR_HASHES' as const; + +export type HandleGetTaskHistoryForHashesMessage = { + type: typeof GET_TASK_HISTORY_FOR_HASHES; + hashes: string[]; +}; + +export function isHandleGetTaskHistoryForHashesMessage( + message: unknown +): message is HandleGetTaskHistoryForHashesMessage { + return ( + typeof message === 'object' && + message !== null && + 'type' in message && + message['type'] === GET_TASK_HISTORY_FOR_HASHES + ); +} + +export const WRITE_TASK_RUNS_TO_HISTORY = 'WRITE_TASK_RUNS_TO_HISTORY' as const; + +export type HandleWriteTaskRunsToHistoryMessage = { + type: typeof WRITE_TASK_RUNS_TO_HISTORY; + taskRuns: TaskRun[]; +}; + +export function isHandleWriteTaskRunsToHistoryMessage( + message: unknown +): message is HandleWriteTaskRunsToHistoryMessage { + return ( + typeof message === 'object' && + message !== null && + 'type' in message && + message['type'] === WRITE_TASK_RUNS_TO_HISTORY + ); +} diff --git a/packages/nx/src/daemon/server/handle-get-task-history.ts b/packages/nx/src/daemon/server/handle-get-task-history.ts new file mode 100644 index 0000000000000..66d3903445330 --- /dev/null +++ b/packages/nx/src/daemon/server/handle-get-task-history.ts @@ -0,0 +1,9 @@ +import { getHistoryForHashes } from '../../utils/task-history'; + +export async function handleGetTaskHistoryForHashes(hashes: string[]) { + const history = await getHistoryForHashes(hashes); + return { + response: JSON.stringify(history), + description: 'handleGetTaskHistoryForHashes', + }; +} diff --git a/packages/nx/src/daemon/server/handle-write-task-runs-to-history.ts b/packages/nx/src/daemon/server/handle-write-task-runs-to-history.ts new file mode 100644 index 0000000000000..b585750d84ce7 --- /dev/null +++ b/packages/nx/src/daemon/server/handle-write-task-runs-to-history.ts @@ -0,0 +1,9 @@ +import { TaskRun, writeTaskRunsToHistory } from '../../utils/task-history'; + +export async function handleWriteTaskRunsToHistory(taskRuns: TaskRun[]) { + await writeTaskRunsToHistory(taskRuns); + return { + response: 'true', + description: 'handleWriteTaskRunsToHistory', + }; +} diff --git a/packages/nx/src/daemon/server/server.ts b/packages/nx/src/daemon/server/server.ts index dcd51bc464aa2..33b709511119a 100644 --- a/packages/nx/src/daemon/server/server.ts +++ b/packages/nx/src/daemon/server/server.ts @@ -70,6 +70,12 @@ import { import { handleGetFilesInDirectory } from './handle-get-files-in-directory'; import { HASH_GLOB, isHandleHashGlobMessage } from '../message-types/hash-glob'; import { handleHashGlob } from './handle-hash-glob'; +import { + isHandleGetTaskHistoryForHashesMessage, + isHandleWriteTaskRunsToHistoryMessage, +} from '../message-types/task-history'; +import { handleGetTaskHistoryForHashes } from './handle-get-task-history'; +import { handleWriteTaskRunsToHistory } from './handle-write-task-runs-to-history'; let performanceObserver: PerformanceObserver | undefined; let workspaceWatcherError: Error | undefined; @@ -202,6 +208,14 @@ async function handleMessage(socket, data: string) { await handleResult(socket, HASH_GLOB, () => handleHashGlob(payload.globs, payload.exclude) ); + } else if (isHandleGetTaskHistoryForHashesMessage(payload)) { + await handleResult(socket, 'GET_TASK_HISTORY_FOR_HASHES', () => + handleGetTaskHistoryForHashes(payload.hashes) + ); + } else if (isHandleWriteTaskRunsToHistoryMessage(payload)) { + await handleResult(socket, 'WRITE_TASK_RUNS_TO_HISTORY', () => + handleWriteTaskRunsToHistory(payload.taskRuns) + ); } else { await respondWithErrorAndExit( socket, diff --git a/packages/nx/src/tasks-runner/default-tasks-runner.ts b/packages/nx/src/tasks-runner/default-tasks-runner.ts index a5cb33bfd7473..eeb4a8f4183d1 100644 --- a/packages/nx/src/tasks-runner/default-tasks-runner.ts +++ b/packages/nx/src/tasks-runner/default-tasks-runner.ts @@ -56,11 +56,11 @@ export const defaultTasksRunner: TasksRunner< (options as any)['parallel'] = Number((options as any)['maxParallel'] || 3); } - options.lifeCycle.startCommand(); + await options.lifeCycle.startCommand(); try { return await runAllTasks(tasks, options, context); } finally { - options.lifeCycle.endCommand(); + await options.lifeCycle.endCommand(); } }; diff --git a/packages/nx/src/tasks-runner/life-cycle.ts b/packages/nx/src/tasks-runner/life-cycle.ts index 1192a73415c16..2bce065d3e93f 100644 --- a/packages/nx/src/tasks-runner/life-cycle.ts +++ b/packages/nx/src/tasks-runner/life-cycle.ts @@ -13,11 +13,11 @@ export interface TaskMetadata { } export interface LifeCycle { - startCommand?(): void; + startCommand?(): void | Promise; - endCommand?(): void; + endCommand?(): void | Promise; - scheduleTask?(task: Task): void; + scheduleTask?(task: Task): void | Promise; /** * @deprecated use startTasks @@ -33,9 +33,12 @@ export interface LifeCycle { */ endTask?(task: Task, code: number): void; - startTasks?(task: Task[], metadata: TaskMetadata): void; + startTasks?(task: Task[], metadata: TaskMetadata): void | Promise; - endTasks?(taskResults: TaskResult[], metadata: TaskMetadata): void; + endTasks?( + taskResults: TaskResult[], + metadata: TaskMetadata + ): void | Promise; printTaskTerminalOutput?( task: Task, @@ -47,26 +50,26 @@ export interface LifeCycle { export class CompositeLifeCycle implements LifeCycle { constructor(private readonly lifeCycles: LifeCycle[]) {} - startCommand(): void { + async startCommand(): Promise { for (let l of this.lifeCycles) { if (l.startCommand) { - l.startCommand(); + await l.startCommand(); } } } - endCommand(): void { + async endCommand(): Promise { for (let l of this.lifeCycles) { if (l.endCommand) { - l.endCommand(); + await l.endCommand(); } } } - scheduleTask(task: Task): void { + async scheduleTask(task: Task): Promise { for (let l of this.lifeCycles) { if (l.scheduleTask) { - l.scheduleTask(task); + await l.scheduleTask(task); } } } @@ -87,20 +90,23 @@ export class CompositeLifeCycle implements LifeCycle { } } - startTasks(tasks: Task[], metadata: TaskMetadata): void { + async startTasks(tasks: Task[], metadata: TaskMetadata): Promise { for (let l of this.lifeCycles) { if (l.startTasks) { - l.startTasks(tasks, metadata); + await l.startTasks(tasks, metadata); } else if (l.startTask) { tasks.forEach((t) => l.startTask(t)); } } } - endTasks(taskResults: TaskResult[], metadata: TaskMetadata): void { + async endTasks( + taskResults: TaskResult[], + metadata: TaskMetadata + ): Promise { for (let l of this.lifeCycles) { if (l.endTasks) { - l.endTasks(taskResults, metadata); + await l.endTasks(taskResults, metadata); } else if (l.endTask) { taskResults.forEach((t) => l.endTask(t.task, t.code)); } diff --git a/packages/nx/src/tasks-runner/life-cycles/task-history-life-cycle.ts b/packages/nx/src/tasks-runner/life-cycles/task-history-life-cycle.ts new file mode 100644 index 0000000000000..c34074d9b4820 --- /dev/null +++ b/packages/nx/src/tasks-runner/life-cycles/task-history-life-cycle.ts @@ -0,0 +1,56 @@ +import { Task } from '../../config/task-graph'; +import { output } from '../../utils/output'; +import { + getHistoryForHashes, + TaskRun, + writeTaskRunsToHistory as writeTaskRunsToHistory, +} from '../../utils/task-history'; +import { LifeCycle, TaskResult } from '../life-cycle'; + +export class TaskHistoryLifeCycle implements LifeCycle { + private startTimings: Record = {}; + private taskRuns: TaskRun[] = []; + + startTasks(tasks: Task[]): void { + for (let task of tasks) { + this.startTimings[task.id] = new Date().getTime(); + } + } + + async endTasks(taskResults: TaskResult[]) { + const taskRuns: TaskRun[] = taskResults.map((taskResult) => ({ + project: taskResult.task.target.project, + target: taskResult.task.target.target, + configuration: taskResult.task.target.configuration, + hash: taskResult.task.hash, + code: taskResult.code, + status: taskResult.status, + start: taskResult.task.startTime ?? this.startTimings[taskResult.task.id], + end: taskResult.task.endTime ?? new Date().getTime(), + })); + this.taskRuns.push(...taskRuns); + } + + async endCommand() { + await writeTaskRunsToHistory(this.taskRuns); + const history = await getHistoryForHashes(this.taskRuns.map((t) => t.hash)); + const flakyTasks: string[] = []; + + // check if any hash has different exit codes => flaky + for (let hash in history) { + if ( + history[hash].length > 1 && + history[hash].some((run) => run.code !== history[hash][0].code) + ) { + flakyTasks.push( + `${history[hash][0].project}:${history[hash][0].target}` + ); + } + } + if (flakyTasks.length > 0) { + output.warn({ + title: `Flaky tasks detected: ${flakyTasks.join(', ')}`, + }); + } + } +} diff --git a/packages/nx/src/tasks-runner/run-command.ts b/packages/nx/src/tasks-runner/run-command.ts index 3b4e05e14e66c..ff32a79bc5088 100644 --- a/packages/nx/src/tasks-runner/run-command.ts +++ b/packages/nx/src/tasks-runner/run-command.ts @@ -28,6 +28,7 @@ import { hashTasksThatDoNotDependOnOutputsOfOtherTasks } from '../hasher/hash-ta 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'; async function getTerminalOutputLifeCycle( initiatingProject: string, @@ -325,6 +326,7 @@ function constructLifeCycles(lifeCycle: LifeCycle) { if (process.env.NX_PROFILE) { lifeCycles.push(new TaskProfilingLifeCycle(process.env.NX_PROFILE)); } + lifeCycles.push(new TaskHistoryLifeCycle()); return lifeCycles; } diff --git a/packages/nx/src/tasks-runner/task-orchestrator.ts b/packages/nx/src/tasks-runner/task-orchestrator.ts index c68feac797665..fbe7295a01573 100644 --- a/packages/nx/src/tasks-runner/task-orchestrator.ts +++ b/packages/nx/src/tasks-runner/task-orchestrator.ts @@ -159,7 +159,7 @@ export class TaskOrchestrator { ); } - this.options.lifeCycle.scheduleTask(task); + await this.options.lifeCycle.scheduleTask(task); return taskSpecificEnv; } @@ -176,7 +176,7 @@ export class TaskOrchestrator { this.batchEnv ); } - this.options.lifeCycle.scheduleTask(task); + await this.options.lifeCycle.scheduleTask(task); }) ); } @@ -520,7 +520,7 @@ export class TaskOrchestrator { // region Lifecycle private async preRunSteps(tasks: Task[], metadata: TaskMetadata) { - this.options.lifeCycle.startTasks(tasks, metadata); + await this.options.lifeCycle.startTasks(tasks, metadata); } private async postRunSteps( @@ -573,7 +573,7 @@ export class TaskOrchestrator { 'cache-results-end' ); } - this.options.lifeCycle.endTasks( + await this.options.lifeCycle.endTasks( results.map((result) => { const code = result.status === 'success' || diff --git a/packages/nx/src/utils/task-history.ts b/packages/nx/src/utils/task-history.ts new file mode 100644 index 0000000000000..392381d78c7a1 --- /dev/null +++ b/packages/nx/src/utils/task-history.ts @@ -0,0 +1,126 @@ +import { appendFileSync, existsSync, readFileSync, writeFileSync } from 'fs'; +import { join } from 'path'; +import { daemonClient } from '../daemon/client/client'; +import { isOnDaemon } from '../daemon/is-on-daemon'; +import { workspaceDataDirectory } from './cache-directory'; + +export interface TaskRun { + project: string; + target: string; + configuration: string; + hash: string; + code: number; + status: string; + start: number; + end: number; +} + +let taskHistory: TaskRun[] | undefined = undefined; +let taskHashToIndicesMap: Map = new Map(); + +export async function getHistoryForHashes(hashes: string[]): Promise<{ + [hash: string]: TaskRun[]; +}> { + if (isOnDaemon() || !daemonClient.enabled()) { + if (taskHistory === undefined) { + loadTaskHistoryFromDisk(); + } + + const result: { [hash: string]: TaskRun[] } = {}; + for (let hash of hashes) { + const indices = taskHashToIndicesMap.get(hash); + if (!indices) { + result[hash] = []; + } else { + result[hash] = indices.map((index) => taskHistory[index]); + } + } + + return result; + } + + return await daemonClient.getTaskHistoryForHashes(hashes); +} + +export async function writeTaskRunsToHistory( + taskRuns: TaskRun[] +): Promise { + if (isOnDaemon() || !daemonClient.enabled()) { + if (taskHistory === undefined) { + loadTaskHistoryFromDisk(); + } + + const serializedLines: string[] = []; + for (let taskRun of taskRuns) { + serializedLines.push( + [ + taskRun.project, + taskRun.target, + taskRun.configuration, + taskRun.hash, + taskRun.code, + taskRun.status, + taskRun.start, + taskRun.end, + ].join(',') + ); + recordTaskRunInMemory(taskRun); + } + + if (!existsSync(taskHistoryFile)) { + writeFileSync( + taskHistoryFile, + 'project,target,configuration,hash,code,status,start,end\n' + ); + } + appendFileSync(taskHistoryFile, serializedLines.join('\n') + '\n'); + } else { + await daemonClient.writeTaskRunsToHistory(taskRuns); + } +} + +export const taskHistoryFile = join(workspaceDataDirectory, 'task-history.csv'); + +function loadTaskHistoryFromDisk() { + taskHashToIndicesMap.clear(); + taskHistory = []; + + if (!existsSync(taskHistoryFile)) { + return; + } + + const fileContent = readFileSync(taskHistoryFile, 'utf8'); + if (!fileContent) { + return; + } + const lines = fileContent.split('\n'); + + // if there are no lines or just the header, return + if (lines.length <= 1) { + return; + } + + const headers = lines[0].trim().split(','); + const contentLines = lines.slice(1).filter((l) => l.trim() !== ''); + + // read the values from csv format where each header is a key and the value is the value + for (let line of contentLines) { + const values = line.trim().split(','); + + const run: any = {}; + headers.forEach((header, index) => { + run[header] = values[index]; + }); + + recordTaskRunInMemory(run as TaskRun); + } +} + +function recordTaskRunInMemory(taskRun: TaskRun) { + const index = taskHistory.push(taskRun) - 1; + if (taskHashToIndicesMap.has(taskRun.hash)) { + taskHashToIndicesMap.get(taskRun.hash).push(index); + } else { + taskHashToIndicesMap.set(taskRun.hash, [index]); + } +} From d40f3d29d3f402a9d37584b0faebc3094418b54a Mon Sep 17 00:00:00 2001 From: Max Kless Date: Fri, 21 Jun 2024 20:08:39 +0200 Subject: [PATCH 2/3] fix(core): review fixes --- .../life-cycles/task-history-life-cycle.ts | 22 +++++++-- packages/nx/src/tasks-runner/run-command.ts | 6 ++- packages/nx/src/utils/serialize-target.ts | 3 ++ packages/nx/src/utils/task-history.ts | 46 +++++++------------ .../workspace/src/utils/cli-config-utils.ts | 3 ++ 5 files changed, 45 insertions(+), 35 deletions(-) create mode 100644 packages/nx/src/utils/serialize-target.ts diff --git a/packages/nx/src/tasks-runner/life-cycles/task-history-life-cycle.ts b/packages/nx/src/tasks-runner/life-cycles/task-history-life-cycle.ts index c34074d9b4820..207c38681720d 100644 --- a/packages/nx/src/tasks-runner/life-cycles/task-history-life-cycle.ts +++ b/packages/nx/src/tasks-runner/life-cycles/task-history-life-cycle.ts @@ -1,3 +1,4 @@ +import { serializeTarget } from '../../utils/serialize-target'; import { Task } from '../../config/task-graph'; import { output } from '../../utils/output'; import { @@ -23,10 +24,12 @@ export class TaskHistoryLifeCycle implements LifeCycle { target: taskResult.task.target.target, configuration: taskResult.task.target.configuration, hash: taskResult.task.hash, - code: taskResult.code, + code: taskResult.code.toString(), status: taskResult.status, - start: taskResult.task.startTime ?? this.startTimings[taskResult.task.id], - end: taskResult.task.endTime ?? new Date().getTime(), + start: ( + taskResult.task.startTime ?? this.startTimings[taskResult.task.id] + ).toString(), + end: (taskResult.task.endTime ?? new Date().getTime()).toString(), })); this.taskRuns.push(...taskRuns); } @@ -43,13 +46,22 @@ export class TaskHistoryLifeCycle implements LifeCycle { history[hash].some((run) => run.code !== history[hash][0].code) ) { flakyTasks.push( - `${history[hash][0].project}:${history[hash][0].target}` + serializeTarget( + history[hash][0].project, + history[hash][0].target, + history[hash][0].configuration + ) ); } } if (flakyTasks.length > 0) { output.warn({ - title: `Flaky tasks detected: ${flakyTasks.join(', ')}`, + title: 'Flaky Tasks:', + bodyLines: [ + `The following targets produce inconsistent results based on the hash:`, + ...flakyTasks.map((t) => ` ${t}`), + `Nx Agents can automatically retry flaky tasks in CI. Learn more at https://nx.dev/ci/features/flaky-tasks`, + ], }); } } diff --git a/packages/nx/src/tasks-runner/run-command.ts b/packages/nx/src/tasks-runner/run-command.ts index ff32a79bc5088..06a22716e8ab7 100644 --- a/packages/nx/src/tasks-runner/run-command.ts +++ b/packages/nx/src/tasks-runner/run-command.ts @@ -16,6 +16,7 @@ import { createRunOneDynamicOutputRenderer } from './life-cycles/dynamic-run-one import { ProjectGraph, ProjectGraphProjectNode } from '../config/project-graph'; import { NxJsonConfiguration, + readNxJson, TargetDefaults, TargetDependencies, } from '../config/nx-json'; @@ -29,6 +30,7 @@ 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 { isNxCloudUsed } from '../utils/nx-cloud-utils'; async function getTerminalOutputLifeCycle( initiatingProject: string, @@ -326,7 +328,9 @@ function constructLifeCycles(lifeCycle: LifeCycle) { if (process.env.NX_PROFILE) { lifeCycles.push(new TaskProfilingLifeCycle(process.env.NX_PROFILE)); } - lifeCycles.push(new TaskHistoryLifeCycle()); + if (!isNxCloudUsed(readNxJson())) { + lifeCycles.push(new TaskHistoryLifeCycle()); + } return lifeCycles; } diff --git a/packages/nx/src/utils/serialize-target.ts b/packages/nx/src/utils/serialize-target.ts new file mode 100644 index 0000000000000..fdc305f34dcec --- /dev/null +++ b/packages/nx/src/utils/serialize-target.ts @@ -0,0 +1,3 @@ +export function serializeTarget(project, target, configuration) { + return [project, target, configuration].filter((part) => !!part).join(':'); +} diff --git a/packages/nx/src/utils/task-history.ts b/packages/nx/src/utils/task-history.ts index 392381d78c7a1..e290944dcf7c2 100644 --- a/packages/nx/src/utils/task-history.ts +++ b/packages/nx/src/utils/task-history.ts @@ -4,16 +4,18 @@ import { daemonClient } from '../daemon/client/client'; import { isOnDaemon } from '../daemon/is-on-daemon'; import { workspaceDataDirectory } from './cache-directory'; -export interface TaskRun { - project: string; - target: string; - configuration: string; - hash: string; - code: number; - status: string; - start: number; - end: number; -} +const taskRunKeys = [ + 'project', + 'target', + 'configuration', + 'hash', + 'code', + 'status', + 'start', + 'end', +] as const; + +export type TaskRun = Record<(typeof taskRunKeys)[number], string>; let taskHistory: TaskRun[] | undefined = undefined; let taskHashToIndicesMap: Map = new Map(); @@ -52,26 +54,13 @@ export async function writeTaskRunsToHistory( const serializedLines: string[] = []; for (let taskRun of taskRuns) { - serializedLines.push( - [ - taskRun.project, - taskRun.target, - taskRun.configuration, - taskRun.hash, - taskRun.code, - taskRun.status, - taskRun.start, - taskRun.end, - ].join(',') - ); + const serializedLine = taskRunKeys.map((key) => taskRun[key]).join(','); + serializedLines.push(serializedLine); recordTaskRunInMemory(taskRun); } if (!existsSync(taskHistoryFile)) { - writeFileSync( - taskHistoryFile, - 'project,target,configuration,hash,code,status,start,end\n' - ); + writeFileSync(taskHistoryFile, `${taskRunKeys.join(',')}\n`); } appendFileSync(taskHistoryFile, serializedLines.join('\n') + '\n'); } else { @@ -100,15 +89,14 @@ function loadTaskHistoryFromDisk() { return; } - const headers = lines[0].trim().split(','); const contentLines = lines.slice(1).filter((l) => l.trim() !== ''); // read the values from csv format where each header is a key and the value is the value for (let line of contentLines) { const values = line.trim().split(','); - const run: any = {}; - headers.forEach((header, index) => { + const run: Partial = {}; + taskRunKeys.forEach((header, index) => { run[header] = values[index]; }); diff --git a/packages/workspace/src/utils/cli-config-utils.ts b/packages/workspace/src/utils/cli-config-utils.ts index 1548dba7cbd8c..507d26ef5f8d7 100644 --- a/packages/workspace/src/utils/cli-config-utils.ts +++ b/packages/workspace/src/utils/cli-config-utils.ts @@ -22,6 +22,9 @@ export function editTarget(targetString: string, callback) { return serializeTarget(callback(parsedTarget)); } +/** + * @deprecated use the utility from nx/src/utils instead + */ export function serializeTarget({ project, target, config }) { return [project, target, config].filter((part) => !!part).join(':'); } From eca828d74a06f4771adb422c3b78e93fe6b284fa Mon Sep 17 00:00:00 2001 From: Max Kless Date: Fri, 21 Jun 2024 21:38:35 +0200 Subject: [PATCH 3/3] fix(core): adjust wording --- .../tasks-runner/life-cycles/task-history-life-cycle.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/nx/src/tasks-runner/life-cycles/task-history-life-cycle.ts b/packages/nx/src/tasks-runner/life-cycles/task-history-life-cycle.ts index 207c38681720d..6768354b90960 100644 --- a/packages/nx/src/tasks-runner/life-cycles/task-history-life-cycle.ts +++ b/packages/nx/src/tasks-runner/life-cycles/task-history-life-cycle.ts @@ -56,11 +56,14 @@ export class TaskHistoryLifeCycle implements LifeCycle { } if (flakyTasks.length > 0) { output.warn({ - title: 'Flaky Tasks:', + title: `Nx detected ${ + flakyTasks.length === 1 ? 'a flaky task' : ' flaky tasks' + }`, bodyLines: [ - `The following targets produce inconsistent results based on the hash:`, + , ...flakyTasks.map((t) => ` ${t}`), - `Nx Agents can automatically retry flaky tasks in CI. Learn more at https://nx.dev/ci/features/flaky-tasks`, + '', + `Flaky tasks can disrupt your CI pipeline. Automatically retry them with Nx Cloud. Learn more at https://nx.dev/ci/features/flaky-tasks`, ], }); }