feat(core): add flaky task detection to tui summary (#30835)

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
Flaky tasks are only shown when not using the tui

## Expected Behavior
Flaky tasks are printed at the end of the summary view
<img width="1053" alt="image"
src="https://github.com/user-attachments/assets/4b068a52-72c3-415e-af91-481c12bb3f12"
/>

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
This commit is contained in:
Craigory Coppola 2025-04-24 01:14:05 -04:00 committed by GitHub
parent e23b25fcaf
commit 2961bce152
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 56 additions and 18 deletions

View File

@ -12,6 +12,7 @@ import { LifeCycle, TaskResult } from '../life-cycle';
export class LegacyTaskHistoryLifeCycle implements LifeCycle { export class LegacyTaskHistoryLifeCycle implements LifeCycle {
private startTimings: Record<string, number> = {}; private startTimings: Record<string, number> = {};
private taskRuns: TaskRun[] = []; private taskRuns: TaskRun[] = [];
private flakyTasks: string[];
startTasks(tasks: Task[]): void { startTasks(tasks: Task[]): void {
for (let task of tasks) { for (let task of tasks) {
@ -38,7 +39,7 @@ export class LegacyTaskHistoryLifeCycle implements LifeCycle {
async endCommand() { async endCommand() {
await writeTaskRunsToHistory(this.taskRuns); await writeTaskRunsToHistory(this.taskRuns);
const history = await getHistoryForHashes(this.taskRuns.map((t) => t.hash)); const history = await getHistoryForHashes(this.taskRuns.map((t) => t.hash));
const flakyTasks: string[] = []; this.flakyTasks = [];
// check if any hash has different exit codes => flaky // check if any hash has different exit codes => flaky
for (let hash in history) { for (let hash in history) {
@ -46,7 +47,7 @@ export class LegacyTaskHistoryLifeCycle implements LifeCycle {
history[hash].length > 1 && history[hash].length > 1 &&
history[hash].some((run) => run.code !== history[hash][0].code) history[hash].some((run) => run.code !== history[hash][0].code)
) { ) {
flakyTasks.push( this.flakyTasks.push(
serializeTarget( serializeTarget(
history[hash][0].project, history[hash][0].project,
history[hash][0].target, history[hash][0].target,
@ -59,14 +60,18 @@ export class LegacyTaskHistoryLifeCycle implements LifeCycle {
if (isTuiEnabled()) { if (isTuiEnabled()) {
return; return;
} }
if (flakyTasks.length > 0) { this.printFlakyTasksMessage();
}
printFlakyTasksMessage() {
if (this.flakyTasks.length > 0) {
output.warn({ output.warn({
title: `Nx detected ${ title: `Nx detected ${
flakyTasks.length === 1 ? 'a flaky task' : ' flaky tasks' this.flakyTasks.length === 1 ? 'a flaky task' : ' flaky tasks'
}`, }`,
bodyLines: [ bodyLines: [
, ,
...flakyTasks.map((t) => ` ${t}`), ...this.flakyTasks.map((t) => ` ${t}`),
'', '',
`Flaky tasks can disrupt your CI pipeline. Automatically retry them with Nx Cloud. 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`,
], ],

View File

@ -1,19 +1,50 @@
import { Task } from '../../config/task-graph'; import { Task } from '../../config/task-graph';
import type { TaskRun as NativeTaskRun } from '../../native'; import { IS_WASM, type TaskRun as NativeTaskRun } from '../../native';
import { output } from '../../utils/output'; import { output } from '../../utils/output';
import { serializeTarget } from '../../utils/serialize-target'; import { serializeTarget } from '../../utils/serialize-target';
import { getTaskHistory, TaskHistory } from '../../utils/task-history'; import { getTaskHistory, TaskHistory } from '../../utils/task-history';
import { isTuiEnabled } from '../is-tui-enabled'; import { isTuiEnabled } from '../is-tui-enabled';
import { LifeCycle, TaskResult } from '../life-cycle'; import { LifeCycle, TaskResult } from '../life-cycle';
import { LegacyTaskHistoryLifeCycle } from './task-history-life-cycle-old';
import { isNxCloudUsed } from '../../utils/nx-cloud-utils';
import { readNxJson } from '../../config/nx-json';
interface TaskRun extends NativeTaskRun { interface TaskRun extends NativeTaskRun {
target: Task['target']; target: Task['target'];
} }
let tasksHistoryLifeCycle: TaskHistoryLifeCycle | LegacyTaskHistoryLifeCycle;
export function getTasksHistoryLifeCycle():
| TaskHistoryLifeCycle
| LegacyTaskHistoryLifeCycle
| null {
if (!isNxCloudUsed(readNxJson())) {
if (!tasksHistoryLifeCycle) {
tasksHistoryLifeCycle =
process.env.NX_DISABLE_DB !== 'true' && !IS_WASM
? new TaskHistoryLifeCycle()
: new LegacyTaskHistoryLifeCycle();
}
return tasksHistoryLifeCycle;
}
return null;
}
export class TaskHistoryLifeCycle implements LifeCycle { export class TaskHistoryLifeCycle implements LifeCycle {
private startTimings: Record<string, number> = {}; private startTimings: Record<string, number> = {};
private taskRuns = new Map<string, TaskRun>(); private taskRuns = new Map<string, TaskRun>();
private taskHistory: TaskHistory | null = getTaskHistory(); private taskHistory: TaskHistory | null = getTaskHistory();
private flakyTasks: string[];
constructor() {
if (tasksHistoryLifeCycle) {
throw new Error(
'TaskHistoryLifeCycle is a singleton and should not be instantiated multiple times'
);
}
tasksHistoryLifeCycle = this;
}
startTasks(tasks: Task[]): void { startTasks(tasks: Task[]): void {
for (let task of tasks) { for (let task of tasks) {
@ -43,21 +74,25 @@ export class TaskHistoryLifeCycle implements LifeCycle {
return; return;
} }
await this.taskHistory.recordTaskRuns(entries.map(([_, v]) => v)); await this.taskHistory.recordTaskRuns(entries.map(([_, v]) => v));
const flakyTasks = await this.taskHistory.getFlakyTasks( this.flakyTasks = await this.taskHistory.getFlakyTasks(
entries.map(([hash]) => hash) entries.map(([hash]) => hash)
); );
// Do not directly print output when using the TUI // Do not directly print output when using the TUI
if (isTuiEnabled()) { if (isTuiEnabled()) {
return; return;
} }
if (flakyTasks.length > 0) { this.printFlakyTasksMessage();
}
printFlakyTasksMessage() {
if (this.flakyTasks.length > 0) {
output.warn({ output.warn({
title: `Nx detected ${ title: `Nx detected ${
flakyTasks.length === 1 ? 'a flaky task' : ' flaky tasks' this.flakyTasks.length === 1 ? 'a flaky task' : ' flaky tasks'
}`, }`,
bodyLines: [ bodyLines: [
, ,
...flakyTasks.map((hash) => { ...this.flakyTasks.map((hash) => {
const taskRun = this.taskRuns.get(hash); const taskRun = this.taskRuns.get(hash);
return ` ${serializeTarget( return ` ${serializeTarget(
taskRun.target.project, taskRun.target.project,

View File

@ -8,6 +8,7 @@ import { formatFlags, formatTargetsAndProjects } from './formatting-utils';
import { prettyTime } from './pretty-time'; import { prettyTime } from './pretty-time';
import { viewLogsFooterRows } from './view-logs-utils'; import { viewLogsFooterRows } from './view-logs-utils';
import figures = require('figures'); import figures = require('figures');
import { getTasksHistoryLifeCycle } from './task-history-life-cycle';
const LEFT_PAD = ` `; const LEFT_PAD = ` `;
const SPACER = ` `; const SPACER = ` `;
@ -112,6 +113,7 @@ export function getTuiTerminalSummaryLifeCycle({
} else { } else {
printRunManySummary(); printRunManySummary();
} }
getTasksHistoryLifeCycle()?.printFlakyTasksMessage();
}; };
const printRunOneSummary = () => { const printRunOneSummary = () => {

View File

@ -53,8 +53,7 @@ import { createRunOneDynamicOutputRenderer } from './life-cycles/dynamic-run-one
import { StaticRunManyTerminalOutputLifeCycle } from './life-cycles/static-run-many-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 { StaticRunOneTerminalOutputLifeCycle } from './life-cycles/static-run-one-terminal-output-life-cycle';
import { StoreRunInformationLifeCycle } from './life-cycles/store-run-information-life-cycle'; import { StoreRunInformationLifeCycle } from './life-cycles/store-run-information-life-cycle';
import { TaskHistoryLifeCycle } from './life-cycles/task-history-life-cycle'; import { getTasksHistoryLifeCycle } 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 { TaskProfilingLifeCycle } from './life-cycles/task-profiling-life-cycle';
import { TaskResultsLifeCycle } from './life-cycles/task-results-life-cycle'; import { TaskResultsLifeCycle } from './life-cycles/task-results-life-cycle';
import { TaskTimingsLifeCycle } from './life-cycles/task-timings-life-cycle'; import { TaskTimingsLifeCycle } from './life-cycles/task-timings-life-cycle';
@ -961,12 +960,9 @@ export function constructLifeCycles(lifeCycle: LifeCycle): LifeCycle[] {
if (process.env.NX_PROFILE) { if (process.env.NX_PROFILE) {
lifeCycles.push(new TaskProfilingLifeCycle(process.env.NX_PROFILE)); lifeCycles.push(new TaskProfilingLifeCycle(process.env.NX_PROFILE));
} }
if (!isNxCloudUsed(readNxJson())) { const historyLifeCycle = getTasksHistoryLifeCycle();
lifeCycles.push( if (historyLifeCycle) {
process.env.NX_DISABLE_DB !== 'true' && !IS_WASM lifeCycles.push(historyLifeCycle);
? new TaskHistoryLifeCycle()
: new LegacyTaskHistoryLifeCycle()
);
} }
return lifeCycles; return lifeCycles;
} }