From c2e0f3ab66d5a1bd3b9189ede28413c90c91f178 Mon Sep 17 00:00:00 2001 From: jericho Date: Mon, 29 Jul 2024 21:56:34 -0400 Subject: [PATCH 1/4] =?UTF-8?q?=EF=BB=BF(#495)=20Allow=20milestones=20with?= =?UTF-8?q?out=20issues?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces a new configuration value in the GitReleaseManager.yaml file, which is disabled by default. When enabled, it will be used to control whether the creation of an "empty" release is allowed. This will make use of a new "empty" Scriban template, which will be introduced in the next commit. --- src/GitReleaseManager.Core/Configuration/Config.cs | 1 + src/GitReleaseManager.Core/Configuration/CreateConfig.cs | 3 +++ src/GitReleaseManager.Core/ReleaseNotes/ReleaseNotesBuilder.cs | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/GitReleaseManager.Core/Configuration/Config.cs b/src/GitReleaseManager.Core/Configuration/Config.cs index 262c12bc..51da2a99 100644 --- a/src/GitReleaseManager.Core/Configuration/Config.cs +++ b/src/GitReleaseManager.Core/Configuration/Config.cs @@ -27,6 +27,7 @@ public Config() ShaSectionHeading = "SHA256 Hashes of the release artifacts", ShaSectionLineFormat = "- `{1}\t{0}`", AllowUpdateToPublishedRelease = false, + AllowMilestonesWithoutIssues = false, IncludeContributors = false, }; diff --git a/src/GitReleaseManager.Core/Configuration/CreateConfig.cs b/src/GitReleaseManager.Core/Configuration/CreateConfig.cs index 00baed7d..06b27389 100644 --- a/src/GitReleaseManager.Core/Configuration/CreateConfig.cs +++ b/src/GitReleaseManager.Core/Configuration/CreateConfig.cs @@ -35,6 +35,9 @@ public class CreateConfig [YamlMember(Alias = "allow-update-to-published")] public bool AllowUpdateToPublishedRelease { get; set; } + [YamlMember(Alias = "allow-milestone-without-issues")] + public bool AllowMilestonesWithoutIssues { get; set; } + [YamlMember(Alias = "include-contributors")] public bool IncludeContributors { get; set; } } diff --git a/src/GitReleaseManager.Core/ReleaseNotes/ReleaseNotesBuilder.cs b/src/GitReleaseManager.Core/ReleaseNotes/ReleaseNotesBuilder.cs index 163c7dbc..a5513893 100644 --- a/src/GitReleaseManager.Core/ReleaseNotes/ReleaseNotesBuilder.cs +++ b/src/GitReleaseManager.Core/ReleaseNotes/ReleaseNotesBuilder.cs @@ -58,7 +58,7 @@ public async Task BuildReleaseNotesAsync(string user, string repository, var numberOfCommits = await _vcsProvider.GetCommitsCountAsync(_user, _repository, @base, head).ConfigureAwait(false); - if (issues.Count == 0) + if (issues.Count == 0 && !_configuration.Create.AllowMilestonesWithoutIssues) { var logMessage = string.Format(CultureInfo.CurrentCulture, "No closed issues have been found for milestone {0}, or all assigned issues are meant to be excluded from release notes, aborting release creation.", _milestoneTitle); throw new InvalidOperationException(logMessage); From 89dce6ad6ba11b62338becc8a1dd867174c8825c Mon Sep 17 00:00:00 2001 From: Jericho Date: Thu, 20 Feb 2025 13:38:54 -0500 Subject: [PATCH 2/4] (#495) Make use of new empty Scriban template When creating a release a new template is used when a milestone does not have any associated issues AND the developer has indicated they want to allow milestones without issues. The decision on which template to use has had to be moved. It can still be overridden when required, but the calculated value is based on how many issues have been found, whether to include contributors, etc. --- .../ReleaseNotes/ReleaseNotesBuilder.cs | 21 +++++++++++- .../Templates/no_issues/create/footer.sbn | 10 ++++++ .../Templates/no_issues/index.sbn | 10 ++++++ .../Templates/no_issues/issues.sbn | 2 ++ .../Templates/no_issues/milestone.sbn | 2 ++ .../Templates/no_issues/release-info.sbn | 4 +++ src/GitReleaseManager.Core/VcsService.cs | 14 ++------ .../ReleaseNotesBuilderIntegrationTests.cs | 33 +++++++++++++++---- 8 files changed, 77 insertions(+), 19 deletions(-) create mode 100644 src/GitReleaseManager.Core/Templates/no_issues/create/footer.sbn create mode 100644 src/GitReleaseManager.Core/Templates/no_issues/index.sbn create mode 100644 src/GitReleaseManager.Core/Templates/no_issues/issues.sbn create mode 100644 src/GitReleaseManager.Core/Templates/no_issues/milestone.sbn create mode 100644 src/GitReleaseManager.Core/Templates/no_issues/release-info.sbn diff --git a/src/GitReleaseManager.Core/ReleaseNotes/ReleaseNotesBuilder.cs b/src/GitReleaseManager.Core/ReleaseNotes/ReleaseNotesBuilder.cs index a5513893..b8fbb815 100644 --- a/src/GitReleaseManager.Core/ReleaseNotes/ReleaseNotesBuilder.cs +++ b/src/GitReleaseManager.Core/ReleaseNotes/ReleaseNotesBuilder.cs @@ -35,7 +35,7 @@ public ReleaseNotesBuilder(IVcsProvider vcsProvider, ILogger logger, IFileSystem _templateFactory = templateFactory; } - public async Task BuildReleaseNotesAsync(string user, string repository, string milestoneTitle, string template) + public async Task BuildReleaseNotesAsync(string user, string repository, string milestoneTitle, string customTemplate) { _user = user; _repository = repository; @@ -64,6 +64,25 @@ public async Task BuildReleaseNotesAsync(string user, string repository, throw new InvalidOperationException(logMessage); } + // By default we use the custom template, if it was provided. + // Otherwise, we determine which template we should use. + var template = customTemplate; + if (string.IsNullOrWhiteSpace(template)) + { + if (issues.Count == 0) + { + template = ReleaseTemplates.NO_ISSUES_NAME; + } + else if (_configuration.Create.IncludeContributors) + { + template = ReleaseTemplates.CONTRIBUTORS_NAME; + } + else + { + template = ReleaseTemplates.DEFAULT_NAME; + } + } + var commitsLink = _vcsProvider.GetCommitsUrl(_user, _repository, _targetMilestone?.Title, previousMilestone?.Title); var issuesDict = GetIssuesDict(issues); diff --git a/src/GitReleaseManager.Core/Templates/no_issues/create/footer.sbn b/src/GitReleaseManager.Core/Templates/no_issues/create/footer.sbn new file mode 100644 index 00000000..b1b08303 --- /dev/null +++ b/src/GitReleaseManager.Core/Templates/no_issues/create/footer.sbn @@ -0,0 +1,10 @@ +{{ if config.create.include_footer }} + +### {{ config.create.footer_heading }} + +{{ if config.create.milestone_replace_text + replace_milestone_title config.create.footer_content config.create.milestone_replace_text milestone.target.title + else + config.create.footer_content + end +end }} diff --git a/src/GitReleaseManager.Core/Templates/no_issues/index.sbn b/src/GitReleaseManager.Core/Templates/no_issues/index.sbn new file mode 100644 index 00000000..1a3856e2 --- /dev/null +++ b/src/GitReleaseManager.Core/Templates/no_issues/index.sbn @@ -0,0 +1,10 @@ +{{- + include 'release-info' + if milestone.target.description + include 'milestone' + end + include 'issues' | string.rstrip + if template_kind == "CREATE" + include 'create/footer' + end +~}} diff --git a/src/GitReleaseManager.Core/Templates/no_issues/issues.sbn b/src/GitReleaseManager.Core/Templates/no_issues/issues.sbn new file mode 100644 index 00000000..4b52bf63 --- /dev/null +++ b/src/GitReleaseManager.Core/Templates/no_issues/issues.sbn @@ -0,0 +1,2 @@ + +This release had no issues associated with it. diff --git a/src/GitReleaseManager.Core/Templates/no_issues/milestone.sbn b/src/GitReleaseManager.Core/Templates/no_issues/milestone.sbn new file mode 100644 index 00000000..b3459681 --- /dev/null +++ b/src/GitReleaseManager.Core/Templates/no_issues/milestone.sbn @@ -0,0 +1,2 @@ + +{{ milestone.target.description }} diff --git a/src/GitReleaseManager.Core/Templates/no_issues/release-info.sbn b/src/GitReleaseManager.Core/Templates/no_issues/release-info.sbn new file mode 100644 index 00000000..3a19a449 --- /dev/null +++ b/src/GitReleaseManager.Core/Templates/no_issues/release-info.sbn @@ -0,0 +1,4 @@ +{{ + if commits.count > 0 +}}As part of this release we had [{{ commits.count }} {{ commits.count | string.pluralize "commit" "commits" }}]({{ commits.html_url }}). +{{ end -}} diff --git a/src/GitReleaseManager.Core/VcsService.cs b/src/GitReleaseManager.Core/VcsService.cs index d86e1990..1578498e 100644 --- a/src/GitReleaseManager.Core/VcsService.cs +++ b/src/GitReleaseManager.Core/VcsService.cs @@ -13,7 +13,6 @@ using GitReleaseManager.Core.Model; using GitReleaseManager.Core.Provider; using GitReleaseManager.Core.ReleaseNotes; -using GitReleaseManager.Core.Templates; using Serilog; namespace GitReleaseManager.Core @@ -46,16 +45,7 @@ public async Task CreateEmptyReleaseAsync(string owner, string reposito public async Task CreateReleaseFromMilestoneAsync(string owner, string repository, string milestone, string releaseName, string targetCommitish, IList assets, bool prerelease, string templateFilePath) { - var templatePath = _configuration.Create.IncludeContributors - ? ReleaseTemplates.CONTRIBUTORS_NAME - : ReleaseTemplates.DEFAULT_NAME; - - if (!string.IsNullOrWhiteSpace(templateFilePath)) - { - templatePath = templateFilePath; - } - - var releaseNotes = await _releaseNotesBuilder.BuildReleaseNotesAsync(owner, repository, milestone, templatePath).ConfigureAwait(false); + var releaseNotes = await _releaseNotesBuilder.BuildReleaseNotesAsync(owner, repository, milestone, templateFilePath).ConfigureAwait(false); var release = await CreateReleaseAsync(owner, repository, releaseName, milestone, releaseNotes, prerelease, targetCommitish, assets).ConfigureAwait(false); return release; @@ -67,7 +57,7 @@ public async Task CreateReleaseFromInputFileAsync(string owner, string _logger.Verbose("Reading release notes from: '{FilePath}'", inputFilePath); - var releaseNotes = File.ReadAllText(inputFilePath); + var releaseNotes = await File.ReadAllTextAsync(inputFilePath).ConfigureAwait(false); var release = await CreateReleaseAsync(owner, repository, name, name, releaseNotes, prerelease, targetCommitish, assets).ConfigureAwait(false); return release; diff --git a/src/GitReleaseManager.IntegrationTests/ReleaseNotesBuilderIntegrationTests.cs b/src/GitReleaseManager.IntegrationTests/ReleaseNotesBuilderIntegrationTests.cs index 296c81f8..2415e5c6 100644 --- a/src/GitReleaseManager.IntegrationTests/ReleaseNotesBuilderIntegrationTests.cs +++ b/src/GitReleaseManager.IntegrationTests/ReleaseNotesBuilderIntegrationTests.cs @@ -74,17 +74,38 @@ public async Task SingleMilestone() var configuration = ConfigurationProvider.Provide(currentDirectory, fileSystem); configuration.IssueLabelsExclude.Add("Internal Refactoring"); // This is necessary to generate the release notes for GitReleaseManager version 0.12.0 - // Indicate whether you want to include the 'Contributors' section in the release notes + // Indicate that we want to include the 'Contributors' section in the release notes configuration.Create.IncludeContributors = true; - // Pick the template based on whether you want to include the 'Contributors' section in the release notes - var templatePath = configuration.Create.IncludeContributors - ? ReleaseTemplates.CONTRIBUTORS_NAME - : ReleaseTemplates.DEFAULT_NAME; + var vcsProvider = new GitHubProvider(_gitHubClient, _mapper, _graphQlClient); + var releaseNotesBuilder = new ReleaseNotesBuilder(vcsProvider, _logger, fileSystem, configuration, new TemplateFactory(fileSystem, configuration, TemplateKind.Create)); + var result = await releaseNotesBuilder.BuildReleaseNotesAsync("GitTools", "GitReleaseManager", "0.12.0", string.Empty).ConfigureAwait(false); // 0.12.0 contains a mix of issues and PRs + Debug.WriteLine(result); + ClipBoardHelper.SetClipboard(result); + } + } + + [Test] + [Explicit] + public async Task MilestoneWithoutIssues() + { + if (string.IsNullOrWhiteSpace(_token)) + { + Assert.Inconclusive("Unable to locate credentials for accessing GitHub API"); + } + else + { + var fileSystem = new FileSystem(new CreateSubOptions()); + var currentDirectory = Environment.CurrentDirectory; + + var configuration = ConfigurationProvider.Provide(currentDirectory, fileSystem); + + // Indicate that we allow milestones without issues + configuration.Create.AllowMilestonesWithoutIssues = true; var vcsProvider = new GitHubProvider(_gitHubClient, _mapper, _graphQlClient); var releaseNotesBuilder = new ReleaseNotesBuilder(vcsProvider, _logger, fileSystem, configuration, new TemplateFactory(fileSystem, configuration, TemplateKind.Create)); - var result = await releaseNotesBuilder.BuildReleaseNotesAsync("GitTools", "GitReleaseManager", "0.12.0", templatePath).ConfigureAwait(false); // 0.12.0 contains a mix of issues and PRs + var result = await releaseNotesBuilder.BuildReleaseNotesAsync("jericho", "_testing", "0.1.0", string.Empty).ConfigureAwait(false); // There are no issues associated with milestone 0.1.0 in my testing repo. Debug.WriteLine(result); ClipBoardHelper.SetClipboard(result); } From 8582f3d09c9eb2cfc4757246901cda70cb727a9c Mon Sep 17 00:00:00 2001 From: Jericho Date: Thu, 20 Feb 2025 13:52:51 -0500 Subject: [PATCH 3/4] (#495) Fix tests as a result of changes Due to the change to allow calculation of which template to use, it is no longer needed to pass in a template value. This is only needed when we specifically want/need to control which template is used. --- .../VcsServiceTests.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/GitReleaseManager.Core.Tests/VcsServiceTests.cs b/src/GitReleaseManager.Core.Tests/VcsServiceTests.cs index 03de7a4d..c547fa38 100644 --- a/src/GitReleaseManager.Core.Tests/VcsServiceTests.cs +++ b/src/GitReleaseManager.Core.Tests/VcsServiceTests.cs @@ -8,7 +8,6 @@ using GitReleaseManager.Core.Model; using GitReleaseManager.Core.Provider; using GitReleaseManager.Core.ReleaseNotes; -using GitReleaseManager.Core.Templates; using NSubstitute; using NUnit.Framework; using Serilog; @@ -303,7 +302,7 @@ public async Task Should_Create_Release_From_Milestone() { var release = new Release(); - _releaseNotesBuilder.BuildReleaseNotesAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ReleaseTemplates.DEFAULT_NAME) + _releaseNotesBuilder.BuildReleaseNotesAsync(OWNER, REPOSITORY, MILESTONE_TITLE, null) .Returns(Task.FromResult(RELEASE_NOTES)); _vcsProvider.GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE) @@ -315,7 +314,7 @@ public async Task Should_Create_Release_From_Milestone() var result = await _vcsService.CreateReleaseFromMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE, MILESTONE_TITLE, null, null, false, null).ConfigureAwait(false); result.ShouldBeSameAs(release); - await _releaseNotesBuilder.Received(1).BuildReleaseNotesAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ReleaseTemplates.DEFAULT_NAME).ConfigureAwait(false); + await _releaseNotesBuilder.Received(1).BuildReleaseNotesAsync(OWNER, REPOSITORY, MILESTONE_TITLE, null).ConfigureAwait(false); await _vcsProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false); await _vcsProvider.Received(1).CreateReleaseAsync(OWNER, REPOSITORY, Arg.Is(o => o.Body == RELEASE_NOTES && @@ -333,7 +332,7 @@ public async Task Should_Create_Release_From_Milestone_With_Assets() var assetsCount = _assets.Count; - _releaseNotesBuilder.BuildReleaseNotesAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ReleaseTemplates.DEFAULT_NAME) + _releaseNotesBuilder.BuildReleaseNotesAsync(OWNER, REPOSITORY, MILESTONE_TITLE, null) .Returns(Task.FromResult(RELEASE_NOTES)); _vcsProvider.GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE) @@ -353,7 +352,7 @@ public async Task Should_Create_Release_From_Milestone_With_Assets() null).ConfigureAwait(false); result.ShouldBeSameAs(release); - await _releaseNotesBuilder.Received(1).BuildReleaseNotesAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ReleaseTemplates.DEFAULT_NAME).ConfigureAwait(false); + await _releaseNotesBuilder.Received(1).BuildReleaseNotesAsync(OWNER, REPOSITORY, MILESTONE_TITLE, null).ConfigureAwait(false); await _vcsProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false); await _vcsProvider.Received(1).CreateReleaseAsync(OWNER, REPOSITORY, Arg.Is(o => o.Body == RELEASE_NOTES && @@ -430,7 +429,7 @@ public async Task Should_Update_Published_Release_On_Creating_Release_From_Miles _configuration.Create.AllowUpdateToPublishedRelease = updatePublishedRelease; - _releaseNotesBuilder.BuildReleaseNotesAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ReleaseTemplates.DEFAULT_NAME) + _releaseNotesBuilder.BuildReleaseNotesAsync(OWNER, REPOSITORY, MILESTONE_TITLE, null) .Returns(Task.FromResult(RELEASE_NOTES)); _vcsProvider.GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE) @@ -442,7 +441,7 @@ public async Task Should_Update_Published_Release_On_Creating_Release_From_Miles var result = await _vcsService.CreateReleaseFromMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE, MILESTONE_TITLE, null, null, false, null).ConfigureAwait(false); result.ShouldBeSameAs(release); - await _releaseNotesBuilder.Received(1).BuildReleaseNotesAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ReleaseTemplates.DEFAULT_NAME).ConfigureAwait(false); + await _releaseNotesBuilder.Received(1).BuildReleaseNotesAsync(OWNER, REPOSITORY, MILESTONE_TITLE, null).ConfigureAwait(false); await _vcsProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false); await _vcsProvider.Received(1).UpdateReleaseAsync(OWNER, REPOSITORY, release).ConfigureAwait(false); @@ -458,7 +457,7 @@ public async Task Should_Throw_Exception_While_Updating_Published_Release_On_Cre _configuration.Create.AllowUpdateToPublishedRelease = false; - _releaseNotesBuilder.BuildReleaseNotesAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ReleaseTemplates.DEFAULT_NAME) + _releaseNotesBuilder.BuildReleaseNotesAsync(OWNER, REPOSITORY, MILESTONE_TITLE, null) .Returns(Task.FromResult(RELEASE_NOTES)); _vcsProvider.GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE) @@ -467,7 +466,7 @@ public async Task Should_Throw_Exception_While_Updating_Published_Release_On_Cre var ex = await Should.ThrowAsync(() => _vcsService.CreateReleaseFromMilestoneAsync(OWNER, REPOSITORY, MILESTONE_TITLE, MILESTONE_TITLE, null, null, false, null)).ConfigureAwait(false); ex.Message.ShouldBe($"Release with tag '{MILESTONE_TITLE}' not in draft state, so not updating"); - await _releaseNotesBuilder.Received(1).BuildReleaseNotesAsync(OWNER, REPOSITORY, MILESTONE_TITLE, ReleaseTemplates.DEFAULT_NAME).ConfigureAwait(false); + await _releaseNotesBuilder.Received(1).BuildReleaseNotesAsync(OWNER, REPOSITORY, MILESTONE_TITLE, null).ConfigureAwait(false); await _vcsProvider.Received(1).GetReleaseAsync(OWNER, REPOSITORY, MILESTONE_TITLE).ConfigureAwait(false); } From c4cdc00b16cd5f8e9e5cf404c3222dd1fc850ccc Mon Sep 17 00:00:00 2001 From: Gary Ewan Park Date: Thu, 3 Apr 2025 08:00:14 +0100 Subject: [PATCH 4/4] (#495) Add docs for new configuration option Add information to the docs section for the new allow-milestone-without-issues option. --- docs/input/docs/configuration/default-configuration.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/input/docs/configuration/default-configuration.md b/docs/input/docs/configuration/default-configuration.md index de4055a6..79b90b78 100644 --- a/docs/input/docs/configuration/default-configuration.md +++ b/docs/input/docs/configuration/default-configuration.md @@ -21,6 +21,7 @@ create: sha-section-line-format: "- `{1}\t{0}`" allow-update-to-published: false include-contributors: false + allow-milestone-without-issues: false export: include-created-date-in-title: false created-date-string-format: '' @@ -142,6 +143,11 @@ control the look and feel of the generated release notes. in the release notes. A contributor is defined as someone who opened an issue or submitted a PR. **NOTE:** This configuration option was added in version 0.19.0 of GitReleaseManager. +- **allow-milestone-without-issues** + - A boolean value which indicates whether an empty release will be created, when + no issues are found to be associated with a milestone. The contents of the + empty release can be controlled via the associated Scriban template. + **NOTE:** This configuration option was added in version 0.20.0 of GitReleaseManager. See the [example create configuration section](create-configuration) to see an example of how a footer can be configured.