diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index e1c6d6cf23..19273f5d99 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -149,7 +149,32 @@ There is no strict process when it comes to merging pull requests. Pull requests * Commits MUST be properly titled and commented as we only use merge commits for the pull request process +## Deployment process + +Every issue/pull request in a release should share a common milestone named with the release version i.e. `4.5.3.5` + +When every issue and PR in the milestone is closed. Create a version bump PR that changes the version numbers. At the time of this writing they exist in the following files + +- `/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj` +- `/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj` +- 2 in `/src/Tgstation.Server.Host.Service/Properties/AssemblyInfo.cs` +- `/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj` + +Merge the pull request with `[TGSDeploy]` somewhere in the commit title. The scripts will handle amalgamating release notes, building, closing the milestone, and publishing the release. This will also trigger update notifications on existing TGS deployments. + +### API/Client Deployment + +The Api/Client project versions must be updated on nuget when changed. The numbers exist in the following files: + +- `/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj` +- `/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj` + +These should not be the same numbers as the main suite. When bumping the API version the client should only receive a minor (3rd number) version bump unless major changes to CLIENT code were made. + +Merge these alongside regular deployments with `[NugetDeploy]` in the commit title (Won't work without an accompanying `[TGSDeploy]`). This will handle the nuget publishing. + ## Banned content + Do not add any of the following in a Pull Request or risk getting the PR closed: * National Socialist Party of Germany content, National Socialist Party of Germany related content, or National Socialist Party of Germany references diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..98fe22ab33 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,13 @@ +[Release Notes]: # (Your PR should contain a detailed list of notable changes, titled appropriately. This includes any observable changes to the server or DMAPI. See example below) + +:cl: +Description of your change +Each newline corresponds to a release note in the upcoming sprint +/:cl: + +:cl: +You can also have multiple sets of release notes per pull request +They will be amalgamated together in the end +/:cl: + +[Why]: # (Please add a short description [two lines down] of why you think these changes would benefit the game. If you can't justify it in words, it might not be worth adding.) diff --git a/appveyor.yml b/appveyor.yml index f7dece7815..5b94661a3e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -40,7 +40,33 @@ build: parallel: false verbosity: minimal publish_nuget: true +test_script: + - OpenCover.Console.exe -returntargetcode -register:user -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -c %CONFIGURATION% --logger:trx;LogFileName=results.trx /p:DebugType=full tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj" -filter:"+[Tgstation.Server*]* -[Tgstation.Server.Api.Tests*]*" -output:".\api_coverage.xml" -oldstyle + - ps: $wc = New-Object 'System.Net.WebClient' + - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\tests\Tgstation.Server.Api.Tests\TestResults\results.trx)) + - OpenCover.Console.exe -returntargetcode -register:user -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -c %CONFIGURATION% --logger:trx;LogFileName=results.trx /p:DebugType=full tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj" -filter:"+[Tgstation.Server*]* -[Tgstation.Server.Client.Tests*]*" -output:".\client_coverage.xml" -oldstyle + - ps: $wc = New-Object 'System.Net.WebClient' + - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\tests\Tgstation.Server.Client.Tests\TestResults\results.trx)) + - OpenCover.Console.exe -returntargetcode -register:user -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -c %CONFIGURATION% --logger:trx;LogFileName=results.trx /p:DebugType=full tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj" -filter:"+[Tgstation.Server*]* -[Tgstation.Server.Host.Tests*]* -[Tgstation.Server.Host]Tgstation.Server.Host.Models.Migrations*" -output:".\host_coverage.xml" -oldstyle + - ps: $wc = New-Object 'System.Net.WebClient' + - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\tests\Tgstation.Server.Host.Tests\TestResults\results.trx)) + - OpenCover.Console.exe -returntargetcode -register:user -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -c %CONFIGURATION% --logger:trx;LogFileName=results.trx /p:DebugType=full tests/Tgstation.Server.Host.Console.Tests/Tgstation.Server.Host.Console.Tests.csproj" -filter:"+[Tgstation.Server*]* -[Tgstation.Server.Host.Console.Tests*]*" -output:".\console_coverage.xml" -oldstyle + - ps: $wc = New-Object 'System.Net.WebClient' + - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\tests\Tgstation.Server.Host.Console.Tests\TestResults\results.trx)) + - set path=%ProgramFiles(x86)%\Microsoft Visual Studio\2017\TestAgent\Common7\IDE\CommonExtensions\Microsoft\TestWindow;%path% + - copy "%ProgramFiles(x86)%\Microsoft Visual Studio\2017\Community\Common7\IDE\CommonExtensions\Microsoft\TestWindow\Extensions\appveyor.*" "%ProgramFiles(x86)%\Microsoft Visual Studio\2017\TestAgent\Common7\IDE\CommonExtensions\Microsoft\TestWindow\Extensions" /y + - vstest.console /logger:trx;LogFileName=results.trx "tests\Tgstation.Server.Host.Service.Tests\bin\%CONFIGURATION%\Tgstation.Server.Host.Service.Tests.dll" /Enablecodecoverage /inIsolation /Platform:x64 + - ps: $wc = New-Object 'System.Net.WebClient' + - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\TestResults\results.trx)) + - OpenCover.Console.exe -returntargetcode -register:user -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -c %CONFIGURATION% --logger:trx;LogFileName=results.trx /p:DebugType=full tests/Tgstation.Server.Host.Watchdog.Tests/Tgstation.Server.Host.Watchdog.Tests.csproj" -filter:"+[Tgstation.Server*]* -[Tgstation.Server.Host.Watchdog.Tests*]*" -output:".\watchdog_coverage.xml" -oldstyle + - ps: $wc = New-Object 'System.Net.WebClient' + - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\tests\Tgstation.Server.Host.Watchdog.Tests\TestResults\results.trx)) + - OpenCover.Console.exe -returntargetcode -register:user -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -c %CONFIGURATION% --logger:trx;LogFileName=results.trx /p:DebugType=full tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj" -filter:"+[Tgstation.Server*]* -[Tgstation.Server.Tests*]* -[Tgstation.Server.Host]Tgstation.Server.Host.Models.Migrations..*" -output:".\server_coverage.xml" -oldstyle + - ps: $wc = New-Object 'System.Net.WebClient' + - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\tests\Tgstation.Server.Tests\TestResults\results.trx)) after_test: + - ps: build/UploadCoverage.ps1 + - ps: build/BuildDox.ps1 #host updater - dotnet publish src/Tgstation.Server.Host/Tgstation.Server.Host.csproj -o ../../artifacts/ServerHost -c %CONFIGURATION% #console @@ -54,8 +80,6 @@ after_test: - ps: Remove-Item artifacts/ServerHost/appsettings.json #deploy stuff - ps: build/prep_deployment.ps1 - - ps: Write-Host $env:TGSReleaseNotes - - ps: Write-Host $env:TGSGoodNotes deploy: - provider: GitHub release: "tgstation-server-v$(TGSVersion)" @@ -63,7 +87,7 @@ deploy: auth_token: secure: lJNGAXwiB5HlWdthz3K4PetqpTG5IEAyRgKaiKxFMQ8HW8CcOjRtB97B05op7BsK artifact: ServerConsole,ServerService,ServerUpdatePackage,DMAPI - draft: $(TGSGoodNotes) + draft: $(TGSDraftNotes) prerelease: true on: TGSDeploy: "Do it." diff --git a/build/prep_deployment.ps1 b/build/prep_deployment.ps1 index 3f1b84ac60..a00431bff8 100644 --- a/build/prep_deployment.ps1 +++ b/build/prep_deployment.ps1 @@ -1,19 +1,25 @@ $env:TGSVersion = [System.Diagnostics.FileVersionInfo]::GetVersionInfo("$env:APPVEYOR_BUILD_FOLDER/artifacts/ServerHost/Tgstation.Server.Host.dll").FileVersion -if (($env:CONFIGURATION -match "Release") -And ($env:APPVEYOR_REPO_BRANCH -match "master")) { - if ($env:APPVEYOR_REPO_COMMIT_MESSAGE -match "\[TGSDeploy\]") { - $env:TGSDeploy = "Do it." + +Write-Host "TGS Version: $env:TGSVersion" + +if (($env:CONFIGURATION -match "Release") -And ($env:APPVEYOR_REPO_BRANCH -match "master") -And ($env:APPVEYOR_REPO_COMMIT_MESSAGE -match "\[TGSDeploy\]")) { + Write-Host "Deploying..." + $env:TGSDeploy = "Do it." + + Write-Host "Generating release notes..." + dotnet run -p "$env:APPVEYOR_BUILD_FOLDER/tools/ReleaseNotes" $env:TGSVersion + $env:TGSDraftNotes = !($?) + $releaseNotesPath = "$env:APPVEYOR_BUILD_FOLDER/tools/ReleaseNotes/release_notes.md" + if (Test-Path $releaseNotesPath -PathType Leaf) { + $env:TGSReleaseNotes = [IO.File]::ReadAllText($releaseNotesPath) } + else { + Write-Host "Release note generation failed, release will be created as a draft!" + $env:TGSReleaseNotes = "Automatic generation failed, please fill manually!" + } + if ($env:APPVEYOR_REPO_COMMIT_MESSAGE -match "\[NugetDeploy\]") { $env:NugetDeploy = "Do it." - } -} - -dotnet run -p "$env:APPVEYOR_BUILD_FOLDER/tools/ReleaseNotes" 4.0.2.0 --no-close -$env:TGSGoodNotes = $? -$releaseNotesPath = "$env:APPVEYOR_BUILD_FOLDER/tools/ReleaseNotes/release_notes.md" -if (Test-Path $releaseNotesPath -PathType Leaf) { - $env:TGSReleaseNotes = [IO.File]::ReadAllText($releaseNotesPath) -} -else { - $env:TGSReleaseNotes = "Automatic generation failed, please fill manually!" + Write-Host "Nuget deployment enabled" + } } diff --git a/tools/ReleaseNotes/Program.cs b/tools/ReleaseNotes/Program.cs index 335d752201..edd1960dd5 100644 --- a/tools/ReleaseNotes/Program.cs +++ b/tools/ReleaseNotes/Program.cs @@ -9,309 +9,301 @@ using System.Threading.Tasks; namespace ReleaseNotes { - class Program - { - static async Task Main(string[] args) - { - if (args.Length < 1) - { - Console.WriteLine("Missing version argument!"); - return 1; - } + /// + /// Contains the application entrypoint + /// + static class Program + { + /// + /// The entrypoint for the + /// + static async Task Main(string[] args) + { + if (args.Length < 1) + { + Console.WriteLine("Missing version argument!"); + return 1; + } - var versionString = args[0]; - if (!Version.TryParse(versionString, out var version)) - { - Console.WriteLine("Invalid version: " + versionString); - return 2; - } + var versionString = args[0]; + if (!Version.TryParse(versionString, out var version)) + { + Console.WriteLine("Invalid version: " + versionString); + return 2; + } - var doNotCloseMilestone = args.Length >= 2 && args[1].ToUpperInvariant() == "--NO-CLOSE"; + var doNotCloseMilestone = args.Length >= 2 && args[1].ToUpperInvariant() == "--NO-CLOSE"; - const string ReleaseNotesEnvVar = "TGS_RELEASE_NOTES_TOKEN"; - var githubToken = Environment.GetEnvironmentVariable(ReleaseNotesEnvVar); - if (String.IsNullOrWhiteSpace(githubToken)) - { - Console.WriteLine("Missing " + ReleaseNotesEnvVar + " environment variable!"); - return 3; - } + const string ReleaseNotesEnvVar = "TGS_RELEASE_NOTES_TOKEN"; + var githubToken = Environment.GetEnvironmentVariable(ReleaseNotesEnvVar); + if (String.IsNullOrWhiteSpace(githubToken)) + { + Console.WriteLine("Missing " + ReleaseNotesEnvVar + " environment variable!"); + return 3; + } - try - { - var client = new GitHubClient(new ProductHeaderValue("tgs_release_notes")); - client.Credentials = new Credentials(githubToken); + try + { + var client = new GitHubClient(new ProductHeaderValue("tgs_release_notes")); + client.Credentials = new Credentials(githubToken); - const string RepoOwner = "tgstation"; - const string RepoName = "tgstation-server"; + const string RepoOwner = "tgstation"; + const string RepoName = "tgstation-server"; - var releasesTask = client.Repository.Release.GetAll(RepoOwner, RepoName); + var releasesTask = client.Repository.Release.GetAll(RepoOwner, RepoName); - Console.WriteLine("Getting pull requests in milestone " + versionString + "..."); - var milestonePRs = await client.Search.SearchIssues(new SearchIssuesRequest - { - Milestone = versionString, - Type = IssueTypeQualifier.PullRequest, - Repos = { { RepoOwner, RepoName } } - }).ConfigureAwait(false); + Console.WriteLine("Getting pull requests in milestone " + versionString + "..."); + var milestonePRs = await client.Search.SearchIssues(new SearchIssuesRequest + { + Milestone = versionString, + Type = IssueTypeQualifier.PullRequest, + Repos = { { RepoOwner, RepoName } } + }).ConfigureAwait(false); - if (milestonePRs.IncompleteResults) - { - Console.WriteLine("Incomplete results for milestone PRs query!"); - return 5; - } - Console.WriteLine(milestonePRs.Items.Count + " total pull requests"); + if (milestonePRs.IncompleteResults) + { + Console.WriteLine("Incomplete results for milestone PRs query!"); + return 5; + } + Console.WriteLine(milestonePRs.Items.Count + " total pull requests"); - Task milestoneTask = null; - var milestoneTaskLock = new object(); - var releaseDictionary = new Dictionary>(); - var authorizedUsers = new Dictionary>(); + Task milestoneTask = null; + var milestoneTaskLock = new object(); + var releaseDictionary = new Dictionary>(); + var authorizedUsers = new Dictionary>(); - async Task GetReleaseNotesFromPR(Issue pullRequest) - { - PullRequest fullPR = null; - async Task GetFullPR() - { - if (fullPR != null) - return; - fullPR = await client.Repository.PullRequest.Get(RepoOwner, RepoName, pullRequest.Number).ConfigureAwait(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); - async Task GetMilestone() - { - await GetFullPR().ConfigureAwait(false); - if (fullPR.Milestone == null) - return null; - return await client.Issue.Milestone.Get(RepoOwner, RepoName, fullPR.Milestone.Number); - }; + async Task GetMilestone() + { + if (fullPR.Milestone == null) + return null; + return await client.Issue.Milestone.Get(RepoOwner, RepoName, fullPR.Milestone.Number); + }; - if (pullRequest.State.Value == ItemState.Closed) - { - //need to check it was merged - await GetFullPR().ConfigureAwait(false); - lock (milestoneTaskLock) - if (milestoneTask == null) - milestoneTask = GetMilestone(); - if (!fullPR.Merged) - return; - }; + lock (milestoneTaskLock) + if (milestoneTask == null) + milestoneTask = GetMilestone(); - async Task BuildNotesFromComment(string comment, User user) - { - var commentSplits = comment.Split('\n'); - var notesOpen = false; - var notesClosed = false; - var notes = new List(); - foreach (var line in commentSplits) - { - var trimmedLine = line.Trim(); - if (!notesOpen) - { - notesOpen = trimmedLine.StartsWith(":cl:", StringComparison.Ordinal); - notesClosed = false; - continue; - } - if (trimmedLine.StartsWith("/:cl:", StringComparison.Ordinal)) - { - notesClosed = true; - notesOpen = false; - continue; - } - if (trimmedLine.Length == 0) - continue; - notes.Add(trimmedLine); - } - if (!notesClosed || notes.Count == 0) - return; + if (!fullPR.Merged) + return; - Task authTask; - TaskCompletionSource ourTcs = null; - lock (authorizedUsers) - { - if (!authorizedUsers.TryGetValue(user.Id, out authTask)) - { - ourTcs = new TaskCompletionSource(); - authTask = ourTcs.Task; - authorizedUsers.Add(user.Id, authTask); - } - } + async Task BuildNotesFromComment(string comment, User user) + { + var commentSplits = comment.Split('\n'); + var notesOpen = false; + var notesClosed = false; + var notes = new List(); + foreach (var line in commentSplits) + { + var trimmedLine = line.Trim(); + if (!notesOpen) + { + notesOpen = trimmedLine.StartsWith(":cl:", StringComparison.Ordinal); + notesClosed = false; + continue; + } + if (trimmedLine.StartsWith("/:cl:", StringComparison.Ordinal)) + { + notesClosed = true; + notesOpen = false; + continue; + } + if (trimmedLine.Length == 0) + continue; + notes.Add(trimmedLine); + } + if (!notesClosed || notes.Count == 0) + return; - if (ourTcs != null) - try - { - //check if the user has access - var perm = await client.Repository.Collaborator.ReviewPermission(RepoOwner, RepoName, user.Login).ConfigureAwait(false); - ourTcs.SetResult(perm.Permission == PermissionLevel.Write || perm.Permission == PermissionLevel.Admin); - } - catch - { - ourTcs.SetResult(false); - throw; - } + Task authTask; + TaskCompletionSource ourTcs = null; + lock (authorizedUsers) + { + if (!authorizedUsers.TryGetValue(user.Id, out authTask)) + { + ourTcs = new TaskCompletionSource(); + authTask = ourTcs.Task; + authorizedUsers.Add(user.Id, authTask); + } + } - var authorized = await authTask.ConfigureAwait(false); - if (!authorized) - return; + if (ourTcs != null) + try + { + //check if the user has access + var perm = await client.Repository.Collaborator.ReviewPermission(RepoOwner, RepoName, user.Login).ConfigureAwait(false); + ourTcs.SetResult(perm.Permission == PermissionLevel.Write || perm.Permission == PermissionLevel.Admin); + } + catch + { + ourTcs.SetResult(false); + throw; + } - lock (releaseDictionary) - { - foreach (var I in notes) - Console.WriteLine("#" + pullRequest.Number + " - " + I + " (@" + user.Login + ")"); - if (releaseDictionary.TryGetValue(pullRequest.Number, out var currentValues)) - currentValues.AddRange(notes); - else - releaseDictionary.Add(pullRequest.Number, notes); - } - } + var authorized = await authTask.ConfigureAwait(false); + if (!authorized) + return; - var comments = await client.Issue.Comment.GetAllForIssue(RepoOwner, RepoName, pullRequest.Number).ConfigureAwait(false); - await Task.WhenAll(BuildNotesFromComment(pullRequest.Body, pullRequest.User), Task.WhenAll(comments.Select(x => BuildNotesFromComment(x.Body, x.User)))).ConfigureAwait(false); + lock (releaseDictionary) + { + foreach (var I in notes) + Console.WriteLine("#" + pullRequest.Number + " - " + I + " (@" + user.Login + ")"); + if (releaseDictionary.TryGetValue(pullRequest.Number, out var currentValues)) + currentValues.AddRange(notes); + else + releaseDictionary.Add(pullRequest.Number, notes); + } + } - lock (milestoneTaskLock) - if (milestoneTask == null) - milestoneTask = GetMilestone(); - } + var comments = await client.Issue.Comment.GetAllForIssue(RepoOwner, RepoName, pullRequest.Number).ConfigureAwait(false); + await Task.WhenAll(BuildNotesFromComment(pullRequest.Body, pullRequest.User), Task.WhenAll(comments.Select(x => BuildNotesFromComment(x.Body, x.User)))).ConfigureAwait(false); + } - var tasks = new List(); - foreach (var I in milestonePRs.Items) - tasks.Add(GetReleaseNotesFromPR(I)); + var tasks = new List(); + foreach (var I in milestonePRs.Items) + tasks.Add(GetReleaseNotesFromPR(I)); - var releases = await releasesTask.ConfigureAwait(false); + var releases = await releasesTask.ConfigureAwait(false); - var releasingSuite = version.Major; + var releasingSuite = version.Major; - Version highestReleaseVersion = null; - Release highestRelease = null; - foreach (var I in releases) - { - if (!Version.TryParse(I.TagName.Replace("tgstation-server-v", String.Empty), out var currentReleaseVersion)) - { - Console.WriteLine("WARNING: Unable to determine version of release " + I.HtmlUrl); - continue; - } + Version highestReleaseVersion = null; + Release highestRelease = null; + foreach (var I in releases) + { + if (!Version.TryParse(I.TagName.Replace("tgstation-server-v", String.Empty), out var currentReleaseVersion)) + { + Console.WriteLine("WARNING: Unable to determine version of release " + I.HtmlUrl); + continue; + } - if (currentReleaseVersion.Major == releasingSuite && (highestReleaseVersion == null || currentReleaseVersion > highestReleaseVersion)) - { - highestReleaseVersion = currentReleaseVersion; - highestRelease = I; - } - } + if (currentReleaseVersion.Major == releasingSuite && (highestReleaseVersion == null || currentReleaseVersion > highestReleaseVersion)) + { + highestReleaseVersion = currentReleaseVersion; + highestRelease = I; + } + } - if (highestReleaseVersion == null) - { - Console.WriteLine("Unable to determine highest release version for suite " + releasingSuite + "!"); - return 6; - } + if (highestReleaseVersion == null) + { + Console.WriteLine("Unable to determine highest release version for suite " + releasingSuite + "!"); + return 6; + } - var oldNotes = highestRelease.Body; + var oldNotes = highestRelease.Body; - var splits = new List(oldNotes.Split('\n')); - //trim away all the lines that don't start with # + var splits = new List(oldNotes.Split('\n')); + //trim away all the lines that don't start with # - string keepThisRelease; - if (version.Revision == 0) - if (version.Build == 0) - keepThisRelease = "# "; - else - keepThisRelease = "## "; - else - keepThisRelease = "### "; + string keepThisRelease; + if (version.Revision == 0) + if (version.Build == 0) + keepThisRelease = "# "; + else + keepThisRelease = "## "; + else + keepThisRelease = "### "; - for (; !splits[0].StartsWith(keepThisRelease, StringComparison.Ordinal); splits.RemoveAt(0)) - if (splits.Count == 1) - { - Console.WriteLine("Error formatting release notes: Can't detemine notes start!"); - return 7; - } + for (; !splits[0].StartsWith(keepThisRelease, StringComparison.Ordinal); splits.RemoveAt(0)) + if (splits.Count == 1) + { + Console.WriteLine("Error formatting release notes: Can't detemine notes start!"); + return 7; + } - oldNotes = String.Join('\n', splits); + oldNotes = String.Join('\n', splits); - string prefix; - switch (releasingSuite) - { - case 3: - prefix = "The /tg/station server suite"; - break; - default: - prefix = "See https://tgstation.github.io/tgstation-server for installation instructions"; - break; - } + string prefix; + switch (releasingSuite) + { + case 3: + prefix = "The /tg/station server suite"; + break; + default: + prefix = "See https://tgstation.github.io/tgstation-server for installation instructions"; + break; + } - var newNotes = new StringBuilder(prefix); - newNotes.Append(Environment.NewLine); - newNotes.Append(Environment.NewLine); - if (version.Revision == 0) - if (version.Build == 0) - { - newNotes.Append("# [Version "); - newNotes.Append(version.Minor); - } - else - { - newNotes.Append("## [Changelog for "); - newNotes.Append(version.Build); - newNotes.Append(".x"); - } - else - { - newNotes.Append("### [Patch "); - newNotes.Append(version.Revision); - } - newNotes.Append("]("); - var milestone = await milestoneTask.ConfigureAwait(false); - if (milestone == null) - { - Console.WriteLine("Unable to detemine milestone!"); - return 9; - } - newNotes.Append(milestone.HtmlUrl); - newNotes.Append("?closed=1)"); - newNotes.Append(Environment.NewLine); + var newNotes = new StringBuilder(prefix); + newNotes.Append(Environment.NewLine); + newNotes.Append(Environment.NewLine); + if (version.Revision == 0) + if (version.Build == 0) + { + newNotes.Append("# [Version "); + newNotes.Append(version.Minor); + } + else + { + newNotes.Append("## [Changelog for "); + newNotes.Append(version.Build); + newNotes.Append(".x"); + } + else + { + newNotes.Append("### [Patch "); + newNotes.Append(version.Revision); + } + newNotes.Append("]("); + var milestone = await milestoneTask.ConfigureAwait(false); + if (milestone == null) + { + Console.WriteLine("Unable to detemine milestone!"); + return 9; + } + newNotes.Append(milestone.HtmlUrl); + newNotes.Append("?closed=1)"); + newNotes.Append(Environment.NewLine); - await Task.WhenAll(tasks).ConfigureAwait(false); + await Task.WhenAll(tasks).ConfigureAwait(false); - if (releaseDictionary.Count == 0) - { - Console.WriteLine("No release notes for this milestone!"); - return 8; - } + if (releaseDictionary.Count == 0) + { + Console.WriteLine("No release notes for this milestone!"); + return 8; + } - foreach (var I in releaseDictionary) - foreach (var note in I.Value) - { - newNotes.Append(Environment.NewLine); - newNotes.Append("- "); - newNotes.Append(note); - newNotes.Append(" (#"); - newNotes.Append(I.Key); - newNotes.Append(')'); - } + foreach (var I in releaseDictionary) + foreach (var note in I.Value) + { + newNotes.Append(Environment.NewLine); + newNotes.Append("- "); + newNotes.Append(note); + newNotes.Append(" (#"); + newNotes.Append(I.Key); + newNotes.Append(')'); + } - newNotes.Append(Environment.NewLine); - newNotes.Append(Environment.NewLine); - newNotes.Append(oldNotes); + newNotes.Append(Environment.NewLine); + newNotes.Append(Environment.NewLine); + newNotes.Append(oldNotes); - Console.WriteLine("Writing out new release notes..."); - var releaseNotes = newNotes.ToString(); - await File.WriteAllTextAsync("release_notes.md", releaseNotes).ConfigureAwait(false); + Console.WriteLine("Writing out new release notes..."); + var releaseNotes = newNotes.ToString(); + await File.WriteAllTextAsync("release_notes.md", releaseNotes).ConfigureAwait(false); - if (doNotCloseMilestone) - Console.WriteLine("Not closing milestone due to parameter!"); - else - { - Console.WriteLine("Closing milestone..."); - await client.Issue.Milestone.Update(RepoOwner, RepoName, milestone.Number, new MilestoneUpdate - { - State = ItemState.Closed - }).ConfigureAwait(false); - } + if (doNotCloseMilestone) + Console.WriteLine("Not closing milestone due to parameter!"); + else + { + Console.WriteLine("Closing milestone..."); + await client.Issue.Milestone.Update(RepoOwner, RepoName, milestone.Number, new MilestoneUpdate + { + State = ItemState.Closed + }).ConfigureAwait(false); + } - return 0; - } - catch (Exception e) - { - Console.WriteLine(e); - return 4; - } - } - } + return 0; + } + catch (Exception e) + { + Console.WriteLine(e); + return 4; + } + } + } }