Skip to content

Commit 1d27c32

Browse files
Copiloticlanton
andcommitted
Add --show-existing-failure-logs parameter and ShowExistingFailureLogsPlugin
Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com>
1 parent 94f7830 commit 1d27c32

2 files changed

Lines changed: 129 additions & 27 deletions

File tree

libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts

Lines changed: 55 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ export class PhasedScriptAction extends BaseScriptAction<IPhasedCommandConfig> i
170170
private readonly _nodeDiagnosticDirParameter: CommandLineStringParameter;
171171
private readonly _debugBuildCacheIdsParameter: CommandLineFlagParameter;
172172
private readonly _includePhaseDeps: CommandLineFlagParameter | undefined;
173+
private readonly _showExistingFailureLogsParameter: CommandLineFlagParameter;
173174

174175
public constructor(options: IPhasedScriptActionOptions) {
175176
super(options);
@@ -326,6 +327,14 @@ export class PhasedScriptAction extends BaseScriptAction<IPhasedCommandConfig> i
326327
'Logs information about the components of the build cache ids for individual operations. This is useful for debugging the incremental build logic.'
327328
});
328329

330+
this._showExistingFailureLogsParameter = this.defineFlagParameter({
331+
parameterLongName: '--show-existing-failure-logs',
332+
description:
333+
'Skips execution of operations and instead displays any existing failure logs for the selected projects. ' +
334+
'This is useful for reviewing failures from a previous run without re-executing the operations. ' +
335+
'Operations without existing failure logs will be silenced.'
336+
});
337+
329338
this.defineScriptParameters();
330339

331340
// Associate parameters with their respective phases
@@ -485,43 +494,62 @@ export class PhasedScriptAction extends BaseScriptAction<IPhasedCommandConfig> i
485494
}
486495

487496
const isWatch: boolean = this._watchParameter?.value || this._alwaysWatch;
497+
const showExistingFailureLogs: boolean = this._showExistingFailureLogsParameter.value;
498+
499+
// If showing existing failure logs, we don't want to enable watch mode
500+
if (showExistingFailureLogs && isWatch) {
501+
throw new Error('The --show-existing-failure-logs parameter cannot be used with --watch.');
502+
}
488503

489504
await measureAsyncFn(`${PERF_PREFIX}:applySituationalPlugins`, async () => {
490-
if (isWatch && this._noIPCParameter?.value === false) {
505+
if (showExistingFailureLogs) {
506+
// Apply the plugin that replays existing failure logs without executing operations
507+
terminal.writeVerboseLine(`Mode: showing existing failure logs`);
508+
const { ShowExistingFailureLogsPlugin } = await import(
509+
/* webpackChunkName: 'ShowExistingFailureLogsPlugin' */
510+
'../../logic/operations/ShowExistingFailureLogsPlugin'
511+
);
512+
new ShowExistingFailureLogsPlugin({
513+
terminal
514+
}).apply(this.hooks);
515+
} else if (isWatch && this._noIPCParameter?.value === false) {
491516
new (
492517
await import(
493518
/* webpackChunkName: 'IPCOperationRunnerPlugin' */ '../../logic/operations/IPCOperationRunnerPlugin'
494519
)
495520
).IPCOperationRunnerPlugin().apply(this.hooks);
496521
}
497522

498-
if (buildCacheConfiguration?.buildCacheEnabled) {
499-
terminal.writeVerboseLine(`Incremental strategy: cache restoration`);
500-
new CacheableOperationPlugin({
501-
allowWarningsInSuccessfulBuild:
502-
!!this.rushConfiguration.experimentsConfiguration.configuration
503-
.buildCacheWithAllowWarningsInSuccessfulBuild,
504-
buildCacheConfiguration,
505-
cobuildConfiguration,
506-
terminal
507-
}).apply(this.hooks);
508-
509-
if (this._debugBuildCacheIdsParameter.value) {
510-
new DebugHashesPlugin(terminal).apply(this.hooks);
523+
// Skip build cache and legacy skip plugins when showing existing failure logs
524+
if (!showExistingFailureLogs) {
525+
if (buildCacheConfiguration?.buildCacheEnabled) {
526+
terminal.writeVerboseLine(`Incremental strategy: cache restoration`);
527+
new CacheableOperationPlugin({
528+
allowWarningsInSuccessfulBuild:
529+
!!this.rushConfiguration.experimentsConfiguration.configuration
530+
.buildCacheWithAllowWarningsInSuccessfulBuild,
531+
buildCacheConfiguration,
532+
cobuildConfiguration,
533+
terminal
534+
}).apply(this.hooks);
535+
536+
if (this._debugBuildCacheIdsParameter.value) {
537+
new DebugHashesPlugin(terminal).apply(this.hooks);
538+
}
539+
} else if (!this._disableBuildCache) {
540+
terminal.writeVerboseLine(`Incremental strategy: output preservation`);
541+
// Explicitly disabling the build cache also disables legacy skip detection.
542+
new LegacySkipPlugin({
543+
allowWarningsInSuccessfulBuild:
544+
this.rushConfiguration.experimentsConfiguration.configuration
545+
.buildSkipWithAllowWarningsInSuccessfulBuild,
546+
terminal,
547+
changedProjectsOnly,
548+
isIncrementalBuildAllowed: this._isIncrementalBuildAllowed
549+
}).apply(this.hooks);
550+
} else {
551+
terminal.writeVerboseLine(`Incremental strategy: none (full rebuild)`);
511552
}
512-
} else if (!this._disableBuildCache) {
513-
terminal.writeVerboseLine(`Incremental strategy: output preservation`);
514-
// Explicitly disabling the build cache also disables legacy skip detection.
515-
new LegacySkipPlugin({
516-
allowWarningsInSuccessfulBuild:
517-
this.rushConfiguration.experimentsConfiguration.configuration
518-
.buildSkipWithAllowWarningsInSuccessfulBuild,
519-
terminal,
520-
changedProjectsOnly,
521-
isIncrementalBuildAllowed: this._isIncrementalBuildAllowed
522-
}).apply(this.hooks);
523-
} else {
524-
terminal.writeVerboseLine(`Incremental strategy: none (full rebuild)`);
525553
}
526554

527555
const showBuildPlan: boolean = this._cobuildPlanParameter?.value ?? false;
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
import { FileSystem } from '@rushstack/node-core-library';
5+
import type { ITerminal } from '@rushstack/terminal';
6+
7+
import { OperationStatus } from './OperationStatus';
8+
import type { IPhasedCommandPlugin, PhasedCommandHooks } from '../../pluginFramework/PhasedCommandHooks';
9+
import type { IOperationRunnerContext } from './IOperationRunner';
10+
import type { OperationExecutionRecord } from './OperationExecutionRecord';
11+
import { getProjectLogFilePaths } from './ProjectLogWritable';
12+
13+
const PLUGIN_NAME: 'ShowExistingFailureLogsPlugin' = 'ShowExistingFailureLogsPlugin';
14+
15+
export interface IShowExistingFailureLogsPluginOptions {
16+
terminal: ITerminal;
17+
}
18+
19+
/**
20+
* Plugin that replays existing failure logs without executing operations.
21+
* This is useful for reviewing failures from a previous run.
22+
*/
23+
export class ShowExistingFailureLogsPlugin implements IPhasedCommandPlugin {
24+
private readonly _options: IShowExistingFailureLogsPluginOptions;
25+
26+
public constructor(options: IShowExistingFailureLogsPluginOptions) {
27+
this._options = options;
28+
}
29+
30+
public apply(hooks: PhasedCommandHooks): void {
31+
hooks.beforeExecuteOperation.tapPromise(
32+
PLUGIN_NAME,
33+
async (runnerContext: IOperationRunnerContext): Promise<OperationStatus | undefined> => {
34+
const record: OperationExecutionRecord = runnerContext as OperationExecutionRecord;
35+
const { operation, _operationMetadataManager: operationMetadataManager } = record;
36+
37+
const { associatedProject: project } = operation;
38+
39+
// Get the path to the error log file
40+
const { error: errorLogPath } = getProjectLogFilePaths({
41+
project,
42+
logFilenameIdentifier: operation.logFilenameIdentifier
43+
});
44+
45+
// Check if an error log exists from a previous run
46+
const errorLogExists: boolean = await FileSystem.existsAsync(errorLogPath);
47+
48+
if (errorLogExists) {
49+
// Replay the failure log
50+
await runnerContext.runWithTerminalAsync(
51+
async (taskTerminal, terminalProvider) => {
52+
// Restore the operation logs
53+
await operationMetadataManager?.tryRestoreAsync({
54+
terminalProvider,
55+
terminal: taskTerminal,
56+
errorLogPath,
57+
cobuildContextId: undefined,
58+
cobuildRunnerId: undefined
59+
});
60+
},
61+
{ createLogFile: false, logFileSuffix: '' }
62+
);
63+
64+
// Return Failure status to indicate this operation had previously failed
65+
return OperationStatus.Failure;
66+
} else {
67+
// No error log exists, so this operation either succeeded or wasn't run
68+
// Return Skipped to silence it
69+
return OperationStatus.Skipped;
70+
}
71+
}
72+
);
73+
}
74+
}

0 commit comments

Comments
 (0)