|
| 1 | +using System; |
| 2 | +using System.Collections.Generic; |
| 3 | +using System.Text.RegularExpressions; |
| 4 | + |
| 5 | +using Cake.Core; |
| 6 | +using Cake.Core.Diagnostics; |
| 7 | +using Cake.Module.Shared; |
| 8 | + |
| 9 | +using JetBrains.Annotations; |
| 10 | + |
| 11 | +namespace Cake.GitLabCI.Module |
| 12 | +{ |
| 13 | + /// <summary> |
| 14 | + /// <see cref="ICakeEngine"/> implementation for GitLab CI. |
| 15 | + /// </summary> |
| 16 | + /// <remarks> |
| 17 | + /// This engine emits additional console output to make GitLab CI render the output of the indiviudal Cake tasks as collapsible sections |
| 18 | + /// (see <see href="https://docs.gitlab.com/ee/ci/yaml/script.html#custom-collapsible-sections">Custom collapsible sections (GitLab Docs)</see>). |
| 19 | + /// </remarks> |
| 20 | + [UsedImplicitly] |
| 21 | + public sealed class GitLabCIEngine : CakeEngineBase |
| 22 | + { |
| 23 | + private readonly IConsole _console; |
| 24 | + private readonly object _sectionNameLock = new object(); |
| 25 | + private readonly Dictionary<string, string> _taskSectionNames = new Dictionary<string, string>(); |
| 26 | + private readonly HashSet<string> _sectionNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase); |
| 27 | + |
| 28 | + /// <summary> |
| 29 | + /// Initializes a new instance of the <see cref="GitLabCIEngine"/> class. |
| 30 | + /// </summary> |
| 31 | + /// <param name="dataService">Implementation of <see cref="ICakeDataService"/>.</param> |
| 32 | + /// <param name="log">Implementation of <see cref="ICakeLog"/>.</param> |
| 33 | + /// <param name="console">Implementation of <see cref="IConsole"/>.</param> |
| 34 | + public GitLabCIEngine(ICakeDataService dataService, ICakeLog log, IConsole console) |
| 35 | + : base(new CakeEngine(dataService, log)) |
| 36 | + { |
| 37 | + _console = console; |
| 38 | + _engine.BeforeSetup += OnBeforeSetup; |
| 39 | + _engine.AfterSetup += OnAfterSetup; |
| 40 | + _engine.BeforeTaskSetup += OnBeforeTaskSetup; |
| 41 | + _engine.AfterTaskTeardown += OnAfterTaskTeardown; |
| 42 | + _engine.BeforeTeardown += OnBeforeTeardown; |
| 43 | + _engine.AfterTeardown += OnAfterTeardown; |
| 44 | + } |
| 45 | + |
| 46 | + private void OnBeforeSetup(object sender, BeforeSetupEventArgs e) |
| 47 | + { |
| 48 | + WriteSectionStart("setup", "Executing Setup"); |
| 49 | + } |
| 50 | + |
| 51 | + private void OnAfterSetup(object sender, AfterSetupEventArgs e) |
| 52 | + { |
| 53 | + WriteSectionEnd("setup"); |
| 54 | + } |
| 55 | + |
| 56 | + private void OnBeforeTaskSetup(object sender, BeforeTaskSetupEventArgs e) |
| 57 | + { |
| 58 | + WriteSectionStart(GetSectionNameForTask(e.TaskSetupContext.Task.Name), $"Executing task \"{e.TaskSetupContext.Task.Name}\""); |
| 59 | + } |
| 60 | + |
| 61 | + private void OnAfterTaskTeardown(object sender, AfterTaskTeardownEventArgs e) |
| 62 | + { |
| 63 | + WriteSectionEnd(GetSectionNameForTask(e.TaskTeardownContext.Task.Name)); |
| 64 | + } |
| 65 | + |
| 66 | + private void OnBeforeTeardown(object sender, BeforeTeardownEventArgs e) |
| 67 | + { |
| 68 | + WriteSectionStart("teardown", "Executing Teardown"); |
| 69 | + } |
| 70 | + |
| 71 | + private void OnAfterTeardown(object sender, AfterTeardownEventArgs e) |
| 72 | + { |
| 73 | + WriteSectionEnd("teardown"); |
| 74 | + } |
| 75 | + |
| 76 | + private void WriteSectionStart(string sectionName, string sectionHeader) |
| 77 | + { |
| 78 | + _console.WriteLine("{0}", $"{AnsiEscapeCodes.SectionMarker}section_start:{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}:{sectionName}\r{AnsiEscapeCodes.SectionMarker}{AnsiEscapeCodes.ForegroundBlue}{sectionHeader}{AnsiEscapeCodes.Reset}"); |
| 79 | + } |
| 80 | + |
| 81 | + private void WriteSectionEnd(string sectionName) |
| 82 | + { |
| 83 | + _console.WriteLine("{0}", $"{AnsiEscapeCodes.SectionMarker}section_end:{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}:{sectionName}\r{AnsiEscapeCodes.SectionMarker}"); |
| 84 | + } |
| 85 | + |
| 86 | + /// <summary> |
| 87 | + /// Computes a unique GitLab CI section name for a task name. |
| 88 | + /// </summary> |
| 89 | + /// <remarks> |
| 90 | + /// GitLab CI requires a section name in both the "start" and "end" markers of a section. |
| 91 | + /// The name can only be composed of letters, numbers, and the _, ., or - characters. |
| 92 | + /// In Cake, each task corresponds to one section. |
| 93 | + /// Since the task name may contain characters not allowed in the section name, unsupprted characters are removed from the task name. |
| 94 | + /// 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. |
| 95 | + /// </remarks> |
| 96 | + private string GetSectionNameForTask(string taskName) |
| 97 | + { |
| 98 | + lock (_sectionNameLock) |
| 99 | + { |
| 100 | + // If there is already a section name for the task, reuse the same name |
| 101 | + if (_taskSectionNames.TryGetValue(taskName, out var sectionName)) |
| 102 | + { |
| 103 | + return sectionName; |
| 104 | + } |
| 105 | + |
| 106 | + // Remove unsuported characters from the task name (everything except letters, numbers or the _, ., and - characters |
| 107 | + var normalizedTaskName = Regex.Replace(taskName, "[^A-Z|a-z|0-9|_|\\-|\\.]*", string.Empty).ToLowerInvariant(); |
| 108 | + |
| 109 | + // Normalizing the task name can cause multiple tasks to be mapped to the same section name |
| 110 | + // To avoid name conflicts, append a number to the end to make the section name unique. |
| 111 | + sectionName = normalizedTaskName; |
| 112 | + var sectionCounter = 0; |
| 113 | + while (!_sectionNames.Add(sectionName)) |
| 114 | + { |
| 115 | + sectionName = string.Concat(sectionName, "_", sectionCounter++); |
| 116 | + } |
| 117 | + |
| 118 | + // Save task name -> section name mapping for subsequent calls of GetSectionNameForTask() |
| 119 | + _taskSectionNames.Add(taskName, sectionName); |
| 120 | + return sectionName; |
| 121 | + } |
| 122 | + } |
| 123 | + } |
| 124 | +} |
0 commit comments