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

View File

@ -1,19 +1,50 @@
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 { serializeTarget } from '../../utils/serialize-target';
import { getTaskHistory, TaskHistory } from '../../utils/task-history';
import { isTuiEnabled } from '../is-tui-enabled';
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 {
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 {
private startTimings: Record<string, number> = {};
private taskRuns = new Map<string, TaskRun>();
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 {
for (let task of tasks) {
@ -43,21 +74,25 @@ export class TaskHistoryLifeCycle implements LifeCycle {
return;
}
await this.taskHistory.recordTaskRuns(entries.map(([_, v]) => v));
const flakyTasks = await this.taskHistory.getFlakyTasks(
this.flakyTasks = await this.taskHistory.getFlakyTasks(
entries.map(([hash]) => hash)
);
// Do not directly print output when using the TUI
if (isTuiEnabled()) {
return;
}
if (flakyTasks.length > 0) {
this.printFlakyTasksMessage();
}
printFlakyTasksMessage() {
if (this.flakyTasks.length > 0) {
output.warn({
title: `Nx detected ${
flakyTasks.length === 1 ? 'a flaky task' : ' flaky tasks'
this.flakyTasks.length === 1 ? 'a flaky task' : ' flaky tasks'
}`,
bodyLines: [
,
...flakyTasks.map((hash) => {
...this.flakyTasks.map((hash) => {
const taskRun = this.taskRuns.get(hash);
return ` ${serializeTarget(
taskRun.target.project,

View File

@ -8,6 +8,7 @@ import { formatFlags, formatTargetsAndProjects } from './formatting-utils';
import { prettyTime } from './pretty-time';
import { viewLogsFooterRows } from './view-logs-utils';
import figures = require('figures');
import { getTasksHistoryLifeCycle } from './task-history-life-cycle';
const LEFT_PAD = ` `;
const SPACER = ` `;
@ -112,6 +113,7 @@ export function getTuiTerminalSummaryLifeCycle({
} else {
printRunManySummary();
}
getTasksHistoryLifeCycle()?.printFlakyTasksMessage();
};
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 { 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 { LegacyTaskHistoryLifeCycle } from './life-cycles/task-history-life-cycle-old';
import { getTasksHistoryLifeCycle } from './life-cycles/task-history-life-cycle';
import { TaskProfilingLifeCycle } from './life-cycles/task-profiling-life-cycle';
import { TaskResultsLifeCycle } from './life-cycles/task-results-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) {
lifeCycles.push(new TaskProfilingLifeCycle(process.env.NX_PROFILE));
}
if (!isNxCloudUsed(readNxJson())) {
lifeCycles.push(
process.env.NX_DISABLE_DB !== 'true' && !IS_WASM
? new TaskHistoryLifeCycle()
: new LegacyTaskHistoryLifeCycle()
);
const historyLifeCycle = getTasksHistoryLifeCycle();
if (historyLifeCycle) {
lifeCycles.push(historyLifeCycle);
}
return lifeCycles;
}