Run reports and history
ModularPipelines can write a schema-versioned JSON report after every pipeline run. Reports contain pipeline and module statuses, timings, skip reasons, exception details, command counts, execution metrics, duration changes from the previous retained run, and correlation metadata.
Write a report
Configure an explicit output path on the pipeline builder:
using ModularPipelines.Extensions;
var builder = Pipeline.CreateBuilder(args);
builder.WriteRunReport("artifacts/run-report.json");
Known CI systems automatically write artifacts/run-report.json when no explicit path is set. To
disable that behavior while keeping an explicitly configured path available, set AutoWriteInCi:
builder.ConfigurePipelineOptions(options => options with
{
RunReport = options.RunReport with
{
AutoWriteInCi = false,
},
});
Relative report and history paths are resolved from the Git repository root when it is available. Outside a Git repository, they are resolved from the application base directory. This keeps the same storage location when a pipeline is launched from different working directories.
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-v3 report has a unique RunId, RunCorrelation metadata for the machine and detected
build system, and the previous run's finish time when it supplies a duration-delta baseline.
Registering the Git or GitHub integration also adds the available commit, branch, and CI run URL.
Correlation strings pass through secret obfuscation before persistence.
When report writing is enabled, add application-specific metadata through a bounded
IRunReportEnricher:
public sealed class DeploymentRunEnricher : IRunReportEnricher
{
public ValueTask EnrichAsync(
RunReportEnrichmentContext context,
CancellationToken cancellationToken)
{
context.GitBranch ??= "deployment";
return ValueTask.CompletedTask;
}
}
builder.AddRunReportEnricher<DeploymentRunEnricher>();
Enrichers run sequentially in registration order. Use ??= for fallback metadata so an earlier
value survives. Overwrite an existing value only when the current source is authoritative; later
authoritative enrichers take precedence. The built-in Git enricher fills gaps, while the GitHub
enricher replaces Git values with CI-provided commit and branch metadata when available.
Local history and deltas
By default, the IRunHistoryStore saves reports under .modularpipelines/run-history on local and
CI runs, even when JSON report writing is disabled. It retains the latest 20 reports and uses the
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. 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:
.modularpipelines/run-history/
Configure or disable retention with RunReportOptions:
builder.ConfigurePipelineOptions(options => options with
{
RunReport = options.RunReport with
{
HistoryDirectory = "artifacts/run-history",
HistoryRetention = 10, // Use 0 to disable history.
GlobalHistoryRetention = 100, // Use 0 for no global limit.
PipelineIdentity = "release-pipeline",
},
});
History is partitioned by pipeline identity and pruning only removes files owned by the built-in
history store. When PipelineIdentity is omitted, Modular Pipelines derives one from the registered
module types; changing only the report path does not fork history. After each save, the default store
applies the per-identity HistoryRetention limit, then keeps the newest GlobalHistoryRetention
reports across all identities. The global limit supersedes the per-identity limit: a quieter identity
can lose all of its history when newer reports from other identities fill the global pool. Set
GlobalHistoryRetention to 0 when every identity must retain its own history, or use stable
pipeline identities and separate history directories for independently bounded histories. A
positive global limit must be at least as large as HistoryRetention.
Report and history I/O failures are logged as warnings and do not replace a pipeline failure.
Report and history files are published atomically, so cancellation or a failed write cannot replace
a complete report with partial JSON. After each successful history save, the built-in store also
removes atomic-write temporary files older than 24 hours while leaving recent files for concurrent
writers.
Query retained reports newest-first through IRunHistoryStore:
await foreach (var failedRun in historyStore.GetRunsAsync(new RunHistoryQuery
{
PipelineIdentity = "release-pipeline",
MaxRuns = 10,
Since = DateTimeOffset.UtcNow.AddDays(-30),
Status = Status.Failed,
}, cancellationToken))
{
// Inspect failedRun.
}
Since is an inclusive cutoff on each run's start time. A run that started before the cutoff is
excluded even if it completed after the cutoff.
GetLatestAsync(pipelineIdentity, cancellationToken) remains available as an extension method over
GetRunsAsync. The registered IRunHistoryReader provides measured, attributable module-duration
samples from the latest runs:
var samples = await historyReader.GetModuleDurationTrendAsync(
moduleTypeName,
lastN: 10,
cancellationToken);
Configure RunReportOptions.PipelineIdentity before using IRunHistoryReader; the reader uses that
identity to select the current pipeline's retained history. Schema-v1 reports remain queryable, but
they have no run ID and are therefore omitted from duration trends.
CI agents are often ephemeral, so restore the history directory from a cache before running the pipeline. For example, a GitHub Actions workflow can restore the newest cache for its branch and save the updated history under a run-specific key:
- uses: actions/cache@v4
with:
path: .modularpipelines/run-history
key: ${{ runner.os }}-modularpipelines-history-${{ github.ref_name }}-${{ github.run_id }}-${{ github.run_attempt }}
restore-keys: |
${{ runner.os }}-modularpipelines-history-${{ github.ref_name }}-
Custom history stores
Implement IRunHistoryStore to keep reports in a database, object store, or another backend, then
register it on the builder:
builder.AddRunHistoryStore<MyRunHistoryStore>();
The store returns matching reports newest-first and saves the completed current report. Custom stores own their retention behavior.
In v4, custom stores implement GetRunsAsync(RunHistoryQuery, CancellationToken). The former
GetLatestAsync interface member is now an extension method, so stores need only implement the
query operation and SaveAsync.