Skip to content

Commit 158038f

Browse files
Improve generated release notes (#4491) (#4494)
This commit improves the generated release notes by consolidating issues that relate to PRs as part of the PR title, reducing duplication Co-authored-by: Russ Cam <russ.cam@elastic.co>
1 parent 6c28705 commit 158038f

File tree

2 files changed

+117
-45
lines changed

2 files changed

+117
-45
lines changed

build/scripts/Paths.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module Paths =
44

55
let OwnerName = "elastic"
66
let RepositoryName = "elasticsearch-net"
7-
let Repository = sprintf "https://github.com/%s/%s" OwnerName RepositoryName
7+
let Repository = sprintf "https://github.com/%s/%s/" OwnerName RepositoryName
88

99
let BuildFolder = "build"
1010
let TargetsFolder = "build/scripts"

build/scripts/ReleaseNotes.fs

Lines changed: 116 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,68 +3,140 @@ namespace Scripts
33
open System.Collections.Generic
44
open System.Linq
55
open System.IO
6+
open System.Text.RegularExpressions
7+
open System.Text;
68
open Octokit
79
open Versioning
810

911
module ReleaseNotes =
10-
11-
let private generateNotes newVersion oldVersion =
12-
let label = sprintf "v%O" newVersion.Full
13-
let releaseNotes = sprintf "ReleaseNotes-%O.md" newVersion.Full |> Paths.Output
14-
let client = new GitHubClient(new ProductHeaderValue("ReleaseNotesGenerator"))
15-
client.Credentials <- Credentials.Anonymous
16-
17-
let filter = new RepositoryIssueRequest()
18-
filter.Labels.Add label
19-
filter.State <- ItemStateFilter.Closed
12+
let issueNumberRegex(url: string) =
13+
let pattern = sprintf "\s(?:#|%sissues/)(?<num>\d+)" url
14+
Regex(pattern, RegexOptions.Multiline ||| RegexOptions.IgnoreCase ||| RegexOptions.CultureInvariant ||| RegexOptions.ExplicitCapture ||| RegexOptions.Compiled)
15+
16+
type GitHubItem(issue: Issue, relatedIssues: int list) =
17+
member val Issue = issue
18+
member val RelatedIssues = relatedIssues
19+
member this.Title =
20+
let builder = StringBuilder("#")
21+
.Append(issue.Number)
22+
.Append(" ")
23+
if issue.PullRequest = null then
24+
builder.AppendFormat("[ISSUE] {0}", issue.Title)
25+
else
26+
builder.Append(issue.Title) |> ignore
27+
if relatedIssues.Length > 0 then
28+
relatedIssues
29+
|> List.map(fun i -> sprintf "#%i" i)
30+
|> String.concat ", "
31+
|> sprintf " (%s: %s)" (if relatedIssues.Length = 1 then "issue" else "issues")
32+
|> builder.Append
33+
else builder
34+
|> ignore
35+
builder.ToString()
36+
37+
member this.Labels = issue.Labels
38+
member this.Number = issue.Number
2039

21-
let labelHeaders =
22-
[("Feature", "Features & Enhancements");
40+
type Config =
41+
{ labels: Map<string,string>
42+
uncategorized: string }
43+
44+
let config = {
45+
labels = Map.ofList <| [
46+
("Feature", "Features & Enhancements");
2347
("Bug", "Bug Fixes");
2448
("Deprecation", "Deprecations");
25-
("Uncategorized", "Uncategorized");]
26-
|> Map.ofList
27-
28-
let groupByLabel (issues:IReadOnlyList<Issue>) =
29-
let dict = new Dictionary<string, Issue list>()
30-
for issue in issues do
31-
let mutable categorized = false
32-
for labelHeader in labelHeaders do
33-
if issue.Labels.Any(fun l -> l.Name = labelHeader.Key) then
34-
let exists,list = dict.TryGetValue(labelHeader.Key)
35-
match exists with
36-
| true -> dict.[labelHeader.Key] <- issue :: list
37-
| false -> dict.Add(labelHeader.Key, [issue])
38-
categorized <- true
39-
40-
if (categorized = false) then
41-
let label = "Uncategorized"
42-
let exists,list = dict.TryGetValue(label)
49+
("Uncategorized", "Uncategorized")
50+
]
51+
uncategorized = "Uncategorized"
52+
};
53+
54+
let groupByLabel (config: Config) (items: List<GitHubItem>) =
55+
let dict = Dictionary<string, GitHubItem list>()
56+
for item in items do
57+
let mutable categorized = false
58+
// if an item is categorized with multiple config labels, it'll appear multiple times, once under each label
59+
for label in config.labels do
60+
if item.Labels.Any(fun l -> l.Name = label.Key) then
61+
let exists,list = dict.TryGetValue(label.Key)
4362
match exists with
44-
| true ->
45-
match List.tryFind(fun (i:Issue)-> i.Number = issue.Number) list with
46-
| Some _ -> ()
47-
| None -> dict.[label] <- issue :: list
48-
| false -> dict.Add(label, [issue])
49-
dict
63+
| true -> dict.[label.Key] <- item :: list
64+
| false -> dict.Add(label.Key, [item])
65+
categorized <- true
66+
67+
if categorized = false then
68+
let exists,list = dict.TryGetValue(config.uncategorized)
69+
match exists with
70+
| true ->
71+
match List.tryFind(fun (i:GitHubItem)-> i.Number = item.Number) list with
72+
| Some _ -> ()
73+
| None -> dict.[config.uncategorized] <- item :: list
74+
| false -> dict.Add(config.uncategorized, [item])
75+
dict
76+
77+
let filterByPullRequests (issueNumberRegex: Regex) (issues:IReadOnlyList<Issue>): List<GitHubItem> =
78+
let extractRelatedIssues(issue: Issue) =
79+
let matches = issueNumberRegex.Matches(issue.Body)
80+
if matches.Count = 0 then list.Empty
81+
else
82+
matches
83+
|> Seq.cast<Match>
84+
|> Seq.filter(fun m -> m.Success)
85+
|> Seq.map(fun m -> m.Groups.["num"].Value |> int)
86+
|> Seq.toList
87+
88+
let collectedIssues = List<GitHubItem>()
89+
let items = List<GitHubItem>()
90+
91+
for issue in issues do
92+
if issue.PullRequest <> null then
93+
let relatedIssues = extractRelatedIssues issue
94+
items.Add(GitHubItem(issue, relatedIssues))
95+
else
96+
collectedIssues.Add(GitHubItem(issue, list.Empty))
97+
98+
// remove all issues that are referenced by pull requests
99+
for pullRequest in items do
100+
for relatedIssue in pullRequest.RelatedIssues do
101+
collectedIssues.RemoveAll(fun i -> i.Issue.Number = relatedIssue) |> ignore
102+
103+
// any remaining issues do not have an associated pull request, so add them
104+
items.AddRange(collectedIssues)
105+
items
106+
107+
let getClosedIssues(label: string, config: Config) =
108+
let issueNumberRegex = issueNumberRegex Paths.Repository
109+
let filter = RepositoryIssueRequest()
110+
filter.Labels.Add label
111+
filter.State <- ItemStateFilter.Closed
112+
113+
let client = GitHubClient(ProductHeaderValue("ReleaseNotesGenerator"))
114+
client.Credentials <- Credentials.Anonymous
115+
116+
client.Issue.GetAllForRepository(Paths.OwnerName, Paths.RepositoryName, filter)
117+
|> Async.AwaitTask
118+
|> Async.RunSynchronously
119+
|> filterByPullRequests issueNumberRegex
120+
|> groupByLabel config
121+
122+
let private generateNotes newVersion oldVersion =
123+
let label = sprintf "v%O" newVersion.Full
124+
let releaseNotes = sprintf "ReleaseNotes-%O.md" newVersion.Full |> Paths.Output
50125

51-
let closedIssues = client.Issue.GetAllForRepository(Paths.OwnerName, Paths.RepositoryName, filter)
52-
|> Async.AwaitTask
53-
|> Async.RunSynchronously
54-
|> groupByLabel
126+
let closedIssues = getClosedIssues(label, config)
55127

56128
use file = File.OpenWrite <| releaseNotes
57129
use writer = new StreamWriter(file)
58-
writer.WriteLine(sprintf "%s/compare/%O...%O" Paths.Repository oldVersion.Full newVersion.Full)
130+
writer.WriteLine(sprintf "%scompare/%O...%O" Paths.Repository oldVersion.Full newVersion.Full)
59131
writer.WriteLine()
60132
for closedIssue in closedIssues do
61-
labelHeaders.[closedIssue.Key] |> sprintf "## %s" |> writer.WriteLine
133+
config.labels.[closedIssue.Key] |> sprintf "## %s" |> writer.WriteLine
62134
writer.WriteLine()
63135
for issue in closedIssue.Value do
64-
sprintf "- #%i %s" issue.Number issue.Title |> writer.WriteLine
136+
sprintf "- %s" issue.Title |> writer.WriteLine
65137
writer.WriteLine()
66138

67-
sprintf "### [View the full list of issues and PRs](%s/issues?utf8=%%E2%%9C%%93&q=label%%3A%s)" Paths.Repository label
139+
sprintf "### [View the full list of issues and PRs](%sissues?utf8=%%E2%%9C%%93&q=label%%3A%s)" Paths.Repository label
68140
|> writer.WriteLine
69141

70142
let GenerateNotes version =

0 commit comments

Comments
 (0)