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(); } } }