Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GitHub actions summary markdown output #526

Merged
merged 26 commits into from
Jul 14, 2024
Merged
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
1 change: 1 addition & 0 deletions src/ModularPipelines.Build/Modules/RunUnitTestsModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public class RunUnitTestsModule : Module<CommandResult[]>
EnvironmentVariables = new Dictionary<string, string?>
{
["GITHUB_ACTIONS"] = null,
["GITHUB_STEP_SUMMARY"] = null,
},
Properties = new KeyValue[]
{
Expand Down
2 changes: 1 addition & 1 deletion src/ModularPipelines.Build/ReleaseNotes.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
null
- GitHub Actions Markdown Summary - Thanks to @MattParkerDev !
2 changes: 2 additions & 0 deletions src/ModularPipelines.GitHub/Extensions/GitHubExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Microsoft.Extensions.DependencyInjection.Extensions;
using ModularPipelines.Context;
using ModularPipelines.Engine;
using ModularPipelines.Extensions;

namespace ModularPipelines.GitHub.Extensions;

Expand All @@ -23,6 +24,7 @@ public static IServiceCollection RegisterGitHubContext(this IServiceCollection s
services.TryAddScoped<IGitHub, GitHub>();
services.TryAddScoped<IGitHubEnvironmentVariables, GitHubEnvironmentVariables>();
services.TryAddSingleton<IGitHubRepositoryInfo, GitHubRepositoryInfo>();
services.AddPipelineGlobalHooks<GitHubMarkdownSummaryGenerator>();
return services;
}

Expand Down
133 changes: 133 additions & 0 deletions src/ModularPipelines.GitHub/GitHubMarkdownSummaryGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
using ModularPipelines.Context;
using ModularPipelines.Enums;
using ModularPipelines.Interfaces;
using ModularPipelines.Models;

namespace ModularPipelines.GitHub;

internal class GitHubMarkdownSummaryGenerator : IPipelineGlobalHooks
{
public Task OnStartAsync(IPipelineHookContext pipelineContext)
{
return Task.CompletedTask;
}

public async Task OnEndAsync(IPipelineHookContext pipelineContext, PipelineSummary pipelineSummary)
{
var mermaid = await GenerateMermaidSummary(pipelineSummary);
var table = await GenerateTableSummary(pipelineSummary);

var stepSummaryVariable = pipelineContext.Environment.EnvironmentVariables.GetEnvironmentVariable("GITHUB_STEP_SUMMARY");

if (string.IsNullOrEmpty(stepSummaryVariable))
{
return;
}

await pipelineContext.FileSystem.GetFile(stepSummaryVariable).WriteAsync($"{mermaid}\n\n{table}");

Check warning on line 27 in src/ModularPipelines.GitHub/GitHubMarkdownSummaryGenerator.cs

View check run for this annotation

Codecov / codecov/patch

src/ModularPipelines.GitHub/GitHubMarkdownSummaryGenerator.cs#L27

Added line #L27 was not covered by tests
}

private async Task<string> GenerateMermaidSummary(PipelineSummary pipelineSummary)
{
var results = await pipelineSummary.GetModuleResultsAsync();

var stepStringList = results
.Where(x => x.ModuleDuration != TimeSpan.Zero)
.OrderBy(x => x.ModuleStart)
.ThenBy(s => s.ModuleEnd)
.Select(x => $"{x.ModuleName} :{AddCritIfFailed(x)} {x.ModuleStart:HH:mm:ss:fff}, {x.ModuleEnd:HH:mm:ss:fff}").ToList();

var text = $"""
```mermaid
---
config:
theme: base
themeVariables:
primaryColor: "#2E7D32"
primaryTextColor: "#fff"
primaryBorderColor: "#558B2F"
lineColor: "#FF8F00"
secondaryColor: "#1B5E20"
tertiaryColor: "#fff"
darkmode: "true"
titleColor: "#fff"
gantt:
leftPadding: 40
rightPadding: 120
---

gantt
dateFormat HH:mm:ss:SSS
title Run Summary
axisFormat %H:%M:%S

{string.Join("\n", stepStringList)}
```
""";

return text;
}

private async Task<string> GenerateTableSummary(PipelineSummary pipelineSummary)
{
var results = await pipelineSummary.GetModuleResultsAsync();

var stepStringList = results.OrderBy(x => x.ModuleEnd)
.ThenBy(s => s.ModuleStart)
.Select(module =>
{
var isSameDay = module.ModuleStart.Date == module.ModuleEnd.Date;

var (startTime, endTime, duration) = (module.ModuleStart, module.ModuleEnd, module.ModuleDuration);
var text = $"| {module.ModuleName} | {GetStatusString(module.ModuleStatus)} | {GetTime(startTime, isSameDay)} | {GetTime(endTime, isSameDay)} | {duration} |";
return text;
}
).ToList();

var isSameDay = pipelineSummary.Start.Date == pipelineSummary.End.Date;
var (globalStartTime, globalEndTime, globalDuration) = (pipelineSummary.Start, pipelineSummary.End, pipelineSummary.TotalDuration);
var pipelineStatusString = GetStatusString(pipelineSummary.Status);
var overallSummaryString = $"| **Total** | **{pipelineStatusString}** | **{GetTime(globalStartTime, isSameDay)}** | **{GetTime(globalEndTime, isSameDay)}** | **{globalDuration}** |";
var text = $"""
### Run Summary
| Step | Status | Start | End | Duration |
| --- | --- | --- | --- | --- |
{string.Join("\n", stepStringList)}
{overallSummaryString}
""";

return text;
}

private static string AddCritIfFailed(IModuleResult moduleResult)
{
return moduleResult.ModuleResultType is ModuleResultType.Failure
? "crit,"
: string.Empty;
}

private static string GetStatusString(Status status)
{
return status switch
{
Status.Successful or Status.UsedHistory => $$$"""${\textsf{\color{lightgreen}{{{status}}}}}$""",
Status.NotYetStarted or Status.IgnoredFailure or Status.Processing or Status.Skipped =>
$$$"""${\textsf{\color{orange}{{{status}}}}}$""",
Status.PipelineTerminated or Status.TimedOut or Status.Failed or Status.Unknown =>
$$$"""${\textsf{\color{red}{{{status}}}}}$""",
_ => throw new ArgumentOutOfRangeException(nameof(status), status, null),

Check warning on line 118 in src/ModularPipelines.GitHub/GitHubMarkdownSummaryGenerator.cs

View check run for this annotation

Codecov / codecov/patch

src/ModularPipelines.GitHub/GitHubMarkdownSummaryGenerator.cs#L118

Added line #L118 was not covered by tests
};
}

private static string GetTime(DateTimeOffset dateTimeOffset, bool isSameDay)
{
if (dateTimeOffset == DateTimeOffset.MinValue)
{
return string.Empty;

Check warning on line 126 in src/ModularPipelines.GitHub/GitHubMarkdownSummaryGenerator.cs

View check run for this annotation

Codecov / codecov/patch

src/ModularPipelines.GitHub/GitHubMarkdownSummaryGenerator.cs#L126

Added line #L126 was not covered by tests
}

return isSameDay
? dateTimeOffset.ToString("h:mm:ss tt")
: dateTimeOffset.ToString("yyyy/MM/dd h:mm:ss tt");
}
}
4 changes: 2 additions & 2 deletions src/ModularPipelines/Engine/DependencyPrinter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ private void Print(string value)
return;
}

Console.WriteLine();
_logger.LogInformation("\n");
_collapsableLogging.StartConsoleLogGroupDirectToConsole("Dependency Chains");
_logger.LogInformation("The following dependency chains have been detected:\r\n{Chain}", value);
_collapsableLogging.EndConsoleLogGroupDirectToConsole("Dependency Chains");
Console.WriteLine();
_logger.LogInformation("\n");
}

private void Append(StringBuilder stringBuilder, ModuleDependencyModel moduleDependencyModel, int dashCount, ISet<ModuleDependencyModel> alreadyPrinted)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,14 @@ public async Task Handle(Exception exception)

private bool IsPipelineCanceled(Exception exception)
{
return exception is TaskCanceledException or OperationCanceledException
return exception is TaskCanceledException or OperationCanceledException or ModuleTimeoutException
&& Context.EngineCancellationToken.IsCancelled;
}

private bool IsModuleTimedOutException(Exception exception)
{
return exception is ModuleTimeoutException or TaskCanceledException or OperationCanceledException
&& ModuleCancellationTokenSource.IsCancellationRequested
&& !Context.EngineCancellationToken.IsCancelled;
var isTimeoutExceed = Module.EndTime - Module.StartTime >= Module.Timeout;
return isTimeoutExceed && exception is ModuleTimeoutException or TaskCanceledException or OperationCanceledException;
}

private async Task SaveFailedResult(Exception exception)
Expand Down
4 changes: 1 addition & 3 deletions src/ModularPipelines/Engine/Executors/PipelineInitializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,7 @@ private async Task<OrganizedModules> InitializeInternal()

PrintEnvironmentVariables();

Console.WriteLine();
_logger.LogInformation("Detected Build System: {BuildSystem}", _buildSystemDetector.GetCurrentBuildSystem());
Console.WriteLine();
_logger.LogInformation("\nDetected Build System: {BuildSystem}\n", _buildSystemDetector.GetCurrentBuildSystem());

await _pipelineFileWriter.WritePipelineFiles();

Expand Down
4 changes: 1 addition & 3 deletions src/ModularPipelines/Engine/UnusedModuleDetector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ public void Log()
return;
}

Console.WriteLine();
_logger.LogWarning("Unregistered Modules: {Modules}", string.Join(Environment.NewLine, unregisteredModules));
Console.WriteLine();
_logger.LogWarning("\nUnregistered Modules: {Modules}\n", string.Join(Environment.NewLine, unregisteredModules));
}
}
6 changes: 3 additions & 3 deletions src/ModularPipelines/Models/ModuleResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,9 @@ protected ModuleResult(ModuleBase module)
{
Module = module;
ModuleName = module.GetType().Name;
ModuleDuration = module.Duration;
ModuleStart = module.StartTime;
ModuleEnd = module.EndTime;
ModuleStart = module.StartTime == DateTimeOffset.MinValue ? DateTimeOffset.Now : module.StartTime;
ModuleEnd = module.EndTime == DateTimeOffset.MinValue ? DateTimeOffset.Now : module.EndTime;
ModuleDuration = ModuleEnd - ModuleStart;
SkipDecision = module.SkipResult;
TypeDiscriminator = GetType().FullName!;
ModuleStatus = module.Status;
Expand Down
29 changes: 27 additions & 2 deletions src/ModularPipelines/Models/PipelineSummary.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text.Json.Serialization;
using EnumerableAsyncProcessor.Extensions;
using ModularPipelines.Enums;
using ModularPipelines.Extensions;
using ModularPipelines.Modules;
Expand Down Expand Up @@ -41,6 +42,13 @@ internal PipelineSummary(IReadOnlyList<ModuleBase> modules,
TotalDuration = totalDuration;
Start = start;
End = end;

// If the pipeline is errored, some modules could still be waiting or processing.
// But we're ending the pipeline so let's signal them to fail.
foreach (var moduleBase in modules)
{
moduleBase.TryCancel();
}
}

/// <summary>
Expand All @@ -57,8 +65,25 @@ public T GetModule<T>()
where T : ModuleBase
=> Modules.GetModule<T>();

public async Task<IReadOnlyList<IModuleResult>> GetModuleResultsAsync() =>
await Task.WhenAll(Modules.Select(x => x.GetModuleResult()));
public async Task<IReadOnlyList<IModuleResult>> GetModuleResultsAsync()
{
return await Modules.SelectAsync(async x =>
{
if (x.Status is Status.Processing or Status.Unknown or Status.NotYetStarted)
{
return new ModuleResult(new TaskCanceledException(), x);
}

try
{
return await x.GetModuleResult();
}
catch (Exception e)
{
return new ModuleResult(e, x);
}
}).ProcessInParallel();
}

private Status GetStatus()
{
Expand Down
12 changes: 10 additions & 2 deletions src/ModularPipelines/Modules/ModuleBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Text.Json.Serialization;
using ModularPipelines.Attributes;
using ModularPipelines.Context;
using ModularPipelines.Engine;
using ModularPipelines.Engine.Executors.ModuleHandlers;
using ModularPipelines.Enums;
using ModularPipelines.Exceptions;
Expand Down Expand Up @@ -47,7 +48,9 @@ protected ModuleBase()
internal abstract IStatusHandler StatusHandler { get; }

internal abstract IErrorHandler ErrorHandler { get; }


internal abstract void TryCancel();

private IPipelineContext? _context; // Late Initialisation

/// <summary>
Expand Down Expand Up @@ -176,7 +179,7 @@ protected async Task SubModule(string name, Func<Task> action)
await submodule.Task;
}

protected EventHandler? OnInitialised { get; set; }
protected EventHandler? OnInitialised { get; set; }
}

/// <summary>
Expand All @@ -187,6 +190,11 @@ public abstract class ModuleBase<T> : ModuleBase
{
internal readonly TaskCompletionSource<ModuleResult<T>> ModuleResultTaskCompletionSource = new();

internal override void TryCancel()
{
ModuleResultTaskCompletionSource.TrySetCanceled();
}

internal abstract IHistoryHandler<T> HistoryHandler { get; }

/// <summary>
Expand Down
3 changes: 2 additions & 1 deletion test/ModularPipelines.TestHelpers/TestPipelineHostBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ public static PipelineHostBuilder Create(TestHostSettings testHostSettings)
opt.DefaultCommandLogging = testHostSettings.CommandLogging;
opt.ShowProgressInConsole = false;
opt.PrintResults = false;
opt.PrintLogo = false;
});

if(testHostSettings.ClearLogProviders)
if (testHostSettings.ClearLogProviders)
{
collection.AddLogging(builder => builder.ClearProviders());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,10 @@ public class GitRepoModule : Module<IGitHubRepositoryInfo>
[Test]
public async Task GitHub_Repository_Information_Is_Populated()
{
var gitRepoModule = await RunModule<GitRepoModule>(new TestHostSettings
{
CommandLogging = CommandLogging.Default,
LogLevel = LogLevel.Debug,
ClearLogProviders = false
});
var gitRepoModule = await RunModule<GitRepoModule>();

var gitHubRepositoryInfo = gitRepoModule.Result.Value!;

Console.WriteLine($"GitHub Repository Info is: {gitHubRepositoryInfo}");

await using (Assert.Multiple())
{
await Assert.That(gitHubRepositoryInfo).Is.Not.Null();
Expand Down
Loading