Skip to content

Commit c596320

Browse files
authored
Fix milestone semantic in Issue Search (#35)
If milestone was not defined in the rule, then only issues with milestones would be searched. The semantic is now: - No milestone set, consider issues without or without milestones - Milestone is set consider only issues with the given milestone - Milestone is * consider only issues with a milestone (any) - Milestone is NONE consider only issues with no milestones set Code was refactored and unit tests were added
1 parent 37903d5 commit c596320

File tree

5 files changed

+735
-208
lines changed

5 files changed

+735
-208
lines changed

docs/Configuring.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,10 @@ The configuration for issues has the following attributes:
104104
- `Assignee` (Optional. String). The user to search for issues assigned to. If omitted assignee(s) are not relevant.
105105
- `Author` (Optional. String). The user to search for issues created by. If omitted author is not relevant.
106106
- `Mention` (Optional. String). The user or team to search for issues mentioned in. If omitted mention is not relevant.
107-
- `Milestone` (Optional. Integer). The milestone **number** (not name) to search for issues in. If omitted milestone is not relevant (you can also specify `*` for all milestones).
107+
- `Milestone` (Optional. Integer). The milestone **number** (not name) to search for issues in.
108+
- If omitted milestone (or empty) is not relevant, issues with or without a milestone will be considered.
109+
- Use `*` issues that have any milestone set.
110+
- Use `NONE` to search for issues with no milestone.
108111
- `Labels` (Optional. Array of strings). The labels to search for issues with. If omitted labels are not relevant.
109112
- `OnlyCreatedBeforeWorkflowCreated` (optional. Boolean). If true only issues created before the workflow run was started will be considered. If false all issues will be considered.
110113

src/Issues.Gate/Models/IssueGateIssues.cs

+2-3
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ public class IssueGateIssues
1616
public string Milestone { get; set; }
1717
public List<string> Labels { get; set; }
1818
public string Message { get; set; }
19-
2019
public bool OnlyCreatedBeforeWorkflowCreated { get; set; }
2120
public IList<string> Validate()
2221
{
@@ -44,9 +43,9 @@ public IList<string> Validate()
4443
{
4544
errors.Add("If Mention is specified it cannot be empty");
4645
}
47-
if (Milestone != null && string.IsNullOrWhiteSpace(Milestone))
46+
if (Milestone != null && (Milestone != "NONE" && Milestone != "*" && !long.TryParse(Milestone, out long _)))
4847
{
49-
errors.Add("If Milestone is specified it cannot be empty");
48+
errors.Add("Milestone needs to be either a number, * or NONE");
5049
}
5150
if (Message != null && string.IsNullOrWhiteSpace(Message))
5251
{

src/Issues.Gate/Rules/IssueGateRulesEvaluator.cs

+67-20
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
using System.Text;
77
using GitHubActions.Gates.Framework.Models;
88
using Microsoft.Extensions.Logging;
9+
using System.Runtime.CompilerServices;
10+
11+
// Needed for unit tests
12+
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
913

1014
namespace Issues.Gate.Rules
1115
{
@@ -22,7 +26,7 @@ public IssueGateRulesEvaluator(IGitHubAppClient client, ILogger log, IssuesConfi
2226
_log = log;
2327
}
2428

25-
public async Task<string> ValidateRules(string environment, Repo repository, long RunId)
29+
public async virtual Task<string> ValidateRules(string environment, Repo repository, long RunId)
2630
{
2731
var rule = _configuration.GetRule(environment) ?? throw new RejectException($"No rule found for {environment} environment");
2832

@@ -57,13 +61,13 @@ public async Task<string> ValidateRules(string environment, Repo repository, lon
5761
return comment.ToString();
5862
}
5963

60-
internal async Task ExecuteIssuesQuery(Repo Repository, IssueGateRule rule, DateTime? workflowCreatedAt, StringBuilder comment)
64+
internal async virtual Task ExecuteIssuesQuery(Repo Repository, IssueGateRule rule, DateTime? workflowCreatedAt, StringBuilder comment)
6165
{
6266
var repo = rule.Issues.Repo != null ? new Repo(rule.Issues.Repo) : Repository;
6367

6468
var graphQLQuery = $@"query($owner: String!, $repo: String!, $limit: Int, $states: [IssueState!] = OPEN, $assignee: String, $author: String, $mention: String, $milestone: String, $labels: [String!], $since: DateTime) {{
6569
repository(owner: $owner, name: $repo) {{
66-
before: issues(first: $limit, states: $states, filterBy: {{ assignee: $assignee, createdBy: $author, mentioned: $mention, milestoneNumber: $milestone, labels: $labels}}) {{
70+
total: issues(first: $limit, states: $states, filterBy: {{ assignee: $assignee, createdBy: $author, mentioned: $mention, milestoneNumber: $milestone, labels: $labels}}) {{
6771
totalCount
6872
}}
6973
after: issues(first: $limit, states: $states, filterBy: {{ since: $since, assignee: $assignee, createdBy: $author, mentioned: $mention, milestoneNumber: $milestone, labels: $labels}}) {{
@@ -72,27 +76,15 @@ internal async Task ExecuteIssuesQuery(Repo Repository, IssueGateRule rule, Date
7276
}}
7377
}}";
7478

75-
var parameters = new
76-
{
77-
owner = repo.Owner,
78-
repo = repo.Name,
79-
limit = 0,
80-
states = rule.Issues.State,
81-
assignee = rule.Issues.Assignee,
82-
author = rule.Issues.Author,
83-
mention = rule.Issues.Mention,
84-
milestone = rule.Issues.Milestone ?? "*",
85-
labels = rule.Issues.Labels,
86-
since = workflowCreatedAt
87-
};
79+
object parameters = BuildIssuesQueryParameters(repo, rule.Issues, workflowCreatedAt);
8880

8981
try
9082
{
9183
dynamic response = await _client.GraphQLAsync(graphQLQuery, parameters);
9284

93-
int nrIssues = response.data.repository.before.totalCount;
85+
int nrIssues = response.data.repository.total.totalCount;
9486

95-
_log.LogInformation($"IssuesQuery: Before {nrIssues} After {response.data.repository.after.totalCount} issues with max {rule.Issues.MaxAllowed}");
87+
_log.LogInformation($"IssuesQuery: Total {nrIssues} After {response.data.repository.after.totalCount} issues with max {rule.Issues.MaxAllowed}");
9688

9789
if (rule.Issues.OnlyCreatedBeforeWorkflowCreated)
9890
{
@@ -118,8 +110,7 @@ internal async Task ExecuteIssuesQuery(Repo Repository, IssueGateRule rule, Date
118110
RejectWithErrors("Issues", e);
119111
}
120112
}
121-
122-
internal async Task ExecuteSearchQuery(IssueGateRule rule, DateTime? workflowCreatedAt, StringBuilder comment)
113+
internal async virtual Task ExecuteSearchQuery(IssueGateRule rule, DateTime? workflowCreatedAt, StringBuilder comment)
123114
{
124115
string query = BuildSearchQuery(rule, workflowCreatedAt);
125116

@@ -161,6 +152,62 @@ internal static string BuildSearchQuery(IssueGateRule rule, DateTime? workflowCr
161152
return query;
162153
}
163154

155+
/// <summary>
156+
/// Build the parameters for the Issues GraphQL query
157+
///
158+
/// Enforces the following semantic for milestones
159+
/// <list type="bullet">
160+
/// <item>If Milestone filter is specified then only issues with that given milestone are returned (use * for any milestone)</item>
161+
/// <item>If Milestone filter is not specified then milestone value should be ignored(all other filter applies)</item>
162+
/// <item>if Milestone filter is NONE then only issues with no milestones should be considered</item>
163+
/// </list>
164+
/// </summary>
165+
/// <param name="repo"></param>
166+
/// <param name="issues"></param>
167+
/// <param name="workflowCreatedAt"></param>
168+
/// <returns></returns>
169+
internal static object BuildIssuesQueryParameters(Repo repo, IssueGateIssues issues, DateTime? workflowCreatedAt)
170+
{
171+
// milestone filter is ONLY added when is not null
172+
173+
// Very ugly code that doesn't scale if we need to apply same logic to other fields.
174+
// Dealing with expandos was not nicer
175+
176+
var parameters = new
177+
{
178+
owner = repo.Owner,
179+
repo = repo.Name,
180+
limit = 0,
181+
states = issues.State,
182+
assignee = issues.Assignee,
183+
author = issues.Author,
184+
mention = issues.Mention,
185+
milestone = issues.Milestone == "NONE" ? null : issues.Milestone,
186+
labels = issues.Labels,
187+
since = workflowCreatedAt
188+
};
189+
190+
if (issues.Milestone == null)
191+
{
192+
// milestone is NOT added on purpose
193+
return new
194+
{
195+
parameters.owner,
196+
parameters.repo,
197+
parameters.limit,
198+
parameters.states,
199+
parameters.assignee,
200+
parameters.author,
201+
parameters.mention,
202+
parameters.labels,
203+
parameters.since
204+
};
205+
}
206+
207+
return parameters;
208+
}
209+
210+
164211
private static void RejectWithErrors(string queryType, GraphQLException e)
165212
{
166213
StringBuilder mdErrors = new();

0 commit comments

Comments
 (0)