diff --git a/src/Nullinside.Cicd.GitHub/.editorconfig b/src/Nullinside.Cicd.GitHub/.editorconfig
index 5ef8a11..d8f5d6e 100644
--- a/src/Nullinside.Cicd.GitHub/.editorconfig
+++ b/src/Nullinside.Cicd.GitHub/.editorconfig
@@ -4,6 +4,13 @@ root = true
[*]
indent_style = space
+# ReSharper properties
+resharper_csharp_wrap_lines = false
+
+# We don't want to code comment auto-generated files
+[GraphQl/Model/*.cs]
+dotnet_diagnostic.CS1591.severity = none
+
# Xml files
[*.xml]
indent_size = 2
diff --git a/src/Nullinside.Cicd.GitHub/Constants.cs b/src/Nullinside.Cicd.GitHub/Constants.cs
index f3d82cc..89ef120 100644
--- a/src/Nullinside.Cicd.GitHub/Constants.cs
+++ b/src/Nullinside.Cicd.GitHub/Constants.cs
@@ -9,6 +9,16 @@ public static class Constants {
///
public const string GITHUB_ORG = "nullinside-development-group";
+ ///
+ /// The url to the graphql endpoint to post against.
+ ///
+ public const string GITHUB_GRAPHQL_URL = "https://api.github.com/graphql";
+
+ ///
+ /// The name of the project that be sent in user agent headers.
+ ///
+ public const string PROJECT_NAME = "nullinside-cicd-github";
+
///
/// The github project's unique identifier on github.
///
diff --git a/src/Nullinside.Cicd.GitHub/GraphQl/AbstractGitHubGraphQlQuery.cs b/src/Nullinside.Cicd.GitHub/GraphQl/AbstractGitHubGraphQlQuery.cs
new file mode 100644
index 0000000..4ee55ae
--- /dev/null
+++ b/src/Nullinside.Cicd.GitHub/GraphQl/AbstractGitHubGraphQlQuery.cs
@@ -0,0 +1,54 @@
+using System.Net.Http.Headers;
+using System.Text;
+
+using Newtonsoft.Json;
+
+using Nullinside.Cicd.GitHub.GraphQl.Model;
+
+namespace Nullinside.Cicd.GitHub.GraphQl;
+
+///
+/// The contract for executing a simple graphql query.
+///
+/// The JSON response POCO to the query, for no response.
+public abstract class AbstractGitHubGraphQlQuery {
+ ///
+ /// The query, typically coded into the object.
+ ///
+ public abstract string Query { get; }
+
+ ///
+ /// The query variables, typically assembled in the constructor.
+ ///
+ public object? QueryVariables { get; set; }
+
+ ///
+ /// Executes the request.
+ ///
+ /// The response object.
+ public virtual async Task SendAsync() {
+ using var httpClient = new HttpClient {
+ BaseAddress = new Uri(Constants.GITHUB_GRAPHQL_URL),
+ DefaultRequestHeaders = {
+ UserAgent = { new ProductInfoHeaderValue(Constants.PROJECT_NAME, "0.0.0") },
+ Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("GITHUB_PAT"))
+ }
+ };
+
+ var queryObject = new {
+ query = Query,
+ variables = QueryVariables
+ };
+
+ var request = new HttpRequestMessage {
+ Method = HttpMethod.Post,
+ Content = new StringContent(JsonConvert.SerializeObject(queryObject), Encoding.UTF8, "application/json")
+ };
+
+ using HttpResponseMessage response = await httpClient.SendAsync(request);
+ response.EnsureSuccessStatusCode();
+ string responseString = await response.Content.ReadAsStringAsync();
+
+ return JsonConvert.DeserializeObject(responseString);
+ }
+}
\ No newline at end of file
diff --git a/src/Nullinside.Cicd.GitHub/GraphQl/Model/GraphQlGenericMutationResponse.cs b/src/Nullinside.Cicd.GitHub/GraphQl/Model/GraphQlGenericMutationResponse.cs
new file mode 100644
index 0000000..72fce99
--- /dev/null
+++ b/src/Nullinside.Cicd.GitHub/GraphQl/Model/GraphQlGenericMutationResponse.cs
@@ -0,0 +1,22 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+using System.Globalization;
+
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
+namespace Nullinside.Cicd.GitHub.GraphQl.Model;
+
+
+public partial class GraphQlGenericMutationResponse
+{
+ [JsonProperty("data")]
+ public object Data { get; set; }
+}
diff --git a/src/Nullinside.Cicd.GitHub/GraphQl/Model/GraphQlProjectIssueResponse.cs b/src/Nullinside.Cicd.GitHub/GraphQl/Model/GraphQlProjectIssueResponse.cs
new file mode 100644
index 0000000..f7903a8
--- /dev/null
+++ b/src/Nullinside.Cicd.GitHub/GraphQl/Model/GraphQlProjectIssueResponse.cs
@@ -0,0 +1,68 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+using Newtonsoft.Json;
+
+namespace Nullinside.Cicd.GitHub.Model;
+
+public class GraphQlProjectIssueResponse {
+ [JsonProperty("data")] public Data Data { get; set; }
+}
+
+public class Data {
+ [JsonProperty("repository")] public Repository Repository { get; set; }
+}
+
+public class Repository {
+ [JsonProperty("id")] public string Id { get; set; }
+
+ [JsonProperty("issues")] public Issues Issues { get; set; }
+}
+
+public class Issues {
+ [JsonProperty("nodes")] public List Nodes { get; set; }
+
+ [JsonProperty("pageInfo")] public PageInfo PageInfo { get; set; }
+}
+
+public class IssuesNode {
+ [JsonProperty("number")] public long Number { get; set; }
+
+ [JsonProperty("closed")] public bool Closed { get; set; }
+
+ [JsonProperty("id")] public string Id { get; set; }
+
+ [JsonProperty("projectItems")] public ProjectItems ProjectItems { get; set; }
+}
+
+public class ProjectItems {
+ [JsonProperty("nodes")] public List Nodes { get; set; }
+
+ [JsonProperty("pageInfo")] public PageInfo PageInfo { get; set; }
+}
+
+public class ProjectItemsNode {
+ [JsonProperty("fieldValueByName")] public FieldValueByName FieldValueByName { get; set; }
+
+ [JsonProperty("id")] public string Id { get; set; }
+}
+
+public class FieldValueByName {
+ [JsonProperty("field")] public Field Field { get; set; }
+
+ [JsonProperty("name")] public string Name { get; set; }
+}
+
+public class Field {
+ [JsonProperty("id")] public string Id { get; set; }
+}
+
+public class PageInfo {
+ [JsonProperty("hasNextPage")] public bool HasNextPage { get; set; }
+}
\ No newline at end of file
diff --git a/src/Nullinside.Cicd.GitHub/GraphQl/MutateIssueProjectStatuses.cs b/src/Nullinside.Cicd.GitHub/GraphQl/MutateIssueProjectStatuses.cs
new file mode 100644
index 0000000..033a6c4
--- /dev/null
+++ b/src/Nullinside.Cicd.GitHub/GraphQl/MutateIssueProjectStatuses.cs
@@ -0,0 +1,28 @@
+using Nullinside.Cicd.GitHub.GraphQl.Model;
+
+namespace Nullinside.Cicd.GitHub.GraphQl;
+
+///
+/// Changes the status of the issue on the GitHub project board. (ex: Backlog, Ready, In progress, In review, Done)
+///
+public class MutateIssueProjectStatuses : AbstractGitHubGraphQlQuery {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The project id
+ /// The status field's id (ex: PVTI_lADOCZOBm84AdCw7zgQfohA NOT I_kwDOLcGb986MVe7p)
+ /// The status field's id (ex: PVTSSF_lADOCZOBm84AdCw7zgSzu_A)
+ /// The unique identifier of the single selection option to change the status to.
+ public MutateIssueProjectStatuses(string projectId, string projectIssueId, string statusFieldId, string selectionId) {
+ QueryVariables = new { project = projectId, item = projectIssueId, field = statusFieldId, selectionId };
+ }
+
+ ///
+ public override string Query => @"mutation($project: ID!, $item: ID!, $field: ID!, $selectionId: String!) {
+ updateProjectV2ItemFieldValue(
+ input: {projectId: $project, itemId: $item, fieldId: $field, value: {singleSelectOptionId: $selectionId}}
+ ) {
+ clientMutationId
+ }
+ }";
+}
\ No newline at end of file
diff --git a/src/Nullinside.Cicd.GitHub/GraphQl/QueryIssueProjectStatuses.cs b/src/Nullinside.Cicd.GitHub/GraphQl/QueryIssueProjectStatuses.cs
new file mode 100644
index 0000000..837b0fc
--- /dev/null
+++ b/src/Nullinside.Cicd.GitHub/GraphQl/QueryIssueProjectStatuses.cs
@@ -0,0 +1,54 @@
+using Nullinside.Cicd.GitHub.Model;
+
+namespace Nullinside.Cicd.GitHub.GraphQl;
+
+///
+/// Queries the issues in a GitHub code base.
+///
+public class QueryIssueProjectStatuses : AbstractGitHubGraphQlQuery {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The owner of the repo on github.
+ /// The name of the repo on github.
+ public QueryIssueProjectStatuses(string repoOwner, string repoName) {
+ QueryVariables = new { owner = repoOwner, name = repoName };
+ }
+
+ ///
+ public override string Query => @"query ($name: String!, $owner: String!) {
+ repository(name: $name, owner: $owner) {
+ id
+ issues(first: 100) {
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
+ nodes {
+ id
+ number
+ closed
+ projectItems(first: 100) {
+ pageInfo {
+ hasNextPage
+ endCursor
+ }
+ nodes {
+ id
+ fieldValueByName(name: ""Status"") {
+ ... on ProjectV2ItemFieldSingleSelectValue {
+ name
+ field {
+ ... on ProjectV2SingleSelectField {
+ id
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }";
+}
\ No newline at end of file
diff --git a/src/Nullinside.Cicd.GitHub/Nullinside.Cicd.GitHub.csproj b/src/Nullinside.Cicd.GitHub/Nullinside.Cicd.GitHub.csproj
index 7ef8816..463235b 100644
--- a/src/Nullinside.Cicd.GitHub/Nullinside.Cicd.GitHub.csproj
+++ b/src/Nullinside.Cicd.GitHub/Nullinside.Cicd.GitHub.csproj
@@ -19,20 +19,20 @@
-
-
-
+
+
+
-
+
-
-
- Always
-
+
+
+ Always
+
diff --git a/src/Nullinside.Cicd.GitHub/Program.cs b/src/Nullinside.Cicd.GitHub/Program.cs
index 1f63f76..10c1b28 100644
--- a/src/Nullinside.Cicd.GitHub/Program.cs
+++ b/src/Nullinside.Cicd.GitHub/Program.cs
@@ -2,7 +2,6 @@
using log4net;
using log4net.Config;
-using log4net.Core;
using Nullinside.Cicd.GitHub;
using Nullinside.Cicd.GitHub.Rule;
@@ -16,7 +15,7 @@
using Query = Octokit.GraphQL.Query;
XmlConfigurator.Configure(new FileInfo("log4net.config"));
-var log = LogManager.GetLogger(typeof(Program));
+ILog log = LogManager.GetLogger(typeof(Program));
IRepoRule?[] rules = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(a => a.GetTypes())
diff --git a/src/Nullinside.Cicd.GitHub/Rule/AssociateIssuesWithProject.cs b/src/Nullinside.Cicd.GitHub/Rule/AssociateIssuesWithProject.cs
index 9bea242..46a38a9 100644
--- a/src/Nullinside.Cicd.GitHub/Rule/AssociateIssuesWithProject.cs
+++ b/src/Nullinside.Cicd.GitHub/Rule/AssociateIssuesWithProject.cs
@@ -15,10 +15,10 @@ namespace Nullinside.Cicd.GitHub.Rule;
///
public class AssociateIssuesWithProject : IRepoRule {
///
- /// The logger
+ /// The logger
///
- private ILog _log = LogManager.GetLogger(typeof(AssociateIssuesWithProject));
-
+ private readonly ILog _log = LogManager.GetLogger(typeof(AssociateIssuesWithProject));
+
///
public async Task Handle(GitHubClient client, Connection graphQl, ID projectId, Repository repo) {
if (!repo.HasIssues) {
diff --git a/src/Nullinside.Cicd.GitHub/Rule/CreateRulesets.cs b/src/Nullinside.Cicd.GitHub/Rule/CreateRulesets.cs
index 336553e..98e6fa3 100644
--- a/src/Nullinside.Cicd.GitHub/Rule/CreateRulesets.cs
+++ b/src/Nullinside.Cicd.GitHub/Rule/CreateRulesets.cs
@@ -15,10 +15,10 @@ namespace Nullinside.Cicd.GitHub.Rule;
///
public class CreateRulesets : IRepoRule {
///
- /// The logger
+ /// The logger
///
- private ILog _log = LogManager.GetLogger(typeof(CreateRulesets));
-
+ private readonly ILog _log = LogManager.GetLogger(typeof(CreateRulesets));
+
///
public async Task Handle(GitHubClient client, Connection graphQl, ID projectId, Repository repo) {
// This currently doesn't run properly. You get an error about not specifying multiple Parameters on the status
diff --git a/src/Nullinside.Cicd.GitHub/Rule/MoveClosedToDone.cs b/src/Nullinside.Cicd.GitHub/Rule/MoveClosedToDone.cs
new file mode 100644
index 0000000..2852846
--- /dev/null
+++ b/src/Nullinside.Cicd.GitHub/Rule/MoveClosedToDone.cs
@@ -0,0 +1,47 @@
+using log4net;
+
+using Nullinside.Cicd.GitHub.GraphQl;
+using Nullinside.Cicd.GitHub.Model;
+
+using Octokit;
+using Octokit.GraphQL;
+
+using Connection = Octokit.GraphQL.Connection;
+using Repository = Octokit.Repository;
+
+namespace Nullinside.Cicd.GitHub.Rule;
+
+///
+/// Handles moving issues to done status.
+///
+public class MoveClosedToDone : IRepoRule {
+ ///
+ /// The logger
+ ///
+ private readonly ILog _log = LogManager.GetLogger(typeof(MoveClosedToDone));
+
+ ///
+ public async Task Handle(GitHubClient client, Connection graphQl, ID projectId, Repository repo) {
+ if (!repo.HasIssues) {
+ return;
+ }
+
+ var query = new QueryIssueProjectStatuses(Constants.GITHUB_ORG, repo.Name);
+ GraphQlProjectIssueResponse? issueInfoResponse = await query.SendAsync();
+
+ var issueStatuses = from issueInfo in issueInfoResponse?.Data.Repository.Issues.Nodes
+ from projects in issueInfo.ProjectItems.Nodes
+ select new { IssueId = issueInfo.Id, IssueNumber = issueInfo.Number, IsClosed = issueInfo.Closed, ProjectIssueId = projects.Id, FieldId = projects.FieldValueByName.Field.Id, Status = projects.FieldValueByName.Name };
+
+ foreach (var issue in issueStatuses) {
+ if (!issue.IsClosed || "Done".Equals(issue.Status, StringComparison.InvariantCultureIgnoreCase)) {
+ continue;
+ }
+
+ _log.Info($"{repo.Name}: Associating issue #{issue.IssueNumber}");
+
+ var mutation = new MutateIssueProjectStatuses(projectId.Value, issue.ProjectIssueId, issue.FieldId, "98236657");
+ await mutation.SendAsync();
+ }
+ }
+}
\ No newline at end of file