Skip to content

Commit b93b163

Browse files
Jerichogep13
authored andcommitted
(#89) Include contributors in release notes
This commit adds the ability to query for, and add, information about the contributors for linked issues and PR's into the generated release notes. This is made possible via a new `include-contributors` option in the create section of the GitReleaseManager.yaml file. This is false by default. In addition, a new scriban template has been created, so allow complete segregation between release notes that have contributors, and those that don't. This was done mainly to allow better maintainability going forward, and to reduce the complexity of the default template. This has been implemented for both GitHug and GitLab. For GitHub, it was necessary to use GraphQL to get the necessary information, where as with GitLab, the required information could be returned via the REST API.
1 parent e8006e2 commit b93b163

26 files changed

+496
-13
lines changed

src/Directory.Packages.props

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
1111
<PackageVersion Include="coverlet.msbuild" Version="6.0.4" />
1212
<PackageVersion Include="Destructurama.Attributed" Version="5.1.0" />
13+
<PackageVersion Include="GraphQL.Client" Version="6.0.1" />
14+
<PackageVersion Include="GraphQL.Client.Serializer.SystemTextJson" Version="6.0.1" />
1315
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
1416
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0" />
1517
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.2" />

src/GitReleaseManager.Cli/Program.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
using GitReleaseManager.Core.Provider;
1414
using GitReleaseManager.Core.ReleaseNotes;
1515
using GitReleaseManager.Core.Templates;
16+
using GraphQL.Client.Http;
17+
using GraphQL.Client.Serializer.SystemTextJson;
1618
using Microsoft.Extensions.DependencyInjection;
1719
using NGitLab;
1820
using Octokit;
@@ -211,6 +213,12 @@ private static void RegisterVcsProvider(BaseVcsOptions vcsOptions, IServiceColle
211213
// default to Github
212214
serviceCollection
213215
.AddSingleton<IGitHubClient>((_) => new GitHubClient(new ProductHeaderValue("GitReleaseManager")) { Credentials = new Credentials(vcsOptions.Token) })
216+
.AddSingleton<GraphQL.Client.Abstractions.IGraphQLClient>(_ =>
217+
{
218+
var client = new GraphQLHttpClient(new GraphQLHttpClientOptions { EndPoint = new Uri("https://api.github.com/graphql") }, new SystemTextJsonSerializer());
219+
client.HttpClient.DefaultRequestHeaders.Add("Authorization", $"bearer {vcsOptions.Token}");
220+
return client;
221+
})
214222
.AddSingleton<IVcsProvider, GitHubProvider>();
215223
}
216224
}

src/GitReleaseManager.Core/Configuration/Config.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public Config()
2727
ShaSectionHeading = "SHA256 Hashes of the release artifacts",
2828
ShaSectionLineFormat = "- `{1}\t{0}`",
2929
AllowUpdateToPublishedRelease = false,
30+
IncludeContributors = false,
3031
};
3132

3233
Export = new ExportConfig

src/GitReleaseManager.Core/Configuration/CreateConfig.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,8 @@ public class CreateConfig
3434

3535
[YamlMember(Alias = "allow-update-to-published")]
3636
public bool AllowUpdateToPublishedRelease { get; set; }
37+
38+
[YamlMember(Alias = "include-contributors")]
39+
public bool IncludeContributors { get; set; }
3740
}
3841
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text.Json;
5+
6+
namespace GitReleaseManager.Core.Extensions
7+
{
8+
internal static class JsonExtensions
9+
{
10+
/// <summary>
11+
/// Get a JsonElement from a path. Each level in the path is seperated by a dot.
12+
/// </summary>
13+
/// <param name="jsonElement">The parent Json element.</param>
14+
/// <param name="path">The path of the desired child element.</param>
15+
/// <returns>The child element.</returns>
16+
public static JsonElement GetJsonElement(this JsonElement jsonElement, string path)
17+
{
18+
if (jsonElement.ValueKind is JsonValueKind.Null || jsonElement.ValueKind is JsonValueKind.Undefined)
19+
{
20+
return default(JsonElement);
21+
}
22+
23+
string[] segments = path.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries);
24+
25+
foreach (var segment in segments)
26+
{
27+
if (int.TryParse(segment, out var index) && jsonElement.ValueKind == JsonValueKind.Array)
28+
{
29+
jsonElement = jsonElement.EnumerateArray().ElementAtOrDefault(index);
30+
if (jsonElement.ValueKind is JsonValueKind.Null || jsonElement.ValueKind is JsonValueKind.Undefined)
31+
{
32+
return default(JsonElement);
33+
}
34+
35+
continue;
36+
}
37+
38+
jsonElement = jsonElement.TryGetProperty(segment, out var value) ? value : default;
39+
40+
if (jsonElement.ValueKind is JsonValueKind.Null || jsonElement.ValueKind is JsonValueKind.Undefined)
41+
{
42+
return default(JsonElement);
43+
}
44+
}
45+
46+
return jsonElement;
47+
}
48+
49+
/// <summary>
50+
/// Get the first JsonElement matching a path from the provided list of paths.
51+
/// </summary>
52+
/// <param name="jsonElement">The parent Json element.</param>
53+
/// <param name="paths">The path of the desired child element.</param>
54+
/// <returns>The child element.</returns>
55+
public static JsonElement GetFirstJsonElement(this JsonElement jsonElement, IEnumerable<string> paths)
56+
{
57+
if (jsonElement.ValueKind is JsonValueKind.Null || jsonElement.ValueKind is JsonValueKind.Undefined)
58+
{
59+
return default(JsonElement);
60+
}
61+
62+
var element = default(JsonElement);
63+
64+
foreach (var path in paths)
65+
{
66+
element = jsonElement.GetJsonElement(path);
67+
68+
if (element.ValueKind is JsonValueKind.Null || element.ValueKind is JsonValueKind.Undefined)
69+
{
70+
continue;
71+
}
72+
73+
break;
74+
}
75+
76+
return element;
77+
}
78+
79+
public static string GetJsonElementValue(this JsonElement jsonElement) => jsonElement.ValueKind != JsonValueKind.Null &&
80+
jsonElement.ValueKind != JsonValueKind.Undefined
81+
? jsonElement.ToString()
82+
: default;
83+
}
84+
}

src/GitReleaseManager.Core/GitReleaseManager.Core.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
<ItemGroup>
2020
<PackageReference Include="CommandLineParser" />
2121
<PackageReference Include="Destructurama.Attributed" />
22+
<PackageReference Include="GraphQL.Client" />
23+
<PackageReference Include="GraphQL.Client.Serializer.SystemTextJson" />
2224
<PackageReference Include="Microsoft.SourceLink.GitHub">
2325
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
2426
<PrivateAssets>all</PrivateAssets>

src/GitReleaseManager.Core/MappingProfiles/GitHubProfile.cs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Text.Json;
23
using AutoMapper;
34
using GitReleaseManager.Core.Extensions;
45

@@ -8,10 +9,11 @@ public class GitHubProfile : Profile
89
{
910
public GitHubProfile()
1011
{
12+
// These mappings convert the result of Octokit queries to model classes
1113
CreateMap<Octokit.Issue, Model.Issue>()
1214
.ForMember(dest => dest.PublicNumber, act => act.MapFrom(src => src.Number))
1315
.ForMember(dest => dest.InternalNumber, act => act.MapFrom(src => src.Id))
14-
.ForMember(dest => dest.IsPullRequest, act => act.MapFrom(src => src.HtmlUrl.IndexOf("/pull/", StringComparison.OrdinalIgnoreCase) >= 0))
16+
.ForMember(dest => dest.IsPullRequest, act => act.MapFrom(src => src.HtmlUrl.Contains("/pull/", StringComparison.OrdinalIgnoreCase)))
1517
.ReverseMap();
1618
CreateMap<Model.IssueComment, Octokit.IssueComment>().ReverseMap();
1719
CreateMap<Model.ItemState, Octokit.ItemState>().ReverseMap();
@@ -23,11 +25,35 @@ public GitHubProfile()
2325
CreateMap<Model.ReleaseAssetUpload, Octokit.ReleaseAssetUpload>().ReverseMap();
2426
CreateMap<Model.Label, Octokit.Label>().ReverseMap();
2527
CreateMap<Model.Label, Octokit.NewLabel>().ReverseMap();
28+
CreateMap<Model.User, Octokit.User>().ReverseMap();
2629
CreateMap<Model.Milestone, Octokit.Milestone>();
2730
CreateMap<Octokit.Milestone, Model.Milestone>()
2831
.ForMember(dest => dest.PublicNumber, act => act.MapFrom(src => src.Number))
2932
.ForMember(dest => dest.InternalNumber, act => act.MapFrom(src => src.Number))
3033
.AfterMap((src, dest) => dest.Version = src.Version());
34+
35+
// These mappings convert the result of GraphQL queries to model classes
36+
CreateMap<JsonElement, Model.Issue>()
37+
.ForMember(dest => dest.PublicNumber, act => act.MapFrom(src => src.GetProperty("number").GetInt32()))
38+
.ForMember(dest => dest.InternalNumber, act => act.MapFrom(src => -1)) // Not available in graphQL (there's a "id" property but it contains a string which represents the Node ID of the object).
39+
.ForMember(dest => dest.Title, act => act.MapFrom(src => src.GetProperty("title").GetString()))
40+
.ForMember(dest => dest.HtmlUrl, act => act.MapFrom(src => src.GetProperty("url").GetString()))
41+
.ForMember(dest => dest.IsPullRequest, act => act.MapFrom(src => src.GetProperty("url").GetString().Contains("/pull/", StringComparison.OrdinalIgnoreCase)))
42+
.ForMember(dest => dest.User, act => act.MapFrom(src => src.GetProperty("author")))
43+
.ForMember(dest => dest.Labels, act => act.MapFrom(src => src.GetJsonElement("labels.nodes").EnumerateArray()))
44+
.ReverseMap();
45+
46+
CreateMap<JsonElement, Model.Label>()
47+
.ForMember(dest => dest.Name, act => act.MapFrom(src => src.GetProperty("name").GetString()))
48+
.ForMember(dest => dest.Color, act => act.MapFrom(src => src.GetProperty("color").GetString()))
49+
.ForMember(dest => dest.Description, act => act.MapFrom(src => src.GetProperty("description").GetString()))
50+
.ReverseMap();
51+
52+
CreateMap<JsonElement, Model.User>()
53+
.ForMember(dest => dest.Login, act => act.MapFrom(src => src.GetProperty("login").GetString()))
54+
.ForMember(dest => dest.HtmlUrl, act => act.MapFrom(src => $"https://github.com{src.GetProperty("resourcePath").GetString()}")) // The resourcePath contains a value similar to "/jericho". That's why we must manually prepend "https://github.com
55+
.ForMember(dest => dest.AvatarUrl, act => act.MapFrom(src => src.GetProperty("avatarUrl").GetString()))
56+
.ReverseMap();
3157
}
3258
}
3359
}

src/GitReleaseManager.Core/Model/Issue.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,9 @@ public sealed class Issue
1515
public IReadOnlyList<Label> Labels { get; set; }
1616

1717
public bool IsPullRequest { get; set; }
18+
19+
public User User { get; set; }
20+
21+
public IReadOnlyList<Issue> LinkedIssues { get; set; }
1822
}
1923
}

src/GitReleaseManager.Core/Model/IssueComment.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,10 @@ public class IssueComment
1111
/// Gets or sets details about the issue comment.
1212
/// </summary>
1313
public string Body { get; set; }
14+
15+
/// <summary>
16+
/// Gets or sets information about the user who made the comment.
17+
/// </summary>
18+
public User User { get; set; }
1419
}
1520
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace GitReleaseManager.Core.Model
2+
{
3+
public sealed class User
4+
{
5+
public string Login { get; set; }
6+
7+
public string HtmlUrl { get; set; }
8+
9+
public string AvatarUrl { get; set; }
10+
}
11+
}

0 commit comments

Comments
 (0)