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:
parent
e23b25fcaf
commit
2961bce152
@ -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`,
|
||||||
],
|
],
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 = () => {
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user