build/scripts/ReleaseNotes.fs (127 lines of code) (raw):

// Licensed to Elasticsearch B.V under one or more agreements. // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. 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 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 type Config = { labels: Map<string,string> uncategorized: string } let config = { labels = Map.ofList <| [ ("Feature", "Features & Enhancements"); ("Bug", "Bug Fixes"); ("Deprecation", "Deprecations"); ("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 -> 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 = getClosedIssues(label, config) use file = File.OpenWrite <| releaseNotes use writer = new StreamWriter(file) writer.WriteLine(sprintf "%scompare/%O...%O" Paths.Repository oldVersion.Full newVersion.Full) writer.WriteLine() for closedIssue in closedIssues do config.labels.[closedIssue.Key] |> sprintf "## %s" |> writer.WriteLine writer.WriteLine() for issue in closedIssue.Value do sprintf "- %s" issue.Title |> writer.WriteLine writer.WriteLine() 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 = match version with | NoChange _ -> failwith "Can not generate release notes if no new version was specified" | Update (newVersion, oldVersion) -> generateNotes newVersion oldVersion