diff --git a/README.md b/README.md index a85dfb97..3055a9ee 100644 --- a/README.md +++ b/README.md @@ -78,9 +78,10 @@ regarding the environment its executed within. * **GitHub Action** - When Bake is executed from within a GitHub action, it automatically recognizes the token and uses that when publishing artifacts and releases -* **Release notes** - If the repository contains a `RELEASE_NOTES.md` file, - the content as well as the version information is used to further enrich - any release artifacts +* **Release notes** - Pull requests merged since latest release are summarized, + similar grouped together, used as release notes. In addition, if the repository + contains a `RELEASE_NOTES.md` file, the content as well as the version information + is used to further enrich any release artifacts After the initial environment information gathering is completed, Bake starts to scan the repository for files and structures it knows how to process. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 00ac926b..f51e14a1 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,7 @@ # 0.22-beta +* New: GitHub releases now have a description that contains the a summary + of all pull requests and issues that are part of the release * Fixed: Cleanup of the artifact post run report # 0.21-beta diff --git a/Source/Bake.Tests/ExplicitTests/GitHubTests.cs b/Source/Bake.Tests/ExplicitTests/GitHubTests.cs new file mode 100644 index 00000000..282ce765 --- /dev/null +++ b/Source/Bake.Tests/ExplicitTests/GitHubTests.cs @@ -0,0 +1,117 @@ +// MIT License +// +// Copyright (c) 2021-2023 Rasmus Mikkelsen +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Bake.Cooking.Cooks.GitHub; +using Bake.Core; +using Bake.Services; +using Bake.Tests.Helpers; +using Bake.ValueObjects; +using FluentAssertions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; + +namespace Bake.Tests.ExplicitTests +{ + [Explicit] + public class GitHubTests : ServiceTest + { + public GitHubTests() : base(null) { } + + [Test] + public async Task GetCommits() + { + var commits = await Sut.GetCommitsAsync( + "e1b486d", + "c54a03b", + new GitHubInformation( + "rasmus", + "Bake", + new Uri("https://github.com/rasmus/Bake"), + new Uri("https://api.github.com/")), + CancellationToken.None); + } + + [Test] + public async Task GetPullRequests() + { + var pullRequests = await Sut.GetPullRequestsAsync( + "e1b486d", + "c54a03b", + new GitHubInformation( + "rasmus", + "Bake", + new Uri("https://github.com/rasmus/Bake"), + new Uri("https://api.github.com/")), + CancellationToken.None); + + foreach (var pullRequest in pullRequests) + { + Console.WriteLine($"new ({pullRequest.Number}, \"{pullRequest.Title}\", new []{{\"{pullRequest.Authors.Single()}\"}}),"); + } + } + + [Test] + public async Task GetPullRequest_NonExisting() + { + // Act + var pullRequest = await Sut.GetPullRequestAsync( + new GitHubInformation( + "rasmus", + "Bake", + new Uri("https://github.com/rasmus/Bake"), + new Uri("https://api.github.com/")), + int.MaxValue, + CancellationToken.None); + + // Assert + pullRequest.Should().BeNull(); + } + + private static string GetToken() + { + return new ConfigurationBuilder() + .AddUserSecrets() + .Build()["github:token"]; + } + + protected override IServiceCollection Configure(IServiceCollection serviceCollection) + { + return base.Configure(serviceCollection) + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(new TestEnvironmentVariables(new Dictionary + { + ["github_personal_token"] = GetToken(), + })); + } + } +} diff --git a/Source/Bake.Tests/Helpers/BakeTest.cs b/Source/Bake.Tests/Helpers/BakeTest.cs index d5c2ff3c..0128dda5 100644 --- a/Source/Bake.Tests/Helpers/BakeTest.cs +++ b/Source/Bake.Tests/Helpers/BakeTest.cs @@ -32,6 +32,8 @@ using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; +// ReSharper disable StringLiteralTypo + namespace Bake.Tests.Helpers { public abstract class BakeTest : TestProject @@ -126,13 +128,48 @@ public Task CreateReleaseAsync( return Task.CompletedTask; } - public Task GetPullRequestInformationAsync( - GitInformation gitInformation, + public Task GetPullRequestInformationAsync(GitInformation gitInformation, GitHubInformation gitHubInformation, CancellationToken cancellationToken) { return Task.FromResult(null); } + + public Task> GetCommitsAsync(string baseSha, string headSha, + GitHubInformation gitHubInformation, + CancellationToken cancellationToken) + { + var commits = new List + { + new("Wrote some awesome tests", "c2dbe6e", DateTimeOffset.Now, new Author("Rasmus Mikkelsen", "r@smus.nu")), + new("Got it working", "4d79e4e", DateTimeOffset.Now, new Author("Rasmus Mikkelsen", "r@smus.nu")) + }; + + return Task.FromResult>(commits); + } + + public Task> GetPullRequestsAsync(string baseSha, + string headSha, + GitHubInformation gitHubInformation, + CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task GetPullRequestAsync( + GitHubInformation gitHubInformation, + int number, + CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task> GetTagsAsync( + GitHubInformation gitHubInformation, + CancellationToken cancellationToken) + { + return Task.FromResult>(Array.Empty()); + } } } } diff --git a/Source/Bake.Tests/UnitTests/Core/SemVerTests.cs b/Source/Bake.Tests/UnitTests/Core/SemVerTests.cs index d5c36edb..bafc360e 100644 --- a/Source/Bake.Tests/UnitTests/Core/SemVerTests.cs +++ b/Source/Bake.Tests/UnitTests/Core/SemVerTests.cs @@ -35,6 +35,12 @@ public class SemVerTests 2, null, null)] + [TestCase( + "3", + 3, + null, + null, + null)] [TestCase( "1.2.3-meta", 1, @@ -62,12 +68,12 @@ public class SemVerTests public void ValidVersions( string str, int expectedMajor, - int expectedMinor, + int? expectedMinor, int? expectedPatch, string expectedMeta) { // Act - var version = SemVer.Parse(str); + var version = SemVer.Parse(str, true); // Assert version.Major.Should().Be(expectedMajor); @@ -117,7 +123,7 @@ public void Compare( { // Act var list = versions - .Select(SemVer.Parse) + .Select(s => SemVer.Parse(s)) .OrderBy(v => v); // Assert diff --git a/Source/Bake.Tests/UnitTests/Services/ChangeLogBuilderTests.cs b/Source/Bake.Tests/UnitTests/Services/ChangeLogBuilderTests.cs new file mode 100644 index 00000000..6f853f46 --- /dev/null +++ b/Source/Bake.Tests/UnitTests/Services/ChangeLogBuilderTests.cs @@ -0,0 +1,134 @@ +// MIT License +// +// Copyright (c) 2021-2023 Rasmus Mikkelsen +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System.Collections.Generic; +using System.Linq; +using Bake.Services; +using Bake.Tests.Helpers; +using Bake.ValueObjects; +using FluentAssertions; +using NUnit.Framework; + +namespace Bake.Tests.UnitTests.Services +{ + public class ChangeLogBuilderTests : TestFor + { + [Test] + public void Build() + { + // Arrange + var pullRequests = Get(); + + // Act + var changes = Sut.Build(pullRequests); + + // Assert + changes.Should().HaveCount(2); + changes[ChangeType.Dependency].Should().HaveCount(21); + changes[ChangeType.Dependency].Select(d => d.Text).Should().Contain("Bump flask 2.3.2 to 3.0.0 in /TestProjects/Python3.Flask (#265, #282, by @dependabot)"); + } + + private static IReadOnlyCollection Get() + { + return new PullRequest[] + { + new (236, "Bump NuGet.Packaging from 6.6.0 to 6.6.1", new []{"dependabot"}), + new (235, "Post release fixes", new []{"rasmus"}), + new (237, "Bump YamlDotNet from 13.1.0 to 13.1.1", new []{"dependabot"}), + new (240, "Bump WireMock.Net from 1.5.28 to 1.5.29", new []{"dependabot"}), + new (241, "Bump semver from 7.5.1 to 7.5.3 in /TestProjects/NodeJS.Service", new []{"dependabot"}), + new (239, "Bump Octokit from 6.0.0 to 6.1.0", new []{"dependabot"}), + new (242, "Bump WireMock.Net from 1.5.29 to 1.5.30", new []{"dependabot"}), + new (243, "Bump Microsoft.NET.Test.Sdk from 17.6.2 to 17.6.3", new []{"dependabot"}), + new (244, "Bump Octokit from 6.1.0 to 6.2.1", new []{"dependabot"}), + new (246, "Bump fastify from 4.18.0 to 4.19.1 in /TestProjects/NodeJS.Service", new []{"dependabot"}), + new (248, "Bump fastify from 4.19.1 to 4.19.2 in /TestProjects/NodeJS.Service", new []{"dependabot"}), + new (247, "Bump Octokit from 6.2.1 to 7.0.0", new []{"dependabot"}), + new (249, "Bump Octokit from 7.0.0 to 7.0.1", new []{"dependabot"}), + new (250, "Bump WireMock.Net from 1.5.30 to 1.5.31", new []{"dependabot"}), + new (252, "Bump github.com/labstack/echo/v4 from 4.10.2 to 4.11.1 in /TestProjects/GoLang.Service", new []{"dependabot"}), + new (254, "Bump fastify from 4.19.2 to 4.20.0 in /TestProjects/NodeJS.Service", new []{"dependabot"}), + new (255, "Bump Octokit from 7.0.1 to 7.1.0", new []{"dependabot"}), + new (253, "Bump WireMock.Net from 1.5.31 to 1.5.32", new []{"dependabot"}), + new (256, "Bump fastify from 4.20.0 to 4.21.0 in /TestProjects/NodeJS.Service", new []{"dependabot"}), + new (259, "Bump Microsoft.NET.Test.Sdk from 17.6.3 to 17.7.0", new []{"dependabot"}), + new (258, "Bump WireMock.Net from 1.5.32 to 1.5.34", new []{"dependabot"}), + new (260, "Remove moq", new []{"rasmus"}), + new (261, "Bump NuGet.Packaging from 6.6.1 to 6.7.0", new []{"dependabot"}), + new (262, "Bump YamlDotNet from 13.1.1 to 13.2.0", new []{"dependabot"}), + new (263, "Bump Microsoft.NET.Test.Sdk from 17.7.0 to 17.7.1", new []{"dependabot"}), + new (264, "Bump WireMock.Net from 1.5.34 to 1.5.35", new []{"dependabot"}), + new (265, "Bump flask from 2.3.2 to 2.3.3 in /TestProjects/Python3.Flask", new []{"dependabot"}), + new (266, "Bump FluentAssertions from 6.11.0 to 6.12.0", new []{"dependabot"}), + new (267, "Bump fastify from 4.21.0 to 4.22.0 in /TestProjects/NodeJS.Service", new []{"dependabot"}), + new (269, "Bump YamlDotNet from 13.2.0 to 13.3.1", new []{"dependabot"}), + new (268, "Bump McMaster.Extensions.CommandLineUtils from 4.0.2 to 4.1.0", new []{"dependabot"}), + new (270, "Bump Microsoft.NET.Test.Sdk from 17.7.1 to 17.7.2", new []{"dependabot"}), + new (271, "Bump fastify from 4.22.0 to 4.22.1 in /TestProjects/NodeJS.Service", new []{"dependabot"}), + new (272, "Bump fastify from 4.22.1 to 4.22.2 in /TestProjects/NodeJS.Service", new []{"dependabot"}), + new (275, "Bump fastify from 4.22.2 to 4.23.0 in /TestProjects/NodeJS.Service", new []{"dependabot"}), + new (276, "Bump fastify from 4.23.0 to 4.23.1 in /TestProjects/NodeJS.Service", new []{"dependabot"}), + new (277, "Bump fastify from 4.23.1 to 4.23.2 in /TestProjects/NodeJS.Service", new []{"dependabot"}), + new (278, "Bump YamlDotNet from 13.3.1 to 13.4.0", new []{"dependabot"}), + new (279, "Bump WireMock.Net from 1.5.35 to 1.5.36", new []{"dependabot"}), + new (281, "Bump WireMock.Net from 1.5.36 to 1.5.37", new []{"dependabot"}), + new (284, "Bump YamlDotNet from 13.4.0 to 13.5.1", new []{"dependabot"}), + new (285, "Bump YamlDotNet from 13.5.1 to 13.5.2", new []{"dependabot"}), + new (286, "Bump WireMock.Net from 1.5.37 to 1.5.39", new []{"dependabot"}), + new (287, "Bump YamlDotNet from 13.5.2 to 13.7.0", new []{"dependabot"}), + new (288, "Bump github.com/labstack/echo/v4 from 4.11.1 to 4.11.2 in /TestProjects/GoLang.Service", new []{"dependabot"}), + new (289, "Bump fastify from 4.23.2 to 4.24.0 in /TestProjects/NodeJS.Service", new []{"dependabot"}), + new (291, "Bump fastify from 4.24.0 to 4.24.2 in /TestProjects/NodeJS.Service", new []{"dependabot"}), + new (293, "Bump fastify from 4.24.2 to 4.24.3 in /TestProjects/NodeJS.Service", new []{"dependabot"}), + new (294, "Bump actions/setup-node from 3 to 4", new []{"dependabot"}), + new (292, "Bump YamlDotNet from 13.7.0 to 13.7.1", new []{"dependabot"}), + new (282, "Bump flask from 2.3.3 to 3.0.0 in /TestProjects/Python3.Flask", new []{"dependabot"}), + new (296, "Bump LibGit2Sharp from 0.27.2 to 0.28.0", new []{"dependabot"}), + new (297, "Bump NUnit from 3.13.3 to 3.14.0", new []{"dependabot"}), + new (273, "Bump actions/checkout from 3 to 4", new []{"dependabot"}), + new (298, "Bump github.com/labstack/echo/v4 from 4.11.2 to 4.11.3 in /TestProjects/GoLang.Service", new []{"dependabot"}), + new (299, "Bump WireMock.Net from 1.5.39 to 1.5.40", new []{"dependabot"}), + new (300, "Bump Microsoft.NET.Test.Sdk from 17.7.2 to 17.8.0", new []{"dependabot"}), + new (308, "Update and test .NET 8", new []{"rasmus"}), + new (309, "Bump NuGet.Packaging from 6.7.0 to 6.8.0", new []{"dependabot"}), + new (311, "Bump actions/setup-dotnet from 3 to 4", new []{"dependabot"}), + new (313, "Bump actions/setup-go from 4 to 5", new []{"dependabot"}), + new (314, "Bump WireMock.Net from 1.5.40 to 1.5.42", new []{"dependabot"}), + new (315, "Bump LibGit2Sharp from 0.28.0 to 0.29.0", new []{"dependabot"}), + new (316, "Bump AutoFixture.AutoNSubstitute from 4.18.0 to 4.18.1", new []{"dependabot"}), + new (318, "Bump WireMock.Net from 1.5.42 to 1.5.43", new []{"dependabot"}), + new (312, "Bump actions/setup-python from 4 to 5", new []{"dependabot"}), + new (319, "Don't print commands", new []{"rasmus"}), + new (321, "Bump fastify from 4.24.3 to 4.25.0 in /TestProjects/NodeJS.Service", new []{"dependabot"}), + new (323, "Bump WireMock.Net from 1.5.43 to 1.5.44", new []{"dependabot"}), + new (325, "Bump fastify from 4.25.0 to 4.25.1 in /TestProjects/NodeJS.Service", new []{"dependabot"}), + new (326, "Bump golang.org/x/crypto from 0.14.0 to 0.17.0 in /TestProjects/GoLang.Service", new []{"dependabot"}), + new (322, "Bump github/codeql-action from 2 to 3", new []{"dependabot"}), + new (317, "Bump NUnit from 3.14.0 to 4.0.1", new []{"dependabot"}), + new (327, "Bump github.com/labstack/echo/v4 from 4.11.3 to 4.11.4 in /TestProjects/GoLang.Service", new []{"dependabot"}), + new (328, "Bump WireMock.Net from 1.5.44 to 1.5.45", new []{"dependabot"}), + new (329, "Bump fastify from 4.25.1 to 4.25.2 in /TestProjects/NodeJS.Service", new []{"dependabot"}), + new (330, "Bump WireMock.Net from 1.5.45 to 1.5.46", new []{"dependabot"}), + }; + } + } +} diff --git a/Source/Bake/Cooking/Cooks/GitHub/GitHubReleaseCook.cs b/Source/Bake/Cooking/Cooks/GitHub/GitHubReleaseCook.cs index 07e2be75..a347d3f7 100644 --- a/Source/Bake/Cooking/Cooks/GitHub/GitHubReleaseCook.cs +++ b/Source/Bake/Cooking/Cooks/GitHub/GitHubReleaseCook.cs @@ -93,6 +93,29 @@ protected override async Task CookAsync( .AppendLine(); } + if (context.Ingredients.Changelog != null && context.Ingredients.Changelog.Any()) + { + foreach (var (changeType, title) in new Dictionary + { + [ChangeType.Dependency] = "Updated dependencies", + [ChangeType.Other] = "Other changes", + }) + { + stringBuilder + .AppendLine($"#### {title}") + .AppendLine(); + + foreach (var change in context.Ingredients.Changelog[changeType]) + { + stringBuilder.AppendLine($"* {change.Text}"); + } + + stringBuilder.AppendLine(); + } + + stringBuilder.AppendLine(); + } + var releaseFiles = (await CreateReleaseFilesAsync(additionalFiles, recipe, cancellationToken)).ToList(); var documentationSite = recipe.Artifacts diff --git a/Source/Bake/Cooking/Ingredients/Gathers/ChangelogGather.cs b/Source/Bake/Cooking/Ingredients/Gathers/ChangelogGather.cs new file mode 100644 index 00000000..ac04cdea --- /dev/null +++ b/Source/Bake/Cooking/Ingredients/Gathers/ChangelogGather.cs @@ -0,0 +1,97 @@ +// MIT License +// +// Copyright (c) 2021-2023 Rasmus Mikkelsen +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Bake.Services; +using Bake.ValueObjects; +using Microsoft.Extensions.Logging; + +namespace Bake.Cooking.Ingredients.Gathers +{ + public class ChangelogGather : IGather + { + private readonly ILogger _logger; + private readonly IGitHub _gitHub; + private readonly IChangeLogBuilder _changeLogBuilder; + + public ChangelogGather( + ILogger logger, + IGitHub gitHub, + IChangeLogBuilder changeLogBuilder) + { + _logger = logger; + _gitHub = gitHub; + _changeLogBuilder = changeLogBuilder; + } + + public async Task GatherAsync( + ValueObjects.Ingredients ingredients, + CancellationToken cancellationToken) + { + GitInformation gitInformation; + GitHubInformation gitHubInformation; + + try + { + gitInformation = await ingredients.GitTask; + gitHubInformation = await ingredients.GitHubTask; + } + catch (OperationCanceledException) + { + ingredients.FailChangelog(); + return; + } + + var tags = await _gitHub.GetTagsAsync( + gitHubInformation, + cancellationToken); + + if (tags.Count == 0) + { + _logger.LogInformation("Did not find any release tags"); + ingredients.FailChangelog(); + return; + } + + var tag = tags + .Where(t => t.Version.LegacyVersion < ingredients.Version.LegacyVersion) + .MaxBy(t => t.Version); + + if (tag == null) + { + _logger.LogInformation("Could not find a su"); + ingredients.FailChangelog(); + return; + } + + var pullRequests = await _gitHub.GetPullRequestsAsync( + tag.Sha, + gitInformation.Sha, + gitHubInformation, cancellationToken); + + ingredients.Changelog = _changeLogBuilder.Build(pullRequests); + } + } +} diff --git a/Source/Bake/Core/SemVer.cs b/Source/Bake/Core/SemVer.cs index efb0f4ac..45e491c3 100644 --- a/Source/Bake/Core/SemVer.cs +++ b/Source/Bake/Core/SemVer.cs @@ -29,7 +29,7 @@ namespace Bake.Core public class SemVer : IComparable, IEquatable, IComparable { private static readonly Regex VersionParser = new( - @"^(v|version){0,1}\s*(?\d+)\.(?\d+)(\.(?\d+)){0,1}(\-(?[a-z0-9\-_]+)){0,1}$", + @"^(v|version){0,1}\s*(?\d+)(\.(?\d+)(\.(?\d+)){0,1}){0,1}(\-(?[a-z0-9\-_]+)){0,1}$", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Random R = new(); @@ -42,9 +42,9 @@ public class SemVer : IComparable, IEquatable, IComparable ? string.Empty : "meta"); - public static SemVer Parse(string str) + public static SemVer Parse(string str, bool allowNoMinor = false) { - var exception = InternalTryParse(str, out var version); + var exception = InternalTryParse(str, out var version, allowNoMinor); if (exception != null) { throw exception; @@ -53,14 +53,15 @@ public static SemVer Parse(string str) return version; } - public static bool TryParse(string str, out SemVer version) + public static bool TryParse(string str, out SemVer version, bool allowNoMinor = false) { - return InternalTryParse(str, out version) == null; + return InternalTryParse(str, out version, allowNoMinor) == null; } public static Exception InternalTryParse( string str, - out SemVer version) + out SemVer version, + bool allowNoMinor) { version = null; if (string.IsNullOrEmpty(str)) @@ -74,8 +75,16 @@ public static Exception InternalTryParse( return new ArgumentException($"'{str}' is not a valid version string"); } + var minorSuccess = match.Groups["minor"].Success; + if (!minorSuccess && !allowNoMinor) + { + return new ArgumentException($"'{str}' is not a valid version string"); + } + var major = int.Parse(match.Groups["major"].Value); - var minor = int.Parse(match.Groups["minor"].Value); + var minor = minorSuccess + ? int.Parse(match.Groups["minor"].Value) + : null as int?; var patch = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : null as int?; @@ -92,16 +101,17 @@ public static Exception InternalTryParse( return null; } - public static SemVer With(int major, - int minor = 0, - int patch = 0, + public static SemVer With( + int major, + int? minor = 0, + int? patch = 0, string meta = null) { return new SemVer(major, minor, patch, meta); } public int Major { get; } - public int Minor { get; } + public int? Minor { get; } public int? Patch { get; } public string Meta { get; } public Version LegacyVersion { get; } @@ -111,7 +121,7 @@ public static SemVer With(int major, private SemVer( int major, - int minor, + int? minor, int? patch , string meta) { @@ -120,12 +130,13 @@ private SemVer( Patch = patch; Meta = (meta ?? string.Empty).Trim('-'); LegacyVersion = patch.HasValue - ? new Version(major, minor, patch.Value) - : new Version(major, minor); + ? new Version(major, minor ?? 0, patch.Value) + : new Version(major, minor ?? 0); _lazyString = new Lazy(() => new StringBuilder() - .Append($"{Major}.{Minor}") + .Append($"{Major}") + .Append(Minor.HasValue ? $".{Minor}" : string.Empty) .Append(Patch.HasValue ? $".{Patch}" : string.Empty) .Append(!string.IsNullOrEmpty(Meta) ? $"-{Meta}" : string.Empty) .ToString()); @@ -169,7 +180,7 @@ public int CompareTo(SemVer other) if (ReferenceEquals(null, other)) return 1; var majorComparison = Major.CompareTo(other.Major); if (majorComparison != 0) return majorComparison; - var minorComparison = Minor.CompareTo(other.Minor); + var minorComparison = Minor.GetValueOrDefault().CompareTo(other.Minor.GetValueOrDefault()); if (minorComparison != 0) return minorComparison; var patchComparison = Patch.GetValueOrDefault().CompareTo(other.Patch.GetValueOrDefault()); if (patchComparison != 0) return patchComparison; @@ -200,7 +211,7 @@ public override int GetHashCode() { return HashCode.Combine( Major, - Minor, + Minor.GetValueOrDefault(), Patch.GetValueOrDefault(), Meta); } diff --git a/Source/Bake/Core/Yaml.cs b/Source/Bake/Core/Yaml.cs index c366f9fa..30834696 100644 --- a/Source/Bake/Core/Yaml.cs +++ b/Source/Bake/Core/Yaml.cs @@ -22,6 +22,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Reflection; using System.Threading; @@ -74,6 +75,7 @@ static Yaml() new DeserializerBuilder(), (b, a) => b.WithTagMapping(a.tag, a.type)) .WithNamingConvention(CamelCaseNamingConvention.Instance) + .WithTypeConverter(new DateTimeOffsetTypeConverter()) .WithTypeConverter(new SemVerYamlTypeConverter()) .IgnoreUnmatchedProperties() .Build(); @@ -82,6 +84,7 @@ static Yaml() (b, a) => b.WithTagMapping(a.tag, a.type)) .WithNamingConvention(CamelCaseNamingConvention.Instance) .WithTypeConverter(new SemVerYamlTypeConverter()) + .WithTypeConverter(new DateTimeOffsetTypeConverter()) .DisableAliases() .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults) .Build(); @@ -125,6 +128,42 @@ public override void Emit(ScalarEventInfo eventInfo, IEmitter emitter) } } + private class DateTimeOffsetTypeConverter : IYamlTypeConverter + { + private static readonly IValueDeserializer ValueDeserializer = new DeserializerBuilder() + .BuildValueDeserializer(); + + private static readonly IValueSerializer ValueSerializer = new SerializerBuilder() + .WithEventEmitter(n => new QuoteSurroundingEventEmitter(n)) + .BuildValueSerializer(); + + public bool Accepts(Type type) => typeof(DateTimeOffset) == type; + + public object? ReadYaml(IParser parser, Type type) + { + var value = (string)ValueDeserializer.DeserializeValue(parser, typeof(string), new SerializerState(), ValueDeserializer); + if (!DateTimeOffset.TryParseExact(value, "O", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var dateTimeOffset)) + { + throw new FormatException($"'{value}' is not a valid DateTimeOffset"); + } + + return dateTimeOffset; + } + + public void WriteYaml(IEmitter emitter, object? value, Type type) + { + if (value == null) + { + ValueSerializer.SerializeValue(emitter, null, typeof(string)); + } + else + { + var dateTimeOffset = (DateTimeOffset)value; + ValueSerializer.SerializeValue(emitter, dateTimeOffset.ToString("O"), typeof(string)); + } + } + } + private class SemVerYamlTypeConverter : IYamlTypeConverter { private static readonly IValueDeserializer ValueDeserializer = new DeserializerBuilder() diff --git a/Source/Bake/Extensions/ServiceCollectionExtensions.cs b/Source/Bake/Extensions/ServiceCollectionExtensions.cs index 9fa9d0c3..05270710 100644 --- a/Source/Bake/Extensions/ServiceCollectionExtensions.cs +++ b/Source/Bake/Extensions/ServiceCollectionExtensions.cs @@ -57,6 +57,7 @@ public static IServiceCollection AddBake( .AddSingleton() .AddTransient() .AddTransient() + .AddSingleton() .AddSingleton() .AddSingleton() .AddTransient() @@ -91,6 +92,7 @@ public static IServiceCollection AddBake( .AddTransient() .AddTransient() .AddTransient() + .AddTransient() .AddTransient() // CLI wrappers diff --git a/Source/Bake/IChangeLogBuilder.cs b/Source/Bake/IChangeLogBuilder.cs new file mode 100644 index 00000000..c600274e --- /dev/null +++ b/Source/Bake/IChangeLogBuilder.cs @@ -0,0 +1,32 @@ +// MIT License +// +// Copyright (c) 2021-2023 Rasmus Mikkelsen +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System.Collections.Generic; +using Bake.ValueObjects; + +namespace Bake +{ + public interface IChangeLogBuilder + { + IReadOnlyDictionary> Build(IReadOnlyCollection pullRequests); + } +} diff --git a/Source/Bake/Services/ChangeLogBuilder.cs b/Source/Bake/Services/ChangeLogBuilder.cs new file mode 100644 index 00000000..6291cd88 --- /dev/null +++ b/Source/Bake/Services/ChangeLogBuilder.cs @@ -0,0 +1,125 @@ +// MIT License +// +// Copyright (c) 2021-2023 Rasmus Mikkelsen +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Bake.Core; +using Bake.ValueObjects; + +namespace Bake.Services +{ + public class ChangeLogBuilder : IChangeLogBuilder + { + private static readonly Regex DependencyDetector = new( + @"Bump (?[^\s]+) from (?[0-9\.\-a-z]+) to (?[0-9\.\-a-z]+)( in (?[^\s]+)){0,1}", + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public IReadOnlyDictionary> Build(IReadOnlyCollection pullRequests) + { + var dependencyChanges = + ( + from pr in pullRequests + let m = DependencyDetector.Match(pr.Title) + where m.Success + let p = m.Groups["project"] + let a = new + { + fromVersion = SemVer.Parse(m.Groups["from"].Value, true), + to = SemVer.Parse(m.Groups["to"].Value, true), + dependency = m.Groups["name"].Value, + project = p.Success ? p.Value : string.Empty, + pr, + } + group a by new { a.dependency, a.project } + into g + orderby g.Key.project, g.Key.dependency + select new DependencyChange( + g.Key.dependency, + g.Key.project, + g + .Select(x => x.pr.Number) + .OrderBy(i => i) + .ToArray(), + g.Min(x => x.fromVersion), + g.Max(x => x.to), + g + .SelectMany(x => x.pr.Authors) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(x => x) + .ToArray()) + ) + .OrderBy(c => c.Name) + .ToArray(); + + var dependencyPRs = new HashSet(dependencyChanges.SelectMany(d => d.PullRequests)); + + var otherChanges = pullRequests + .Where(pr => !dependencyPRs.Contains(pr.Number)) + .OrderBy(pr => pr.Number) + .Select(pr => pr.ToChange()) + .ToArray(); + + return new Dictionary> + { + [ChangeType.Other] = otherChanges, + [ChangeType.Dependency] = dependencyChanges.Select(d => d.ToChange()).ToArray(), + }; + } + + private class DependencyChange + { + public string Name { get; } + public string Project { get; } + public int[] PullRequests { get; } + public SemVer From { get; } + public SemVer To { get; } + public IReadOnlyCollection Authors { get; } + + public DependencyChange( + string name, + string project, + int[] pullRequests, + SemVer from, + SemVer to, + IReadOnlyCollection authors) + { + Name = name; + Project = project; + PullRequests = pullRequests; + From = from; + To = to; + Authors = authors; + } + + public Change ToChange() + { + var message = string.IsNullOrEmpty(Project) + ? $"Bump {Name} {From} to {To} ({string.Join(", ", PullRequests.Select(pr => $"#{pr}"))}, by {string.Join(", ", Authors.Select(a => $"@{a}"))})" + : $"Bump {Name} {From} to {To} in {Project} ({string.Join(", ", PullRequests.Select(pr => $"#{pr}"))}, by {string.Join(", ", Authors.Select(a => $"@{a}"))})"; + + return new Change(ChangeType.Dependency, message); + } + } + } +} diff --git a/Source/Bake/Services/GitHub.cs b/Source/Bake/Services/GitHub.cs index 6247a562..701c2f3a 100644 --- a/Source/Bake/Services/GitHub.cs +++ b/Source/Bake/Services/GitHub.cs @@ -20,6 +20,8 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text.RegularExpressions; @@ -29,6 +31,9 @@ using Bake.ValueObjects; using Microsoft.Extensions.Logging; using Octokit; +using Author = Bake.ValueObjects.Author; +using Commit = Bake.ValueObjects.Commit; +using PullRequest = Bake.ValueObjects.PullRequest; using Release = Bake.ValueObjects.Release; namespace Bake.Services @@ -41,6 +46,9 @@ public class GitHub : IGitHub public static readonly Regex SpecialMergeCommitMessageParser = new( @"Merge\s+(?[a-f0-9]+)\s+into\s+(?[a-f0-9]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex IsMergeCommit = new( + @"^Merge\s+pull\s+request\s+\#(?[0-9]+)\s+.*", + RegexOptions.Compiled | RegexOptions.IgnoreCase); public GitHub( ILogger logger, @@ -89,14 +97,66 @@ await gitHubClient.Repository.Release.Edit( gitHubReleaseUpdate); } + public async Task> GetTagsAsync( + GitHubInformation gitHubInformation, + CancellationToken cancellationToken) + { + var gitHubClient = await CreateGitHubClientAsync(gitHubInformation, cancellationToken); + if (gitHubClient == null) + { + return Array.Empty(); + } + + var releases = await gitHubClient.Repository.GetAllTags( + gitHubInformation.Owner, + gitHubInformation.Repository); + + return releases + .Select(t => new + { + version = SemVer.TryParse(t.Name, out var v, true) ? v : null, + tag = t, + }) + .Where(a => a.version != null) + .Select(a => new Tag(a.version, a.tag.Commit.Sha)) + .ToArray(); + } + public async Task GetPullRequestInformationAsync( GitInformation gitInformation, GitHubInformation gitHubInformation, CancellationToken cancellationToken) { var gitHubClient = await CreateGitHubClientAsync(gitHubInformation, cancellationToken); + if (gitHubClient == null) + { + return null; + } + + var pullRequestInformation = await SearchAsync(gitInformation.Sha); + if (pullRequestInformation != null) + { + return pullRequestInformation; + } - async Task SearchAsync(string c) + if (string.IsNullOrEmpty(gitInformation.Message)) + { + return null; + } + + var match = SpecialMergeCommitMessageParser.Match(gitInformation.Message); + if (!match.Success) + { + return null; + } + + _logger.LogDebug( + "This commit {PreMergeCommitSha} looks like a special GitHub pre-merge commit for the actual commit {CommitSha}", + match.Groups["pr"].Value, match.Groups["base"].Value); + + return await SearchAsync(match.Groups["pr"].Value); + + async Task SearchAsync(string c) { var issues = await gitHubClient.Search.SearchIssues(new SearchIssuesRequest(c) { @@ -115,44 +175,137 @@ async Task SearchAsync(string c) return new PullRequestInformation( issue.Labels.Select(l => l.Name).ToArray()); } + } - var pullRequestInformation = await SearchAsync(gitInformation.Sha); - if (pullRequestInformation != null) + private async Task CreateGitHubClientAsync( + GitHubInformation gitHubInformation, + CancellationToken cancellationToken) + { + var token = await _credentials.TryGetGitHubTokenAsync( + gitHubInformation.Url, + cancellationToken); + if (string.IsNullOrEmpty(token)) { - return pullRequestInformation; + return null; } - if (string.IsNullOrEmpty(gitInformation.Message)) + var gitHubClient = await _gitHubClientFactory.CreateAsync( + token, + gitHubInformation.ApiUrl, + cancellationToken); + + return gitHubClient; + } + + public async Task> GetCommitsAsync( + string baseSha, + string headSha, + GitHubInformation gitHubInformation, + CancellationToken cancellationToken) + { + var gitHubClient = await CreateGitHubClientAsync( + gitHubInformation, + cancellationToken); + + if (gitHubClient == null) { - return null; + return Array.Empty(); } - var match = SpecialMergeCommitMessageParser.Match(gitInformation.Message); - if (!match.Success) + var compareResult = await gitHubClient.Repository.Commit.Compare( + gitHubInformation.Owner, + gitHubInformation.Repository, + baseSha, + headSha); + if (compareResult.AheadBy <= 0) { - return null; + return Array.Empty(); } - _logger.LogDebug( - "This commit {PreMergeCommitSha} looks like a special GitHub pre-merge commit for the actual commit {CommitSha}", - match.Groups["pr"].Value, match.Groups["base"].Value); + return compareResult.Commits + .Select(c => new Commit( + c.Commit.Message, + c.Sha, + c.Commit.Author.Date, + new Author( + c.Commit.Author.Name, + c.Commit.Author.Email))) + .ToList(); + } - return await SearchAsync(match.Groups["pr"].Value); + public async Task> GetPullRequestsAsync( + string baseSha, + string headSha, + GitHubInformation gitHubInformation, + CancellationToken cancellationToken) + { + var commits = await GetCommitsAsync( + baseSha, + headSha, + gitHubInformation, + cancellationToken); + + var pullRequestTasks = commits + .Select(c => IsMergeCommit.Match(c.Message)) + .Where(m => m.Success) + .Select(m => GetPullRequestAsync(gitHubInformation, int.Parse(m.Groups["pr"].Value), cancellationToken)); + + var pullRequests = await Task.WhenAll(pullRequestTasks); + + return pullRequests + .Where(pr => pr != null) + .ToArray(); } - private async Task CreateGitHubClientAsync( + public async Task GetPullRequestAsync( GitHubInformation gitHubInformation, + int number, CancellationToken cancellationToken) { var token = await _credentials.TryGetGitHubTokenAsync( gitHubInformation.Url, cancellationToken); + if (string.IsNullOrEmpty(token)) + { + return null; + } + var gitHubClient = await _gitHubClientFactory.CreateAsync( token, gitHubInformation.ApiUrl, cancellationToken); - return gitHubClient; + + try + { + var pullRequest = await gitHubClient.PullRequest.Get( + gitHubInformation.Owner, + gitHubInformation.Repository, + number); + + var author = pullRequest.User.Login; + var i = author.IndexOf('['); + var length = i < 0 ? author.Length : i; + + return new PullRequest( + pullRequest.Number, + pullRequest.Title, + new [] + { + author[..length], + }); + } + catch (NotFoundException e) + { + // Puke! (why throw exception for something that is likely to happen) + + _logger.LogDebug( + e, "Could not find pull request #{Number} in {Owner}/{Repository}", + number, + gitHubInformation.Owner, + gitHubInformation.Repository); + return null; + } } private async Task UploadFileAsync( diff --git a/Source/Bake/Services/IGitHub.cs b/Source/Bake/Services/IGitHub.cs index aeb161c9..dff52405 100644 --- a/Source/Bake/Services/IGitHub.cs +++ b/Source/Bake/Services/IGitHub.cs @@ -20,6 +20,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Bake.ValueObjects; @@ -33,8 +34,25 @@ Task CreateReleaseAsync( GitHubInformation gitHubInformation, CancellationToken cancellationToken); - Task GetPullRequestInformationAsync( - GitInformation gitInformation, + Task GetPullRequestInformationAsync(GitInformation gitInformation, + GitHubInformation gitHubInformation, + CancellationToken cancellationToken); + + Task> GetCommitsAsync(string baseSha, string headSha, + GitHubInformation gitHubInformation, + CancellationToken cancellationToken); + + Task> GetPullRequestsAsync(string baseSha, + string headSha, + GitHubInformation gitHubInformation, + CancellationToken cancellationToken); + + Task GetPullRequestAsync( + GitHubInformation gitHubInformation, + int number, + CancellationToken cancellationToken); + + Task> GetTagsAsync( GitHubInformation gitHubInformation, CancellationToken cancellationToken); } diff --git a/Source/Bake/ValueObjects/Author.cs b/Source/Bake/ValueObjects/Author.cs new file mode 100644 index 00000000..2fd02e40 --- /dev/null +++ b/Source/Bake/ValueObjects/Author.cs @@ -0,0 +1,47 @@ +// MIT License +// +// Copyright (c) 2021-2023 Rasmus Mikkelsen +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using YamlDotNet.Serialization; + +namespace Bake.ValueObjects +{ + public class Author + { + [YamlMember] + public string Name { get; [Obsolete] set; } + + [YamlMember] + public string Email { get; [Obsolete] set; } + + [Obsolete] + public Author() { } + + public Author( + string name, + string email) + { + Name = name; + Email = email; + } + } +} diff --git a/Source/Bake/ValueObjects/Change.cs b/Source/Bake/ValueObjects/Change.cs new file mode 100644 index 00000000..c12145b5 --- /dev/null +++ b/Source/Bake/ValueObjects/Change.cs @@ -0,0 +1,54 @@ +// MIT License +// +// Copyright (c) 2021-2023 Rasmus Mikkelsen +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using YamlDotNet.Serialization; + +namespace Bake.ValueObjects +{ + public class Change + { + [YamlMember] + public ChangeType Type { get; [Obsolete] set; } + + [YamlMember] + public string Text { get; [Obsolete] set; } + + [Obsolete] + public Change() { } + + public Change( + ChangeType type, + string text) + { +#pragma warning disable CS0612 // Type or member is obsolete + Type = type; + Text = text; +#pragma warning restore CS0612 // Type or member is obsolete + } + + public override string ToString() + { + return $"{Type}: {Text}"; + } + } +} diff --git a/Source/Bake/ValueObjects/ChangeType.cs b/Source/Bake/ValueObjects/ChangeType.cs new file mode 100644 index 00000000..6c26fb05 --- /dev/null +++ b/Source/Bake/ValueObjects/ChangeType.cs @@ -0,0 +1,30 @@ +// MIT License +// +// Copyright (c) 2021-2023 Rasmus Mikkelsen +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +namespace Bake.ValueObjects +{ + public enum ChangeType + { + Other = 0, + Dependency = 1, + } +} diff --git a/Source/Bake/ValueObjects/Commit.cs b/Source/Bake/ValueObjects/Commit.cs new file mode 100644 index 00000000..5fe066d3 --- /dev/null +++ b/Source/Bake/ValueObjects/Commit.cs @@ -0,0 +1,64 @@ +// MIT License +// +// Copyright (c) 2021-2023 Rasmus Mikkelsen +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using YamlDotNet.Serialization; + +namespace Bake.ValueObjects +{ + public class Commit + { + [YamlMember] + public string Message { get; [Obsolete] set; } + + [YamlMember] + public string Sha { get; [Obsolete] set; } + + [YamlMember] + public Author Author { get; [Obsolete] set; } + + [YamlMember] + public DateTimeOffset Time { get; [Obsolete] set; } + + [Obsolete] + public Commit() { } + + public Commit( + string message, + string sha, + DateTimeOffset time, + Author author) + { +#pragma warning disable CS0612 // Type or member is obsolete + Message = message ?? string.Empty; + Sha = sha ?? string.Empty; + Author = author; + Time = time; +#pragma warning restore CS0612 // Type or member is obsolete + } + + public override string ToString() + { + return $"{Sha[..5]}: {Message}"; + } + } +} diff --git a/Source/Bake/ValueObjects/Ingredients.cs b/Source/Bake/ValueObjects/Ingredients.cs index da55daff..7297450c 100644 --- a/Source/Bake/ValueObjects/Ingredients.cs +++ b/Source/Bake/ValueObjects/Ingredients.cs @@ -59,6 +59,20 @@ public static Ingredients New( [YamlMember] public List Destinations { get; [Obsolete] set; } = new(); + [YamlMember] + public IReadOnlyDictionary> Changelog + { + get => _changelog.Task.IsCompletedSuccessfully ? _changelog.Task.Result : null; + set + { + if (value == null) + { + return; + } + _changelog.SetResult(value); + } + } + [YamlMember] public GitInformation Git { @@ -150,12 +164,16 @@ [Obsolete] set [YamlIgnore] public Task GitHubTask => _gitHub.Task; + [YamlIgnore] + public Task>> ChangelogTask => _changelog.Task; + [YamlIgnore] public Task PullRequestTask => _pullRequest.Task; - + private readonly TaskCompletionSource _git = new(); private readonly TaskCompletionSource _releaseNotes = new(); private readonly TaskCompletionSource _gitHub = new(); + private readonly TaskCompletionSource>> _changelog = new(); private readonly TaskCompletionSource _description = new(); private readonly TaskCompletionSource _pullRequest = new(); @@ -178,6 +196,7 @@ public Ingredients( public void FailGit() => _git.SetCanceled(); public void FailGitHub() => _gitHub.SetCanceled(); + public void FailChangelog() => _changelog.SetCanceled(); public void FailDescription() => _description.SetCanceled(); public void FailReleaseNotes() => _releaseNotes.SetCanceled(); public void FailPullRequest() => _pullRequest.SetCanceled(); @@ -198,6 +217,11 @@ public void FailOutstanding() { _pullRequest.SetCanceled(); } + + if (!_changelog.Task.IsCompleted) + { + _changelog.SetCanceled(); + } } } } diff --git a/Source/Bake/ValueObjects/PullRequest.cs b/Source/Bake/ValueObjects/PullRequest.cs new file mode 100644 index 00000000..abc9d1f6 --- /dev/null +++ b/Source/Bake/ValueObjects/PullRequest.cs @@ -0,0 +1,54 @@ +// MIT License +// +// Copyright (c) 2021-2023 Rasmus Mikkelsen +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System.Collections.Generic; +using System.Linq; + +namespace Bake.ValueObjects +{ + public class PullRequest + { + public int Number { get; } + public string Title { get; } + public IReadOnlyCollection Authors { get; } + + public PullRequest( + int number, + string title, + string[] authors) + { + Number = number; + Title = title; + Authors = authors; + } + + public Change ToChange() + { + return new Change(ChangeType.Other, $"{Title} (#{Number}, by {string.Join(", ", Authors.Select(a => $"@{a}"))})"); + } + + public override string ToString() + { + return $"#{Number}: {Title} by {string.Join(", ", Authors)}"; + } + } +} diff --git a/Source/Bake/ValueObjects/Release.cs b/Source/Bake/ValueObjects/Release.cs index 610ca82f..a7f0bf39 100644 --- a/Source/Bake/ValueObjects/Release.cs +++ b/Source/Bake/ValueObjects/Release.cs @@ -25,10 +25,8 @@ namespace Bake.ValueObjects { - public class Release + public class Release : Tag { - public SemVer Version { get; } - public string Sha { get; } public string Body { get; } public IReadOnlyCollection Files { get; } @@ -37,9 +35,8 @@ public Release( string sha, string body, IReadOnlyCollection files) + : base(version, sha) { - Version = version; - Sha = sha; Body = body; Files = files; } diff --git a/Source/Bake/ValueObjects/Tag.cs b/Source/Bake/ValueObjects/Tag.cs new file mode 100644 index 00000000..aa8a95fb --- /dev/null +++ b/Source/Bake/ValueObjects/Tag.cs @@ -0,0 +1,55 @@ +// MIT License +// +// Copyright (c) 2021-2023 Rasmus Mikkelsen +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using Bake.Core; +using YamlDotNet.Serialization; + +namespace Bake.ValueObjects +{ + public class Tag + { + [YamlMember] + public SemVer Version { get; [Obsolete] set; } + + [YamlMember] + public string Sha { get; [Obsolete] set; } + + [Obsolete] + public Tag(){} + + public Tag( + SemVer version, + string sha) + { +#pragma warning disable CS0612 // Type or member is obsolete + Version = version; + Sha = sha; +#pragma warning restore CS0612 // Type or member is obsolete + } + + public override string ToString() + { + return $"{Version}: {Sha}"; + } + } +} diff --git a/TestProjects/NetV8.Service/Source/Program.cs b/TestProjects/NetV8.Service/Source/Program.cs index 1785f550..2ba9b723 100644 --- a/TestProjects/NetV8.Service/Source/Program.cs +++ b/TestProjects/NetV8.Service/Source/Program.cs @@ -1,4 +1,26 @@ -var builder = WebApplication.CreateBuilder(args); +// MIT License +// +// Copyright (c) 2021-2023 Rasmus Mikkelsen +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapGet("/ping", () => "Pong!");