From 2819e3e9cdef63ab28c117eafc37145d45c0d12c Mon Sep 17 00:00:00 2001 From: Jan Staelens Date: Mon, 22 Jul 2024 13:51:50 +0200 Subject: [PATCH 1/3] Initial Development --- ...MSTeamsWorkflowWebhookCard.LibTests.csproj | 28 +++ .../CicdCardTests.cs | 82 +++++++ .../MatchTeamsRequirement/ExpectedJson.cs | 217 ++++++++++++++++++ .../TeamsAdaptiveCardMatcher.cs | 132 +++++++++++ ...CD.Tools.MSTeamsWorkflowWebhookCard.csproj | 42 ++++ .../LICENSE.txt | 22 ++ .../Program.cs | 121 ++++++++++ .../README.md | 29 +++ .../nuget/Icon.png | Bin 0 -> 20877 bytes LICENSE | 22 ++ README.md | 27 +++ ...ools.MSTeamsWorkflowWebhookCard.Lib.csproj | 34 +++ .../CicdCard.cs | 214 +++++++++++++++++ .../CicdResult.cs | 28 +++ .../LICENSE.txt | 22 ++ .../README.md | 23 ++ .../nuget/Icon.png | Bin 0 -> 20877 bytes ....CICD.Tools.MSTeamsWorkflowWebhookCard.sln | 43 ++++ 18 files changed, 1086 insertions(+) create mode 100644 CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests.csproj create mode 100644 CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/CicdCardTests.cs create mode 100644 CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/MatchTeamsRequirement/ExpectedJson.cs create mode 100644 CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/MatchTeamsRequirement/TeamsAdaptiveCardMatcher.cs create mode 100644 CICD.Tools.MSTeamsWorkflowWebhookCard/CICD.Tools.MSTeamsWorkflowWebhookCard.csproj create mode 100644 CICD.Tools.MSTeamsWorkflowWebhookCard/LICENSE.txt create mode 100644 CICD.Tools.MSTeamsWorkflowWebhookCard/Program.cs create mode 100644 CICD.Tools.MSTeamsWorkflowWebhookCard/README.md create mode 100644 CICD.Tools.MSTeamsWorkflowWebhookCard/nuget/Icon.png create mode 100644 LICENSE create mode 100644 README.md create mode 100644 Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/CICD.Tools.MSTeamsWorkflowWebhookCard.Lib.csproj create mode 100644 Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/CicdCard.cs create mode 100644 Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/CicdResult.cs create mode 100644 Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/LICENSE.txt create mode 100644 Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/README.md create mode 100644 Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/nuget/Icon.png create mode 100644 Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.sln diff --git a/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests.csproj b/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests.csproj new file mode 100644 index 0000000..b1af155 --- /dev/null +++ b/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + diff --git a/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/CicdCardTests.cs b/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/CicdCardTests.cs new file mode 100644 index 0000000..373da51 --- /dev/null +++ b/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/CicdCardTests.cs @@ -0,0 +1,82 @@ +namespace Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib.Tests +{ + using System.Text; + + using Microsoft.Extensions.Logging; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + using RichardSzalay.MockHttp; + + using Serilog; + + [TestClass()] + public class CicdCardTests + { + [TestMethod()] + public async Task SendAsyncTest_TestFormatIsValidForTeams() + { + // Arrange + var mockHttp = new MockHttpMessageHandler(); + + var logConfig = new LoggerConfiguration().WriteTo.Console(); + logConfig.MinimumLevel.Is(Serilog.Events.LogEventLevel.Debug); + var seriLog = logConfig.CreateLogger(); + + using var loggerFactory = LoggerFactory.Create(builder => builder.AddSerilog(seriLog)); + var logger = loggerFactory.CreateLogger("Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard"); + + string name = "TestPipeline"; + CicdResult result = CicdResult.Success; + string details = "Some Details"; + string pathToBuild = "https://skyline.be/skyline/about"; + string iconOfService = "https://skyline.be/skylicons/duotone/SkylineLogo_Duo_Light.png"; + string url = "https://skyline.be/"; + + var matcher = new TeamsAdaptiveCardMatcher(logger); + var responseContent = new StringContent("OK", Encoding.UTF8, "application/string"); + mockHttp.When(HttpMethod.Post, url).With(matcher).Respond(System.Net.HttpStatusCode.OK, responseContent); + + using var client = mockHttp.ToHttpClient(); + + // Act + var card = new CicdCard(logger, client); + card.ApplyConfiguration(name, result, details, pathToBuild, iconOfService); + await card.SendAsync(url); + + // Assert + mockHttp.VerifyNoOutstandingExpectation(); + } + + + [TestMethod(), Ignore] + public async Task SendAsyncTest_IntegrationTest() + { + // Arrange + var logConfig = new LoggerConfiguration().WriteTo.Console(); + logConfig.MinimumLevel.Is(Serilog.Events.LogEventLevel.Debug); + var seriLog = logConfig.CreateLogger(); + + using var loggerFactory = LoggerFactory.Create(builder => builder.AddSerilog(seriLog)); + var logger = loggerFactory.CreateLogger("Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard"); + + string name = "IntegrationTestPipeline"; + CicdResult result = CicdResult.Success; + string details = "This is an integration test running from the testbattery. \r\n This should be on a second line!"; + string pathToBuild = "https://github.com/SkylineCommunications/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard"; + string iconOfService = "https://skyline.be/skylicons/duotone/SkylineLogo_Duo_Light.png"; + string url = "EnterWebHookFromTeamsChannelHere"; + + using HttpClient client = new HttpClient(); + + // Act + var card = new CicdCard(logger, client); + card.ApplyConfiguration(name, result, details, pathToBuild, iconOfService); + await card.SendAsync(url); + + // Assert + + // Manual Verification of content as this is Graphical Design. + Assert.IsTrue(true); + } + } +} \ No newline at end of file diff --git a/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/MatchTeamsRequirement/ExpectedJson.cs b/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/MatchTeamsRequirement/ExpectedJson.cs new file mode 100644 index 0000000..82c1964 --- /dev/null +++ b/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/MatchTeamsRequirement/ExpectedJson.cs @@ -0,0 +1,217 @@ +namespace Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib.Tests +{ + using System.Collections.Generic; + + /// + /// Represents an action in a body element. + /// + public class Action + { + /// + /// Gets or sets the title of the action. + /// + public string? Title { get; set; } + + /// + /// Gets or sets the type of the action. + /// + public string? Type { get; set; } + + /// + /// Gets or sets the URL associated with the action. + /// + public string? Url { get; set; } + } + + /// + /// Represents an attachment in a Teams message. + /// + public class Attachment + { + /// + /// Gets or sets the content of the attachment. + /// + public Content? Content { get; set; } + + /// + /// Gets or sets the content type of the attachment. + /// + public string? ContentType { get; set; } + } + + /// + /// Represents an element in the body of an attachment content. + /// + public class BodyElement + { + /// + /// Gets or sets the list of actions in the body element. + /// + public List? Actions { get; set; } + + /// + /// Gets or sets the alternative text for the body element. + /// + public string? AltText { get; set; } + + /// + /// Gets or sets the list of columns in the body element. + /// + public List? Columns { get; set; } + + /// + /// Gets or sets the list of facts in the body element. + /// + public List? Facts { get; set; } + + /// + /// Gets or sets the list of items in the body element. + /// + public List? Items { get; set; } + + /// + /// Gets or sets the size of the body element. + /// + public string? Size { get; set; } + + /// + /// Gets or sets the text of the body element. + /// + public string? Text { get; set; } + + /// + /// Gets or sets the type of the body element. + /// + public string? Type { get; set; } + + /// + /// Gets or sets the URL associated with the body element. + /// + public string? Url { get; set; } + + /// + /// Gets or sets the width of the body element. + /// + public string? Width { get; set; } + + /// + /// Gets or sets a value indicating whether the text should wrap. + /// + public bool Wrap { get; set; } + } + + /// + /// Represents a column in a body element. + /// + public class Column + { + /// + /// Gets or sets the list of items in the column. + /// + public List? Items { get; set; } + + /// + /// Gets or sets the type of the column. + /// + public string? Type { get; set; } + + /// + /// Gets or sets the width of the column. + /// + public string? Width { get; set; } + } + + /// + /// Represents the content of an attachment. + /// + public class Content + { + /// + /// Gets or sets the list of body elements in the content. + /// + public List? Body { get; set; } + + /// + /// Gets or sets the type of the content. + /// + public string? Type { get; set; } + + /// + /// Gets or sets the version of the content. + /// + public string? Version { get; set; } + } + + /// + /// Represents a fact in a body element. + /// + public class Fact + { + /// + /// Gets or sets the title of the fact. + /// + public string? Title { get; set; } + + /// + /// Gets or sets the value of the fact. + /// + public string? Value { get; set; } + } + + /// + /// Represents an item in a body element or column. + /// + public class Item + { + /// + /// Gets or sets the alternative text for the item. + /// + public string? AltText { get; set; } + + /// + /// Gets or sets the size of the item. + /// + public string? Size { get; set; } + + /// + /// Gets or sets the text of the item. + /// + public string? Text { get; set; } + + /// + /// Gets or sets the type of the item. + /// + public string? Type { get; set; } + + /// + /// Gets or sets the URL associated with the item. + /// + public string? Url { get; set; } + + /// + /// Gets or sets the weight of the item. + /// + public string? Weight { get; set; } + + /// + /// Gets or sets a value indicating whether the text should wrap. + /// + public bool Wrap { get; set; } + } + + /// + /// Represents a Teams message. + /// + public class Message + { + /// + /// Gets or sets the list of attachments in the message. + /// + public List? Attachments { get; set; } + + /// + /// Gets or sets the type of the message. + /// + public string? Type { get; set; } + } +} \ No newline at end of file diff --git a/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/MatchTeamsRequirement/TeamsAdaptiveCardMatcher.cs b/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/MatchTeamsRequirement/TeamsAdaptiveCardMatcher.cs new file mode 100644 index 0000000..54cabed --- /dev/null +++ b/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/MatchTeamsRequirement/TeamsAdaptiveCardMatcher.cs @@ -0,0 +1,132 @@ +namespace Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib.Tests +{ + using System; + using System.Linq; + using System.Net.Http; + + using Microsoft.Extensions.Logging; + + using Newtonsoft.Json; + + using RichardSzalay.MockHttp; + + /// + /// Matches HTTP POST requests to expected Teams adaptive card format. + /// Checks the structure of the request but not the card content. + /// + public class TeamsAdaptiveCardMatcher : IMockedRequestMatcher + { + private readonly ILogger logger; + + /// + /// Initializes a new instance of the class. + /// + /// The logger instance. + public TeamsAdaptiveCardMatcher(ILogger logger) + { + this.logger = logger; + } + + /// + /// Determines whether the specified HTTP request message matches the expected Teams adaptive card format. + /// + /// The HTTP request message to check. + /// True if the request matches; otherwise, false. + public bool Matches(HttpRequestMessage message) + { + try + { + if (message == null) + { + logger.LogError("message is null"); + return false; + } + + if (message.Content == null) + { + logger.LogError("message.Content is null"); + return false; + } + + var rootString = message.Content.ReadAsStringAsync().Result; + + if (String.IsNullOrEmpty(rootString)) + { + logger.LogError("rootString is null or empty"); + return false; + } + + var rootObject = JsonConvert.DeserializeObject(rootString); + + if (rootObject == null) + { + logger.LogError("rootObject is null"); + return false; + } + + if (rootObject.Type != "message") + { + logger.LogError($"rootObject.Type {rootObject.Type} does not match 'message'"); + return false; + } + + if (rootObject.Attachments == null) + { + logger.LogError("rootObject.Attachments is null"); + return false; + } + + if (rootObject.Attachments.Count != 1) + { + logger.LogError($"rootObject.Attachments.Count {rootObject.Attachments.Count} does not match 1"); + return false; + } + + var attachment = rootObject.Attachments.FirstOrDefault(); + + if (attachment == null) + { + logger.LogError("First attachment is null"); + return false; + } + + if (attachment.ContentType != "application/vnd.microsoft.card.adaptive") + { + logger.LogError($"attachment.ContentType {attachment.ContentType} does not match 'application/vnd.microsoft.card.adaptive'"); + return false; + } + + if (attachment.Content == null) + { + logger.LogError("Content is null"); + return false; + } + + if (attachment.Content.Type == null) + { + logger.LogError("Content.Type is null"); + return false; + } + + if (attachment.Content.Body == null) + { + logger.LogError("Content.Body is null"); + return false; + } + + if (attachment.Content.Type != "AdaptiveCard") + { + logger.LogError($"attachment.Content.Type {attachment.Content.Type} does not match 'AdaptiveCard'"); + return false; + } + + return true; + } + catch (Exception ex) + { + logger.LogError(ex.ToString()); + return false; + } + } + } +} \ No newline at end of file diff --git a/CICD.Tools.MSTeamsWorkflowWebhookCard/CICD.Tools.MSTeamsWorkflowWebhookCard.csproj b/CICD.Tools.MSTeamsWorkflowWebhookCard/CICD.Tools.MSTeamsWorkflowWebhookCard.csproj new file mode 100644 index 0000000..d7b4442 --- /dev/null +++ b/CICD.Tools.MSTeamsWorkflowWebhookCard/CICD.Tools.MSTeamsWorkflowWebhookCard.csproj @@ -0,0 +1,42 @@ + + + Exe + net8.0 + true + webhook-to-teams + disable + disable + Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard + Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard + 1.0.1 + 1.0.1 + Skyline;DataMiner + https://skyline.be + README.md + LICENSE.txt + True + Icon.png + True + True + SkylineCommunications + Skyline Communications + Allows posting adaptive cards to an MSTeams workflow through a webhook. + + + + + + + + + + + + + + + + + + + diff --git a/CICD.Tools.MSTeamsWorkflowWebhookCard/LICENSE.txt b/CICD.Tools.MSTeamsWorkflowWebhookCard/LICENSE.txt new file mode 100644 index 0000000..e632315 --- /dev/null +++ b/CICD.Tools.MSTeamsWorkflowWebhookCard/LICENSE.txt @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2024 Skyline Communications + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/CICD.Tools.MSTeamsWorkflowWebhookCard/Program.cs b/CICD.Tools.MSTeamsWorkflowWebhookCard/Program.cs new file mode 100644 index 0000000..2278ebe --- /dev/null +++ b/CICD.Tools.MSTeamsWorkflowWebhookCard/Program.cs @@ -0,0 +1,121 @@ +namespace Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard +{ + using System; + + using System.CommandLine; + + using System.Net.Http; + using System.Threading.Tasks; + + using Microsoft.Extensions.Logging; + + using Serilog; + + using Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib; + + /// + /// Allows posting adaptive cards to an MSTeams workflow through a webhook. + /// + public static class Program + { + /// + /// Code that will be called when running the tool. + /// + /// Extra arguments. + /// 0 if successful. + public static async Task Main(string[] args) + { + var httpPostUrl = new Option( + name: "--http-post-url", + description: "The HTTP POST URL as configured in the MSTeams workflow for receiving webhook requests.") + { + IsRequired = true, + }; + + var isDebug = new Option( + name: "--debug", + description: "Indicates the tool should write out debug logging.") + { + IsRequired = false, + }; + + var rootCommand = new RootCommand("Sends an adaptive card to an MS Teams workflow.") + { + httpPostUrl, + isDebug + }; + + var name = new Option("--name") + { + Description = "Name of this pipeline.", + IsRequired = true + }; + + var result = new Option("--result") + { + Description = "Indicates the result of the CICD run.", + IsRequired = true + }; + + var details = new Option("--details") + { + Description = "Indicates the details of the CICD run. If something went wrong, a short description can be added here.", + IsRequired = false + }; + + var pathToBuild = new Option("--url-to-build") + { + Description = "A URL to the current build that can be opened by the user.", + IsRequired = false + }; + + var pathToServiceIcon = new Option("--url-to-service-icon") + { + Description = "A URL to an icon you want to display on the card.", + IsRequired = false + }; + + var fromCicd = new Command("from-cicd", "Sends a card formatted as results from a CICD pipeline or workflow.") + { + name, + result, + details, + pathToBuild, + pathToServiceIcon + }; + + rootCommand.AddCommand(fromCicd); + + fromCicd.SetHandler(ProcessFromCicd, isDebug, name, httpPostUrl, result, details, pathToBuild, pathToServiceIcon); + + return await rootCommand.InvokeAsync(args); + } + + private static async Task ProcessFromCicd(bool isDebug, string name, string httpPostUrl, CicdResult result, string details, string pathToBuild, string iconOfService) + { + if (isDebug) Console.WriteLine("Started MSTeamsWorkflowWebHookCard: from cicd."); + try + { + var logConfig = new LoggerConfiguration().WriteTo.Console(); + logConfig.MinimumLevel.Is(isDebug ? Serilog.Events.LogEventLevel.Debug : Serilog.Events.LogEventLevel.Information); + var seriLog = logConfig.CreateLogger(); + + using var loggerFactory = LoggerFactory.Create(builder => builder.AddSerilog(seriLog)); + var logger = loggerFactory.CreateLogger("Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard"); + + using var client = new HttpClient(); + var card = new CicdCard(logger, client); + card.ApplyConfiguration(name, result, details, pathToBuild, iconOfService); + await card.SendAsync(httpPostUrl); + } + catch (Exception e) + { + Console.WriteLine(e.ToString()); + return 1; + } + + if (isDebug) Console.WriteLine("Finished MSTeamsWorkflowWebHookCard: from cicd."); + return 0; + } + } +} \ No newline at end of file diff --git a/CICD.Tools.MSTeamsWorkflowWebhookCard/README.md b/CICD.Tools.MSTeamsWorkflowWebhookCard/README.md new file mode 100644 index 0000000..96307ef --- /dev/null +++ b/CICD.Tools.MSTeamsWorkflowWebhookCard/README.md @@ -0,0 +1,29 @@ +# Skyline.DataMiner.CICD.MSTeamsWorkflowWebhookCard + +## About + +Allows posting adaptive cards to an MSTeams workflow through a webhook. + +### About DataMiner + +DataMiner is a transformational platform that provides vendor-independent control and monitoring of devices and services. Out of the box and by design, it addresses key challenges such as security, complexity, multi-cloud, and much more. It has a pronounced open architecture and powerful capabilities enabling users to evolve easily and continuously. + +The foundation of DataMiner is its powerful and versatile data acquisition and control layer. With DataMiner, there are no restrictions to what data users can access. Data sources may reside on premises, in the cloud, or in a hybrid setup. + +A unique catalog of 7000+ connectors already exist. In addition, you can leverage DataMiner Development Packages to build you own connectors (also known as "protocols" or "drivers"). + +> **Note** +> See also: [About DataMiner](https://aka.dataminer.services/about-dataminer). + +### About Skyline Communications + +At Skyline Communications, we deal in world-class solutions that are deployed by leading companies around the globe. Check out [our proven track record](https://aka.dataminer.services/about-skyline) and see how we make our customers' lives easier by empowering them to take their operations to the next level. + +## Getting Started + +In commandline: +dotnet tool install -g Skyline.DataMiner.CICD.MSTeamsWorkflowWebhookCard + +Then run the command +webhook-to-teams help + diff --git a/CICD.Tools.MSTeamsWorkflowWebhookCard/nuget/Icon.png b/CICD.Tools.MSTeamsWorkflowWebhookCard/nuget/Icon.png new file mode 100644 index 0000000000000000000000000000000000000000..38d067af6b95a154518b2c9b3904b228d58c1098 GIT binary patch literal 20877 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 **Note** +> See also: [About DataMiner](https://aka.dataminer.services/about-dataminer). + +### About Skyline Communications + +At Skyline Communications, we deal with world-class solutions that are deployed by leading companies around the globe. Check out [our proven track record](https://aka.dataminer.services/about-skyline) and see how we make our customers' lives easier by empowering them to take their operations to the next level. + + + diff --git a/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/CICD.Tools.MSTeamsWorkflowWebhookCard.Lib.csproj b/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/CICD.Tools.MSTeamsWorkflowWebhookCard.Lib.csproj new file mode 100644 index 0000000..b65f899 --- /dev/null +++ b/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/CICD.Tools.MSTeamsWorkflowWebhookCard.Lib.csproj @@ -0,0 +1,34 @@ + + + netstandard2.0 + disable + disable + Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib + Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib + 1.0.1 + 1.0.1 + Skyline;DataMiner + https://skyline.be + README.md + LICENSE.txt + True + Icon.png + True + True + SkylineCommunications + Skyline Communications + Library that can be used to send an adaptive card to a Teams Workflow through webhooks. + + + + + + + + + + + + + + diff --git a/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/CicdCard.cs b/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/CicdCard.cs new file mode 100644 index 0000000..5b1677c --- /dev/null +++ b/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/CicdCard.cs @@ -0,0 +1,214 @@ +namespace Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Text; + using System.Threading.Tasks; + + using AdaptiveCards; + + using Microsoft.Extensions.Logging; + + using Newtonsoft.Json; + + /// + /// Represents a CICD card for MS Teams webhook integration. + /// + public class CicdCard + { + private readonly AdaptiveCard card; + private readonly HttpClient httpClient; + private readonly ILogger logger; + + /// + /// Initializes a new instance of the class. + /// + /// The logger instance. + /// The HTTP client instance. + public CicdCard(ILogger logger, HttpClient httpClient) + { + this.logger = logger; + this.httpClient = httpClient; + card = new AdaptiveCard("1.2"); + } + + /// + /// Applies configuration to the adaptive card. + /// + /// Name of the pipeline. + /// Result of the CICD process. + /// Details of the result. + /// Path to the pipeline run. + /// Icon of the service. + public void ApplyConfiguration(string nameOfPipeline, CicdResult result, string detailsOfResult = null, string pathToPipelineRun = null, string iconOfService = null) + { + logger.LogDebug("Creating Card..."); + + string iconOK = "https://skyline.be/skylicons/duotone/SuccesfullIdea_Duo_Light.png"; + string iconUnstable = "https://skyline.be/skylicons/duotone/Warning_Duo_Light.png"; + string iconWarning = "https://skyline.be/skylicons/duotone/FaultManager_Duo_Light.png"; + string iconFailure = "https://skyline.be/skylicons/duotone/Forbidden_Duo_Light.png"; + + Uri statusIcon; + switch (result) + { + case CicdResult.Success: + statusIcon = new Uri(iconOK); + break; + + case CicdResult.Unstable: + statusIcon = new Uri(iconUnstable); + break; + + case CicdResult.Failure: + statusIcon = new Uri(iconFailure); + break; + + case CicdResult.Warning: + statusIcon = new Uri(iconWarning); + break; + + default: + statusIcon = new Uri(iconOK); + break; + } + + List cardElements = new List + { + new AdaptiveColumnSet + { + Columns = new List + { + new AdaptiveColumn + { + Width = "stretch", + Items = new List + { + new AdaptiveTextBlock + { + Text = $"{nameOfPipeline}: {result}", + Size = AdaptiveTextSize.Large, + Weight = AdaptiveTextWeight.Bolder, + Wrap = true + } + } + }, + new AdaptiveColumn + { + Width = "auto", + Items = new List + { + new AdaptiveImage + { + Url = statusIcon, + Size = AdaptiveImageSize.Small, + AltText = "Status Icon" + } + } + } + } + } + }; + + if (!String.IsNullOrEmpty(iconOfService)) + { + logger.LogDebug("Icon provided, adding..."); + cardElements.Add(new AdaptiveImage + { + Url = new Uri(iconOfService), + Size = AdaptiveImageSize.Medium + }); + } + else + { + logger.LogDebug("No icon provided, skipping..."); + } + + if (!String.IsNullOrEmpty(pathToPipelineRun)) + { + logger.LogDebug("Path to build provided, adding..."); + cardElements.Add(new AdaptiveActionSet + { + Actions = new List + { + new AdaptiveOpenUrlAction + { + Title = "View Build", + Url = new Uri(pathToPipelineRun) + } + } + }); + } + else + { + logger.LogDebug("No path to build provided, skipping..."); + } + + cardElements.Add(new AdaptiveFactSet + { + Facts = new List + { + new AdaptiveFact("Status", result.ToString()) + } + }); + + if (!String.IsNullOrEmpty(detailsOfResult)) + { + logger.LogDebug("Details provided, adding..."); + + string formatedDetails = detailsOfResult.Replace("\r\n", "\n").Replace("\n\n", "\n").Replace("\n", "\n\n"); + + cardElements.Add(new AdaptiveTextBlock + { + Text = formatedDetails, + Wrap = false, + IsSubtle = false, + Separator = true + }); + } + else + { + logger.LogDebug("No details provided, skipping..."); + } + + card.Body = cardElements; + } + + /// + /// Sends the adaptive card to the specified webhook URL asynchronously. + /// + /// The URL to post the card to. + /// A task that represents the asynchronous operation. + public async Task SendAsync(string httpPostUrl) + { + logger.LogDebug("Serializing card to JSON..."); + var cardJson = JsonConvert.SerializeObject(new + { + type = "message", + attachments = new[] + { + new + { + contentType = "application/vnd.microsoft.card.adaptive", + content = card + } + } + }); + + logger.LogDebug(cardJson); + logger.LogDebug("Sending HTTP Post to webhook..."); + var requestContent = new StringContent(cardJson, Encoding.UTF8, "application/json"); + + var response = await httpClient.PostAsync(httpPostUrl, requestContent); + if (response.IsSuccessStatusCode) + { + logger.LogInformation("Successfully sent the card to MS Teams."); + } + else + { + logger.LogError($"Failed to send the card to MS Teams. Status code: {response.StatusCode}. Details: {response.Content}"); + } + } + } +} \ No newline at end of file diff --git a/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/CicdResult.cs b/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/CicdResult.cs new file mode 100644 index 0000000..db3a8b7 --- /dev/null +++ b/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/CicdResult.cs @@ -0,0 +1,28 @@ +namespace Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib +{ + /// + /// Represents the result of a CICD process. + /// + public enum CicdResult + { + /// + /// Indicates a successful result. + /// + Success, + + /// + /// Indicates an unstable result. + /// + Unstable, + + /// + /// Indicates a failed result. + /// + Failure, + + /// + /// Indicates a warning result. + /// + Warning + } +} \ No newline at end of file diff --git a/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/LICENSE.txt b/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/LICENSE.txt new file mode 100644 index 0000000..e632315 --- /dev/null +++ b/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/LICENSE.txt @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2024 Skyline Communications + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/README.md b/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/README.md new file mode 100644 index 0000000..affcb36 --- /dev/null +++ b/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/README.md @@ -0,0 +1,23 @@ +# Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib + +## About + +Library that can be used to send an adaptive card to a Teams Workflow through webhooks. + +### About DataMiner + +DataMiner is a transformational platform that provides vendor-independent control and monitoring of devices and services. Out of the box and by design, it addresses key challenges such as security, complexity, multi-cloud, and much more. It has a pronounced open architecture and powerful capabilities enabling users to evolve easily and continuously. + +The foundation of DataMiner is its powerful and versatile data acquisition and control layer. With DataMiner, there are no restrictions to what data users can access. Data sources may reside on premises, in the cloud, or in a hybrid setup. + +A unique catalog of 7000+ connectors already exists. In addition, you can leverage DataMiner Development Packages to build your own connectors (also known as "protocols" or "drivers"). + +> **Note** +> See also: [About DataMiner](https://aka.dataminer.services/about-dataminer). + +### About Skyline Communications + +At Skyline Communications, we deal in world-class solutions that are deployed by leading companies around the globe. Check out [our proven track record](https://aka.dataminer.services/about-skyline) and see how we make our customers' lives easier by empowering them to take their operations to the next level. + + + diff --git a/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/nuget/Icon.png b/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/nuget/Icon.png new file mode 100644 index 0000000000000000000000000000000000000000..38d067af6b95a154518b2c9b3904b228d58c1098 GIT binary patch literal 20877 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 Date: Mon, 22 Jul 2024 16:36:08 +0200 Subject: [PATCH 2/3] Adding editorconfig + CR changes --- .editorconfig | 14 + ...MSTeamsWorkflowWebhookCard.LibTests.csproj | 18 +- .../CicdCardTests.cs | 171 +++---- .../MatchTeamsRequirement/ExpectedJson.cs | 430 +++++++++--------- .../TeamsAdaptiveCardMatcher.cs | 260 +++++------ ...CD.Tools.MSTeamsWorkflowWebhookCard.csproj | 77 ++-- .../Program.cs | 236 +++++----- ...ools.MSTeamsWorkflowWebhookCard.Lib.csproj | 58 ++- .../CicdCard.cs | 424 ++++++++--------- .../CicdResult.cs | 44 +- 10 files changed, 879 insertions(+), 853 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e542b68 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ + root = true + + [*] + indent_style = space + indent_size = 4 + trim_trailing_whitespace = true + + [*.csproj] + indent_size = 2 + + [*.cs] + dotnet_sort_system_directives_first = true + dotnet_style_predefined_type_for_locals_parameters_members = true + dotnet_style_predefined_type_for_member_access = false \ No newline at end of file diff --git a/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests.csproj b/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests.csproj index b1af155..6747903 100644 --- a/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests.csproj +++ b/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests.csproj @@ -1,9 +1,8 @@  - - net8.0 - enable - enable + net48;net8.0 + disable + disable false true @@ -11,10 +10,13 @@ - - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/CicdCardTests.cs b/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/CicdCardTests.cs index 373da51..42431ac 100644 --- a/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/CicdCardTests.cs +++ b/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/CicdCardTests.cs @@ -1,82 +1,93 @@ -namespace Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib.Tests +namespace CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests { - using System.Text; - - using Microsoft.Extensions.Logging; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - using RichardSzalay.MockHttp; - - using Serilog; - - [TestClass()] - public class CicdCardTests - { - [TestMethod()] - public async Task SendAsyncTest_TestFormatIsValidForTeams() - { - // Arrange - var mockHttp = new MockHttpMessageHandler(); - - var logConfig = new LoggerConfiguration().WriteTo.Console(); - logConfig.MinimumLevel.Is(Serilog.Events.LogEventLevel.Debug); - var seriLog = logConfig.CreateLogger(); - - using var loggerFactory = LoggerFactory.Create(builder => builder.AddSerilog(seriLog)); - var logger = loggerFactory.CreateLogger("Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard"); - - string name = "TestPipeline"; - CicdResult result = CicdResult.Success; - string details = "Some Details"; - string pathToBuild = "https://skyline.be/skyline/about"; - string iconOfService = "https://skyline.be/skylicons/duotone/SkylineLogo_Duo_Light.png"; - string url = "https://skyline.be/"; - - var matcher = new TeamsAdaptiveCardMatcher(logger); - var responseContent = new StringContent("OK", Encoding.UTF8, "application/string"); - mockHttp.When(HttpMethod.Post, url).With(matcher).Respond(System.Net.HttpStatusCode.OK, responseContent); - - using var client = mockHttp.ToHttpClient(); - - // Act - var card = new CicdCard(logger, client); - card.ApplyConfiguration(name, result, details, pathToBuild, iconOfService); - await card.SendAsync(url); - - // Assert - mockHttp.VerifyNoOutstandingExpectation(); - } - - - [TestMethod(), Ignore] - public async Task SendAsyncTest_IntegrationTest() - { - // Arrange - var logConfig = new LoggerConfiguration().WriteTo.Console(); - logConfig.MinimumLevel.Is(Serilog.Events.LogEventLevel.Debug); - var seriLog = logConfig.CreateLogger(); - - using var loggerFactory = LoggerFactory.Create(builder => builder.AddSerilog(seriLog)); - var logger = loggerFactory.CreateLogger("Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard"); - - string name = "IntegrationTestPipeline"; - CicdResult result = CicdResult.Success; - string details = "This is an integration test running from the testbattery. \r\n This should be on a second line!"; - string pathToBuild = "https://github.com/SkylineCommunications/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard"; - string iconOfService = "https://skyline.be/skylicons/duotone/SkylineLogo_Duo_Light.png"; - string url = "EnterWebHookFromTeamsChannelHere"; - - using HttpClient client = new HttpClient(); - - // Act - var card = new CicdCard(logger, client); - card.ApplyConfiguration(name, result, details, pathToBuild, iconOfService); - await card.SendAsync(url); - - // Assert - - // Manual Verification of content as this is Graphical Design. - Assert.IsTrue(true); - } - } + using System.Net.Http; + using System.Text; + using System.Threading.Tasks; + + using CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests.MatchTeamsRequirement; + + using Microsoft.Extensions.Logging; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + using RichardSzalay.MockHttp; + + using Serilog; + + using Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib; + + [TestClass] + public class CicdCardTests + { + [TestMethod] + public async Task SendAsyncTest_TestFormatIsValidForTeams() + { + // Arrange + var mockHttp = new MockHttpMessageHandler(); + + var logConfig = new LoggerConfiguration().WriteTo.Console(); + logConfig.MinimumLevel.Is(Serilog.Events.LogEventLevel.Debug); + var seriLog = logConfig.CreateLogger(); + + using (var loggerFactory = LoggerFactory.Create(builder => builder.AddSerilog(seriLog))) + { + var logger = loggerFactory.CreateLogger("Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard"); + + const string name = "TestPipeline"; + const CicdResult result = CicdResult.Success; + const string details = "Some Details"; + const string pathToBuild = "https://skyline.be/skyline/about"; + const string iconOfService = "https://skyline.be/skylicons/duotone/SkylineLogo_Duo_Light.png"; + const string url = "https://skyline.be/"; + + var matcher = new TeamsAdaptiveCardMatcher(logger); + var responseContent = new StringContent("OK", Encoding.UTF8, "application/string"); + mockHttp.When(HttpMethod.Post, url).With(matcher).Respond(System.Net.HttpStatusCode.OK, responseContent); + + using (var client = mockHttp.ToHttpClient()) + { + // Act + var card = new CicdCard(logger, client); + card.ApplyConfiguration(name, result, details, pathToBuild, iconOfService); + await card.SendAsync(url); + + // Assert + mockHttp.VerifyNoOutstandingExpectation(); + } + } + } + + [TestMethod, Ignore] + public async Task SendAsyncTest_IntegrationTest() + { + // Arrange + var logConfig = new LoggerConfiguration().WriteTo.Console(); + logConfig.MinimumLevel.Is(Serilog.Events.LogEventLevel.Debug); + var seriLog = logConfig.CreateLogger(); + + using (var loggerFactory = LoggerFactory.Create(builder => builder.AddSerilog(seriLog))) + { + var logger = loggerFactory.CreateLogger("Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard"); + + const string name = "IntegrationTestPipeline"; + const CicdResult result = CicdResult.Success; + const string details = "This is an integration test running from the testbattery. \r\n This should be on a second line!"; + const string pathToBuild = "https://github.com/SkylineCommunications/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard"; + const string iconOfService = "https://skyline.be/skylicons/duotone/SkylineLogo_Duo_Light.png"; + const string url = "EnterWebHookFromTeamsChannelHere"; + + using (HttpClient client = new HttpClient()) + { + // Act + var card = new CicdCard(logger, client); + card.ApplyConfiguration(name, result, details, pathToBuild, iconOfService); + await card.SendAsync(url); + + // Assert + + // Manual Verification of content as this is Graphical Design. + Assert.IsTrue(true); + } + } + } + } } \ No newline at end of file diff --git a/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/MatchTeamsRequirement/ExpectedJson.cs b/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/MatchTeamsRequirement/ExpectedJson.cs index 82c1964..887c38e 100644 --- a/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/MatchTeamsRequirement/ExpectedJson.cs +++ b/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/MatchTeamsRequirement/ExpectedJson.cs @@ -1,217 +1,217 @@ -namespace Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib.Tests +namespace CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests.MatchTeamsRequirement { - using System.Collections.Generic; - - /// - /// Represents an action in a body element. - /// - public class Action - { - /// - /// Gets or sets the title of the action. - /// - public string? Title { get; set; } - - /// - /// Gets or sets the type of the action. - /// - public string? Type { get; set; } - - /// - /// Gets or sets the URL associated with the action. - /// - public string? Url { get; set; } - } - - /// - /// Represents an attachment in a Teams message. - /// - public class Attachment - { - /// - /// Gets or sets the content of the attachment. - /// - public Content? Content { get; set; } - - /// - /// Gets or sets the content type of the attachment. - /// - public string? ContentType { get; set; } - } - - /// - /// Represents an element in the body of an attachment content. - /// - public class BodyElement - { - /// - /// Gets or sets the list of actions in the body element. - /// - public List? Actions { get; set; } - - /// - /// Gets or sets the alternative text for the body element. - /// - public string? AltText { get; set; } - - /// - /// Gets or sets the list of columns in the body element. - /// - public List? Columns { get; set; } - - /// - /// Gets or sets the list of facts in the body element. - /// - public List? Facts { get; set; } - - /// - /// Gets or sets the list of items in the body element. - /// - public List? Items { get; set; } - - /// - /// Gets or sets the size of the body element. - /// - public string? Size { get; set; } - - /// - /// Gets or sets the text of the body element. - /// - public string? Text { get; set; } - - /// - /// Gets or sets the type of the body element. - /// - public string? Type { get; set; } - - /// - /// Gets or sets the URL associated with the body element. - /// - public string? Url { get; set; } - - /// - /// Gets or sets the width of the body element. - /// - public string? Width { get; set; } - - /// - /// Gets or sets a value indicating whether the text should wrap. - /// - public bool Wrap { get; set; } - } - - /// - /// Represents a column in a body element. - /// - public class Column - { - /// - /// Gets or sets the list of items in the column. - /// - public List? Items { get; set; } - - /// - /// Gets or sets the type of the column. - /// - public string? Type { get; set; } - - /// - /// Gets or sets the width of the column. - /// - public string? Width { get; set; } - } - - /// - /// Represents the content of an attachment. - /// - public class Content - { - /// - /// Gets or sets the list of body elements in the content. - /// - public List? Body { get; set; } - - /// - /// Gets or sets the type of the content. - /// - public string? Type { get; set; } - - /// - /// Gets or sets the version of the content. - /// - public string? Version { get; set; } - } - - /// - /// Represents a fact in a body element. - /// - public class Fact - { - /// - /// Gets or sets the title of the fact. - /// - public string? Title { get; set; } - - /// - /// Gets or sets the value of the fact. - /// - public string? Value { get; set; } - } - - /// - /// Represents an item in a body element or column. - /// - public class Item - { - /// - /// Gets or sets the alternative text for the item. - /// - public string? AltText { get; set; } - - /// - /// Gets or sets the size of the item. - /// - public string? Size { get; set; } - - /// - /// Gets or sets the text of the item. - /// - public string? Text { get; set; } - - /// - /// Gets or sets the type of the item. - /// - public string? Type { get; set; } - - /// - /// Gets or sets the URL associated with the item. - /// - public string? Url { get; set; } - - /// - /// Gets or sets the weight of the item. - /// - public string? Weight { get; set; } - - /// - /// Gets or sets a value indicating whether the text should wrap. - /// - public bool Wrap { get; set; } - } - - /// - /// Represents a Teams message. - /// - public class Message - { - /// - /// Gets or sets the list of attachments in the message. - /// - public List? Attachments { get; set; } - - /// - /// Gets or sets the type of the message. - /// - public string? Type { get; set; } - } + using System.Collections.Generic; + + /// + /// Represents an action in a body element. + /// + public class Action + { + /// + /// Gets or sets the title of the action. + /// + public string Title { get; set; } + + /// + /// Gets or sets the type of the action. + /// + public string Type { get; set; } + + /// + /// Gets or sets the URL associated with the action. + /// + public string Url { get; set; } + } + + /// + /// Represents an attachment in a Teams message. + /// + public class Attachment + { + /// + /// Gets or sets the content of the attachment. + /// + public Content Content { get; set; } + + /// + /// Gets or sets the content type of the attachment. + /// + public string ContentType { get; set; } + } + + /// + /// Represents an element in the body of an attachment content. + /// + public class BodyElement + { + /// + /// Gets or sets the list of actions in the body element. + /// + public List Actions { get; set; } + + /// + /// Gets or sets the alternative text for the body element. + /// + public string AltText { get; set; } + + /// + /// Gets or sets the list of columns in the body element. + /// + public List Columns { get; set; } + + /// + /// Gets or sets the list of facts in the body element. + /// + public List Facts { get; set; } + + /// + /// Gets or sets the list of items in the body element. + /// + public List Items { get; set; } + + /// + /// Gets or sets the size of the body element. + /// + public string Size { get; set; } + + /// + /// Gets or sets the text of the body element. + /// + public string Text { get; set; } + + /// + /// Gets or sets the type of the body element. + /// + public string Type { get; set; } + + /// + /// Gets or sets the URL associated with the body element. + /// + public string Url { get; set; } + + /// + /// Gets or sets the width of the body element. + /// + public string Width { get; set; } + + /// + /// Gets or sets a value indicating whether the text should wrap. + /// + public bool Wrap { get; set; } + } + + /// + /// Represents a column in a body element. + /// + public class Column + { + /// + /// Gets or sets the list of items in the column. + /// + public List Items { get; set; } + + /// + /// Gets or sets the type of the column. + /// + public string Type { get; set; } + + /// + /// Gets or sets the width of the column. + /// + public string Width { get; set; } + } + + /// + /// Represents the content of an attachment. + /// + public class Content + { + /// + /// Gets or sets the list of body elements in the content. + /// + public List Body { get; set; } + + /// + /// Gets or sets the type of the content. + /// + public string Type { get; set; } + + /// + /// Gets or sets the version of the content. + /// + public string Version { get; set; } + } + + /// + /// Represents a fact in a body element. + /// + public class Fact + { + /// + /// Gets or sets the title of the fact. + /// + public string Title { get; set; } + + /// + /// Gets or sets the value of the fact. + /// + public string Value { get; set; } + } + + /// + /// Represents an item in a body element or column. + /// + public class Item + { + /// + /// Gets or sets the alternative text for the item. + /// + public string AltText { get; set; } + + /// + /// Gets or sets the size of the item. + /// + public string Size { get; set; } + + /// + /// Gets or sets the text of the item. + /// + public string Text { get; set; } + + /// + /// Gets or sets the type of the item. + /// + public string Type { get; set; } + + /// + /// Gets or sets the URL associated with the item. + /// + public string Url { get; set; } + + /// + /// Gets or sets the weight of the item. + /// + public string Weight { get; set; } + + /// + /// Gets or sets a value indicating whether the text should wrap. + /// + public bool Wrap { get; set; } + } + + /// + /// Represents a Teams message. + /// + public class Message + { + /// + /// Gets or sets the list of attachments in the message. + /// + public List Attachments { get; set; } + + /// + /// Gets or sets the type of the message. + /// + public string Type { get; set; } + } } \ No newline at end of file diff --git a/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/MatchTeamsRequirement/TeamsAdaptiveCardMatcher.cs b/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/MatchTeamsRequirement/TeamsAdaptiveCardMatcher.cs index 54cabed..b1b6774 100644 --- a/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/MatchTeamsRequirement/TeamsAdaptiveCardMatcher.cs +++ b/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/MatchTeamsRequirement/TeamsAdaptiveCardMatcher.cs @@ -1,132 +1,132 @@ -namespace Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib.Tests +namespace CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests.MatchTeamsRequirement { - using System; - using System.Linq; - using System.Net.Http; - - using Microsoft.Extensions.Logging; - - using Newtonsoft.Json; - - using RichardSzalay.MockHttp; - - /// - /// Matches HTTP POST requests to expected Teams adaptive card format. - /// Checks the structure of the request but not the card content. - /// - public class TeamsAdaptiveCardMatcher : IMockedRequestMatcher - { - private readonly ILogger logger; - - /// - /// Initializes a new instance of the class. - /// - /// The logger instance. - public TeamsAdaptiveCardMatcher(ILogger logger) - { - this.logger = logger; - } - - /// - /// Determines whether the specified HTTP request message matches the expected Teams adaptive card format. - /// - /// The HTTP request message to check. - /// True if the request matches; otherwise, false. - public bool Matches(HttpRequestMessage message) - { - try - { - if (message == null) - { - logger.LogError("message is null"); - return false; - } - - if (message.Content == null) - { - logger.LogError("message.Content is null"); - return false; - } - - var rootString = message.Content.ReadAsStringAsync().Result; - - if (String.IsNullOrEmpty(rootString)) - { - logger.LogError("rootString is null or empty"); - return false; - } - - var rootObject = JsonConvert.DeserializeObject(rootString); - - if (rootObject == null) - { - logger.LogError("rootObject is null"); - return false; - } - - if (rootObject.Type != "message") - { - logger.LogError($"rootObject.Type {rootObject.Type} does not match 'message'"); - return false; - } - - if (rootObject.Attachments == null) - { - logger.LogError("rootObject.Attachments is null"); - return false; - } - - if (rootObject.Attachments.Count != 1) - { - logger.LogError($"rootObject.Attachments.Count {rootObject.Attachments.Count} does not match 1"); - return false; - } - - var attachment = rootObject.Attachments.FirstOrDefault(); - - if (attachment == null) - { - logger.LogError("First attachment is null"); - return false; - } - - if (attachment.ContentType != "application/vnd.microsoft.card.adaptive") - { - logger.LogError($"attachment.ContentType {attachment.ContentType} does not match 'application/vnd.microsoft.card.adaptive'"); - return false; - } - - if (attachment.Content == null) - { - logger.LogError("Content is null"); - return false; - } - - if (attachment.Content.Type == null) - { - logger.LogError("Content.Type is null"); - return false; - } - - if (attachment.Content.Body == null) - { - logger.LogError("Content.Body is null"); - return false; - } - - if (attachment.Content.Type != "AdaptiveCard") - { - logger.LogError($"attachment.Content.Type {attachment.Content.Type} does not match 'AdaptiveCard'"); - return false; - } - - return true; - } - catch (Exception ex) - { - logger.LogError(ex.ToString()); - return false; - } - } - } + using System; + using System.Linq; + using System.Net.Http; + + using Microsoft.Extensions.Logging; + + using Newtonsoft.Json; + + using RichardSzalay.MockHttp; + + /// + /// Matches HTTP POST requests to expected Teams adaptive card format. + /// Checks the structure of the request but not the card content. + /// + public class TeamsAdaptiveCardMatcher : IMockedRequestMatcher + { + private readonly ILogger logger; + + /// + /// Initializes a new instance of the class. + /// + /// The logger instance. + public TeamsAdaptiveCardMatcher(ILogger logger) + { + this.logger = logger; + } + + /// + /// Determines whether the specified HTTP request message matches the expected Teams adaptive card format. + /// + /// The HTTP request message to check. + /// True if the request matches; otherwise, false. + public bool Matches(HttpRequestMessage message) + { + try + { + if (message == null) + { + logger.LogError("message is null"); + return false; + } + + if (message.Content == null) + { + logger.LogError("message.Content is null"); + return false; + } + + var rootString = message.Content.ReadAsStringAsync().Result; + + if (String.IsNullOrEmpty(rootString)) + { + logger.LogError("rootString is null or empty"); + return false; + } + + var rootObject = JsonConvert.DeserializeObject(rootString); + + if (rootObject == null) + { + logger.LogError("rootObject is null"); + return false; + } + + if (rootObject.Type != "message") + { + logger.LogError($"rootObject.Type {rootObject.Type} does not match 'message'"); + return false; + } + + if (rootObject.Attachments == null) + { + logger.LogError("rootObject.Attachments is null"); + return false; + } + + if (rootObject.Attachments.Count != 1) + { + logger.LogError($"rootObject.Attachments.Count {rootObject.Attachments.Count} does not match 1"); + return false; + } + + var attachment = rootObject.Attachments.FirstOrDefault(); + + if (attachment == null) + { + logger.LogError("First attachment is null"); + return false; + } + + if (attachment.ContentType != "application/vnd.microsoft.card.adaptive") + { + logger.LogError($"attachment.ContentType {attachment.ContentType} does not match 'application/vnd.microsoft.card.adaptive'"); + return false; + } + + if (attachment.Content == null) + { + logger.LogError("Content is null"); + return false; + } + + if (attachment.Content.Type == null) + { + logger.LogError("Content.Type is null"); + return false; + } + + if (attachment.Content.Body == null) + { + logger.LogError("Content.Body is null"); + return false; + } + + if (attachment.Content.Type != "AdaptiveCard") + { + logger.LogError($"attachment.Content.Type {attachment.Content.Type} does not match 'AdaptiveCard'"); + return false; + } + + return true; + } + catch (Exception ex) + { + logger.LogError(ex.ToString()); + return false; + } + } + } } \ No newline at end of file diff --git a/CICD.Tools.MSTeamsWorkflowWebhookCard/CICD.Tools.MSTeamsWorkflowWebhookCard.csproj b/CICD.Tools.MSTeamsWorkflowWebhookCard/CICD.Tools.MSTeamsWorkflowWebhookCard.csproj index d7b4442..dc3fa9e 100644 --- a/CICD.Tools.MSTeamsWorkflowWebhookCard/CICD.Tools.MSTeamsWorkflowWebhookCard.csproj +++ b/CICD.Tools.MSTeamsWorkflowWebhookCard/CICD.Tools.MSTeamsWorkflowWebhookCard.csproj @@ -1,42 +1,41 @@  - - Exe - net8.0 - true - webhook-to-teams - disable - disable - Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard - Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard - 1.0.1 - 1.0.1 - Skyline;DataMiner - https://skyline.be - README.md - LICENSE.txt - True - Icon.png - True - True - SkylineCommunications - Skyline Communications - Allows posting adaptive cards to an MSTeams workflow through a webhook. - + + Exe + net8.0 + true + webhook-to-teams + disable + disable + Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard + Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard + Skyline;DataMiner + https://skyline.be + README.md + LICENSE.txt + True + Icon.png + True + True + SkylineCommunications + Skyline Communications + Allows posting adaptive cards to an MSTeams workflow through a webhook. + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + diff --git a/CICD.Tools.MSTeamsWorkflowWebhookCard/Program.cs b/CICD.Tools.MSTeamsWorkflowWebhookCard/Program.cs index 2278ebe..2aa1eaf 100644 --- a/CICD.Tools.MSTeamsWorkflowWebhookCard/Program.cs +++ b/CICD.Tools.MSTeamsWorkflowWebhookCard/Program.cs @@ -1,121 +1,121 @@ namespace Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard { - using System; - - using System.CommandLine; - - using System.Net.Http; - using System.Threading.Tasks; - - using Microsoft.Extensions.Logging; - - using Serilog; - - using Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib; - - /// - /// Allows posting adaptive cards to an MSTeams workflow through a webhook. - /// - public static class Program - { - /// - /// Code that will be called when running the tool. - /// - /// Extra arguments. - /// 0 if successful. - public static async Task Main(string[] args) - { - var httpPostUrl = new Option( - name: "--http-post-url", - description: "The HTTP POST URL as configured in the MSTeams workflow for receiving webhook requests.") - { - IsRequired = true, - }; - - var isDebug = new Option( - name: "--debug", - description: "Indicates the tool should write out debug logging.") - { - IsRequired = false, - }; - - var rootCommand = new RootCommand("Sends an adaptive card to an MS Teams workflow.") - { - httpPostUrl, - isDebug - }; - - var name = new Option("--name") - { - Description = "Name of this pipeline.", - IsRequired = true - }; - - var result = new Option("--result") - { - Description = "Indicates the result of the CICD run.", - IsRequired = true - }; - - var details = new Option("--details") - { - Description = "Indicates the details of the CICD run. If something went wrong, a short description can be added here.", - IsRequired = false - }; - - var pathToBuild = new Option("--url-to-build") - { - Description = "A URL to the current build that can be opened by the user.", - IsRequired = false - }; - - var pathToServiceIcon = new Option("--url-to-service-icon") - { - Description = "A URL to an icon you want to display on the card.", - IsRequired = false - }; - - var fromCicd = new Command("from-cicd", "Sends a card formatted as results from a CICD pipeline or workflow.") - { - name, - result, - details, - pathToBuild, - pathToServiceIcon - }; - - rootCommand.AddCommand(fromCicd); - - fromCicd.SetHandler(ProcessFromCicd, isDebug, name, httpPostUrl, result, details, pathToBuild, pathToServiceIcon); - - return await rootCommand.InvokeAsync(args); - } - - private static async Task ProcessFromCicd(bool isDebug, string name, string httpPostUrl, CicdResult result, string details, string pathToBuild, string iconOfService) - { - if (isDebug) Console.WriteLine("Started MSTeamsWorkflowWebHookCard: from cicd."); - try - { - var logConfig = new LoggerConfiguration().WriteTo.Console(); - logConfig.MinimumLevel.Is(isDebug ? Serilog.Events.LogEventLevel.Debug : Serilog.Events.LogEventLevel.Information); - var seriLog = logConfig.CreateLogger(); - - using var loggerFactory = LoggerFactory.Create(builder => builder.AddSerilog(seriLog)); - var logger = loggerFactory.CreateLogger("Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard"); - - using var client = new HttpClient(); - var card = new CicdCard(logger, client); - card.ApplyConfiguration(name, result, details, pathToBuild, iconOfService); - await card.SendAsync(httpPostUrl); - } - catch (Exception e) - { - Console.WriteLine(e.ToString()); - return 1; - } - - if (isDebug) Console.WriteLine("Finished MSTeamsWorkflowWebHookCard: from cicd."); - return 0; - } - } + using System; + + using System.CommandLine; + + using System.Net.Http; + using System.Threading.Tasks; + + using Microsoft.Extensions.Logging; + + using Serilog; + + using Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib; + + /// + /// Allows posting adaptive cards to an MSTeams workflow through a webhook. + /// + public static class Program + { + /// + /// Code that will be called when running the tool. + /// + /// Extra arguments. + /// 0 if successful. + public static async Task Main(string[] args) + { + var httpPostUrl = new Option( + name: "--http-post-url", + description: "The HTTP POST URL as configured in the MSTeams workflow for receiving webhook requests.") + { + IsRequired = true, + }; + + var isDebug = new Option( + name: "--debug", + description: "Indicates the tool should write out debug logging.") + { + IsRequired = false, + }; + + var rootCommand = new RootCommand("Sends an adaptive card to an MS Teams workflow.") + { + httpPostUrl, + isDebug + }; + + var name = new Option("--name") + { + Description = "Name of this pipeline.", + IsRequired = true + }; + + var result = new Option("--result") + { + Description = "Indicates the result of the CICD run.", + IsRequired = true + }; + + var details = new Option("--details") + { + Description = "Indicates the details of the CICD run. If something went wrong, a short description can be added here.", + IsRequired = false + }; + + var pathToBuild = new Option("--url-to-build") + { + Description = "A URL to the current build that can be opened by the user.", + IsRequired = false + }; + + var pathToServiceIcon = new Option("--url-to-service-icon") + { + Description = "A URL to an icon you want to display on the card.", + IsRequired = false + }; + + var fromCicd = new Command("from-cicd", "Sends a card formatted as results from a CICD pipeline or workflow.") + { + name, + result, + details, + pathToBuild, + pathToServiceIcon + }; + + rootCommand.AddCommand(fromCicd); + + fromCicd.SetHandler(ProcessFromCicd, isDebug, name, httpPostUrl, result, details, pathToBuild, pathToServiceIcon); + + return await rootCommand.InvokeAsync(args); + } + + private static async Task ProcessFromCicd(bool isDebug, string name, string httpPostUrl, CicdResult result, string details, string pathToBuild, string iconOfService) + { + if (isDebug) Console.WriteLine("Started MSTeamsWorkflowWebHookCard: from cicd."); + try + { + var logConfig = new LoggerConfiguration().WriteTo.Console(); + logConfig.MinimumLevel.Is(isDebug ? Serilog.Events.LogEventLevel.Debug : Serilog.Events.LogEventLevel.Information); + var seriLog = logConfig.CreateLogger(); + + using var loggerFactory = LoggerFactory.Create(builder => builder.AddSerilog(seriLog)); + var logger = loggerFactory.CreateLogger("Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard"); + + using var client = new HttpClient(); + var card = new CicdCard(logger, client); + card.ApplyConfiguration(name, result, details, pathToBuild, iconOfService); + await card.SendAsync(httpPostUrl); + } + catch (Exception e) + { + Console.WriteLine(e.ToString()); + return 1; + } + + if (isDebug) Console.WriteLine("Finished MSTeamsWorkflowWebHookCard: from cicd."); + return 0; + } + } } \ No newline at end of file diff --git a/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/CICD.Tools.MSTeamsWorkflowWebhookCard.Lib.csproj b/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/CICD.Tools.MSTeamsWorkflowWebhookCard.Lib.csproj index b65f899..2063a5c 100644 --- a/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/CICD.Tools.MSTeamsWorkflowWebhookCard.Lib.csproj +++ b/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/CICD.Tools.MSTeamsWorkflowWebhookCard.Lib.csproj @@ -1,34 +1,32 @@ - - - netstandard2.0 - disable - disable - Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib - Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib - 1.0.1 - 1.0.1 - Skyline;DataMiner - https://skyline.be - README.md - LICENSE.txt - True - Icon.png - True - True - SkylineCommunications - Skyline Communications - Library that can be used to send an adaptive card to a Teams Workflow through webhooks. - + + + netstandard2.0 + disable + disable + Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib + Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib + Skyline;DataMiner + https://skyline.be + README.md + LICENSE.txt + True + Icon.png + True + True + SkylineCommunications + Skyline Communications + Library that can be used to send an adaptive card to a Teams Workflow through webhooks. + - - - - - + + + + + - - - - + + + + diff --git a/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/CicdCard.cs b/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/CicdCard.cs index 5b1677c..bc832da 100644 --- a/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/CicdCard.cs +++ b/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/CicdCard.cs @@ -1,214 +1,216 @@ namespace Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib { - using System; - using System.Collections.Generic; - using System.Net.Http; - using System.Text; - using System.Threading.Tasks; - - using AdaptiveCards; - - using Microsoft.Extensions.Logging; - - using Newtonsoft.Json; - - /// - /// Represents a CICD card for MS Teams webhook integration. - /// - public class CicdCard - { - private readonly AdaptiveCard card; - private readonly HttpClient httpClient; - private readonly ILogger logger; - - /// - /// Initializes a new instance of the class. - /// - /// The logger instance. - /// The HTTP client instance. - public CicdCard(ILogger logger, HttpClient httpClient) - { - this.logger = logger; - this.httpClient = httpClient; - card = new AdaptiveCard("1.2"); - } - - /// - /// Applies configuration to the adaptive card. - /// - /// Name of the pipeline. - /// Result of the CICD process. - /// Details of the result. - /// Path to the pipeline run. - /// Icon of the service. - public void ApplyConfiguration(string nameOfPipeline, CicdResult result, string detailsOfResult = null, string pathToPipelineRun = null, string iconOfService = null) - { - logger.LogDebug("Creating Card..."); - - string iconOK = "https://skyline.be/skylicons/duotone/SuccesfullIdea_Duo_Light.png"; - string iconUnstable = "https://skyline.be/skylicons/duotone/Warning_Duo_Light.png"; - string iconWarning = "https://skyline.be/skylicons/duotone/FaultManager_Duo_Light.png"; - string iconFailure = "https://skyline.be/skylicons/duotone/Forbidden_Duo_Light.png"; - - Uri statusIcon; - switch (result) - { - case CicdResult.Success: - statusIcon = new Uri(iconOK); - break; - - case CicdResult.Unstable: - statusIcon = new Uri(iconUnstable); - break; - - case CicdResult.Failure: - statusIcon = new Uri(iconFailure); - break; - - case CicdResult.Warning: - statusIcon = new Uri(iconWarning); - break; - - default: - statusIcon = new Uri(iconOK); - break; - } - - List cardElements = new List - { - new AdaptiveColumnSet - { - Columns = new List - { - new AdaptiveColumn - { - Width = "stretch", - Items = new List - { - new AdaptiveTextBlock - { - Text = $"{nameOfPipeline}: {result}", - Size = AdaptiveTextSize.Large, - Weight = AdaptiveTextWeight.Bolder, - Wrap = true - } - } - }, - new AdaptiveColumn - { - Width = "auto", - Items = new List - { - new AdaptiveImage - { - Url = statusIcon, - Size = AdaptiveImageSize.Small, - AltText = "Status Icon" - } - } - } - } - } - }; - - if (!String.IsNullOrEmpty(iconOfService)) - { - logger.LogDebug("Icon provided, adding..."); - cardElements.Add(new AdaptiveImage - { - Url = new Uri(iconOfService), - Size = AdaptiveImageSize.Medium - }); - } - else - { - logger.LogDebug("No icon provided, skipping..."); - } - - if (!String.IsNullOrEmpty(pathToPipelineRun)) - { - logger.LogDebug("Path to build provided, adding..."); - cardElements.Add(new AdaptiveActionSet - { - Actions = new List - { - new AdaptiveOpenUrlAction - { - Title = "View Build", - Url = new Uri(pathToPipelineRun) - } - } - }); - } - else - { - logger.LogDebug("No path to build provided, skipping..."); - } - - cardElements.Add(new AdaptiveFactSet - { - Facts = new List - { - new AdaptiveFact("Status", result.ToString()) - } - }); - - if (!String.IsNullOrEmpty(detailsOfResult)) - { - logger.LogDebug("Details provided, adding..."); - - string formatedDetails = detailsOfResult.Replace("\r\n", "\n").Replace("\n\n", "\n").Replace("\n", "\n\n"); - - cardElements.Add(new AdaptiveTextBlock - { - Text = formatedDetails, - Wrap = false, - IsSubtle = false, - Separator = true - }); - } - else - { - logger.LogDebug("No details provided, skipping..."); - } - - card.Body = cardElements; - } - - /// - /// Sends the adaptive card to the specified webhook URL asynchronously. - /// - /// The URL to post the card to. - /// A task that represents the asynchronous operation. - public async Task SendAsync(string httpPostUrl) - { - logger.LogDebug("Serializing card to JSON..."); - var cardJson = JsonConvert.SerializeObject(new - { - type = "message", - attachments = new[] - { - new - { - contentType = "application/vnd.microsoft.card.adaptive", - content = card - } - } - }); - - logger.LogDebug(cardJson); - logger.LogDebug("Sending HTTP Post to webhook..."); - var requestContent = new StringContent(cardJson, Encoding.UTF8, "application/json"); - - var response = await httpClient.PostAsync(httpPostUrl, requestContent); - if (response.IsSuccessStatusCode) - { - logger.LogInformation("Successfully sent the card to MS Teams."); - } - else - { - logger.LogError($"Failed to send the card to MS Teams. Status code: {response.StatusCode}. Details: {response.Content}"); - } - } - } + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Text; + using System.Threading.Tasks; + + using AdaptiveCards; + + using Microsoft.Extensions.Logging; + + using Newtonsoft.Json; + + /// + /// Represents a CICD card for MS Teams webhook integration. + /// + public class CicdCard + { + private readonly AdaptiveCard card; + private readonly HttpClient httpClient; + private readonly ILogger logger; + + /// + /// Initializes a new instance of the class. + /// + /// The logger instance. + /// The HTTP client instance. + public CicdCard(ILogger logger, HttpClient httpClient) + { + this.logger = logger; + this.httpClient = httpClient; + card = new AdaptiveCard("1.2"); + } + + /// + /// Applies configuration to the adaptive card. + /// + /// Name of the pipeline. + /// Result of the CICD process. + /// Details of the result. + /// Path to the pipeline run. + /// Icon of the service. + public void ApplyConfiguration(string nameOfPipeline, CicdResult result, string detailsOfResult = null, string pathToPipelineRun = null, string iconOfService = null) + { + logger.LogDebug("Creating Card..."); + + const string iconOk = "https://skyline.be/skylicons/duotone/SuccesfullIdea_Duo_Light.png"; + const string iconUnstable = "https://skyline.be/skylicons/duotone/Warning_Duo_Light.png"; + const string iconWarning = "https://skyline.be/skylicons/duotone/FaultManager_Duo_Light.png"; + const string iconFailure = "https://skyline.be/skylicons/duotone/Forbidden_Duo_Light.png"; + + Uri statusIcon; + switch (result) + { + case CicdResult.Success: + statusIcon = new Uri(iconOk); + break; + + case CicdResult.Unstable: + statusIcon = new Uri(iconUnstable); + break; + + case CicdResult.Failure: + statusIcon = new Uri(iconFailure); + break; + + case CicdResult.Warning: + statusIcon = new Uri(iconWarning); + break; + + default: + statusIcon = new Uri(iconOk); + break; + } + + List cardElements = new List + { + new AdaptiveColumnSet + { + Columns = new List + { + new AdaptiveColumn + { + Width = "stretch", + Items = new List + { + new AdaptiveTextBlock + { + Text = $"{nameOfPipeline}: {result}", + Size = AdaptiveTextSize.Large, + Weight = AdaptiveTextWeight.Bolder, + Wrap = true + } + } + }, + new AdaptiveColumn + { + Width = "auto", + Items = new List + { + new AdaptiveImage + { + Url = statusIcon, + Size = AdaptiveImageSize.Small, + AltText = "Status Icon" + } + } + } + } + } + }; + + if (!String.IsNullOrWhiteSpace(iconOfService)) + { + logger.LogDebug("Icon provided, adding..."); + cardElements.Add(new AdaptiveImage + { + Url = new Uri(iconOfService), + Size = AdaptiveImageSize.Medium + }); + } + else + { + logger.LogDebug("No icon provided, skipping..."); + } + + if (!String.IsNullOrWhiteSpace(pathToPipelineRun)) + { + logger.LogDebug("Path to build provided, adding..."); + cardElements.Add(new AdaptiveActionSet + { + Actions = new List + { + new AdaptiveOpenUrlAction + { + Title = "View Build", + Url = new Uri(pathToPipelineRun) + } + } + }); + } + else + { + logger.LogDebug("No path to build provided, skipping..."); + } + + cardElements.Add(new AdaptiveFactSet + { + Facts = new List + { + new AdaptiveFact("Status", result.ToString()) + } + }); + + if (!String.IsNullOrWhiteSpace(detailsOfResult)) + { + logger.LogDebug("Details provided, adding..."); + + string formatedDetails = detailsOfResult.Replace("\r\n", "\n") + .Replace("\n\n", "\n") + .Replace("\n", "\n\n"); + + cardElements.Add(new AdaptiveTextBlock + { + Text = formatedDetails, + Wrap = false, + IsSubtle = false, + Separator = true + }); + } + else + { + logger.LogDebug("No details provided, skipping..."); + } + + card.Body = cardElements; + } + + /// + /// Sends the adaptive card to the specified webhook URL asynchronously. + /// + /// The URL to post the card to. + /// A task that represents the asynchronous operation. + public async Task SendAsync(string httpPostUrl) + { + logger.LogDebug("Serializing card to JSON..."); + var cardJson = JsonConvert.SerializeObject(new + { + type = "message", + attachments = new[] + { + new + { + contentType = "application/vnd.microsoft.card.adaptive", + content = card + } + } + }); + + logger.LogDebug(cardJson); + logger.LogDebug("Sending HTTP Post to webhook..."); + var requestContent = new StringContent(cardJson, Encoding.UTF8, "application/json"); + + var response = await httpClient.PostAsync(httpPostUrl, requestContent); + if (response.IsSuccessStatusCode) + { + logger.LogInformation("Successfully sent the card to MS Teams."); + } + else + { + logger.LogError($"Failed to send the card to MS Teams. Status code: {response.StatusCode}. Details: {response.Content}"); + } + } + } } \ No newline at end of file diff --git a/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/CicdResult.cs b/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/CicdResult.cs index db3a8b7..78be3a4 100644 --- a/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/CicdResult.cs +++ b/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/CicdResult.cs @@ -1,28 +1,28 @@ namespace Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib { - /// - /// Represents the result of a CICD process. - /// - public enum CicdResult - { - /// - /// Indicates a successful result. - /// - Success, + /// + /// Represents the result of a CICD process. + /// + public enum CicdResult + { + /// + /// Indicates a successful result. + /// + Success, - /// - /// Indicates an unstable result. - /// - Unstable, + /// + /// Indicates an unstable result. + /// + Unstable, - /// - /// Indicates a failed result. - /// - Failure, + /// + /// Indicates a failed result. + /// + Failure, - /// - /// Indicates a warning result. - /// - Warning - } + /// + /// Indicates a warning result. + /// + Warning + } } \ No newline at end of file From 8d64f20e8a0c57c40b4426afc23c061d309af46b Mon Sep 17 00:00:00 2001 From: Jan Staelens Date: Tue, 23 Jul 2024 10:48:10 +0200 Subject: [PATCH 3/3] Updated Ignore Message --- CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/CicdCardTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/CicdCardTests.cs b/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/CicdCardTests.cs index 42431ac..6652237 100644 --- a/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/CicdCardTests.cs +++ b/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/CicdCardTests.cs @@ -56,7 +56,7 @@ public async Task SendAsyncTest_TestFormatIsValidForTeams() } } - [TestMethod, Ignore] + [TestMethod, Ignore("For Manual Running. Fill in a valid webhook url to a teams workflow.")] public async Task SendAsyncTest_IntegrationTest() { // Arrange