Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/Nullinside.Cicd.GitHub/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions src/Nullinside.Cicd.GitHub/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ public static class Constants {
/// </summary>
public const string GITHUB_ORG = "nullinside-development-group";

/// <summary>
/// The url to the graphql endpoint to post against.
/// </summary>
public const string GITHUB_GRAPHQL_URL = "https://api.github.com/graphql";

/// <summary>
/// The name of the project that be sent in user agent headers.
/// </summary>
public const string PROJECT_NAME = "nullinside-cicd-github";

/// <summary>
/// The github project's unique identifier on github.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// The contract for executing a simple graphql query.
/// </summary>
/// <typeparam name="T">The JSON response POCO to the query, <see cref="GraphQlGenericMutationResponse" /> for no response.</typeparam>
public abstract class AbstractGitHubGraphQlQuery<T> {
/// <summary>
/// The query, typically coded into the object.
/// </summary>
public abstract string Query { get; }

/// <summary>
/// The query variables, typically assembled in the constructor.
/// </summary>
public object? QueryVariables { get; set; }

/// <summary>
/// Executes the request.
/// </summary>
/// <returns>The response object.</returns>
public virtual async Task<T?> 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<T>(responseString);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

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; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

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<IssuesNode> 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<ProjectItemsNode> 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; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Nullinside.Cicd.GitHub.GraphQl.Model;

namespace Nullinside.Cicd.GitHub.GraphQl;

/// <summary>
/// Changes the status of the issue on the GitHub project board. (ex: Backlog, Ready, In progress, In review, Done)
/// </summary>
public class MutateIssueProjectStatuses : AbstractGitHubGraphQlQuery<GraphQlGenericMutationResponse> {
/// <summary>
/// Initializes a new instance of the <see cref="MutateIssueProjectStatuses" /> class.
/// </summary>
/// <param name="projectId">The project id</param>
/// <param name="projectIssueId">The status field's id (ex: PVTI_lADOCZOBm84AdCw7zgQfohA NOT I_kwDOLcGb986MVe7p)</param>
/// <param name="statusFieldId">The status field's id (ex: PVTSSF_lADOCZOBm84AdCw7zgSzu_A)</param>
/// <param name="selectionId">The unique identifier of the single selection option to change the status to.</param>
public MutateIssueProjectStatuses(string projectId, string projectIssueId, string statusFieldId, string selectionId) {
QueryVariables = new { project = projectId, item = projectIssueId, field = statusFieldId, selectionId };
}

/// <inheritdoc />
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
}
}";
}
54 changes: 54 additions & 0 deletions src/Nullinside.Cicd.GitHub/GraphQl/QueryIssueProjectStatuses.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using Nullinside.Cicd.GitHub.Model;

namespace Nullinside.Cicd.GitHub.GraphQl;

/// <summary>
/// Queries the issues in a GitHub code base.
/// </summary>
public class QueryIssueProjectStatuses : AbstractGitHubGraphQlQuery<GraphQlProjectIssueResponse> {
/// <summary>
/// Initializes a new instance of the <see cref="QueryIssueProjectStatuses" /> class.
/// </summary>
/// <param name="repoOwner">The owner of the repo on github.</param>
/// <param name="repoName">The name of the repo on github.</param>
public QueryIssueProjectStatuses(string repoOwner, string repoName) {
QueryVariables = new { owner = repoOwner, name = repoName };
}

/// <inheritdoc />
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
}
}
}
}
}
}
}
}
}
}";
}
16 changes: 8 additions & 8 deletions src/Nullinside.Cicd.GitHub/Nullinside.Cicd.GitHub.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,20 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="log4net" Version="3.0.4" />
<PackageReference Include="log4net.Ext.Json" Version="3.0.3" />
<PackageReference Include="Octokit" Version="14.0.0" />
<PackageReference Include="log4net" Version="3.0.4"/>
<PackageReference Include="log4net.Ext.Json" Version="3.0.3"/>
<PackageReference Include="Octokit" Version="14.0.0"/>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\octokit.graphql.net\src\Octokit.GraphQL\Octokit.GraphQL.csproj" />
<ProjectReference Include="..\octokit.graphql.net\src\Octokit.GraphQL\Octokit.GraphQL.csproj"/>
</ItemGroup>

<ItemGroup>
<None Remove="log4net.config" />
<Content Include="log4net.config">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<None Remove="log4net.config"/>
<Content Include="log4net.config">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>

</Project>
3 changes: 1 addition & 2 deletions src/Nullinside.Cicd.GitHub/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

using log4net;
using log4net.Config;
using log4net.Core;

using Nullinside.Cicd.GitHub;
using Nullinside.Cicd.GitHub.Rule;
Expand All @@ -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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ namespace Nullinside.Cicd.GitHub.Rule;
/// </summary>
public class AssociateIssuesWithProject : IRepoRule {
/// <summary>
/// The logger
/// The logger
/// </summary>
private ILog _log = LogManager.GetLogger(typeof(AssociateIssuesWithProject));
private readonly ILog _log = LogManager.GetLogger(typeof(AssociateIssuesWithProject));

/// <inheritdoc />
public async Task Handle(GitHubClient client, Connection graphQl, ID projectId, Repository repo) {
if (!repo.HasIssues) {
Expand Down
6 changes: 3 additions & 3 deletions src/Nullinside.Cicd.GitHub/Rule/CreateRulesets.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ namespace Nullinside.Cicd.GitHub.Rule;
/// </summary>
public class CreateRulesets : IRepoRule {
/// <summary>
/// The logger
/// The logger
/// </summary>
private ILog _log = LogManager.GetLogger(typeof(CreateRulesets));
private readonly ILog _log = LogManager.GetLogger(typeof(CreateRulesets));

/// <inheritdoc />
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
Expand Down
47 changes: 47 additions & 0 deletions src/Nullinside.Cicd.GitHub/Rule/MoveClosedToDone.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Handles moving issues to done status.
/// </summary>
public class MoveClosedToDone : IRepoRule {
/// <summary>
/// The logger
/// </summary>
private readonly ILog _log = LogManager.GetLogger(typeof(MoveClosedToDone));

/// <inheritdoc />
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();
}
}
}
Loading