From bc943cb45368cf6f7c82925031f51159c7598c11 Mon Sep 17 00:00:00 2001 From: Gamer025 <33846895+Gamer025@users.noreply.github.com> Date: Sat, 16 Oct 2021 13:52:44 +0200 Subject: [PATCH] Change action to longer act as notifier but as parser Remove true/false output Now outputs the owner itself to use as check or input in different action Also output as formated text with user defined prefix and sufix which can then be used in comment posting action --- CodeOwnersNotifier.sln | 2 +- CodeOwnersNotifier/ActionInputs.cs | 27 +++++++- ...otifier.csproj => CodeOwnersParser.csproj} | 2 + CodeOwnersNotifier/Github/Pulls/PRComment.cs | 2 +- CodeOwnersNotifier/Github/Pulls/PRFile.cs | 2 +- CodeOwnersNotifier/Github/User.cs | 2 +- CodeOwnersNotifier/Helpers.cs | 62 +++++++++++++++---- CodeOwnersNotifier/Program.cs | 30 +++++---- .../Properties/launchSettings.json | 5 +- action.yml | 36 +++++++++-- 10 files changed, 131 insertions(+), 39 deletions(-) rename CodeOwnersNotifier/{CodeOwnersNotifier.csproj => CodeOwnersParser.csproj} (83%) diff --git a/CodeOwnersNotifier.sln b/CodeOwnersNotifier.sln index b985ca3..735f64b 100644 --- a/CodeOwnersNotifier.sln +++ b/CodeOwnersNotifier.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.31402.337 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CodeOwnersNotifier", "CodeOwnersNotifier\CodeOwnersNotifier.csproj", "{0498B1C2-6A6F-44CA-A484-82158D812FFA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CodeOwnersParser", "CodeOwnersNotifier\CodeOwnersParser.csproj", "{0498B1C2-6A6F-44CA-A484-82158D812FFA}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{19BE45FC-47E3-41FD-ACD4-782A0EAECF41}" ProjectSection(SolutionItems) = preProject diff --git a/CodeOwnersNotifier/ActionInputs.cs b/CodeOwnersNotifier/ActionInputs.cs index 0ef4a29..bc507ce 100644 --- a/CodeOwnersNotifier/ActionInputs.cs +++ b/CodeOwnersNotifier/ActionInputs.cs @@ -1,7 +1,7 @@ using System; using CommandLine; -namespace CodeOwnersNotifier +namespace CodeOwnersParser { public class ActionInputs { @@ -37,11 +37,34 @@ public string Name Default = "/.github/CODEOWNERS")] public string file { get; set; } = null!; - [Option('p', "pullID", + [Option('i', "pullID", Required = true, HelpText = "ID of the PR. Assign from `github.event_path.pull_request.number`.")] public string pullID { get; set; } = null!; + [Option('d', "separator", + Required = false, + HelpText = "String used to seperate multiple owners in the output.", + Default = " ")] + public string seperator { get; set; } = null!; + + [Option('p', "prefix", + Required = false, + HelpText = "Will be prefixed to the output, useful if output is used in comments action. Also used for finding existing mentions.", + Default = "")] + public string prefix { get; set; } = null!; + + [Option('s', "sufix", + Required = false, + HelpText = "Will be prefixed to the output, useful if output is used in comments action. Also used for finding existing mentions.", + Default = "")] + public string sufix { get; set; } = null!; + + [Option('b', "botname", + Required = false, + HelpText = "If set existing comments of this user will be parsed to find already mentioned users.")] + public string botname { get; set; } = null!; + static void ParseAndAssign(string? value, Action<string> assign) { if (value is { Length: > 0 } && assign is not null) diff --git a/CodeOwnersNotifier/CodeOwnersNotifier.csproj b/CodeOwnersNotifier/CodeOwnersParser.csproj similarity index 83% rename from CodeOwnersNotifier/CodeOwnersNotifier.csproj rename to CodeOwnersNotifier/CodeOwnersParser.csproj index 0b5858a..45d4baf 100644 --- a/CodeOwnersNotifier/CodeOwnersNotifier.csproj +++ b/CodeOwnersNotifier/CodeOwnersParser.csproj @@ -4,6 +4,8 @@ <OutputType>Exe</OutputType> <TargetFramework>net5.0</TargetFramework> <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> + <AssemblyName>CodeOwnersParser</AssemblyName> + <RootNamespace>CodeOwnersParser</RootNamespace> </PropertyGroup> <ItemGroup> diff --git a/CodeOwnersNotifier/Github/Pulls/PRComment.cs b/CodeOwnersNotifier/Github/Pulls/PRComment.cs index cc8eae0..05cb0d2 100644 --- a/CodeOwnersNotifier/Github/Pulls/PRComment.cs +++ b/CodeOwnersNotifier/Github/Pulls/PRComment.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace CodeOwnersNotifier.Github.Pulls +namespace CodeOwnersParser.Github.Pulls { class PRComment { diff --git a/CodeOwnersNotifier/Github/Pulls/PRFile.cs b/CodeOwnersNotifier/Github/Pulls/PRFile.cs index 5ac1042..3d811c8 100644 --- a/CodeOwnersNotifier/Github/Pulls/PRFile.cs +++ b/CodeOwnersNotifier/Github/Pulls/PRFile.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace CodeOwnersNotifier.Github.Pulls +namespace CodeOwnersParser.Github.Pulls { class PRFile { diff --git a/CodeOwnersNotifier/Github/User.cs b/CodeOwnersNotifier/Github/User.cs index 314adeb..324b3d9 100644 --- a/CodeOwnersNotifier/Github/User.cs +++ b/CodeOwnersNotifier/Github/User.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace CodeOwnersNotifier.Github +namespace CodeOwnersParser.Github { public class User { diff --git a/CodeOwnersNotifier/Helpers.cs b/CodeOwnersNotifier/Helpers.cs index a2e9818..3a11d70 100644 --- a/CodeOwnersNotifier/Helpers.cs +++ b/CodeOwnersNotifier/Helpers.cs @@ -5,9 +5,9 @@ using System.Threading.Tasks; using System.Text.RegularExpressions; using System.IO; -using CodeOwnersNotifier.Github.Pulls; +using CodeOwnersParser.Github.Pulls; -namespace CodeOwnersNotifier +namespace CodeOwnersParser { static class Helpers { @@ -100,6 +100,8 @@ public static string GenerateRegexForCodeownerPath(string path) //File: ^.*(?<=(\/|^))(Test1\.txt)$ // ^.*(?<=(\/|^))(Test1\.txt)(?=$|\/).*$ string regexString = ""; + //String containing the prepared path (escapeing, replacing etc.). Used so path doesn't get modified because its used as key + string preparedPath; //Entry ends with / aka only mathches folders not files bool folderOnly = false; //No slash at the beginning or middle, can match at any depth @@ -108,7 +110,7 @@ public static string GenerateRegexForCodeownerPath(string path) bool fileMode = false; //If Regex for entry was already calcualted return that - if (pathRegexLookup.TryGetValue(path.TrimStart('/'), out regexString)) + if (pathRegexLookup.TryGetValue(path, out regexString)) { return regexString; } @@ -121,28 +123,42 @@ public static string GenerateRegexForCodeownerPath(string path) fileMode = true; //Remove leading slash before generating Regex as modified files from PR don't start with slash aka src/code/Program.cs and not /src... - path = path.TrimStart('/'); - Regex.Escape(path); + preparedPath = path.TrimStart('/'); - path = path.Replace("*", @"[^\/]*"); - path = path.Replace("?", @"[^\/]"); + preparedPath = EscapeOwnerEntry(preparedPath); if (fileMode) { - regexString = @$"(?<=(\/|^))({path})(?=$)"; + regexString = @$"(?<=(\/|^))({preparedPath})(?=$)"; pathRegexLookup.Add(path, regexString); return regexString; } else { - regexString = path; + regexString = preparedPath; pathRegexLookup.Add(path, regexString); return regexString; } } + /// <summary> + /// Formats/escapes a Codeowner entry, e.g replace *,? with the corresponding regex syntax and escape special chars + /// </summary> + /// <param name="entry"></param> + /// <returns></returns> + public static string EscapeOwnerEntry (string entry) + { + //Escape the input + entry = Regex.Escape(entry); + + //Replace the new escaped chars with special meaning (?,*,**) with Regex that emualtes gitignore behaviour + entry = entry.Replace(@"\*\*", @".?*"); + entry = entry.Replace(@"\*", @"[^\/]*"); + entry = entry.Replace(@"\?", @"[^\/]"); + return entry; + } /// <summary> /// Get already mentioned owners from a list of Github comments /// </summary> @@ -150,16 +166,36 @@ public static string GenerateRegexForCodeownerPath(string path) /// <param name="username">Name of the user posting the mentions</param> /// <param name="bodyPrefix">Prefix of notify comments</param> /// <returns></returns> - public static List<string> GetMentionedOwners(List<PRComment> comments, string username, string bodyPrefix) + public static List<string> GetMentionedOwners(List<PRComment> comments, string username, string bodyPrefix, string bodySuffix, string separator) { return comments .Where( - comment => comment.user.login == username && comment.body.StartsWith(bodyPrefix)) - .Select(comment => comment.body[bodyPrefix.Length..]) - .SelectMany(owners => owners.Split(" ").ToList()) + comment => comment.user.login == username && comment.body.StartsWith(bodyPrefix) && comment.body.EndsWith(bodySuffix)) + .Select(comment => comment.body) + .Select (body => body.TrimStart(bodyPrefix).TrimEnd(bodySuffix)) + .SelectMany(owners => owners.Split(separator)) .Distinct() .ToList(); } + + public static string TrimStart(this string input, string trimString) + { + if (!String.IsNullOrEmpty(input) && input.StartsWith(trimString)) + { + return input.Remove(0, trimString.Length); + } + return input; + } + + public static string TrimEnd(this string input, string trimString) + { + if (!String.IsNullOrEmpty(input) && input.EndsWith(trimString)) + { + return input.Remove(input.Length-trimString.Length-1, trimString.Length); + } + return input; + } + } } diff --git a/CodeOwnersNotifier/Program.cs b/CodeOwnersNotifier/Program.cs index 91197b9..5ceb1dd 100644 --- a/CodeOwnersNotifier/Program.cs +++ b/CodeOwnersNotifier/Program.cs @@ -2,11 +2,11 @@ using System.Linq; using CommandLine; using static CommandLine.Parser; -using CodeOwnersNotifier; +using CodeOwnersParser; using System.Collections.Generic; using System.Net.Http.Json; using System.Net.Http; -using CodeOwnersNotifier.Github.Pulls; +using CodeOwnersParser.Github.Pulls; var parser = Default.ParseArguments<ActionInputs>(() => new(), args); parser.WithNotParsed( @@ -20,8 +20,6 @@ static void NotifyOwners(ActionInputs inputs) { - string commentBody = "Notifying code owners: "; - string botname = "github-actions[bot]"; Console.WriteLine($"Parsing codeowner file at: {inputs.WorkspaceDirectory}{inputs.file}"); Dictionary<string, List<string>> codeowners = Helpers.ParseCodeownersFile(inputs.WorkspaceDirectory + inputs.file); @@ -29,18 +27,24 @@ static void NotifyOwners(ActionInputs inputs) HttpClient httpClient = new HttpClient(); httpClient.BaseAddress = new Uri("https://api.github.com"); //Add User-Agent otherwise Github API will return 403 - httpClient.DefaultRequestHeaders.Add("User-Agent", "CodeownersNotifier"); + httpClient.DefaultRequestHeaders.Add("User-Agent", "CodeOwnersParser"); Console.WriteLine($"Getting PR files from: {httpClient.BaseAddress}repos/{inputs.Owner}/{inputs.Name}/pulls/{inputs.pullID}/files"); PRFile[] modifiedFiles = httpClient.GetFromJsonAsync<PRFile[]>($"repos/{inputs.Owner}/{inputs.Name}/pulls/{inputs.pullID}/files").Result; List<string> ownersWithModifiedFiles = Helpers.GetOwnersWithModifiedFiles(codeowners, modifiedFiles.ToList()); - PRComment[] PRcomments = httpClient.GetFromJsonAsync<PRComment[]>($"repos/{inputs.Owner}/{inputs.Name}/issues/{inputs.pullID}/comments").Result; - List<string> notifiedOwners = Helpers.GetMentionedOwners(PRcomments.ToList(), botname, commentBody); - List<string> ownersToNotify = ownersWithModifiedFiles.Except(notifiedOwners).ToList(); - - Console.WriteLine($"Comment needed: {(ownersToNotify.Count > 0 ? "true" : "false")}"); - Console.WriteLine($"::set-output name=comment-needed::{(ownersToNotify.Count > 0 ? "true" : "false")}"); - Console.WriteLine($"Owners to notify: {String.Join(" ", ownersToNotify)}"); - Console.WriteLine($"::set-output name=comment-content::{commentBody} {String.Join(" ", ownersToNotify)}"); + //If we were provided a botname parse its comments and find already notifed owners + if (inputs.botname is not null) + { + PRComment[] PRcomments = httpClient.GetFromJsonAsync<PRComment[]>($"repos/{inputs.Owner}/{inputs.Name}/issues/{inputs.pullID}/comments").Result; + List<string> notifiedOwners = Helpers.GetMentionedOwners(PRcomments.ToList(), inputs.botname, inputs.prefix, inputs.sufix, inputs.seperator); + ownersWithModifiedFiles = ownersWithModifiedFiles.Except(notifiedOwners).ToList(); + } + + string output = String.Join(inputs.seperator, ownersWithModifiedFiles); + + Console.WriteLine($"Owners with file changes: {output}"); + Console.WriteLine($"::set-output name=owners::{output}"); + Console.WriteLine($"Output: {inputs.prefix + output + inputs.sufix}"); + Console.WriteLine($"::set-output name=formated-owners::{inputs.prefix + output + inputs.sufix}"); } diff --git a/CodeOwnersNotifier/Properties/launchSettings.json b/CodeOwnersNotifier/Properties/launchSettings.json index c442ae9..5a321d7 100644 --- a/CodeOwnersNotifier/Properties/launchSettings.json +++ b/CodeOwnersNotifier/Properties/launchSettings.json @@ -1,7 +1,8 @@ { "profiles": { - "CodeOwnersNotifier": { - "commandName": "Project" + "CodeOwnersParser": { + "commandName": "Project", + "commandLineArgs": "-o Gamer025 -n CodeownerTest -w \"D:\\lotsofspace\\CodeOwnerTest\\CodeOwnerTest\" -i 19 -b github-actions[bot]" }, "Docker": { "commandName": "Docker" diff --git a/action.yml b/action.yml index 6f6da77..91f91e1 100644 --- a/action.yml +++ b/action.yml @@ -26,13 +26,31 @@ inputs: description: "ID of the PR to process. Assign from `github.event_path.pull_request.number`." required: true + separator: + description: + "Separator used if multiple owners are returned. Defaults to space." + required: true + prefix: + description: + "Prefix that will be added to formated-owners output + used for finding existing mentions." + required: false + default: "" + sufix: + description: + "Sufix that will be added to formated-owners output + used for finding existing mentions." + required: false + default: "" + botname: + description: + "Name of the bot/user that posts notification comments. If this is specified only owners that haven't been mentioned by this user will be output. Should be 'github-actions[bot]'" + required: false outputs: - comment-needed: + owners: description: - 'true/false to determine if there are codeowners to be notified' - comment-content: + 'Raw list of owners, seperated with the separator parameter. Use this to determine if owners were found and as input for other actions.' + formated-owners: description: - 'Comment contend to be used in seperate action e.g github-scripts' + 'Owners with sufix and prefix paramter added. Use this output as input for comment posting actions.' runs: using: 'docker' image: 'Dockerfile' @@ -45,5 +63,13 @@ runs: - ${{ inputs.workspace }} - '-f' - ${{ inputs.file }} + - '-i' + - ${{ inputs.pullID }} + - '-d' + - ${{ inputs.separator }} - '-p' - - ${{ inputs.pullID }} \ No newline at end of file + - ${{ inputs.prefix }} + - '-s' + - ${{ inputs.sufix }} + - '-d' + - ${{ inputs.botname }} \ No newline at end of file