From 9b097a0a56731621382f2b48138b0266b8884834 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Gr=C3=BCnwald?= Date: Fri, 30 Aug 2024 21:59:03 +0200 Subject: [PATCH 1/3] In GitLab CI, render Cake tasks as collapsible sections Extend module for GitLab CI with a custom CakeEngine that adds collapsible sections to the GitLab CI log (analogous to the groups in GitHubActionsEngine) For reference on the collapsible sections in GitLab CI, refer to https://docs.gitlab.com/ee/ci/yaml/script.html#custom-collapsible-sections --- src/Cake.GitLabCI.Module/AnsiEscapeCodes.cs | 1 + src/Cake.GitLabCI.Module/GitLabCIEngine.cs | 124 ++++++++++++++++++++ src/Cake.GitLabCI.Module/GitLabCIModule.cs | 2 + 3 files changed, 127 insertions(+) create mode 100644 src/Cake.GitLabCI.Module/GitLabCIEngine.cs diff --git a/src/Cake.GitLabCI.Module/AnsiEscapeCodes.cs b/src/Cake.GitLabCI.Module/AnsiEscapeCodes.cs index 14e975e9..1b3552a4 100644 --- a/src/Cake.GitLabCI.Module/AnsiEscapeCodes.cs +++ b/src/Cake.GitLabCI.Module/AnsiEscapeCodes.cs @@ -9,6 +9,7 @@ internal static class AnsiEscapeCodes public static readonly string ForegroundDarkGray = string.Format(FORMAT, 90); 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..45f1f347 --- /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"); + } + + private void OnAfterSetup(object sender, AfterSetupEventArgs e) + { + WriteSectionEnd("Setup"); + } + + private void OnBeforeTaskSetup(object sender, BeforeTaskSetupEventArgs e) + { + WriteSectionStart(GetSectionNameForTask(e.TaskSetupContext.Task.Name), 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"); + } + + private void OnAfterTeardown(object sender, AfterTeardownEventArgs e) + { + WriteSectionEnd("Teardown"); + } + + private void WriteSectionStart(string sectionName, string sectionHeader = null) + { + _console.WriteLine("{0}", $"{AnsiEscapeCodes.SectionMarker}section_start:{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}:{sectionName}\r{AnsiEscapeCodes.SectionMarker}{sectionHeader ?? sectionName}"); + } + + 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(); } } } From 3141f75171a13e538c6845a8afbb709862626023 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Gr=C3=BCnwald?= Date: Mon, 2 Sep 2024 20:07:28 +0200 Subject: [PATCH 2/3] Colorize the GitLab CI section header Highlight the section header in the GitLab CI log to make it easier to differentiate between the section header and the log output inside the section --- src/Cake.GitLabCI.Module/AnsiEscapeCodes.cs | 1 + src/Cake.GitLabCI.Module/GitLabCIEngine.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Cake.GitLabCI.Module/AnsiEscapeCodes.cs b/src/Cake.GitLabCI.Module/AnsiEscapeCodes.cs index 1b3552a4..7d722b89 100644 --- a/src/Cake.GitLabCI.Module/AnsiEscapeCodes.cs +++ b/src/Cake.GitLabCI.Module/AnsiEscapeCodes.cs @@ -7,6 +7,7 @@ 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"; diff --git a/src/Cake.GitLabCI.Module/GitLabCIEngine.cs b/src/Cake.GitLabCI.Module/GitLabCIEngine.cs index 45f1f347..305b206d 100644 --- a/src/Cake.GitLabCI.Module/GitLabCIEngine.cs +++ b/src/Cake.GitLabCI.Module/GitLabCIEngine.cs @@ -75,7 +75,7 @@ private void OnAfterTeardown(object sender, AfterTeardownEventArgs e) private void WriteSectionStart(string sectionName, string sectionHeader = null) { - _console.WriteLine("{0}", $"{AnsiEscapeCodes.SectionMarker}section_start:{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}:{sectionName}\r{AnsiEscapeCodes.SectionMarker}{sectionHeader ?? sectionName}"); + _console.WriteLine("{0}", $"{AnsiEscapeCodes.SectionMarker}section_start:{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}:{sectionName}\r{AnsiEscapeCodes.SectionMarker}{AnsiEscapeCodes.ForegroundBlue}{sectionHeader ?? sectionName}{AnsiEscapeCodes.Reset}"); } private void WriteSectionEnd(string sectionName) From 367377e74a957730401ff6f0549e070685d2a59b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Gr=C3=BCnwald?= Date: Mon, 2 Sep 2024 20:09:10 +0200 Subject: [PATCH 3/3] Make the section header more descriptive Instead of just using the task name as section header, use "Executing task TASKNAME". This makes the section headers more consistent with the section headers used by GitLab CI which uses e.g. "Executing "step_script" stage of the job script" --- src/Cake.GitLabCI.Module/GitLabCIEngine.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Cake.GitLabCI.Module/GitLabCIEngine.cs b/src/Cake.GitLabCI.Module/GitLabCIEngine.cs index 305b206d..f4782f08 100644 --- a/src/Cake.GitLabCI.Module/GitLabCIEngine.cs +++ b/src/Cake.GitLabCI.Module/GitLabCIEngine.cs @@ -45,17 +45,17 @@ public GitLabCIEngine(ICakeDataService dataService, ICakeLog log, IConsole conso private void OnBeforeSetup(object sender, BeforeSetupEventArgs e) { - WriteSectionStart("Setup"); + WriteSectionStart("setup", "Executing Setup"); } private void OnAfterSetup(object sender, AfterSetupEventArgs e) { - WriteSectionEnd("Setup"); + WriteSectionEnd("setup"); } private void OnBeforeTaskSetup(object sender, BeforeTaskSetupEventArgs e) { - WriteSectionStart(GetSectionNameForTask(e.TaskSetupContext.Task.Name), e.TaskSetupContext.Task.Name); + WriteSectionStart(GetSectionNameForTask(e.TaskSetupContext.Task.Name), $"Executing task \"{e.TaskSetupContext.Task.Name}\""); } private void OnAfterTaskTeardown(object sender, AfterTaskTeardownEventArgs e) @@ -65,17 +65,17 @@ private void OnAfterTaskTeardown(object sender, AfterTaskTeardownEventArgs e) private void OnBeforeTeardown(object sender, BeforeTeardownEventArgs e) { - WriteSectionStart("Teardown"); + WriteSectionStart("teardown", "Executing Teardown"); } private void OnAfterTeardown(object sender, AfterTeardownEventArgs e) { - WriteSectionEnd("Teardown"); + WriteSectionEnd("teardown"); } - private void WriteSectionStart(string sectionName, string sectionHeader = null) + private void WriteSectionStart(string sectionName, string sectionHeader) { - _console.WriteLine("{0}", $"{AnsiEscapeCodes.SectionMarker}section_start:{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}:{sectionName}\r{AnsiEscapeCodes.SectionMarker}{AnsiEscapeCodes.ForegroundBlue}{sectionHeader ?? sectionName}{AnsiEscapeCodes.Reset}"); + _console.WriteLine("{0}", $"{AnsiEscapeCodes.SectionMarker}section_start:{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}:{sectionName}\r{AnsiEscapeCodes.SectionMarker}{AnsiEscapeCodes.ForegroundBlue}{sectionHeader}{AnsiEscapeCodes.Reset}"); } private void WriteSectionEnd(string sectionName)