diff --git a/build/scripts/Paths.fs b/build/scripts/Paths.fs index 8b595753042..cb698196e27 100644 --- a/build/scripts/Paths.fs +++ b/build/scripts/Paths.fs @@ -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" diff --git a/build/scripts/ReleaseNotes.fs b/build/scripts/ReleaseNotes.fs index 5c5de979037..632a9af47c0 100644 --- a/build/scripts/ReleaseNotes.fs +++ b/build/scripts/ReleaseNotes.fs @@ -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/)(?\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 + uncategorized: string } + + let config = { + labels = Map.ofList <| [ + ("Feature", "Features & Enhancements"); ("Bug", "Bug Fixes"); ("Deprecation", "Deprecations"); - ("Uncategorized", "Uncategorized");] - |> Map.ofList - - let groupByLabel (issues:IReadOnlyList) = - let dict = new Dictionary() - 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) = + let dict = Dictionary() + 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): List = + let extractRelatedIssues(issue: Issue) = + let matches = issueNumberRegex.Matches(issue.Body) + if matches.Count = 0 then list.Empty + else + matches + |> Seq.cast + |> Seq.filter(fun m -> m.Success) + |> Seq.map(fun m -> m.Groups.["num"].Value |> int) + |> Seq.toList + + let collectedIssues = List() + let items = List() + + 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 =