diff --git a/Directory.Build.props b/Directory.Build.props index b4b2ee4..6a8a532 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,19 +4,16 @@ Exe - netcoreapp3.1 + net6.0 enable + enable - $(NoWarn),SA0001,CA1014 + $(NoWarn),SA0001,SA1200,SA1516,CA1014,CA1812 6.1.0 4.1.0 0.31.0 - - - - diff --git a/FakeItEasy.Deploy/Program.cs b/FakeItEasy.Deploy/Program.cs index ff90510..0438e16 100644 --- a/FakeItEasy.Deploy/Program.cs +++ b/FakeItEasy.Deploy/Program.cs @@ -1,147 +1,134 @@ -namespace FakeItEasy.Deploy +using FakeItEasy.Deploy; +using FakeItEasy.Tools; +using Octokit; +using static FakeItEasy.Tools.ReleaseHelpers; +using static SimpleExec.Command; + +if (args.Length != 1) +{ + Console.WriteLine("Illegal arguments. Usage:"); + Console.WriteLine(" "); +} + +string artifactsFolder = args[0]; + +var releaseName = GetAppVeyorTagName(); +if (string.IsNullOrEmpty(releaseName)) +{ + Console.WriteLine("No Appveyor tag name supplied. Not deploying."); + return; +} + +var nugetServerUrl = GetNuGetServerUrl(); +var nugetApiKey = GetNuGetApiKey(); +var (repoOwner, repoName) = GetRepositoryName(); +var gitHubClient = GetAuthenticatedGitHubClient(); + +Console.WriteLine($"Deploying {releaseName}"); +Console.WriteLine($"Looking for GitHub release {releaseName}"); + +var releases = await gitHubClient.Repository.Release.GetAll(repoOwner, repoName); +var release = releases.FirstOrDefault(r => r.Name == releaseName) + ?? throw new Exception($"Can't find release {releaseName}"); + +const string artifactsPattern = "*.nupkg"; + +var artifacts = Directory.GetFiles(artifactsFolder, artifactsPattern); +if (!artifacts.Any()) +{ + throw new Exception("Can't find any artifacts to publish"); +} + +Console.WriteLine($"Uploading artifacts to GitHub release {releaseName}"); +foreach (var file in artifacts) +{ + await UploadArtifactToGitHubReleaseAsync(gitHubClient, release, file); +} + +Console.WriteLine($"Pushing nupkgs to {nugetServerUrl}"); +foreach (var file in artifacts) +{ + await UploadPackageToNuGetAsync(file, nugetServerUrl, nugetApiKey); +} + +var issueNumbersInCurrentRelease = GetIssueNumbersReferencedFromReleases(new[] { release }); +var preReleases = GetPreReleasesContributingToThisRelease(release, releases); +var issueNumbersInPreReleases = GetIssueNumbersReferencedFromReleases(preReleases); +var newIssueNumbers = issueNumbersInCurrentRelease.Except(issueNumbersInPreReleases); + +Console.WriteLine($"Adding 'released as part of' notes to {newIssueNumbers.Count()} issues"); +var commentText = $"This change has been released as part of [{repoName} {releaseName}](https://github.com/{repoOwner}/{repoName}/releases/tag/{releaseName})."; +await Task.WhenAll(newIssueNumbers.Select(n => gitHubClient.Issue.Comment.Create(repoOwner, repoName, n, commentText))); + +Console.WriteLine("Finished deploying"); + +static IEnumerable GetPreReleasesContributingToThisRelease(Release release, IReadOnlyList releases) { - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Threading.Tasks; - using FakeItEasy.Tools; - using Octokit; - using static FakeItEasy.Tools.ReleaseHelpers; - using static SimpleExec.Command; - - internal static class Program + if (release.Prerelease) { - public static async Task Main(string[] args) - { - if (args.Length != 1) - { - Console.WriteLine("Illegal arguments. Usage:"); - Console.WriteLine(" "); - } - - string artifactsFolder = args[0]; - - var releaseName = GetAppVeyorTagName(); - if (string.IsNullOrEmpty(releaseName)) - { - Console.WriteLine("No Appveyor tag name supplied. Not deploying."); - return; - } - - var nugetServerUrl = GetNuGetServerUrl(); - var nugetApiKey = GetNuGetApiKey(); - var (repoOwner, repoName) = GetRepositoryName(); - var gitHubClient = GetAuthenticatedGitHubClient(); - - Console.WriteLine($"Deploying {releaseName}"); - Console.WriteLine($"Looking for GitHub release {releaseName}"); - - var releases = await gitHubClient.Repository.Release.GetAll(repoOwner, repoName); - var release = releases.FirstOrDefault(r => r.Name == releaseName) - ?? throw new Exception($"Can't find release {releaseName}"); - - const string artifactsPattern = "*.nupkg"; - - var artifacts = Directory.GetFiles(artifactsFolder, artifactsPattern); - if (!artifacts.Any()) - { - throw new Exception("Can't find any artifacts to publish"); - } - - Console.WriteLine($"Uploading artifacts to GitHub release {releaseName}"); - foreach (var file in artifacts) - { - await UploadArtifactToGitHubReleaseAsync(gitHubClient, release, file); - } - - Console.WriteLine($"Pushing nupkgs to {nugetServerUrl}"); - foreach (var file in artifacts) - { - await UploadPackageToNuGetAsync(file, nugetServerUrl, nugetApiKey); - } - - var issueNumbersInCurrentRelease = GetIssueNumbersReferencedFromReleases(new[] { release }); - var preReleases = GetPreReleasesContributingToThisRelease(release, releases); - var issueNumbersInPreReleases = GetIssueNumbersReferencedFromReleases(preReleases); - var newIssueNumbers = issueNumbersInCurrentRelease.Except(issueNumbersInPreReleases); - - Console.WriteLine($"Adding 'released as part of' notes to {newIssueNumbers.Count()} issues"); - var commentText = $"This change has been released as part of [{repoName} {releaseName}](https://github.com/{repoOwner}/{repoName}/releases/tag/{releaseName})."; - await Task.WhenAll(newIssueNumbers.Select(n => gitHubClient.Issue.Comment.Create(repoOwner, repoName, n, commentText))); - - Console.WriteLine("Finished deploying"); - } - - private static IEnumerable GetPreReleasesContributingToThisRelease(Release release, IReadOnlyList releases) - { - if (release.Prerelease) - { - return Enumerable.Empty(); - } + return Enumerable.Empty(); + } - string baseName = BaseName(release); - return releases.Where(r => r.Prerelease && BaseName(r) == baseName); + string baseName = BaseName(release); + return releases.Where(r => r.Prerelease && BaseName(r) == baseName); - string BaseName(Release release) => release.Name.Split('-')[0]; - } + string BaseName(Release release) => release.Name.Split('-')[0]; +} - private static async Task UploadArtifactToGitHubReleaseAsync(GitHubClient client, Release release, string path) - { - var name = Path.GetFileName(path); - Console.WriteLine($"Uploading {name}"); - using (var stream = File.OpenRead(path)) - { - var upload = new ReleaseAssetUpload - { - FileName = name, - ContentType = "application/octet-stream", - RawData = stream, - Timeout = TimeSpan.FromSeconds(100) - }; - - var asset = await client.Repository.Release.UploadAsset(release, upload); - Console.WriteLine($"Uploaded {asset.Name}"); - } - } - - private static async Task UploadPackageToNuGetAsync(string path, string nugetServerUrl, string nugetApiKey) +static async Task UploadArtifactToGitHubReleaseAsync(GitHubClient client, Release release, string path) +{ + var name = Path.GetFileName(path); + Console.WriteLine($"Uploading {name}"); + using (var stream = File.OpenRead(path)) + { + var upload = new ReleaseAssetUpload { - string name = Path.GetFileName(path); - Console.WriteLine($"Pushing {name}"); - await RunAsync(ToolPaths.NuGet, $"push \"{path}\" -ApiKey {nugetApiKey} -Source {nugetServerUrl} -NonInteractive -ForceEnglishOutput", noEcho: true); - Console.WriteLine($"Pushed {name}"); - } + FileName = name, + ContentType = "application/octet-stream", + RawData = stream, + Timeout = TimeSpan.FromSeconds(100) + }; + + var asset = await client.Repository.Release.UploadAsset(release, upload); + Console.WriteLine($"Uploaded {asset.Name}"); + } +} - private static (string repoOwner, string repoName) GetRepositoryName() - { - var repoNameWithOwner = GetRequiredEnvironmentVariable("APPVEYOR_REPO_NAME"); - var parts = repoNameWithOwner.Split('/'); - return (parts[0], parts[1]); - } +static async Task UploadPackageToNuGetAsync(string path, string nugetServerUrl, string nugetApiKey) +{ + string name = Path.GetFileName(path); + Console.WriteLine($"Pushing {name}"); + await RunAsync(ToolPaths.NuGet, $"push \"{path}\" -ApiKey {nugetApiKey} -Source {nugetServerUrl} -NonInteractive -ForceEnglishOutput", noEcho: true); + Console.WriteLine($"Pushed {name}"); +} - private static GitHubClient GetAuthenticatedGitHubClient() - { - var token = GitHubTokenSource.GetAccessToken(); - var credentials = new Credentials(token); - return new GitHubClient(new ProductHeaderValue("FakeItEasy-build-scripts")) { Credentials = credentials }; - } +static (string repoOwner, string repoName) GetRepositoryName() +{ + var repoNameWithOwner = GetRequiredEnvironmentVariable("APPVEYOR_REPO_NAME"); + var parts = repoNameWithOwner.Split('/'); + return (parts[0], parts[1]); +} - private static string? GetAppVeyorTagName() => Environment.GetEnvironmentVariable("APPVEYOR_REPO_TAG_NAME"); +static GitHubClient GetAuthenticatedGitHubClient() +{ + var token = GitHubTokenSource.GetAccessToken(); + var credentials = new Credentials(token); + return new GitHubClient(new ProductHeaderValue("FakeItEasy-build-scripts")) { Credentials = credentials }; +} - private static string GetNuGetServerUrl() => GetRequiredEnvironmentVariable("NUGET_SERVER_URL"); +static string? GetAppVeyorTagName() => Environment.GetEnvironmentVariable("APPVEYOR_REPO_TAG_NAME"); - private static string GetNuGetApiKey() => GetRequiredEnvironmentVariable("NUGET_API_KEY"); +static string GetNuGetServerUrl() => GetRequiredEnvironmentVariable("NUGET_SERVER_URL"); - private static string GetRequiredEnvironmentVariable(string key) - { - var environmentValue = Environment.GetEnvironmentVariable(key); - if (string.IsNullOrEmpty(environmentValue)) - { - throw new Exception($"Required environment variable {key} is not set. Unable to continue."); - } - - return environmentValue; - } +static string GetNuGetApiKey() => GetRequiredEnvironmentVariable("NUGET_API_KEY"); + +static string GetRequiredEnvironmentVariable(string key) +{ + var environmentValue = Environment.GetEnvironmentVariable(key); + if (string.IsNullOrEmpty(environmentValue)) + { + throw new Exception($"Required environment variable {key} is not set. Unable to continue."); } + + return environmentValue; } diff --git a/FakeItEasy.PrepareRelease/GitHubHelper.cs b/FakeItEasy.PrepareRelease/GitHubHelper.cs new file mode 100644 index 0000000..56d78bf --- /dev/null +++ b/FakeItEasy.PrepareRelease/GitHubHelper.cs @@ -0,0 +1,117 @@ +namespace FakeItEasy.PrepareRelease; + +using Octokit; +using static FakeItEasy.Tools.ReleaseHelpers; + +internal class GitHubHelper +{ + private readonly IGitHubClient gitHubClient; + private readonly string repoOwner; + private readonly string repoName; + + public GitHubHelper(IGitHubClient gitHubClient, string repoOwner, string repoName) + { + this.gitHubClient = gitHubClient; + this.repoOwner = repoOwner; + this.repoName = repoName; + } + + public async Task GetExistingMilestone(string existingMilestoneTitle) + { + Console.WriteLine($"Fetching milestone '{existingMilestoneTitle}'..."); + var milestoneRequest = new MilestoneRequest { State = ItemStateFilter.Open }; + var existingMilestone = (await this.gitHubClient.Issue.Milestone.GetAllForRepository(this.repoOwner, this.repoName, milestoneRequest)) + .Single(milestone => milestone.Title == existingMilestoneTitle); + Console.WriteLine($"Fetched milestone '{existingMilestone.Title}'"); + return existingMilestone; + } + + public async Task> GetAllReleases() + { + Console.WriteLine("Fetching all GitHub releases..."); + var allReleases = await this.gitHubClient.Repository.Release.GetAll(this.repoOwner, this.repoName); + Console.WriteLine("Fetched all GitHub releases"); + return allReleases; + } + + public async Task> GetIssuesInMilestone(Milestone milestone) + { + Console.WriteLine($"Fetching issues in milestone '{milestone.Title}'...'"); + var issueRequest = new RepositoryIssueRequest { Milestone = milestone.Number.ToString(), State = ItemStateFilter.All }; + var issues = (await this.gitHubClient.Issue.GetAllForRepository(this.repoOwner, this.repoName, issueRequest)).ToList(); + Console.WriteLine($"Fetched {issues.Count} issues in milestone '{milestone.Title}'"); + return issues; + } + + public async Task RenameMilestone(Milestone existingMilestone, string version) + { + var milestoneUpdate = new MilestoneUpdate { Title = version }; + Console.WriteLine($"Renaming milestone '{existingMilestone.Title}' to '{milestoneUpdate.Title}'..."); + var updatedMilestone = await this.gitHubClient.Issue.Milestone.Update(this.repoOwner, this.repoName, existingMilestone.Number, milestoneUpdate); + Console.WriteLine($"Renamed milestone '{existingMilestone.Title}' to '{updatedMilestone.Title}'"); + } + + public async Task CreateNextMilestone(string nextReleaseName) + { + var newMilestone = new NewMilestone(nextReleaseName); + Console.WriteLine($"Creating new milestone '{newMilestone.Title}'..."); + var nextMilestone = await this.gitHubClient.Issue.Milestone.Create(this.repoOwner, this.repoName, newMilestone); + Console.WriteLine($"Created new milestone '{nextMilestone.Title}'"); + return nextMilestone; + } + + public async Task UpdateRelease(Release existingRelease, string version) + { + var releaseUpdate = new ReleaseUpdate { Name = version, TagName = version, Prerelease = IsPreRelease(version) }; + Console.WriteLine($"Renaming GitHub release '{existingRelease.Name}' to {releaseUpdate.Name}..."); + var updatedRelease = await this.gitHubClient.Repository.Release.Edit(this.repoOwner, this.repoName, existingRelease.Id, releaseUpdate); + Console.WriteLine($"Renamed GitHub release '{existingRelease.Name}' to {updatedRelease.Name}"); + } + + public async Task CreateNextRelease(string nextReleaseName) + { + const string newReleaseBody = @" +### Changed + +### New +* Issue Title (#12345) + +### Fixed + +### Additional Items + +### With special thanks for contributions to this release from: +* Real Name - @githubhandle +"; + + var newRelease = new NewRelease(nextReleaseName) { Draft = true, Name = nextReleaseName, Body = newReleaseBody.Trim() }; + Console.WriteLine($"Creating new GitHub release '{newRelease.Name}'..."); + var nextRelease = await this.gitHubClient.Repository.Release.Create(this.repoOwner, this.repoName, newRelease); + Console.WriteLine($"Created new GitHub release '{nextRelease.Name}'"); + } + + public async Task UpdateIssue(Issue existingIssue, Milestone existingMilestone, string version) + { + var issueUpdate = new IssueUpdate { Title = $"Release {version}", Milestone = existingMilestone.Number }; + Console.WriteLine($"Renaming release issue '{existingIssue.Title}' to '{issueUpdate.Title}'..."); + var updatedIssue = await this.gitHubClient.Issue.Update(this.repoOwner, this.repoName, existingIssue.Number, issueUpdate); + Console.WriteLine($"Renamed release issue '{existingIssue.Title}' to '{updatedIssue.Title}'"); + } + + public async Task CreateNextIssue(Issue existingIssue, Milestone nextMilestone, string nextReleaseName) + { + var newIssue = new NewIssue($"Release {nextReleaseName}") + { + Milestone = nextMilestone.Number, + Body = existingIssue.Body.Replace("[x]", "[ ]", StringComparison.OrdinalIgnoreCase), + }; + foreach (var label in existingIssue.Labels) + { + newIssue.Labels.Add(label.Name); + } + + Console.WriteLine($"Creating new release issue '{newIssue.Title}'..."); + var nextIssue = await this.gitHubClient.Issue.Create(this.repoOwner, this.repoName, newIssue); + Console.WriteLine($"Created new release issue #{nextIssue.Number}: '{newIssue.Title}'"); + } +} diff --git a/FakeItEasy.PrepareRelease/Program.cs b/FakeItEasy.PrepareRelease/Program.cs index adedfa5..e186bd6 100644 --- a/FakeItEasy.PrepareRelease/Program.cs +++ b/FakeItEasy.PrepareRelease/Program.cs @@ -1,263 +1,147 @@ -namespace FakeItEasy.PrepareRelease -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Threading.Tasks; - using FakeItEasy.Tools; - using Octokit; - using static FakeItEasy.Tools.ReleaseHelpers; - - internal static class Program - { - private const string RepoOwner = "FakeItEasy"; - private static string repoName = string.Empty; - - public static async Task Main(string[] args) - { - if (args.Length != 4 || (args[1] != "next" && args[1] != "fork")) - { - Console.WriteLine("Illegal arguments. Must be one of the following:"); - Console.WriteLine(" next "); - Console.WriteLine(" fork "); - return; - } - - repoName = args[0]; - var action = args[1]; - var version = args[2]; - var existingReleaseName = args[3]; - - var gitHubClient = GetAuthenticatedGitHubClient(); - var existingMilestone = await gitHubClient.GetExistingMilestone(existingReleaseName); - var issuesInExistingMilestone = await gitHubClient.GetIssuesInMilestone(existingMilestone); - var existingReleaseIssue = GetExistingReleaseIssue(issuesInExistingMilestone, existingReleaseName); - - if (action == "next") - { - var nextReleaseName = existingReleaseName; - - var allReleases = await gitHubClient.GetAllReleases(); - var existingRelease = allReleases.Single(release => release.Name == existingReleaseName && release.Draft); - - var releasesForExistingMilestone = GetReleasesForExistingMilestone(allReleases, existingRelease, version); +using FakeItEasy.PrepareRelease; +using FakeItEasy.Tools; +using Octokit; +using static FakeItEasy.Tools.ReleaseHelpers; - var nonReleaseIssuesInMilestone = ExcludeReleaseIssues(issuesInExistingMilestone, releasesForExistingMilestone); - - var issueNumbersReferencedFromReleases = GetIssueNumbersReferencedFromReleases(releasesForExistingMilestone); - - if (!CrossReferenceIssues(nonReleaseIssuesInMilestone, issueNumbersReferencedFromReleases)) - { - return; - } - - Milestone nextMilestone; - if (IsPreRelease(version)) - { - nextMilestone = existingMilestone; - } - else - { - await gitHubClient.RenameMilestone(existingMilestone, version); - nextMilestone = await gitHubClient.CreateNextMilestone(nextReleaseName); - } +if (args.Length != 4 || (args[1] != "next" && args[1] != "fork")) +{ + Console.WriteLine("Illegal arguments. Must be one of the following:"); + Console.WriteLine(" next "); + Console.WriteLine(" fork "); + return; +} - await gitHubClient.UpdateRelease(existingRelease, version); - await gitHubClient.CreateNextRelease(nextReleaseName); - await gitHubClient.UpdateIssue(existingReleaseIssue, existingMilestone, version); - await gitHubClient.CreateNextIssue(existingReleaseIssue, nextMilestone, nextReleaseName); - } - else - { - var nextReleaseName = version; +const string repoOwner = "FakeItEasy"; +var repoName = args[0]; +var action = args[1]; +var version = args[2]; +var existingReleaseName = args[3]; - var nextMilestone = await gitHubClient.CreateNextMilestone(nextReleaseName); - await gitHubClient.CreateNextRelease(nextReleaseName); - await gitHubClient.CreateNextIssue(existingReleaseIssue, nextMilestone, nextReleaseName); - } - } +var gitHubClient = GetAuthenticatedGitHubClient(); +var gitHubHelper = new GitHubHelper(gitHubClient, repoOwner, repoName); - private static List GetReleasesForExistingMilestone(IReadOnlyCollection allReleases, Release existingRelease, string version) - { - var releasesForExistingMilestone = new List { existingRelease }; - var versionRoot = IsPreRelease(version) ? version.Substring(0, version.IndexOf('-', StringComparison.Ordinal)) : version; - releasesForExistingMilestone.AddRange(allReleases.Where(release => release.Name.StartsWith(versionRoot, StringComparison.OrdinalIgnoreCase))); - return releasesForExistingMilestone; - } +var existingMilestone = await gitHubHelper.GetExistingMilestone(existingReleaseName); +var issuesInExistingMilestone = await gitHubHelper.GetIssuesInMilestone(existingMilestone); +var existingReleaseIssue = GetExistingReleaseIssue(issuesInExistingMilestone, existingReleaseName); - private static GitHubClient GetAuthenticatedGitHubClient() - { - var token = GitHubTokenSource.GetAccessToken(); - var credentials = new Credentials(token); - return new GitHubClient(new ProductHeaderValue("FakeItEasy-build-scripts")) { Credentials = credentials }; - } +if (action == "next") +{ + var nextReleaseName = existingReleaseName; - private static async Task GetExistingMilestone(this IGitHubClient gitHubClient, string existingMilestoneTitle) - { - Console.WriteLine($"Fetching milestone '{existingMilestoneTitle}'..."); - var milestoneRequest = new MilestoneRequest { State = ItemStateFilter.Open }; - var existingMilestone = (await gitHubClient.Issue.Milestone.GetAllForRepository(RepoOwner, repoName, milestoneRequest)) - .Single(milestone => milestone.Title == existingMilestoneTitle); - Console.WriteLine($"Fetched milestone '{existingMilestone.Title}'"); - return existingMilestone; - } + var allReleases = await gitHubHelper.GetAllReleases(); + var existingRelease = allReleases.Single(release => release.Name == existingReleaseName && release.Draft); - private static async Task> GetAllReleases(this IGitHubClient gitHubClient) - { - Console.WriteLine("Fetching all GitHub releases..."); - var allReleases = await gitHubClient.Repository.Release.GetAll(RepoOwner, repoName); - Console.WriteLine("Fetched all GitHub releases"); - return allReleases; - } + var releasesForExistingMilestone = GetReleasesForExistingMilestone(allReleases, existingRelease, version); - private static async Task> GetIssuesInMilestone(this IGitHubClient gitHubClient, Milestone milestone) - { - Console.WriteLine($"Fetching issues in milestone '{milestone.Title}'...'"); - var issueRequest = new RepositoryIssueRequest { Milestone = milestone.Number.ToString(), State = ItemStateFilter.All }; - var issues = (await gitHubClient.Issue.GetAllForRepository(RepoOwner, repoName, issueRequest)).ToList(); - Console.WriteLine($"Fetched {issues.Count} issues in milestone '{milestone.Title}'"); - return issues; - } + var nonReleaseIssuesInMilestone = ExcludeReleaseIssues(issuesInExistingMilestone, releasesForExistingMilestone); - private static Issue GetExistingReleaseIssue(IList issues, string existingReleaseName) - { - var issue = issues.Single(i => i.Title == $"Release {existingReleaseName}"); - Console.WriteLine($"Found release issue #{issue.Number}: '{issue.Title}'"); - return issue; - } + var issueNumbersReferencedFromReleases = GetIssueNumbersReferencedFromReleases(releasesForExistingMilestone); - private static IList ExcludeReleaseIssues(IList issues, IEnumerable releases) - { - return issues.Where(issue => releases.All(release => $"Release {release.Name}" != issue.Title)).ToList(); - } - - private static bool CrossReferenceIssues(ICollection issuesInMilestone, ICollection issueNumbersReferencedFromRelease) - { - var issueNumbersInMilestone = issuesInMilestone.Select(i => i.Number); - var issueNumbersInReleaseButNotMilestone = issueNumbersReferencedFromRelease.Except(issueNumbersInMilestone).ToList(); - var issuesInMilestoneButNotRelease = issuesInMilestone.Where(i => !issueNumbersReferencedFromRelease.Contains(i.Number)).ToList(); - - if (!issuesInMilestoneButNotRelease.Any() && !issueNumbersInReleaseButNotMilestone.Any()) - { - Console.WriteLine("The release refers to the same issues included in the milestone. Congratulations."); - return true; - } + if (!CrossReferenceIssues(nonReleaseIssuesInMilestone, issueNumbersReferencedFromReleases)) + { + return; + } - Console.WriteLine(); + Milestone nextMilestone; + if (IsPreRelease(version)) + { + nextMilestone = existingMilestone; + } + else + { + await gitHubHelper.RenameMilestone(existingMilestone, version); + nextMilestone = await gitHubHelper.CreateNextMilestone(nextReleaseName); + } - if (issuesInMilestoneButNotRelease.Any()) - { - Console.WriteLine("The following issues are linked to the milestone but not referenced in the release:"); - foreach (var issue in issuesInMilestoneButNotRelease) - { - Console.WriteLine($" #{issue.Number}: {issue.Title}"); - } + await gitHubHelper.UpdateRelease(existingRelease, version); + await gitHubHelper.CreateNextRelease(nextReleaseName); + await gitHubHelper.UpdateIssue(existingReleaseIssue, existingMilestone, version); + await gitHubHelper.CreateNextIssue(existingReleaseIssue, nextMilestone, nextReleaseName); +} +else +{ + var nextReleaseName = version; - Console.WriteLine(); - } + var nextMilestone = await gitHubHelper.CreateNextMilestone(nextReleaseName); + await gitHubHelper.CreateNextRelease(nextReleaseName); + await gitHubHelper.CreateNextIssue(existingReleaseIssue, nextMilestone, nextReleaseName); +} - if (issueNumbersInReleaseButNotMilestone.Any()) - { - Console.WriteLine("The following issues are referenced in the release but not linked to the milestone:"); - foreach (var issueNumber in issueNumbersInReleaseButNotMilestone) - { - Console.WriteLine($" #{issueNumber}"); - } +static List GetReleasesForExistingMilestone(IReadOnlyCollection allReleases, Release existingRelease, string version) +{ + var releasesForExistingMilestone = new List { existingRelease }; + var versionRoot = IsPreRelease(version) ? version.Substring(0, version.IndexOf('-', StringComparison.Ordinal)) : version; + releasesForExistingMilestone.AddRange(allReleases.Where(release => release.Name.StartsWith(versionRoot, StringComparison.OrdinalIgnoreCase))); + return releasesForExistingMilestone; +} - Console.WriteLine(); - } +static GitHubClient GetAuthenticatedGitHubClient() +{ + var token = GitHubTokenSource.GetAccessToken(); + var credentials = new Credentials(token); + return new GitHubClient(new ProductHeaderValue("FakeItEasy-build-scripts")) { Credentials = credentials }; +} - Console.WriteLine("Prepare release anyhow? (y/N)"); - var response = Console.ReadLine().Trim(); - if (string.Equals(response, "y", StringComparison.OrdinalIgnoreCase)) - { - return true; - } +static Issue GetExistingReleaseIssue(IList issues, string existingReleaseName) +{ + var issue = issues.Single(i => i.Title == $"Release {existingReleaseName}"); + Console.WriteLine($"Found release issue #{issue.Number}: '{issue.Title}'"); + return issue; +} - if (string.Equals(response, "n", StringComparison.OrdinalIgnoreCase)) - { - return false; - } +static IList ExcludeReleaseIssues(IList issues, IEnumerable releases) +{ + return issues.Where(issue => releases.All(release => $"Release {release.Name}" != issue.Title)).ToList(); +} - Console.WriteLine($"Unknown response '{response}' received. Treating as 'n'."); - return false; - } +static bool CrossReferenceIssues(ICollection issuesInMilestone, ICollection issueNumbersReferencedFromRelease) +{ + var issueNumbersInMilestone = issuesInMilestone.Select(i => i.Number); + var issueNumbersInReleaseButNotMilestone = issueNumbersReferencedFromRelease.Except(issueNumbersInMilestone).ToList(); + var issuesInMilestoneButNotRelease = issuesInMilestone.Where(i => !issueNumbersReferencedFromRelease.Contains(i.Number)).ToList(); - private static bool IsPreRelease(string version) - { - return version.Contains('-', StringComparison.Ordinal); - } + if (!issuesInMilestoneButNotRelease.Any() && !issueNumbersInReleaseButNotMilestone.Any()) + { + Console.WriteLine("The release refers to the same issues included in the milestone. Congratulations."); + return true; + } - private static async Task RenameMilestone(this IGitHubClient gitHubClient, Milestone existingMilestone, string version) - { - var milestoneUpdate = new MilestoneUpdate { Title = version }; - Console.WriteLine($"Renaming milestone '{existingMilestone.Title}' to '{milestoneUpdate.Title}'..."); - var updatedMilestone = await gitHubClient.Issue.Milestone.Update(RepoOwner, repoName, existingMilestone.Number, milestoneUpdate); - Console.WriteLine($"Renamed milestone '{existingMilestone.Title}' to '{updatedMilestone.Title}'"); - } + Console.WriteLine(); - private static async Task CreateNextMilestone(this IGitHubClient gitHubClient, string nextReleaseName) + if (issuesInMilestoneButNotRelease.Any()) + { + Console.WriteLine("The following issues are linked to the milestone but not referenced in the release:"); + foreach (var issue in issuesInMilestoneButNotRelease) { - var newMilestone = new NewMilestone(nextReleaseName); - Console.WriteLine($"Creating new milestone '{newMilestone.Title}'..."); - var nextMilestone = await gitHubClient.Issue.Milestone.Create(RepoOwner, repoName, newMilestone); - Console.WriteLine($"Created new milestone '{nextMilestone.Title}'"); - return nextMilestone; + Console.WriteLine($" #{issue.Number}: {issue.Title}"); } - private static async Task UpdateRelease(this IGitHubClient gitHubClient, Release existingRelease, string version) - { - var releaseUpdate = new ReleaseUpdate { Name = version, TagName = version, Prerelease = IsPreRelease(version) }; - Console.WriteLine($"Renaming GitHub release '{existingRelease.Name}' to {releaseUpdate.Name}..."); - var updatedRelease = await gitHubClient.Repository.Release.Edit(RepoOwner, repoName, existingRelease.Id, releaseUpdate); - Console.WriteLine($"Renamed GitHub release '{existingRelease.Name}' to {updatedRelease.Name}"); - } + Console.WriteLine(); + } - private static async Task CreateNextRelease(this IGitHubClient gitHubClient, string nextReleaseName) + if (issueNumbersInReleaseButNotMilestone.Any()) + { + Console.WriteLine("The following issues are referenced in the release but not linked to the milestone:"); + foreach (var issueNumber in issueNumbersInReleaseButNotMilestone) { - const string newReleaseBody = @" -### Changed - -### New -* Issue Title (#12345) - -### Fixed - -### Additional Items - -### With special thanks for contributions to this release from: -* Real Name - @githubhandle -"; - - var newRelease = new NewRelease(nextReleaseName) { Draft = true, Name = nextReleaseName, Body = newReleaseBody.Trim() }; - Console.WriteLine($"Creating new GitHub release '{newRelease.Name}'..."); - var nextRelease = await gitHubClient.Repository.Release.Create(RepoOwner, repoName, newRelease); - Console.WriteLine($"Created new GitHub release '{nextRelease.Name}'"); + Console.WriteLine($" #{issueNumber}"); } - private static async Task UpdateIssue(this IGitHubClient gitHubClient, Issue existingIssue, Milestone existingMilestone, string version) - { - var issueUpdate = new IssueUpdate { Title = $"Release {version}", Milestone = existingMilestone.Number }; - Console.WriteLine($"Renaming release issue '{existingIssue.Title}' to '{issueUpdate.Title}'..."); - var updatedIssue = await gitHubClient.Issue.Update(RepoOwner, repoName, existingIssue.Number, issueUpdate); - Console.WriteLine($"Renamed release issue '{existingIssue.Title}' to '{updatedIssue.Title}'"); - } + Console.WriteLine(); + } - private static async Task CreateNextIssue(this IGitHubClient gitHubClient, Issue existingIssue, Milestone nextMilestone, string nextReleaseName) - { - var newIssue = new NewIssue($"Release {nextReleaseName}") - { - Milestone = nextMilestone.Number, - Body = existingIssue.Body.Replace("[x]", "[ ]", StringComparison.OrdinalIgnoreCase), - }; - foreach (var label in existingIssue.Labels) - { - newIssue.Labels.Add(label.Name); - } + Console.WriteLine("Prepare release anyhow? (y/N)"); + var response = Console.ReadLine()?.Trim(); + if (string.Equals(response, "y", StringComparison.OrdinalIgnoreCase)) + { + return true; + } - Console.WriteLine($"Creating new release issue '{newIssue.Title}'..."); - var nextIssue = await gitHubClient.Issue.Create(RepoOwner, repoName, newIssue); - Console.WriteLine($"Created new release issue #{nextIssue.Number}: '{newIssue.Title}'"); - } + if (string.Equals(response, "n", StringComparison.OrdinalIgnoreCase)) + { + return false; } + + Console.WriteLine($"Unknown response '{response}' received. Treating as 'n'."); + return false; } diff --git a/GitHubTokenSource.cs b/GitHubTokenSource.cs index 0e1da11..5916c24 100644 --- a/GitHubTokenSource.cs +++ b/GitHubTokenSource.cs @@ -1,30 +1,26 @@ -namespace FakeItEasy.Tools -{ - using System; - using System.IO; +namespace FakeItEasy.Tools; - using static FakeItEasy.Tools.ToolHelpers; +using static FakeItEasy.Tools.ToolHelpers; - public static class GitHubTokenSource +public static class GitHubTokenSource +{ + public static string GetAccessToken() { - public static string GetAccessToken() + var tokenFilePath = Path.Combine(GetCurrentScriptDirectory(), ".githubtoken"); + var token = Environment.GetEnvironmentVariable("GITHUB_TOKEN"); + if (string.IsNullOrEmpty(token)) { - var tokenFilePath = Path.Combine(GetCurrentScriptDirectory(), ".githubtoken"); - var token = Environment.GetEnvironmentVariable("GITHUB_TOKEN"); - if (string.IsNullOrEmpty(token)) - { - if (File.Exists(tokenFilePath)) - { - token = File.ReadAllText(tokenFilePath)?.Trim(); - } - } - - if (string.IsNullOrEmpty(token)) + if (File.Exists(tokenFilePath)) { - throw new Exception($"GitHub access token is missing; please put it in '{tokenFilePath}', or in the GITHUB_TOKEN environment variable."); + token = File.ReadAllText(tokenFilePath).Trim(); } + } - return token; + if (string.IsNullOrEmpty(token)) + { + throw new Exception($"GitHub access token is missing; please put it in '{tokenFilePath}', or in the GITHUB_TOKEN environment variable."); } + + return token; } } diff --git a/ReleaseHelpers.cs b/ReleaseHelpers.cs index 1bb1528..bb2917d 100644 --- a/ReleaseHelpers.cs +++ b/ReleaseHelpers.cs @@ -1,31 +1,33 @@ -namespace FakeItEasy.Tools -{ - using System.Collections.Generic; - using System.Globalization; - using System.Linq; - using System.Text.RegularExpressions; - using Octokit; +namespace FakeItEasy.Tools; + +using System.Globalization; +using System.Text.RegularExpressions; +using Octokit; - public static class ReleaseHelpers +internal static class ReleaseHelpers +{ + public static ICollection GetIssueNumbersReferencedFromReleases(IEnumerable releases) { - public static ICollection GetIssueNumbersReferencedFromReleases(IEnumerable releases) + if (releases is null) { - if (releases is null) - { - throw new System.ArgumentNullException(nameof(releases)); - } + throw new ArgumentNullException(nameof(releases)); + } - var issuesReferencedFromRelease = new HashSet(); - foreach (var release in releases) + var issuesReferencedFromRelease = new HashSet(); + foreach (var release in releases) + { + foreach (var capture in Regex.Matches(release.Body, @"\(\s*#(?[0-9]+)(,\s*#(?[0-9]+))*\s*\)") + .SelectMany(match => match.Groups["issueNumber"].Captures)) { - foreach (var capture in Regex.Matches(release.Body, @"\(\s*#(?[0-9]+)(,\s*#(?[0-9]+))*\s*\)") - .SelectMany(match => match.Groups["issueNumber"].Captures)) - { - issuesReferencedFromRelease.Add(int.Parse(capture.Value, NumberStyles.Integer, NumberFormatInfo.InvariantInfo)); - } + issuesReferencedFromRelease.Add(int.Parse(capture.Value, NumberStyles.Integer, NumberFormatInfo.InvariantInfo)); } - - return issuesReferencedFromRelease; } + + return issuesReferencedFromRelease; + } + + public static bool IsPreRelease(string version) + { + return version.Contains('-', StringComparison.Ordinal); } } diff --git a/ToolHelpers.cs b/ToolHelpers.cs index f9c6a2c..7db1ff5 100644 --- a/ToolHelpers.cs +++ b/ToolHelpers.cs @@ -1,12 +1,9 @@ -namespace FakeItEasy.Tools -{ - using System; - using System.IO; - using System.Runtime.CompilerServices; +namespace FakeItEasy.Tools; + +using System.Runtime.CompilerServices; - public static class ToolHelpers - { - public static string GetCurrentScriptDirectory([CallerFilePath] string path = "") => Path.GetDirectoryName(path) - ?? throw new Exception("Can't find current script directory."); - } +public static class ToolHelpers +{ + public static string GetCurrentScriptDirectory([CallerFilePath] string path = "") => Path.GetDirectoryName(path) + ?? throw new Exception("Can't find current script directory."); }