|
|
|
@@ -1,14 +1,28 @@
|
|
|
|
|
using System;
|
|
|
|
|
// This program is minimal effort and should be sent to remedial school
|
|
|
|
|
|
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Concurrent;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Diagnostics;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Net;
|
|
|
|
|
using System.Net.Http;
|
|
|
|
|
using System.Net.Sockets;
|
|
|
|
|
using System.Text;
|
|
|
|
|
using System.Text.RegularExpressions;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
using System.Xml.Linq;
|
|
|
|
|
|
|
|
|
|
using Newtonsoft.Json;
|
|
|
|
|
|
|
|
|
|
using Octokit;
|
|
|
|
|
using Octokit.GraphQL;
|
|
|
|
|
|
|
|
|
|
using Tgstation.Server.Host.Extensions.Converters;
|
|
|
|
|
|
|
|
|
|
using YamlDotNet.Serialization;
|
|
|
|
|
|
|
|
|
|
namespace Tgstation.Server.ReleaseNotes
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
@@ -34,8 +48,9 @@ namespace Tgstation.Server.ReleaseNotes
|
|
|
|
|
var ensureRelease = versionString.Equals("--ensure-release", StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
var linkWinget = versionString.Equals("--link-winget", StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
var shaCheck = versionString.Equals("--winget-template-check", StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
var fullNotes = versionString.Equals("--generate-full-notes", StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
|
|
|
|
|
if ((!Version.TryParse(versionString, out var version) || version.Revision != -1) && !ensureRelease && !linkWinget && !shaCheck)
|
|
|
|
|
if ((!Version.TryParse(versionString, out var version) || version.Revision != -1) && !ensureRelease && !linkWinget && !shaCheck && !fullNotes)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine("Invalid version: " + versionString);
|
|
|
|
|
return 2;
|
|
|
|
@@ -84,6 +99,11 @@ namespace Tgstation.Server.ReleaseNotes
|
|
|
|
|
return await Winget(client, null, args[1]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (fullNotes)
|
|
|
|
|
{
|
|
|
|
|
return await FullNotes(client);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var releasesTask = client.Repository.Release.GetAll(RepoOwner, RepoName);
|
|
|
|
|
|
|
|
|
|
Console.WriteLine("Getting merged pull requests in milestone " + versionString + "...");
|
|
|
|
@@ -101,103 +121,12 @@ namespace Tgstation.Server.ReleaseNotes
|
|
|
|
|
}
|
|
|
|
|
Console.WriteLine(milestonePRs.Items.Count + " total pull requests");
|
|
|
|
|
|
|
|
|
|
Task<Milestone> milestoneTask = null;
|
|
|
|
|
var milestoneTaskLock = new object();
|
|
|
|
|
var releaseDictionary = new Dictionary<string, List<Tuple<string, int, string>>>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
|
|
|
|
|
bool postControlPanelMessage = false;
|
|
|
|
|
|
|
|
|
|
async Task GetReleaseNotesFromPR(Issue pullRequest)
|
|
|
|
|
{
|
|
|
|
|
//need to check it was merged
|
|
|
|
|
var fullPR = await client.Repository.PullRequest.Get(RepoOwner, RepoName, pullRequest.Number).ConfigureAwait(false);
|
|
|
|
|
var noteTasks = new List<Task<Tuple<Dictionary<Component, Changelist>, Dictionary<Component, Version>, bool>>>();
|
|
|
|
|
|
|
|
|
|
if (!fullPR.Merged)
|
|
|
|
|
{
|
|
|
|
|
if (!doNotCloseMilestone && fullPR.Milestone != null)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine($"Removing trash PR #{fullPR.Number} from milestone...");
|
|
|
|
|
await client.Issue.Update(RepoOwner, RepoName, fullPR.Number, new IssueUpdate
|
|
|
|
|
{
|
|
|
|
|
Milestone = null
|
|
|
|
|
}).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async Task<Milestone> GetMilestone()
|
|
|
|
|
{
|
|
|
|
|
if (fullPR.Milestone == null)
|
|
|
|
|
return null;
|
|
|
|
|
return await client.Issue.Milestone.Get(RepoOwner, RepoName, fullPR.Milestone.Number);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
lock (milestoneTaskLock)
|
|
|
|
|
milestoneTask ??= GetMilestone();
|
|
|
|
|
|
|
|
|
|
// if (!fullPR.Merged)
|
|
|
|
|
//return;
|
|
|
|
|
|
|
|
|
|
void BuildNotesFromComment(string comment, User user)
|
|
|
|
|
{
|
|
|
|
|
if (comment == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
void CommitNotes(string component, List<string> notes)
|
|
|
|
|
{
|
|
|
|
|
lock (releaseDictionary)
|
|
|
|
|
{
|
|
|
|
|
foreach (var I in notes)
|
|
|
|
|
Console.WriteLine(component + " #" + fullPR.Number + " - " + I + " (@" + user.Login + ")");
|
|
|
|
|
|
|
|
|
|
var tupleSelector = notes.Select(note => Tuple.Create(note, fullPR.Number, user.Login));
|
|
|
|
|
if (releaseDictionary.TryGetValue(component, out var currentValues))
|
|
|
|
|
currentValues.AddRange(tupleSelector);
|
|
|
|
|
else
|
|
|
|
|
releaseDictionary.Add(component, tupleSelector.ToList());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var commentSplits = comment.Split('\n');
|
|
|
|
|
string targetComponent = null;
|
|
|
|
|
var notes = new List<string>();
|
|
|
|
|
foreach (var line in commentSplits)
|
|
|
|
|
{
|
|
|
|
|
var trimmedLine = line.Trim();
|
|
|
|
|
if (targetComponent == null)
|
|
|
|
|
{
|
|
|
|
|
if (trimmedLine.StartsWith(":cl:", StringComparison.Ordinal))
|
|
|
|
|
{
|
|
|
|
|
targetComponent = trimmedLine[4..].Trim();
|
|
|
|
|
if (targetComponent.Length == 0)
|
|
|
|
|
targetComponent = "Core";
|
|
|
|
|
}
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (trimmedLine.StartsWith("/:cl:", StringComparison.Ordinal))
|
|
|
|
|
{
|
|
|
|
|
CommitNotes(targetComponent, notes);
|
|
|
|
|
targetComponent = null;
|
|
|
|
|
notes.Clear();
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (trimmedLine.Length == 0)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
notes.Add(trimmedLine);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var comments = await client.Issue.Comment.GetAllForIssue(RepoOwner, RepoName, fullPR.Number).ConfigureAwait(false);
|
|
|
|
|
BuildNotesFromComment(fullPR.Body, fullPR.User);
|
|
|
|
|
foreach(var x in comments)
|
|
|
|
|
BuildNotesFromComment(x.Body, x.User);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var tasks = new List<Task>();
|
|
|
|
|
foreach (var I in milestonePRs.Items)
|
|
|
|
|
tasks.Add(GetReleaseNotesFromPR(I));
|
|
|
|
|
noteTasks.Add(GetReleaseNotesFromPR(client, I, doNotCloseMilestone, false, false));
|
|
|
|
|
|
|
|
|
|
var releases = await releasesTask.ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
@@ -299,19 +228,21 @@ namespace Tgstation.Server.ReleaseNotes
|
|
|
|
|
newNotes.Append(version.Build);
|
|
|
|
|
}
|
|
|
|
|
newNotes.Append("](");
|
|
|
|
|
var milestone = await milestoneTask.ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
var milestone = await milestoneTasks.Single().Value.ConfigureAwait(false);
|
|
|
|
|
if (milestone == null)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine("Unable to detemine milestone!");
|
|
|
|
|
return 9;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var allTasks = new List<Task>(noteTasks);
|
|
|
|
|
if (doNotCloseMilestone)
|
|
|
|
|
Console.WriteLine("Not closing milestone due to parameter!");
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine("Closing milestone...");
|
|
|
|
|
tasks.Add(client.Issue.Milestone.Update(RepoOwner, RepoName, milestone.Number, new MilestoneUpdate
|
|
|
|
|
allTasks.Add(client.Issue.Milestone.Update(RepoOwner, RepoName, milestone.Number, new MilestoneUpdate
|
|
|
|
|
{
|
|
|
|
|
State = ItemState.Closed
|
|
|
|
|
}));
|
|
|
|
@@ -358,14 +289,16 @@ namespace Tgstation.Server.ReleaseNotes
|
|
|
|
|
if (I.PullRequest != null)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine($"Adding additional merged PR #{I.Number}...");
|
|
|
|
|
tasks.Add(GetReleaseNotesFromPR(I));
|
|
|
|
|
var task = GetReleaseNotesFromPR(client, I, doNotCloseMilestone, false, false);
|
|
|
|
|
noteTasks.Add(task);
|
|
|
|
|
allTasks.Add(task);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await Task.WhenAll(issueUpdateTasks).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tasks.Add(client.Issue.Milestone.Delete(RepoOwner, RepoName, milestoneToDelete.Number));
|
|
|
|
|
allTasks.Add(client.Issue.Milestone.Delete(RepoOwner, RepoName, milestoneToDelete.Number));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create the next minor milestone
|
|
|
|
@@ -378,7 +311,7 @@ namespace Tgstation.Server.ReleaseNotes
|
|
|
|
|
{
|
|
|
|
|
Description = "Next minor version"
|
|
|
|
|
});
|
|
|
|
|
tasks.Add(nextMinorMilestoneTask);
|
|
|
|
|
allTasks.Add(nextMinorMilestoneTask);
|
|
|
|
|
|
|
|
|
|
// Move unfinished stuff to new minor milestone
|
|
|
|
|
Console.WriteLine($"Moving {milestone.OpenIssues} abandoned issue(s) from previous milestone to new one...");
|
|
|
|
@@ -393,7 +326,7 @@ namespace Tgstation.Server.ReleaseNotes
|
|
|
|
|
{
|
|
|
|
|
var nextMinorMilestone = await nextMinorMilestoneTask.ConfigureAwait(false);
|
|
|
|
|
foreach (var I in abandonedIssues.Items)
|
|
|
|
|
tasks.Add(client.Issue.Update(RepoOwner, RepoName, I.Number, new IssueUpdate
|
|
|
|
|
allTasks.Add(client.Issue.Update(RepoOwner, RepoName, I.Number, new IssueUpdate
|
|
|
|
|
{
|
|
|
|
|
Milestone = nextMinorMilestone.Number
|
|
|
|
|
}));
|
|
|
|
@@ -405,7 +338,40 @@ namespace Tgstation.Server.ReleaseNotes
|
|
|
|
|
newNotes.Append("?closed=1)");
|
|
|
|
|
newNotes.Append(Environment.NewLine);
|
|
|
|
|
|
|
|
|
|
await Task.WhenAll(tasks).ConfigureAwait(false);
|
|
|
|
|
await Task.WhenAll(allTasks).ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
var componentVersionDict = new Dictionary<Component, Version>
|
|
|
|
|
{
|
|
|
|
|
{ Component.Configuration, configVersion },
|
|
|
|
|
{ Component.HttpApi, apiVersion },
|
|
|
|
|
{ Component.DreamMakerApi, dmApiVersion },
|
|
|
|
|
{ Component.InteropApi, interopVersion },
|
|
|
|
|
{ Component.WebControlPanel, webControlVersion },
|
|
|
|
|
{ Component.HostWatchdog, hostWatchdogVersion },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var releaseDictionary = new Dictionary<Component, Changelist>(
|
|
|
|
|
noteTasks
|
|
|
|
|
.SelectMany(task => task.Result.Item1)
|
|
|
|
|
.GroupBy(kvp => kvp.Key)
|
|
|
|
|
.Select(grouping =>
|
|
|
|
|
{
|
|
|
|
|
var component = grouping.Key;
|
|
|
|
|
var changelist = new Changelist
|
|
|
|
|
{
|
|
|
|
|
Changes = grouping.SelectMany(kvp => kvp.Value.Changes).ToList()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (component == Component.Core)
|
|
|
|
|
{
|
|
|
|
|
changelist.Version = coreVersion;
|
|
|
|
|
changelist.ComponentVersions = componentVersionDict;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
changelist.Version = componentVersionDict[component];
|
|
|
|
|
|
|
|
|
|
return new KeyValuePair<Component, Changelist>(component, changelist);
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
if (releaseDictionary.Count == 0)
|
|
|
|
|
{
|
|
|
|
@@ -417,20 +383,31 @@ namespace Tgstation.Server.ReleaseNotes
|
|
|
|
|
{
|
|
|
|
|
newNotes.Append(Environment.NewLine);
|
|
|
|
|
newNotes.Append("#### ");
|
|
|
|
|
newNotes.Append(I.Key);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var noteTuple in I.Value)
|
|
|
|
|
string componentName = I.Key switch
|
|
|
|
|
{
|
|
|
|
|
newNotes.Append(Environment.NewLine);
|
|
|
|
|
newNotes.Append("- ");
|
|
|
|
|
newNotes.Append(noteTuple.Item1);
|
|
|
|
|
newNotes.Append(" (#");
|
|
|
|
|
newNotes.Append(noteTuple.Item2);
|
|
|
|
|
newNotes.Append(" @");
|
|
|
|
|
newNotes.Append(noteTuple.Item3);
|
|
|
|
|
newNotes.Append(')');
|
|
|
|
|
}
|
|
|
|
|
Component.HttpApi => "HTTP API",
|
|
|
|
|
Component.InteropApi => "Interop API",
|
|
|
|
|
Component.Configuration => "**Configuration**",
|
|
|
|
|
Component.DreamMakerApi => "DreamMaker API",
|
|
|
|
|
Component.HostWatchdog => "Host Watchdog",
|
|
|
|
|
Component.Core => "Core",
|
|
|
|
|
Component.WebControlPanel => "Web Control Panel",
|
|
|
|
|
_ => throw new Exception($"Unknown Component: {I.Key}"),
|
|
|
|
|
};
|
|
|
|
|
newNotes.Append(componentName);
|
|
|
|
|
|
|
|
|
|
foreach (var change in I.Value.Changes)
|
|
|
|
|
foreach (var line in change.Descriptions)
|
|
|
|
|
{
|
|
|
|
|
newNotes.Append(Environment.NewLine);
|
|
|
|
|
newNotes.Append("- ");
|
|
|
|
|
newNotes.Append(line);
|
|
|
|
|
newNotes.Append(" (#");
|
|
|
|
|
newNotes.Append(change.PullRequest);
|
|
|
|
|
newNotes.Append(" @");
|
|
|
|
|
newNotes.Append(change.Author);
|
|
|
|
|
newNotes.Append(')');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
newNotes.Append(Environment.NewLine);
|
|
|
|
|
}
|
|
|
|
@@ -473,6 +450,237 @@ namespace Tgstation.Server.ReleaseNotes
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static ConcurrentDictionary<int, Task<Milestone>> milestoneTasks = new ConcurrentDictionary<int, Task<Milestone>>();
|
|
|
|
|
static Task<Milestone> GetMilestone(IGitHubClient client, int number)
|
|
|
|
|
=> milestoneTasks.GetOrAdd(number, localNumber => client.Issue.Milestone.Get(RepoOwner, RepoName, localNumber));
|
|
|
|
|
|
|
|
|
|
static async Task<Tuple<Dictionary<Component, Changelist>, Dictionary<Component, Version>, bool>> GetReleaseNotesFromPR(IGitHubClient client, Issue pullRequest, bool doNotCloseMilestone, bool needComponentExactVersions, bool forAllComponents)
|
|
|
|
|
{
|
|
|
|
|
//need to check it was merged
|
|
|
|
|
var fullPR = await RLR(() => client.Repository.PullRequest.Get(RepoOwner, RepoName, pullRequest.Number));
|
|
|
|
|
|
|
|
|
|
if (!fullPR.Merged)
|
|
|
|
|
{
|
|
|
|
|
if (!doNotCloseMilestone && fullPR.Milestone != null)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine($"Removing trash PR #{fullPR.Number} from milestone...");
|
|
|
|
|
await RLR(() => client.Issue.Update(RepoOwner, RepoName, fullPR.Number, new IssueUpdate
|
|
|
|
|
{
|
|
|
|
|
Milestone = null
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (fullPR.Milestone == null)
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var commentsTask = TripleCheckGitHubPagination(apiOptions => client.Issue.Comment.GetAllForIssue(fullPR.Base.Repository.Id, pullRequest.Number, apiOptions), comment => comment.Id);
|
|
|
|
|
|
|
|
|
|
bool isReleasePR = false;
|
|
|
|
|
async Task<bool> ShouldGetExtendedComponentVersions()
|
|
|
|
|
{
|
|
|
|
|
if (forAllComponents)
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
var commit = await RLR(() => client.Repository.Commit.Get(fullPR.Base.Repository.Id, fullPR.MergeCommitSha));
|
|
|
|
|
|
|
|
|
|
return isReleasePR = commit.Commit.Message.Contains("[TGSDeploy]");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Task<bool> needExtendedComponentVersions = null;
|
|
|
|
|
async Task<Dictionary<Component, Version>> GetComponentVersions()
|
|
|
|
|
{
|
|
|
|
|
var mergeCommit = fullPR.MergeCommitSha;
|
|
|
|
|
// we don't care about unreleased web control panel changes
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
needExtendedComponentVersions = ShouldGetExtendedComponentVersions();
|
|
|
|
|
|
|
|
|
|
var versionsBytes = await RLR(() => client.Repository.Content.GetRawContentByRef(RepoOwner, RepoName, "build/Version.props", mergeCommit));
|
|
|
|
|
|
|
|
|
|
XDocument doc;
|
|
|
|
|
using (var ms = new MemoryStream(versionsBytes))
|
|
|
|
|
doc = XDocument.Load(ms);
|
|
|
|
|
|
|
|
|
|
var project = doc.Root;
|
|
|
|
|
var xmlNamespace = project.GetDefaultNamespace();
|
|
|
|
|
var versionsPropertyGroup = project.Elements().First(x => x.Name == xmlNamespace + "PropertyGroup");
|
|
|
|
|
|
|
|
|
|
Version Parse(string elemName, bool controlPanel = false)
|
|
|
|
|
{
|
|
|
|
|
var element = versionsPropertyGroup.Element(xmlNamespace + elemName);
|
|
|
|
|
if (element == null)
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
return Version.Parse(element.Value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var dict = new Dictionary<Component, Version>
|
|
|
|
|
{
|
|
|
|
|
{ Component.Core, Parse("TgsCoreVersion") },
|
|
|
|
|
{ Component.HttpApi, Parse("TgsApiVersion") },
|
|
|
|
|
{ Component.DreamMakerApi, Parse("TgsDmapiVersion") },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (await needExtendedComponentVersions)
|
|
|
|
|
{
|
|
|
|
|
// only grab some versions at release time
|
|
|
|
|
// we aggregate later
|
|
|
|
|
dict.Add(Component.Configuration, Parse("TgsConfigVersion"));
|
|
|
|
|
dict.Add(Component.InteropApi, Parse("TgsInteropVersion"));
|
|
|
|
|
dict.Add(Component.HostWatchdog, Parse("TgsHostWatchdogVersion"));
|
|
|
|
|
dict.Add(Component.NugetCommon, Parse("TgsCommonLibraryVersion"));
|
|
|
|
|
dict.Add(Component.NugetApi, Parse("TgsApiLibraryVersion"));
|
|
|
|
|
dict.Add(Component.NugetClient, Parse("TgsClientVersion"));
|
|
|
|
|
|
|
|
|
|
var webVersion = Parse("TgsControlPanelVersion");
|
|
|
|
|
if (webVersion != null)
|
|
|
|
|
{
|
|
|
|
|
dict.Add(Component.WebControlPanel, webVersion);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
var controlPanelVersionBytes = await RLR(() => client.Repository.Content.GetRawContentByRef(RepoOwner, RepoName, "build/ControlPanelVersion.props", mergeCommit));
|
|
|
|
|
using (var ms = new MemoryStream(controlPanelVersionBytes))
|
|
|
|
|
doc = XDocument.Load(ms);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
project = doc.Root;
|
|
|
|
|
var controlPanelXmlNamespace = project.GetDefaultNamespace();
|
|
|
|
|
var controlPanelVersionsPropertyGroup = project.Elements().First(x => x.Name == controlPanelXmlNamespace + "PropertyGroup");
|
|
|
|
|
dict.Add(Component.WebControlPanel, Version.Parse(controlPanelVersionsPropertyGroup.Element(controlPanelXmlNamespace + "TgsControlPanelVersion").Value));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return dict;
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
return new Dictionary<Component, Version>();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var componentVersions = needComponentExactVersions ? GetComponentVersions() : Task.FromResult<Dictionary<Component, Version>>(null);
|
|
|
|
|
var changelists = new ConcurrentDictionary<Component, Changelist>();
|
|
|
|
|
async Task BuildNotesFromComment(string comment, User user, Task localPreviousTask)
|
|
|
|
|
{
|
|
|
|
|
await localPreviousTask;
|
|
|
|
|
if (comment == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
async Task CommitNotes(Component component, List<string> notes)
|
|
|
|
|
{
|
|
|
|
|
foreach (var I in notes)
|
|
|
|
|
Console.WriteLine(component + " #" + fullPR.Number + " - " + I + " (@" + user.Login + ")");
|
|
|
|
|
|
|
|
|
|
var tupleSelector = notes.Select(note => new Change
|
|
|
|
|
{
|
|
|
|
|
Descriptions = new List<string> { note },
|
|
|
|
|
PullRequest = fullPR.Number,
|
|
|
|
|
Author = user.Login
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
var useExtendedComponentVersions = await needExtendedComponentVersions;
|
|
|
|
|
var componentVersionsResult = await componentVersions;
|
|
|
|
|
lock (changelists)
|
|
|
|
|
if (changelists.TryGetValue(component, out var currentChangelist))
|
|
|
|
|
currentChangelist.Changes.AddRange(tupleSelector);
|
|
|
|
|
else
|
|
|
|
|
Debug.Assert(changelists.TryAdd(component, new Changelist
|
|
|
|
|
{
|
|
|
|
|
Changes = tupleSelector.ToList(),
|
|
|
|
|
Unreleased = false,
|
|
|
|
|
Version = needComponentExactVersions && componentVersionsResult.TryGetValue(component, out var componentVersion)
|
|
|
|
|
? componentVersion
|
|
|
|
|
: null,
|
|
|
|
|
ComponentVersions = component == Component.Core && needComponentExactVersions && useExtendedComponentVersions
|
|
|
|
|
? new Dictionary<Component, Version>(componentVersionsResult.Where(kvp => kvp.Key != Component.Core))
|
|
|
|
|
: null
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var commentSplits = comment.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
|
|
|
string targetComponent = null;
|
|
|
|
|
var notes = new List<string>();
|
|
|
|
|
foreach (var line in commentSplits)
|
|
|
|
|
{
|
|
|
|
|
var trimmedLine = line.Trim();
|
|
|
|
|
if (targetComponent == null)
|
|
|
|
|
{
|
|
|
|
|
if (trimmedLine.StartsWith(":cl:", StringComparison.Ordinal))
|
|
|
|
|
{
|
|
|
|
|
targetComponent = trimmedLine[4..].Trim();
|
|
|
|
|
if (targetComponent.Length == 0)
|
|
|
|
|
targetComponent = "Core";
|
|
|
|
|
}
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (trimmedLine.StartsWith("/:cl:", StringComparison.Ordinal))
|
|
|
|
|
{
|
|
|
|
|
if(!Enum.TryParse<Component>(targetComponent, out var component))
|
|
|
|
|
switch (targetComponent.ToUpperInvariant())
|
|
|
|
|
{
|
|
|
|
|
case "**CONFIGURATION**":
|
|
|
|
|
case "CONFIGURATION":
|
|
|
|
|
case "CONFIG":
|
|
|
|
|
component = Component.Configuration;
|
|
|
|
|
break;
|
|
|
|
|
case "HTTP API":
|
|
|
|
|
component = Component.HttpApi;
|
|
|
|
|
break;
|
|
|
|
|
case "WEB CONTROL PANEL":
|
|
|
|
|
component = Component.WebControlPanel;
|
|
|
|
|
break;
|
|
|
|
|
case "DMAPI":
|
|
|
|
|
case "DREAMMAKER API":
|
|
|
|
|
component = Component.DreamMakerApi;
|
|
|
|
|
break;
|
|
|
|
|
case "INTEROP API":
|
|
|
|
|
component = Component.InteropApi;
|
|
|
|
|
break;
|
|
|
|
|
case "HOST WATCHDOG":
|
|
|
|
|
component = Component.HostWatchdog;
|
|
|
|
|
break;
|
|
|
|
|
case "NUGET: API":
|
|
|
|
|
component = Component.NugetApi;
|
|
|
|
|
break;
|
|
|
|
|
case "NUGET: COMMON":
|
|
|
|
|
component = Component.NugetCommon;
|
|
|
|
|
break;
|
|
|
|
|
case "NUGET: CLIENT":
|
|
|
|
|
component = Component.NugetClient;
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
throw new Exception($"Unknown component: \"{targetComponent}\"");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await CommitNotes(component, notes);
|
|
|
|
|
targetComponent = null;
|
|
|
|
|
notes.Clear();
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (trimmedLine.Length == 0)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
notes.Add(trimmedLine);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var previousTask = BuildNotesFromComment(fullPR.Body, fullPR.User, Task.CompletedTask);
|
|
|
|
|
var comments = await commentsTask;
|
|
|
|
|
foreach (var x in comments)
|
|
|
|
|
previousTask = BuildNotesFromComment(x.Body, x.User, previousTask);
|
|
|
|
|
|
|
|
|
|
await previousTask;
|
|
|
|
|
|
|
|
|
|
return Tuple.Create(changelists.ToDictionary(kvp => kvp.Key, kvp => kvp.Value), await componentVersions, isReleasePR);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class ExtendedReleaseUpdate : ReleaseUpdate
|
|
|
|
|
{
|
|
|
|
|
public bool? MakeLatest { get; set; }
|
|
|
|
@@ -570,8 +778,429 @@ The user account that created this pull request is available to correct any issu
|
|
|
|
|
{
|
|
|
|
|
Body = prBody,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static async Task<T> RLR<T>(Func<Task<T>> func)
|
|
|
|
|
{
|
|
|
|
|
while (true)
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
return await func();
|
|
|
|
|
}
|
|
|
|
|
catch (HttpRequestException ex) when (ex.InnerException is IOException ioEx && ioEx.InnerException is SocketException sockEx && sockEx.ErrorCode == 10053)
|
|
|
|
|
{
|
|
|
|
|
await Task.Delay(15000);
|
|
|
|
|
}
|
|
|
|
|
catch (SecondaryRateLimitExceededException)
|
|
|
|
|
{
|
|
|
|
|
await Task.Delay(15000);
|
|
|
|
|
}
|
|
|
|
|
catch (RateLimitExceededException ex)
|
|
|
|
|
{
|
|
|
|
|
var now = DateTimeOffset.UtcNow.AddSeconds(-10);
|
|
|
|
|
if (ex.Reset > now)
|
|
|
|
|
{
|
|
|
|
|
var delay = ex.Reset - now;
|
|
|
|
|
await Task.Delay(delay);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static async Task<List<T>> TripleCheckGitHubPagination<T>(Func<ApiOptions, Task<IReadOnlyList<T>>> apiCall, Func<T, long> idSelector)
|
|
|
|
|
{
|
|
|
|
|
// I've seen GitHub pagination return incomplete result sets in the past
|
|
|
|
|
// It has an in-built pagination limit of 100
|
|
|
|
|
var apiOptions = new ApiOptions
|
|
|
|
|
{
|
|
|
|
|
PageSize = 100
|
|
|
|
|
};
|
|
|
|
|
var results = await RLR(() => apiCall(apiOptions));
|
|
|
|
|
Dictionary<long, T> distinctEntries = new Dictionary<long, T>(results.Count);
|
|
|
|
|
foreach (var result in results)
|
|
|
|
|
distinctEntries.Add(idSelector(result), result);
|
|
|
|
|
|
|
|
|
|
if (results.Count > 100)
|
|
|
|
|
{
|
|
|
|
|
results = await RLR(() => apiCall(apiOptions));
|
|
|
|
|
foreach (var result in results)
|
|
|
|
|
distinctEntries.TryAdd(idSelector(result), result);
|
|
|
|
|
|
|
|
|
|
results = await RLR(() => apiCall(apiOptions));
|
|
|
|
|
foreach (var result in results)
|
|
|
|
|
distinctEntries.TryAdd(idSelector(result), result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return distinctEntries.Values.ToList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static async Task<Task<ReleaseNotes>> ProcessMilestone(IGitHubClient client, Milestone milestone)
|
|
|
|
|
{
|
|
|
|
|
// have to trust this works
|
|
|
|
|
SearchIssuesResult results;
|
|
|
|
|
|
|
|
|
|
var milestoneTask = Task.FromResult(milestone);
|
|
|
|
|
var pullRequests = new Dictionary<int, Issue>();
|
|
|
|
|
var iteration = 0;
|
|
|
|
|
while (true)
|
|
|
|
|
{
|
|
|
|
|
results = await RLR(() => client.Search.SearchIssues(new SearchIssuesRequest
|
|
|
|
|
{
|
|
|
|
|
Type = IssueTypeQualifier.PullRequest,
|
|
|
|
|
Milestone = milestone.Title,
|
|
|
|
|
Repos = new RepositoryCollection
|
|
|
|
|
{
|
|
|
|
|
{ RepoOwner, RepoName },
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
foreach (var result in results.Items)
|
|
|
|
|
if (pullRequests.TryAdd(result.Number, result))
|
|
|
|
|
milestoneTasks.TryAdd(result.Number, milestoneTask);
|
|
|
|
|
|
|
|
|
|
if (results.IncompleteResults)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
if (results.TotalCount <= 100 || ++iteration == 3)
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async Task<ReleaseNotes> RunPRs()
|
|
|
|
|
{
|
|
|
|
|
var milestoneVersion = Version.Parse(milestone.Title[1..]);
|
|
|
|
|
var prTasks = pullRequests.Select(
|
|
|
|
|
kvp => GetReleaseNotesFromPR(client, kvp.Value, true, true, milestone.State.Value == ItemState.Open))
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
await Task.WhenAll(prTasks);
|
|
|
|
|
|
|
|
|
|
var prResults = prTasks.Select(x => x.Result).Where(result => result != null).ToList();
|
|
|
|
|
|
|
|
|
|
var releasePRResult = prResults.FirstOrDefault(x => x.Item3);
|
|
|
|
|
|
|
|
|
|
Dictionary<Component, Version> releasedComponentVersions;
|
|
|
|
|
if (releasePRResult != null)
|
|
|
|
|
releasedComponentVersions = releasePRResult.Item2;
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
releasedComponentVersions = new Dictionary<Component, Version>(
|
|
|
|
|
prResults
|
|
|
|
|
.SelectMany(result => result.Item2)
|
|
|
|
|
.GroupBy(kvp => kvp.Key)
|
|
|
|
|
.Select(grouping => new KeyValuePair<Component, Version>(grouping.Key, grouping.Max(kvp => kvp.Value))));
|
|
|
|
|
|
|
|
|
|
foreach(var maxVersionKvp in prResults.SelectMany(x => x.Item1)
|
|
|
|
|
.Where(x => !releasedComponentVersions.ContainsKey(x.Key))
|
|
|
|
|
.GroupBy(x => x.Key)
|
|
|
|
|
.Select(group => {
|
|
|
|
|
var versions = group
|
|
|
|
|
.Where(x => x.Value.Version != null)
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
if (versions.Count == 0)
|
|
|
|
|
return new KeyValuePair<Component, Version>(group.Key, null);
|
|
|
|
|
|
|
|
|
|
return new KeyValuePair<Component, Version>(group.Key, versions.Max(x => x.Value.Version));
|
|
|
|
|
})
|
|
|
|
|
.Where(kvp => kvp.Value != null)
|
|
|
|
|
.ToList())
|
|
|
|
|
{
|
|
|
|
|
releasedComponentVersions.Add(maxVersionKvp.Key, maxVersionKvp.Value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var finalResults = new Dictionary<Component, List<Changelist>>();
|
|
|
|
|
foreach (var componentKvp in releasedComponentVersions)
|
|
|
|
|
{
|
|
|
|
|
var component = componentKvp.Key;
|
|
|
|
|
var list = new List<Changelist>();
|
|
|
|
|
|
|
|
|
|
foreach(var changelistDict in prResults.Select(x => x.Item1))
|
|
|
|
|
{
|
|
|
|
|
if (!changelistDict.TryGetValue(component, out var changelist))
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
Version componentVersion = milestoneVersion;
|
|
|
|
|
var unreleased = milestone.State.Value == ItemState.Open;
|
|
|
|
|
if (component != Component.Core)
|
|
|
|
|
{
|
|
|
|
|
componentVersion = changelist.Version ?? componentKvp.Value;
|
|
|
|
|
if (releasedNonCoreVersions != null
|
|
|
|
|
&& releasedNonCoreVersions.TryGetValue(component, out var releasedVersions)
|
|
|
|
|
&& !releasedVersions.Any(x => x == componentVersion))
|
|
|
|
|
{
|
|
|
|
|
// roll forward
|
|
|
|
|
var newList = releasedVersions
|
|
|
|
|
.ToList();
|
|
|
|
|
newList.Add(componentVersion);
|
|
|
|
|
newList = newList.OrderBy(x => x).ToList();
|
|
|
|
|
|
|
|
|
|
var index = newList.IndexOf(componentVersion);
|
|
|
|
|
Debug.Assert(index != -1);
|
|
|
|
|
if (index != (newList.Count - 1))
|
|
|
|
|
{
|
|
|
|
|
componentVersion = newList[index + 1];
|
|
|
|
|
unreleased = false;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
unreleased = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var entry = list.FirstOrDefault(x => x.Version == componentVersion);
|
|
|
|
|
if (entry == null)
|
|
|
|
|
{
|
|
|
|
|
entry = changelist;
|
|
|
|
|
entry.Version = componentVersion;
|
|
|
|
|
entry.Unreleased = unreleased;
|
|
|
|
|
list.Add(entry);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
entry.Changes.AddRange(changelist.Changes);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Debug.Assert(list.Select(x => x.Version.ToString()).Distinct().Count() == list.Count);
|
|
|
|
|
if (component == Component.Core)
|
|
|
|
|
{
|
|
|
|
|
Debug.Assert(list.All(x => x.Version == milestoneVersion));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
list = list.OrderByDescending(x => x.Version).ToList();
|
|
|
|
|
finalResults.Add(component, list);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!finalResults.ContainsKey(Component.Core) || finalResults[Component.Core].Count == 0)
|
|
|
|
|
{
|
|
|
|
|
finalResults.Remove(Component.Core);
|
|
|
|
|
finalResults.Add(Component.Core, new List<Changelist>
|
|
|
|
|
{
|
|
|
|
|
new Changelist
|
|
|
|
|
{
|
|
|
|
|
Changes = new List<Change>(),
|
|
|
|
|
ComponentVersions = releasedComponentVersions,
|
|
|
|
|
Unreleased = milestone.State.Value == ItemState.Open,
|
|
|
|
|
Version = milestoneVersion,
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
Debug.Assert(finalResults[Component.Core].All(x => x.Version == milestoneVersion));
|
|
|
|
|
|
|
|
|
|
var notes = new ReleaseNotes
|
|
|
|
|
{
|
|
|
|
|
Components = new SortedDictionary<Component, List<Changelist>>(finalResults),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return notes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return RunPRs();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static async Task<int> FullNotes(IGitHubClient client)
|
|
|
|
|
{
|
|
|
|
|
var startRateLimit = (client.GetLastApiInfo()?.RateLimit ?? (await client.RateLimit.GetRateLimits()).Rate).Remaining;
|
|
|
|
|
|
|
|
|
|
ReleaseNotes existingNotes = null;
|
|
|
|
|
if (File.Exists("changelog.yml"))
|
|
|
|
|
{
|
|
|
|
|
var existingYml = await File.ReadAllTextAsync("changelog.yml");
|
|
|
|
|
var deserializer = new DeserializerBuilder()
|
|
|
|
|
.Build();
|
|
|
|
|
|
|
|
|
|
existingNotes = deserializer.Deserialize<ReleaseNotes>(existingYml);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var releaseNotes = await GenerateNotes(client, existingNotes);
|
|
|
|
|
|
|
|
|
|
Console.WriteLine($"Generating all release notes took {startRateLimit - client.GetLastApiInfo().RateLimit.Remaining} requests.");
|
|
|
|
|
|
|
|
|
|
var serializer = new SerializerBuilder()
|
|
|
|
|
.ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults)
|
|
|
|
|
.WithTypeConverter(new VersionConverter())
|
|
|
|
|
.Build();
|
|
|
|
|
|
|
|
|
|
var serializedYaml = serializer.Serialize(releaseNotes);
|
|
|
|
|
await File.WriteAllTextAsync("changelog.yml", serializedYaml).ConfigureAwait(false);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static HttpClient httpClient = new HttpClient(new HttpClientHandler()
|
|
|
|
|
{
|
|
|
|
|
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
|
|
|
|
|
});
|
|
|
|
|
static async Task<HashSet<Version>> EnumerateNugetVersions(string package)
|
|
|
|
|
{
|
|
|
|
|
var url = new Uri($"https://api.nuget.org/v3/registration5-gz-semver2/{package.ToLowerInvariant()}/index.json");
|
|
|
|
|
|
|
|
|
|
using var req = new HttpRequestMessage();
|
|
|
|
|
req.Headers.UserAgent.Add(new System.Net.Http.Headers.ProductInfoHeaderValue("Tgstation.Server.ReleaseNotes", "0.1.0"));
|
|
|
|
|
req.Method = HttpMethod.Get;
|
|
|
|
|
req.RequestUri = url;
|
|
|
|
|
|
|
|
|
|
using var resp = await httpClient.SendAsync(req);
|
|
|
|
|
resp.EnsureSuccessStatusCode();
|
|
|
|
|
|
|
|
|
|
var json = await resp.Content.ReadAsStringAsync();
|
|
|
|
|
|
|
|
|
|
dynamic dynamicJson = JsonConvert.DeserializeObject(json);
|
|
|
|
|
|
|
|
|
|
var versions = (IEnumerable<dynamic>)dynamicJson.items[0].items;
|
|
|
|
|
var results = versions
|
|
|
|
|
.Select(x => Version.TryParse((string)x.catalogEntry.version, out var version) ? version : null)
|
|
|
|
|
.Where(version => version != null)
|
|
|
|
|
.OrderBy(x => x)
|
|
|
|
|
.ToHashSet();
|
|
|
|
|
return results;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static IReadOnlyDictionary<Component, IReadOnlySet<Version>> releasedNonCoreVersions;
|
|
|
|
|
|
|
|
|
|
static async Task<ReleaseNotes> GenerateNotes(IGitHubClient client, ReleaseNotes previousNotes)
|
|
|
|
|
{
|
|
|
|
|
var releasesTask = TripleCheckGitHubPagination(
|
|
|
|
|
apiOptions => client.Repository.Release.GetAll(RepoOwner, RepoName, apiOptions),
|
|
|
|
|
release => release.Id);
|
|
|
|
|
|
|
|
|
|
var milestones = await TripleCheckGitHubPagination(
|
|
|
|
|
apiOptions => client.Issue.Milestone.GetAllForRepository(RepoOwner, RepoName, new MilestoneRequest {
|
|
|
|
|
State = ItemStateFilter.All
|
|
|
|
|
}, apiOptions),
|
|
|
|
|
milestone => milestone.Id);
|
|
|
|
|
|
|
|
|
|
var versionMilestones = milestones
|
|
|
|
|
.Where(milestone => Regex.IsMatch(milestone.Title, @"v[1-9][0-9]*\.[1-9]*[0-9]+\.[1-9]*[0-9]+$"))
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
var releases = await releasesTask;
|
|
|
|
|
|
|
|
|
|
var nugetCommonVersions = EnumerateNugetVersions("Tgstation.Server.Common");
|
|
|
|
|
var nugetApiVersions = EnumerateNugetVersions("Tgstation.Server.Api");
|
|
|
|
|
var nugetClientVersions = EnumerateNugetVersions("Tgstation.Server.Client");
|
|
|
|
|
|
|
|
|
|
releasedNonCoreVersions = new Dictionary<Component, IReadOnlySet<Version>> {
|
|
|
|
|
{ Component.HttpApi, releases
|
|
|
|
|
.Where(x => x.TagName.StartsWith("api-v"))
|
|
|
|
|
.Select(x => Version.Parse(x.TagName[5..]))
|
|
|
|
|
.OrderBy(x => x)
|
|
|
|
|
.ToHashSet() },
|
|
|
|
|
{ Component.DreamMakerApi, releases
|
|
|
|
|
.Where(x => x.TagName.StartsWith("dmapi-v"))
|
|
|
|
|
.Select(x => Version.Parse(x.TagName[7..]))
|
|
|
|
|
.OrderBy(x => x)
|
|
|
|
|
.ToHashSet() },
|
|
|
|
|
{ Component.NugetCommon, await nugetCommonVersions },
|
|
|
|
|
{ Component.NugetApi, await nugetApiVersions },
|
|
|
|
|
{ Component.NugetClient, await nugetClientVersions }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var milestonesToProcess = versionMilestones;
|
|
|
|
|
if (previousNotes != null)
|
|
|
|
|
{
|
|
|
|
|
var releasedVersions = previousNotes.Components[Component.Core].Where(cl => !cl.Unreleased).ToList();
|
|
|
|
|
milestonesToProcess = milestonesToProcess
|
|
|
|
|
.Where(x => !releasedVersions.Any(
|
|
|
|
|
version => version.Version == Version.Parse(x.Title.AsSpan(1))))
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
foreach (var kvp in previousNotes.Components)
|
|
|
|
|
if (releasedNonCoreVersions.TryGetValue(kvp.Key, out var releasedComponentVersions))
|
|
|
|
|
kvp.Value.RemoveAll(x => x.Unreleased = !releasedComponentVersions.Any(y => y == x.Version));
|
|
|
|
|
else
|
|
|
|
|
kvp.Value.RemoveAll(x => x.Unreleased);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var milestonePRTasks = milestonesToProcess
|
|
|
|
|
.Select(milestone => ProcessMilestone(client, milestone))
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
await Task.WhenAll(milestonePRTasks);
|
|
|
|
|
|
|
|
|
|
await Task.WhenAll(milestonePRTasks.Select(task => task.Result));
|
|
|
|
|
|
|
|
|
|
var coreCls = milestonePRTasks
|
|
|
|
|
.SelectMany(task => task.Result.Result.Components)
|
|
|
|
|
.Where(x => x.Key == Component.Core)
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
Debug.Assert(
|
|
|
|
|
coreCls.Count == milestonesToProcess.Count);
|
|
|
|
|
|
|
|
|
|
var distinctCoreVersions = coreCls
|
|
|
|
|
.SelectMany(x => x.Value)
|
|
|
|
|
.Select(x => x.Version.ToString())
|
|
|
|
|
.Distinct()
|
|
|
|
|
.Select(Version.Parse)
|
|
|
|
|
.OrderBy(x => x)
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
var missingCoreVersions = milestonesToProcess
|
|
|
|
|
.Where(x => !distinctCoreVersions.Any(y => Version.Parse(x.Title.Substring(1)) == y))
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
Debug.Assert(missingCoreVersions.Count == 0);
|
|
|
|
|
foreach (var missingCoreVersion in missingCoreVersions)
|
|
|
|
|
await await ProcessMilestone(client, missingCoreVersion);
|
|
|
|
|
|
|
|
|
|
var changelistsGroupedByComponent =
|
|
|
|
|
milestonePRTasks
|
|
|
|
|
.SelectMany(task => task.Result.Result.Components)
|
|
|
|
|
.GroupBy(kvp => kvp.Key)
|
|
|
|
|
.ToDictionary(grouping => grouping.Key, grouping => grouping.SelectMany(kvp => kvp.Value));
|
|
|
|
|
|
|
|
|
|
var releaseNotes = new ReleaseNotes
|
|
|
|
|
{
|
|
|
|
|
Components = new SortedDictionary<Component, List<Changelist>>(
|
|
|
|
|
changelistsGroupedByComponent
|
|
|
|
|
.ToDictionary(
|
|
|
|
|
kvp => kvp.Key,
|
|
|
|
|
kvp => kvp
|
|
|
|
|
.Value
|
|
|
|
|
.GroupBy(changelist => changelist.Version)
|
|
|
|
|
.Select(grouping =>
|
|
|
|
|
{
|
|
|
|
|
var firstEntry = grouping.First();
|
|
|
|
|
return new Changelist
|
|
|
|
|
{
|
|
|
|
|
Changes = grouping.SelectMany(cl => cl.Changes).ToList(),
|
|
|
|
|
ComponentVersions = firstEntry.ComponentVersions,
|
|
|
|
|
Unreleased = firstEntry.Unreleased,
|
|
|
|
|
Version = grouping.Key
|
|
|
|
|
};
|
|
|
|
|
})
|
|
|
|
|
.OrderByDescending(cl => cl.Version)
|
|
|
|
|
.ToList()))
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Debug.Assert(releaseNotes.Components.ContainsKey(Component.Core) && releaseNotes.Components[Component.Core].Count == milestonesToProcess.Count);
|
|
|
|
|
|
|
|
|
|
if (previousNotes != null)
|
|
|
|
|
{
|
|
|
|
|
foreach (var component in Enum.GetValues<Component>())
|
|
|
|
|
{
|
|
|
|
|
if (!previousNotes.Components.ContainsKey(component))
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
if (releaseNotes.Components.TryGetValue(component, out var changelists))
|
|
|
|
|
releaseNotes.Components[component] = changelists
|
|
|
|
|
.Concat(previousNotes.Components[component])
|
|
|
|
|
.OrderByDescending(cl => cl.Version)
|
|
|
|
|
.ToList();
|
|
|
|
|
else
|
|
|
|
|
releaseNotes.Components[component] = previousNotes.Components[component];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (var kvp in releaseNotes.Components)
|
|
|
|
|
{
|
|
|
|
|
var distinctCount = kvp.Value.Select(changelist => changelist.Version.ToString()).Distinct().Count();
|
|
|
|
|
Debug.Assert(distinctCount == kvp.Value.Count);
|
|
|
|
|
|
|
|
|
|
foreach (var cl in kvp.Value)
|
|
|
|
|
cl.DeduplicateChanges();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return releaseNotes;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|