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
2 changes: 1 addition & 1 deletion build/scripts/Paths.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module Paths =

let OwnerName = "elastic"
let RepositoryName = "elasticsearch-net"
let Repository = sprintf "https://github.com/%s/%s" OwnerName RepositoryName
let Repository = sprintf "https://github.com/%s/%s/" OwnerName RepositoryName

let BuildFolder = "build"
let TargetsFolder = "build/scripts"
Expand Down
160 changes: 116 additions & 44 deletions build/scripts/ReleaseNotes.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,68 +3,140 @@ namespace Scripts
open System.Collections.Generic
open System.Linq
open System.IO
open System.Text.RegularExpressions
open System.Text;
open Octokit
open Versioning

module ReleaseNotes =

let private generateNotes newVersion oldVersion =
let label = sprintf "v%O" newVersion.Full
let releaseNotes = sprintf "ReleaseNotes-%O.md" newVersion.Full |> Paths.Output
let client = new GitHubClient(new ProductHeaderValue("ReleaseNotesGenerator"))
client.Credentials <- Credentials.Anonymous

let filter = new RepositoryIssueRequest()
filter.Labels.Add label
filter.State <- ItemStateFilter.Closed
let issueNumberRegex(url: string) =
let pattern = sprintf "\s(?:#|%sissues/)(?<num>\d+)" url
Regex(pattern, RegexOptions.Multiline ||| RegexOptions.IgnoreCase ||| RegexOptions.CultureInvariant ||| RegexOptions.ExplicitCapture ||| RegexOptions.Compiled)

type GitHubItem(issue: Issue, relatedIssues: int list) =
member val Issue = issue
member val RelatedIssues = relatedIssues
member this.Title =
let builder = StringBuilder("#")
.Append(issue.Number)
.Append(" ")
if issue.PullRequest = null then
builder.AppendFormat("[ISSUE] {0}", issue.Title)
else
builder.Append(issue.Title) |> ignore
if relatedIssues.Length > 0 then
relatedIssues
|> List.map(fun i -> sprintf "#%i" i)
|> String.concat ", "
|> sprintf " (%s: %s)" (if relatedIssues.Length = 1 then "issue" else "issues")
|> builder.Append
else builder
|> ignore
builder.ToString()

member this.Labels = issue.Labels
member this.Number = issue.Number

let labelHeaders =
[("Feature", "Features & Enhancements");
type Config =
{ labels: Map<string,string>
uncategorized: string }

let config = {
labels = Map.ofList <| [
("Feature", "Features & Enhancements");
("Bug", "Bug Fixes");
("Deprecation", "Deprecations");
("Uncategorized", "Uncategorized");]
|> Map.ofList

let groupByLabel (issues:IReadOnlyList<Issue>) =
let dict = new Dictionary<string, Issue list>()
for issue in issues do
let mutable categorized = false
for labelHeader in labelHeaders do
if issue.Labels.Any(fun l -> l.Name = labelHeader.Key) then
let exists,list = dict.TryGetValue(labelHeader.Key)
match exists with
| true -> dict.[labelHeader.Key] <- issue :: list
| false -> dict.Add(labelHeader.Key, [issue])
categorized <- true

if (categorized = false) then
let label = "Uncategorized"
let exists,list = dict.TryGetValue(label)
("Uncategorized", "Uncategorized")
]
uncategorized = "Uncategorized"
};

let groupByLabel (config: Config) (items: List<GitHubItem>) =
let dict = Dictionary<string, GitHubItem list>()
for item in items do
let mutable categorized = false
// if an item is categorized with multiple config labels, it'll appear multiple times, once under each label
for label in config.labels do
if item.Labels.Any(fun l -> l.Name = label.Key) then
let exists,list = dict.TryGetValue(label.Key)
match exists with
| true ->
match List.tryFind(fun (i:Issue)-> i.Number = issue.Number) list with
| Some _ -> ()
| None -> dict.[label] <- issue :: list
| false -> dict.Add(label, [issue])
dict
| true -> dict.[label.Key] <- item :: list
| false -> dict.Add(label.Key, [item])
categorized <- true

if categorized = false then
let exists,list = dict.TryGetValue(config.uncategorized)
match exists with
| true ->
match List.tryFind(fun (i:GitHubItem)-> i.Number = item.Number) list with
| Some _ -> ()
| None -> dict.[config.uncategorized] <- item :: list
| false -> dict.Add(config.uncategorized, [item])
dict

let filterByPullRequests (issueNumberRegex: Regex) (issues:IReadOnlyList<Issue>): List<GitHubItem> =
let extractRelatedIssues(issue: Issue) =
let matches = issueNumberRegex.Matches(issue.Body)
if matches.Count = 0 then list.Empty
else
matches
|> Seq.cast<Match>
|> Seq.filter(fun m -> m.Success)
|> Seq.map(fun m -> m.Groups.["num"].Value |> int)
|> Seq.toList

let collectedIssues = List<GitHubItem>()
let items = List<GitHubItem>()

for issue in issues do
if issue.PullRequest <> null then
let relatedIssues = extractRelatedIssues issue
items.Add(GitHubItem(issue, relatedIssues))
else
collectedIssues.Add(GitHubItem(issue, list.Empty))

// remove all issues that are referenced by pull requests
for pullRequest in items do
for relatedIssue in pullRequest.RelatedIssues do
collectedIssues.RemoveAll(fun i -> i.Issue.Number = relatedIssue) |> ignore

// any remaining issues do not have an associated pull request, so add them
items.AddRange(collectedIssues)
items

let getClosedIssues(label: string, config: Config) =
let issueNumberRegex = issueNumberRegex Paths.Repository
let filter = RepositoryIssueRequest()
filter.Labels.Add label
filter.State <- ItemStateFilter.Closed

let client = GitHubClient(ProductHeaderValue("ReleaseNotesGenerator"))
client.Credentials <- Credentials.Anonymous

client.Issue.GetAllForRepository(Paths.OwnerName, Paths.RepositoryName, filter)
|> Async.AwaitTask
|> Async.RunSynchronously
|> filterByPullRequests issueNumberRegex
|> groupByLabel config

let private generateNotes newVersion oldVersion =
let label = sprintf "v%O" newVersion.Full
let releaseNotes = sprintf "ReleaseNotes-%O.md" newVersion.Full |> Paths.Output

let closedIssues = client.Issue.GetAllForRepository(Paths.OwnerName, Paths.RepositoryName, filter)
|> Async.AwaitTask
|> Async.RunSynchronously
|> groupByLabel
let closedIssues = getClosedIssues(label, config)

use file = File.OpenWrite <| releaseNotes
use writer = new StreamWriter(file)
writer.WriteLine(sprintf "%s/compare/%O...%O" Paths.Repository oldVersion.Full newVersion.Full)
writer.WriteLine(sprintf "%scompare/%O...%O" Paths.Repository oldVersion.Full newVersion.Full)
writer.WriteLine()
for closedIssue in closedIssues do
labelHeaders.[closedIssue.Key] |> sprintf "## %s" |> writer.WriteLine
config.labels.[closedIssue.Key] |> sprintf "## %s" |> writer.WriteLine
writer.WriteLine()
for issue in closedIssue.Value do
sprintf "- #%i %s" issue.Number issue.Title |> writer.WriteLine
sprintf "- %s" issue.Title |> writer.WriteLine
writer.WriteLine()

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

let GenerateNotes version =
Expand Down