diff --git a/src/Cake.GitLabCI.Module/AnsiEscapeCodes.cs b/src/Cake.GitLabCI.Module/AnsiEscapeCodes.cs
index 14e975e9..7d722b89 100644
--- a/src/Cake.GitLabCI.Module/AnsiEscapeCodes.cs
+++ b/src/Cake.GitLabCI.Module/AnsiEscapeCodes.cs
@@ -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";
}
diff --git a/src/Cake.GitLabCI.Module/GitLabCIEngine.cs b/src/Cake.GitLabCI.Module/GitLabCIEngine.cs
new file mode 100644
index 00000000..f4782f08
--- /dev/null
+++ b/src/Cake.GitLabCI.Module/GitLabCIEngine.cs
@@ -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
+{
+ ///
+ /// implementation for GitLab CI.
+ ///
+ ///
+ /// This engine emits additional console output to make GitLab CI render the output of the indiviudal Cake tasks as collapsible sections
+ /// (see Custom collapsible sections (GitLab Docs)).
+ ///
+ [UsedImplicitly]
+ public sealed class GitLabCIEngine : CakeEngineBase
+ {
+ private readonly IConsole _console;
+ private readonly object _sectionNameLock = new object();
+ private readonly Dictionary _taskSectionNames = new Dictionary();
+ private readonly HashSet _sectionNames = new HashSet(StringComparer.OrdinalIgnoreCase);
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Implementation of .
+ /// Implementation of .
+ /// Implementation of .
+ 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}");
+ }
+
+ ///
+ /// Computes a unique GitLab CI section name for a task name.
+ ///
+ ///
+ /// 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.
+ ///
+ 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;
+ }
+ }
+ }
+}
diff --git a/src/Cake.GitLabCI.Module/GitLabCIModule.cs b/src/Cake.GitLabCI.Module/GitLabCIModule.cs
index 6f78824c..8c852b69 100644
--- a/src/Cake.GitLabCI.Module/GitLabCIModule.cs
+++ b/src/Cake.GitLabCI.Module/GitLabCIModule.cs
@@ -1,5 +1,6 @@
using System;
+using Cake.Core;
using Cake.Core.Annotations;
using Cake.Core.Composition;
using Cake.Core.Diagnostics;
@@ -19,6 +20,7 @@ public void Register(ICakeContainerRegistrar registrar)
if (StringComparer.OrdinalIgnoreCase.Equals(Environment.GetEnvironmentVariable("CI_SERVER"), "yes"))
{
registrar.RegisterType().As().Singleton();
+ registrar.RegisterType().As().Singleton();
}
}
}