tgstation-server
The /tg/station 13 server suite
Static Private Member Functions | List of all members
ReleaseNotes.Program Class Reference

Contains the application entrypoint More...

Static Private Member Functions

static async Task< int > Main (string[] args)
 The entrypoint for the Program More...
 

Detailed Description

Contains the application entrypoint

Definition at line 15 of file Program.cs.

Member Function Documentation

static async Task<int> ReleaseNotes.Program.Main ( string[]  args)
staticprivate

The entrypoint for the Program

Definition at line 20 of file Program.cs.

21  {
22  if (args.Length < 1)
23  {
24  Console.WriteLine("Missing version argument!");
25  return 1;
26  }
27 
28  var versionString = args[0];
29  if (!Version.TryParse(versionString, out var version))
30  {
31  Console.WriteLine("Invalid version: " + versionString);
32  return 2;
33  }
34 
35  var doNotCloseMilestone = args.Length >= 2 && args[1].ToUpperInvariant() == "--NO-CLOSE";
36 
37  const string ReleaseNotesEnvVar = "TGS_RELEASE_NOTES_TOKEN";
38  var githubToken = Environment.GetEnvironmentVariable(ReleaseNotesEnvVar);
39  if (String.IsNullOrWhiteSpace(githubToken))
40  {
41  Console.WriteLine("Missing " + ReleaseNotesEnvVar + " environment variable!");
42  return 3;
43  }
44 
45  try
46  {
47  var client = new GitHubClient(new ProductHeaderValue("tgs_release_notes"));
48  client.Credentials = new Credentials(githubToken);
49 
50  const string RepoOwner = "tgstation";
51  const string RepoName = "tgstation-server";
52 
53  var releasesTask = client.Repository.Release.GetAll(RepoOwner, RepoName);
54 
55  Console.WriteLine("Getting pull requests in milestone " + versionString + "...");
56  var milestonePRs = await client.Search.SearchIssues(new SearchIssuesRequest
57  {
58  Milestone = versionString,
59  Type = IssueTypeQualifier.PullRequest,
60  Repos = { { RepoOwner, RepoName } }
61  }).ConfigureAwait(false);
62 
63  if (milestonePRs.IncompleteResults)
64  {
65  Console.WriteLine("Incomplete results for milestone PRs query!");
66  return 5;
67  }
68  Console.WriteLine(milestonePRs.Items.Count + " total pull requests");
69 
70  Task<Milestone> milestoneTask = null;
71  var milestoneTaskLock = new object();
72  var releaseDictionary = new Dictionary<int, List<string>>();
73  var authorizedUsers = new Dictionary<long, Task<bool>>();
74 
75  async Task GetReleaseNotesFromPR(Issue pullRequest)
76  {
77  //need to check it was merged
78  var fullPR = await client.Repository.PullRequest.Get(RepoOwner, RepoName, pullRequest.Number).ConfigureAwait(false);
79 
80  async Task<Milestone> GetMilestone()
81  {
82  if (fullPR.Milestone == null)
83  return null;
84  return await client.Issue.Milestone.Get(RepoOwner, RepoName, fullPR.Milestone.Number);
85  };
86 
87  lock (milestoneTaskLock)
88  if (milestoneTask == null)
89  milestoneTask = GetMilestone();
90 
91  if (!fullPR.Merged)
92  return;
93 
94  async Task BuildNotesFromComment(string comment, User user)
95  {
96  var commentSplits = comment.Split('\n');
97  var notesOpen = false;
98  var notesClosed = false;
99  var notes = new List<string>();
100  foreach (var line in commentSplits)
101  {
102  var trimmedLine = line.Trim();
103  if (!notesOpen)
104  {
105  notesOpen = trimmedLine.StartsWith(":cl:", StringComparison.Ordinal);
106  notesClosed = false;
107  continue;
108  }
109  if (trimmedLine.StartsWith("/:cl:", StringComparison.Ordinal))
110  {
111  notesClosed = true;
112  notesOpen = false;
113  continue;
114  }
115  if (trimmedLine.Length == 0)
116  continue;
117  notes.Add(trimmedLine);
118  }
119  if (!notesClosed || notes.Count == 0)
120  return;
121 
122  Task<bool> authTask;
123  TaskCompletionSource<bool> ourTcs = null;
124  lock (authorizedUsers)
125  {
126  if (!authorizedUsers.TryGetValue(user.Id, out authTask))
127  {
128  ourTcs = new TaskCompletionSource<bool>();
129  authTask = ourTcs.Task;
130  authorizedUsers.Add(user.Id, authTask);
131  }
132  }
133 
134  if (ourTcs != null)
135  try
136  {
137  //check if the user has access
138  var perm = await client.Repository.Collaborator.ReviewPermission(RepoOwner, RepoName, user.Login).ConfigureAwait(false);
139  ourTcs.SetResult(perm.Permission == PermissionLevel.Write || perm.Permission == PermissionLevel.Admin);
140  }
141  catch
142  {
143  ourTcs.SetResult(false);
144  throw;
145  }
146 
147  var authorized = await authTask.ConfigureAwait(false);
148  if (!authorized)
149  return;
150 
151  lock (releaseDictionary)
152  {
153  foreach (var I in notes)
154  Console.WriteLine("#" + fullPR.Number + " - " + I + " (@" + user.Login + ")");
155  if (releaseDictionary.TryGetValue(fullPR.Number, out var currentValues))
156  currentValues.AddRange(notes);
157  else
158  releaseDictionary.Add(fullPR.Number, notes);
159  }
160  }
161 
162  var comments = await client.Issue.Comment.GetAllForIssue(RepoOwner, RepoName, fullPR.Number).ConfigureAwait(false);
163  await Task.WhenAll(BuildNotesFromComment(fullPR.Body, fullPR.User), Task.WhenAll(comments.Select(x => BuildNotesFromComment(x.Body, x.User)))).ConfigureAwait(false);
164  }
165 
166  var tasks = new List<Task>();
167  foreach (var I in milestonePRs.Items)
168  tasks.Add(GetReleaseNotesFromPR(I));
169 
170  var releases = await releasesTask.ConfigureAwait(false);
171 
172  var releasingSuite = version.Major;
173 
174  Version highestReleaseVersion = null;
175  Release highestRelease = null;
176  foreach (var I in releases)
177  {
178  if (!Version.TryParse(I.TagName.Replace("tgstation-server-v", String.Empty), out var currentReleaseVersion))
179  {
180  Console.WriteLine("WARNING: Unable to determine version of release " + I.HtmlUrl);
181  continue;
182  }
183 
184  if (currentReleaseVersion.Major == releasingSuite && (highestReleaseVersion == null || currentReleaseVersion > highestReleaseVersion))
185  {
186  highestReleaseVersion = currentReleaseVersion;
187  highestRelease = I;
188  }
189  }
190 
191  if (highestReleaseVersion == null)
192  {
193  Console.WriteLine("Unable to determine highest release version for suite " + releasingSuite + "!");
194  return 6;
195  }
196 
197  var oldNotes = highestRelease.Body;
198 
199  var splits = new List<string>(oldNotes.Split('\n'));
200  //trim away all the lines that don't start with #
201 
202  string keepThisRelease;
203  if (version.Revision == 0)
204  if (version.Build == 0)
205  keepThisRelease = "# ";
206  else
207  keepThisRelease = "## ";
208  else
209  keepThisRelease = "### ";
210 
211  for (; !splits[0].StartsWith(keepThisRelease, StringComparison.Ordinal); splits.RemoveAt(0))
212  if (splits.Count == 1)
213  {
214  Console.WriteLine("Error formatting release notes: Can't detemine notes start!");
215  return 7;
216  }
217 
218  oldNotes = String.Join('\n', splits);
219 
220  string prefix;
221  switch (releasingSuite)
222  {
223  case 3:
224  prefix = "The /tg/station server suite";
225  break;
226  default:
227  prefix = "See https://tgstation.github.io/tgstation-server for installation instructions";
228  break;
229  }
230 
231  var newNotes = new StringBuilder(prefix);
232  newNotes.Append(Environment.NewLine);
233  newNotes.Append(Environment.NewLine);
234  if (version.Revision == 0)
235  if (version.Build == 0)
236  {
237  newNotes.Append("# [Version ");
238  newNotes.Append(version.Minor);
239  }
240  else
241  {
242  newNotes.Append("## [Changelog for ");
243  newNotes.Append(version.Build);
244  newNotes.Append(".x");
245  }
246  else
247  {
248  newNotes.Append("### [Patch ");
249  newNotes.Append(version.Revision);
250  }
251  newNotes.Append("](");
252  var milestone = await milestoneTask.ConfigureAwait(false);
253  if (milestone == null)
254  {
255  Console.WriteLine("Unable to detemine milestone!");
256  return 9;
257  }
258  newNotes.Append(milestone.HtmlUrl);
259  newNotes.Append("?closed=1)");
260  newNotes.Append(Environment.NewLine);
261 
262  await Task.WhenAll(tasks).ConfigureAwait(false);
263 
264  if (releaseDictionary.Count == 0)
265  {
266  Console.WriteLine("No release notes for this milestone!");
267  return 8;
268  }
269 
270  foreach (var I in releaseDictionary)
271  foreach (var note in I.Value)
272  {
273  newNotes.Append(Environment.NewLine);
274  newNotes.Append("- ");
275  newNotes.Append(note);
276  newNotes.Append(" (#");
277  newNotes.Append(I.Key);
278  newNotes.Append(')');
279  }
280 
281  newNotes.Append(Environment.NewLine);
282  newNotes.Append(Environment.NewLine);
283  newNotes.Append(oldNotes);
284 
285  Console.WriteLine("Writing out new release notes...");
286  var releaseNotes = newNotes.ToString();
287  await File.WriteAllTextAsync("release_notes.md", releaseNotes).ConfigureAwait(false);
288 
289  if (doNotCloseMilestone)
290  Console.WriteLine("Not closing milestone due to parameter!");
291  else
292  {
293  Console.WriteLine("Closing milestone...");
294  await client.Issue.Milestone.Update(RepoOwner, RepoName, milestone.Number, new MilestoneUpdate
295  {
296  State = ItemState.Closed
297  }).ConfigureAwait(false);
298  }
299 
300  return 0;
301  }
302  catch (Exception e)
303  {
304  Console.WriteLine(e);
305  return 4;
306  }
307  }

The documentation for this class was generated from the following file: