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 new file mode 100644 index 0000000..6747903 --- /dev/null +++ b/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests.csproj @@ -0,0 +1,30 @@ + + + net48;net8.0 + disable + disable + + false + true + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/CicdCardTests.cs b/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/CicdCardTests.cs new file mode 100644 index 0000000..6652237 --- /dev/null +++ b/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/CicdCardTests.cs @@ -0,0 +1,93 @@ +namespace CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests +{ + 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("For Manual Running. Fill in a valid webhook url to a teams workflow.")] + 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 new file mode 100644 index 0000000..887c38e --- /dev/null +++ b/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/MatchTeamsRequirement/ExpectedJson.cs @@ -0,0 +1,217 @@ +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; } + } +} \ 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..b1b6774 --- /dev/null +++ b/CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests/MatchTeamsRequirement/TeamsAdaptiveCardMatcher.cs @@ -0,0 +1,132 @@ +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; + } + } + } +} \ 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..dc3fa9e --- /dev/null +++ b/CICD.Tools.MSTeamsWorkflowWebhookCard/CICD.Tools.MSTeamsWorkflowWebhookCard.csproj @@ -0,0 +1,41 @@ + + + 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/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..2aa1eaf --- /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 0000000..38d067a Binary files /dev/null and b/CICD.Tools.MSTeamsWorkflowWebhookCard/nuget/Icon.png differ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e632315 --- /dev/null +++ b/LICENSE @@ -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/README.md b/README.md new file mode 100644 index 0000000..d2307db --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# Skyline.DataMiner.CICD.MSTeamsWorkflowWebhookCard + +## About + +Allows posting adaptive cards to an MSTeams workflow through a webhook. + +## Projects + +* For more information about Skyline.DataMiner.CICD.MSTeamsWorkflowWebhookCard, see [CICD.MSTeamsWorkflowWebhookCard/README.md](CICD.MSTeamsWorkflowWebhookCard/README.md). + +### 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 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..2063a5c --- /dev/null +++ b/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/CICD.Tools.MSTeamsWorkflowWebhookCard.Lib.csproj @@ -0,0 +1,32 @@ + + + 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 new file mode 100644 index 0000000..bc832da --- /dev/null +++ b/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/CicdCard.cs @@ -0,0 +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..."); + + 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 new file mode 100644 index 0000000..78be3a4 --- /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 0000000..38d067a Binary files /dev/null and b/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib/nuget/Icon.png differ diff --git a/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.sln b/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.sln new file mode 100644 index 0000000..ace974b --- /dev/null +++ b/Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.sln @@ -0,0 +1,43 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33213.308 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CICD.Tools.MSTeamsWorkflowWebhookCard", "CICD.Tools.MSTeamsWorkflowWebhookCard\CICD.Tools.MSTeamsWorkflowWebhookCard.csproj", "{99D81C1F-9251-4FBF-92E9-21A2AC844C8D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{EFB39731-1A7C-41CA-BA55-3C2AE52AE1D0}" + ProjectSection(SolutionItems) = preProject + LICENSE = LICENSE + README.md = README.md + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CICD.Tools.MSTeamsWorkflowWebhookCard.Lib", "Skyline.DataMiner.CICD.Tools.MSTeamsWorkflowWebhookCard.Lib\CICD.Tools.MSTeamsWorkflowWebhookCard.Lib.csproj", "{263068D9-49E0-4661-BEC1-51DF97CE8BA9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests", "CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests\CICD.Tools.MSTeamsWorkflowWebhookCard.LibTests.csproj", "{CBAC6031-4F95-4612-A8A9-EB56947C4C15}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {99D81C1F-9251-4FBF-92E9-21A2AC844C8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {99D81C1F-9251-4FBF-92E9-21A2AC844C8D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {99D81C1F-9251-4FBF-92E9-21A2AC844C8D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {99D81C1F-9251-4FBF-92E9-21A2AC844C8D}.Release|Any CPU.Build.0 = Release|Any CPU + {263068D9-49E0-4661-BEC1-51DF97CE8BA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {263068D9-49E0-4661-BEC1-51DF97CE8BA9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {263068D9-49E0-4661-BEC1-51DF97CE8BA9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {263068D9-49E0-4661-BEC1-51DF97CE8BA9}.Release|Any CPU.Build.0 = Release|Any CPU + {CBAC6031-4F95-4612-A8A9-EB56947C4C15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CBAC6031-4F95-4612-A8A9-EB56947C4C15}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CBAC6031-4F95-4612-A8A9-EB56947C4C15}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CBAC6031-4F95-4612-A8A9-EB56947C4C15}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F6118163-48FD-4823-92C5-2D50C077F934} + EndGlobalSection +EndGlobal