Skip to content

Commit

Permalink
Merge pull request #239 from ap0llo/gitlab-ci-log-sections
Browse files Browse the repository at this point in the history
In GitLab CI, render Cake tasks as collapsible sections
  • Loading branch information
nils-a authored Sep 30, 2024
2 parents 5205aa5 + 367377e commit 58752b8
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/Cake.GitLabCI.Module/AnsiEscapeCodes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ internal static class AnsiEscapeCodes
public static readonly string ForegroundYellow = string.Format(FORMAT, 33);
public static readonly string ForegroundLightGray = string.Format(FORMAT, 37);
public static readonly string ForegroundDarkGray = string.Format(FORMAT, 90);
public static readonly string ForegroundBlue = string.Format(FORMAT, 34);
public static readonly string BackgroundMagenta = string.Format(FORMAT, 45);
public static readonly string BackgroundRed = string.Format(FORMAT, 41);
public static readonly string SectionMarker = "\u001B[0K";

private const string FORMAT = "\u001B[{0}m";
}
Expand Down
124 changes: 124 additions & 0 deletions src/Cake.GitLabCI.Module/GitLabCIEngine.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;

using Cake.Core;
using Cake.Core.Diagnostics;
using Cake.Module.Shared;

using JetBrains.Annotations;

namespace Cake.GitLabCI.Module
{
/// <summary>
/// <see cref="ICakeEngine"/> implementation for GitLab CI.
/// </summary>
/// <remarks>
/// This engine emits additional console output to make GitLab CI render the output of the indiviudal Cake tasks as collapsible sections
/// (see <see href="https://docs.gitlab.com/ee/ci/yaml/script.html#custom-collapsible-sections">Custom collapsible sections (GitLab Docs)</see>).
/// </remarks>
[UsedImplicitly]
public sealed class GitLabCIEngine : CakeEngineBase
{
private readonly IConsole _console;
private readonly object _sectionNameLock = new object();
private readonly Dictionary<string, string> _taskSectionNames = new Dictionary<string, string>();
private readonly HashSet<string> _sectionNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

/// <summary>
/// Initializes a new instance of the <see cref="GitLabCIEngine"/> class.
/// </summary>
/// <param name="dataService">Implementation of <see cref="ICakeDataService"/>.</param>
/// <param name="log">Implementation of <see cref="ICakeLog"/>.</param>
/// <param name="console">Implementation of <see cref="IConsole"/>.</param>
public GitLabCIEngine(ICakeDataService dataService, ICakeLog log, IConsole console)
: base(new CakeEngine(dataService, log))
{
_console = console;
_engine.BeforeSetup += OnBeforeSetup;
_engine.AfterSetup += OnAfterSetup;
_engine.BeforeTaskSetup += OnBeforeTaskSetup;
_engine.AfterTaskTeardown += OnAfterTaskTeardown;
_engine.BeforeTeardown += OnBeforeTeardown;
_engine.AfterTeardown += OnAfterTeardown;
}

private void OnBeforeSetup(object sender, BeforeSetupEventArgs e)
{
WriteSectionStart("setup", "Executing Setup");
}

private void OnAfterSetup(object sender, AfterSetupEventArgs e)
{
WriteSectionEnd("setup");
}

private void OnBeforeTaskSetup(object sender, BeforeTaskSetupEventArgs e)
{
WriteSectionStart(GetSectionNameForTask(e.TaskSetupContext.Task.Name), $"Executing task \"{e.TaskSetupContext.Task.Name}\"");
}

private void OnAfterTaskTeardown(object sender, AfterTaskTeardownEventArgs e)
{
WriteSectionEnd(GetSectionNameForTask(e.TaskTeardownContext.Task.Name));
}

private void OnBeforeTeardown(object sender, BeforeTeardownEventArgs e)
{
WriteSectionStart("teardown", "Executing Teardown");
}

private void OnAfterTeardown(object sender, AfterTeardownEventArgs e)
{
WriteSectionEnd("teardown");
}

private void WriteSectionStart(string sectionName, string sectionHeader)
{
_console.WriteLine("{0}", $"{AnsiEscapeCodes.SectionMarker}section_start:{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}:{sectionName}\r{AnsiEscapeCodes.SectionMarker}{AnsiEscapeCodes.ForegroundBlue}{sectionHeader}{AnsiEscapeCodes.Reset}");
}

private void WriteSectionEnd(string sectionName)
{
_console.WriteLine("{0}", $"{AnsiEscapeCodes.SectionMarker}section_end:{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}:{sectionName}\r{AnsiEscapeCodes.SectionMarker}");
}

/// <summary>
/// Computes a unique GitLab CI section name for a task name.
/// </summary>
/// <remarks>
/// GitLab CI requires a section name in both the "start" and "end" markers of a section.
/// The name can only be composed of letters, numbers, and the _, ., or - characters.
/// In Cake, each task corresponds to one section.
/// Since the task name may contain characters not allowed in the section name, unsupprted characters are removed from the task name.
/// Additionally, this method ensures that the section name is unique and the same task name will be mapped to the same section name for each call.
/// </remarks>
private string GetSectionNameForTask(string taskName)
{
lock (_sectionNameLock)
{
// If there is already a section name for the task, reuse the same name
if (_taskSectionNames.TryGetValue(taskName, out var sectionName))
{
return sectionName;
}

// Remove unsuported characters from the task name (everything except letters, numbers or the _, ., and - characters
var normalizedTaskName = Regex.Replace(taskName, "[^A-Z|a-z|0-9|_|\\-|\\.]*", string.Empty).ToLowerInvariant();

// Normalizing the task name can cause multiple tasks to be mapped to the same section name
// To avoid name conflicts, append a number to the end to make the section name unique.
sectionName = normalizedTaskName;
var sectionCounter = 0;
while (!_sectionNames.Add(sectionName))
{
sectionName = string.Concat(sectionName, "_", sectionCounter++);
}

// Save task name -> section name mapping for subsequent calls of GetSectionNameForTask()
_taskSectionNames.Add(taskName, sectionName);
return sectionName;
}
}
}
}
2 changes: 2 additions & 0 deletions src/Cake.GitLabCI.Module/GitLabCIModule.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;

using Cake.Core;
using Cake.Core.Annotations;
using Cake.Core.Composition;
using Cake.Core.Diagnostics;
Expand All @@ -19,6 +20,7 @@ public void Register(ICakeContainerRegistrar registrar)
if (StringComparer.OrdinalIgnoreCase.Equals(Environment.GetEnvironmentVariable("CI_SERVER"), "yes"))
{
registrar.RegisterType<GitLabCILog>().As<ICakeLog>().Singleton();
registrar.RegisterType<GitLabCIEngine>().As<ICakeEngine>().Singleton();
}
}
}
Expand Down

0 comments on commit 58752b8

Please sign in to comment.