Skip to content

Commit

Permalink
Change action to longer act as notifier but as parser
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Gamer025 committed Oct 16, 2021
1 parent 248703f commit bc943cb
Show file tree
Hide file tree
Showing 10 changed files with 131 additions and 39 deletions.
2 changes: 1 addition & 1 deletion CodeOwnersNotifier.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 25 additions & 2 deletions CodeOwnersNotifier/ActionInputs.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System;
using CommandLine;

namespace CodeOwnersNotifier
namespace CodeOwnersParser
{
public class ActionInputs
{
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<AssemblyName>CodeOwnersParser</AssemblyName>
<RootNamespace>CodeOwnersParser</RootNamespace>
</PropertyGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion CodeOwnersNotifier/Github/Pulls/PRComment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
using System.Text;
using System.Threading.Tasks;

namespace CodeOwnersNotifier.Github.Pulls
namespace CodeOwnersParser.Github.Pulls
{
class PRComment
{
Expand Down
2 changes: 1 addition & 1 deletion CodeOwnersNotifier/Github/Pulls/PRFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
using System.Text;
using System.Threading.Tasks;

namespace CodeOwnersNotifier.Github.Pulls
namespace CodeOwnersParser.Github.Pulls
{
class PRFile
{
Expand Down
2 changes: 1 addition & 1 deletion CodeOwnersNotifier/Github/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
using System.Text;
using System.Threading.Tasks;

namespace CodeOwnersNotifier.Github
namespace CodeOwnersParser.Github
{
public class User
{
Expand Down
62 changes: 49 additions & 13 deletions CodeOwnersNotifier/Helpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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
Expand All @@ -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;
}
Expand All @@ -121,45 +123,79 @@ 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>
/// <param name="comments">List of Github comments</param>
/// <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;
}

}
}
30 changes: 17 additions & 13 deletions CodeOwnersNotifier/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -20,27 +20,31 @@

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);

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}");
}

5 changes: 3 additions & 2 deletions CodeOwnersNotifier/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
36 changes: 31 additions & 5 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -45,5 +63,13 @@ runs:
- ${{ inputs.workspace }}
- '-f'
- ${{ inputs.file }}
- '-i'
- ${{ inputs.pullID }}
- '-d'
- ${{ inputs.separator }}
- '-p'
- ${{ inputs.pullID }}
- ${{ inputs.prefix }}
- '-s'
- ${{ inputs.sufix }}
- '-d'
- ${{ inputs.botname }}

0 comments on commit bc943cb

Please sign in to comment.