Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/docs/how-to/run-reports.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ builder.ConfigurePipelineOptions(options => options with

The current schema version is available as `PipelineRunReport.CurrentSchemaVersion`. The completed
report is also exposed through `PipelineSummary.RunReport`.
After a successful write, an information log records the report's resolved path.

Each schema-v2 report has a unique `RunId` plus `RunCorrelation` metadata for the machine and
detected build system. Registering the Git or GitHub integration also adds the available commit,
Expand Down Expand Up @@ -67,7 +68,8 @@ CI runs, even when JSON report writing is disabled. It retains the latest 20 rep
newest compatible report to calculate module and total-duration deltas. When a previous duration
exists, the final results table includes a `Δ previous` column. Deltas compare only successful
runs and successful module executions, so failed or timed-out durations do not create false
regressions on a later run.
regressions on a later run. A footer below the table identifies the baseline run by its UTC finish
time.

Add the default history directory to `.gitignore` if you do not want to commit local run data:

Expand Down
3 changes: 3 additions & 0 deletions src/ModularPipelines/Engine/PipelineRunReportFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ public PipelineRunReport Create(
&& previousReport?.Status == Status.Successful
? previousReport.TotalDuration
: null;
var hasDeltaBaseline = previousTotalDuration.HasValue
|| modules.Any(static module => module.DurationDelta.HasValue);

return new PipelineRunReport
{
Expand All @@ -83,6 +85,7 @@ public PipelineRunReport Create(
Start = summary.Start,
End = summary.End,
TotalDuration = summary.TotalDuration,
PreviousEnd = hasDeltaBaseline ? previousReport!.End : null,
PreviousTotalDuration = previousTotalDuration,
TotalDurationDelta = previousTotalDuration is null
? null
Expand Down
3 changes: 3 additions & 0 deletions src/ModularPipelines/Engine/RunReportService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,9 @@ await AtomicFileWriter.WriteAllTextAsync(
RunReportJsonSerializer.Serialize(report),
token)
.ConfigureAwait(false);
logger.LogInformation(
"Run report written to {RunReportPath}",
fullPath);
},
ReportWriteTimeout,
cancellationToken,
Expand Down
8 changes: 8 additions & 0 deletions src/ModularPipelines/Helpers/SpectreResultsPrinter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Microsoft.Extensions.Options;
using ModularPipelines.Engine;
using ModularPipelines.Extensions;
Expand Down Expand Up @@ -201,6 +202,13 @@ private static Table CreateModulesTableCore(PipelineSummary pipelineSummary)

AddTotalRow(table, pipelineSummary, showDeltas);

if (showDeltas && pipelineSummary.RunReport?.PreviousEnd is { } previousEnd)
{
var baseline = previousEnd.ToUniversalTime()
.ToString("yyyy-MM-dd HH:mm 'UTC'", CultureInfo.InvariantCulture);
table.Caption($"[dim]Δ vs run finished {baseline}[/]");
}

return table;
}

Expand Down
5 changes: 5 additions & 0 deletions src/ModularPipelines/Models/PipelineRunReport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ public sealed record PipelineRunReport
/// </summary>
public TimeSpan TotalDuration { get; init; }

/// <summary>
/// Gets when the previous retained run used as the delta baseline finished.
/// </summary>
public DateTimeOffset? PreviousEnd { get; init; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bump the run-report schema for PreviousEnd

Adding PreviousEnd changes the serialized, schema-versioned run-report JSON while CurrentSchemaVersion still remains at 2, so downstream consumers cannot distinguish earlier v2 reports that lack the baseline timestamp from new reports where the baseline is intentionally absent. Since the previous report-shape addition bumped this version and the history store uses it for compatibility decisions, please advance the schema version and update the related compatibility/docs expectations with this new field.

Useful? React with 👍 / 👎.


/// <summary>
/// Gets the previous retained run's total duration, when available.
/// </summary>
Expand Down
37 changes: 37 additions & 0 deletions test/ModularPipelines.UnitTests/Engine/RunReportTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ await Assert.That(firstReport.Modules.Single(module => module.ModuleTypeName ==
{
await Assert.That(secondSummary.RunReport!.PreviousTotalDuration).IsNull();
await Assert.That(secondReport!.TotalDurationDelta).IsNull();
await Assert.That(secondReport.PreviousEnd).IsEqualTo(firstReport!.End);
await Assert.That(secondReport.Modules
.Single(module => module.Status == Status.Successful)
.PreviousDuration)
Expand Down Expand Up @@ -303,6 +304,8 @@ public async Task RunHistoryPersistsAndCalculatesDeltasWithoutReportWriting()
{
await Assert.That(firstSummary.RunReport!.PreviousTotalDuration).IsNull();
await Assert.That(secondSummary.RunReport!.PreviousTotalDuration).IsNotNull();
await Assert.That(secondSummary.RunReport.PreviousEnd)
.IsEqualTo(firstSummary.RunReport.End);
await Assert.That(Directory.GetFiles(historyPath, "*.json")).Count().IsEqualTo(2);
}
}
Expand Down Expand Up @@ -1360,6 +1363,40 @@ private static PipelineOptions CreateReportingOptions(string reportPath) =>
},
};

[Test]
public async Task SuccessfulReportWriteLogsFullPath()
{
var directory = CreateTemporaryDirectory();
var reportPath = Path.Combine(directory, "artifacts", "run-report.json");
var log = new StringBuilder();
var distributedOptions = OptionsFactory.Create(new DistributedOptions());
var commandExecutionCounter = new CommandExecutionCounter();
var service = new RunReportService(
Mock.Of<IRunHistoryStore>(),
new PipelineRunReportFactory(
commandExecutionCounter,
new PassthroughSecretObfuscator()),
Mock.Of<IBuildSystemDetector>(),
OptionsFactory.Create(CreateReportingOptions(reportPath)),
distributedOptions,
new RoleDetector(distributedOptions),
Mock.Of<IDistributedCoordinator>(),
commandExecutionCounter,
new StringLogger<RunReportService>(log));

try
{
await service.CompleteAsync(CreateEmptySummary());

await Assert.That(log.ToString())
.Contains($"Run report written to {Path.GetFullPath(reportPath)}");
}
finally
{
Directory.Delete(directory, recursive: true);
}
}

[Test]
public async Task RunReportEnrichersPopulateObfuscatedCorrelation()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ public async Task ModulesTable_ShowsPreviousRunDurationDelta()
{
RunReport = new PipelineRunReport
{
PreviousEnd = new DateTimeOffset(2026, 7, 27, 11, 58, 0, TimeSpan.Zero),
TotalDurationDelta = TimeSpan.FromSeconds(2),
Modules =
[
Expand All @@ -115,6 +116,7 @@ public async Task ModulesTable_ShowsPreviousRunDurationDelta()

await Assert.That(output).Contains("Δ previous");
await Assert.That(output).Contains("+2s");
await Assert.That(output).Contains("Δ vs run finished 2026-07-27 11:58 UTC");
}

[Test]
Expand Down
Loading