diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index 59de92ba..241a84b6 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -19,13 +19,32 @@ jobs: include-changelog-entry: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2.3.4 - with: - fetch-depth: 0 - - if: ${{ github.actor != 'dependabot-preview[bot]' }} - uses: Zomzog/changelog-checker@v1.1.0 - with: - fileName: CHANGELOG.md - noChangelogLabel: Changelog Not Required - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/checkout@v2.3.4 + with: + fetch-depth: 0 + - if: ${{ github.actor != 'dependabot[bot]' }} + uses: Zomzog/changelog-checker@v1.1.0 + with: + fileName: CHANGELOG.md + noChangelogLabel: Changelog Not Required + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + change-log-entry-is-in-unreleased: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2.3.4 + with: + fetch-depth: 0 + - uses: credfeto/action-dotnet-version-detect@v1.1.1 + - uses: actions/setup-dotnet@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + - name: Enable dotnet tools + run: dotnet new tool-manifest + - name: Install Changelog tool + run: dotnet tool install --local Credfeto.ChangeLog.Cmd + - name: Check Changelog + run: dotnet changelog -changelog D:\Work\changelog-manager\CHANGELOG.md -check-insert ${{ github.base_ref}} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a52ca7df..123c20fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Please ADD ALL Changes to the UNRELEASED SECTION and not a specific release ## [Unreleased] ### Added +- Added support for checking where changelog entries were entered ### Fixed ### Changed ### Removed diff --git a/README.md b/README.md index 6598824e..bb40f286 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,20 @@ -# cs-template -C# Template +# Changelog manager .net tool + +## Installation + +```shell +dotnet tool install Credfeto.ChangeLog.Cmd +``` + +## Usage + +Extract the release notes for a pre-release build +```shell +dotnet changelog -changelog CHANGELOG.md -extract RELEASE_NOTES.md -version 1.0.1.27-master +``` + +Extract the release notes for a release build +```shell +dotnet changelog -changelog CHANGELOG.md -extract RELEASE_NOTES.md -version 1.0.2.77 +``` + diff --git a/src/Credfeto.ChangeLog.Cmd/Credfeto.ChangeLog.Cmd.csproj b/src/Credfeto.ChangeLog.Cmd/Credfeto.ChangeLog.Cmd.csproj index 85657243..316d3cb5 100644 --- a/src/Credfeto.ChangeLog.Cmd/Credfeto.ChangeLog.Cmd.csproj +++ b/src/Credfeto.ChangeLog.Cmd/Credfeto.ChangeLog.Cmd.csproj @@ -1,7 +1,7 @@ Exe - net5.0 + netcoreapp3.1 win10-x64;win81-x64;osx.10.12-x64 true @@ -26,7 +26,6 @@ - diff --git a/src/Credfeto.ChangeLog.Cmd/Program.cs b/src/Credfeto.ChangeLog.Cmd/Program.cs index 54420af6..8614ef9b 100644 --- a/src/Credfeto.ChangeLog.Cmd/Program.cs +++ b/src/Credfeto.ChangeLog.Cmd/Program.cs @@ -36,7 +36,7 @@ private static async Task Main(string[] args) { string version = configuration.GetValue("version"); - string text = await ChangeLogReader.ExtractReleasNodesFromFileAsync(changeLogFileName: changeLog, version: version); + string text = await ChangeLogReader.ExtractReleaseNodesFromFileAsync(changeLogFileName: changeLog, version: version); await File.WriteAllTextAsync(path: extractFileName, contents: text, encoding: Encoding.UTF8); @@ -61,6 +61,26 @@ private static async Task Main(string[] args) return SUCCESS; } + string? branchName = configuration.GetValue("check-insert"); + + if (!string.IsNullOrWhiteSpace(branchName)) + { + bool valid = await ChangeLogChecker.ChangeLogModifiedInReleaseSectionAsync(changeLogFileName: changeLog, originBranchName: branchName); + + if (valid) + { + Console.WriteLine("Changelog is valid"); + + return SUCCESS; + } + + await Console.Error.WriteLineAsync("ERROR: Changelog modified in released section"); + + return ERROR; + } + + Console.WriteLine("ERROR: No known action specified"); + return ERROR; } catch (Exception exception) @@ -80,7 +100,8 @@ private static IConfigurationRoot LoadConfiguration(string[] args) {@"-version", @"version"}, {@"-extract", @"extract"}, {@"-add", @"add"}, - {@"-message", @"message"} + {@"-message", @"message"}, + {@"-check-insert", @"check-insert"} }) .Build(); } diff --git a/src/Credfeto.ChangeLog.Tests/Credfeto.ChangeLog.Tests.csproj b/src/Credfeto.ChangeLog.Tests/Credfeto.ChangeLog.Tests.csproj index 71ffe4df..1f855dc9 100644 --- a/src/Credfeto.ChangeLog.Tests/Credfeto.ChangeLog.Tests.csproj +++ b/src/Credfeto.ChangeLog.Tests/Credfeto.ChangeLog.Tests.csproj @@ -1,7 +1,7 @@ Library - net5.0 + netcoreapp3.1 true True diff --git a/src/Credfeto.ChangeLog/BranchMissingException.cs b/src/Credfeto.ChangeLog/BranchMissingException.cs new file mode 100644 index 00000000..4c9ae05d --- /dev/null +++ b/src/Credfeto.ChangeLog/BranchMissingException.cs @@ -0,0 +1,22 @@ +using System; + +namespace Credfeto.ChangeLog.Management +{ + public sealed class BranchMissingException : Exception + { + public BranchMissingException() + : this("Could not find branch") + { + } + + public BranchMissingException(string message) + : base(message) + { + } + + public BranchMissingException(string message, Exception innerException) + : base(message: message, innerException: innerException) + { + } + } +} \ No newline at end of file diff --git a/src/Credfeto.ChangeLog/ChangeLogChecker.cs b/src/Credfeto.ChangeLog/ChangeLogChecker.cs new file mode 100644 index 00000000..8f010a44 --- /dev/null +++ b/src/Credfeto.ChangeLog/ChangeLogChecker.cs @@ -0,0 +1,107 @@ +using System; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using LibGit2Sharp; + +namespace Credfeto.ChangeLog.Management +{ + public static class ChangeLogChecker + { + private static readonly Regex HunkPositionRegex = + new Regex(pattern: @"^@@\s*\-(?\d*)(,(?\d*))?\s*\+(?\d*)(,(?\d*))?\s*@@", + RegexOptions.Compiled | RegexOptions.Multiline); + + public static async Task ChangeLogModifiedInReleaseSectionAsync(string changeLogFileName, string originBranchName) + { + int? position = await ChangeLogReader.FindFirstReleaseVersionPositionAsync(changeLogFileName); + + if (position == null) + { + return false; + } + + string changelogDir = Path.GetDirectoryName(changeLogFileName)!; + + using (Repository repo = OpenRepository(changelogDir)) + { + string? sha = repo.Head.Tip.Sha; + + Branch? originBranch = repo.Branches.FirstOrDefault(b => b.FriendlyName == originBranchName); + + if (originBranch == null) + { + throw new BranchMissingException($"Could not find branch {originBranchName}"); + } + + if (originBranch.Tip.Sha == sha) + { + // same branch/commit + return false; + } + + string changeLogInRepoPath = FindChangeLogPositionInRepo(repo: repo, changeLogFileName: changeLogFileName); + + int firstReleaseVersionIndex = position.Value; + + Patch changes = repo.Diff.Compare(oldTree: originBranch.Tip.Tree, + newTree: repo.Head.Tip.Tree, + new CompareOptions {ContextLines = 0, InterhunkLines = 0, IncludeUnmodified = false}); + + foreach (var change in changes) + { + if (change.Path == changeLogInRepoPath) + { + string patchDetails = change.Patch; + Console.WriteLine(patchDetails); + + MatchCollection matches = HunkPositionRegex.Matches(patchDetails); + + foreach (Match? match in matches) + { + if (match == null) + { + continue; + } + + int changeStart = Convert.ToInt32(match.Groups["CurrentFileStart"] + .Value); + + if (!int.TryParse(s: match.Groups["CurrentFileChangeLength"] + .Value, + out int changeLength)) + { + changeLength = 1; + } + + int changeEnd = changeStart + changeLength; + + if (changeEnd >= firstReleaseVersionIndex) + { + return false; + } + } + + return true; + } + } + } + + return true; + } + + private static string FindChangeLogPositionInRepo(Repository repo, string changeLogFileName) + { + return changeLogFileName.Substring(repo.Info.WorkingDirectory.Length) + .Replace(oldValue: "\\", newValue: "/"); + } + + private static Repository OpenRepository(string workDir) + { + string found = Repository.Discover(workDir); + + return new Repository(found); + } + } +} \ No newline at end of file diff --git a/src/Credfeto.ChangeLog/ChangeLogReader.cs b/src/Credfeto.ChangeLog/ChangeLogReader.cs index 3b104ca9..edaaceb9 100644 --- a/src/Credfeto.ChangeLog/ChangeLogReader.cs +++ b/src/Credfeto.ChangeLog/ChangeLogReader.cs @@ -7,11 +7,12 @@ namespace Credfeto.ChangeLog.Management { - public sealed class ChangeLogReader + public static class ChangeLogReader { private static readonly Regex RemoveComments = new Regex(pattern: "", RegexOptions.Compiled | RegexOptions.Multiline); + private static readonly Regex VersionHeaderMatch = new Regex(pattern: @"^##\s\[(\d+)", options: RegexOptions.Compiled); - public static async Task ExtractReleasNodesFromFileAsync(string changeLogFileName, string version) + public static async Task ExtractReleaseNodesFromFileAsync(string changeLogFileName, string version) { string textBlock = await File.ReadAllTextAsync(path: changeLogFileName, encoding: Encoding.UTF8); @@ -28,8 +29,6 @@ public static string ExtractReleaseNotes(string changeLog, string version) FindSectionForBuild(text: text, version: releaseVersion, out int foundStart, out int foundEnd); - StringBuilder releaseNotes = new StringBuilder(); - if (foundStart == -1) { return string.Empty; @@ -40,7 +39,9 @@ public static string ExtractReleaseNotes(string changeLog, string version) foundEnd = text.Length; } - string previousLine = ""; + string previousLine = string.Empty; + + StringBuilder releaseNotes = new StringBuilder(); for (int i = foundStart; i < foundEnd; i++) { @@ -78,6 +79,23 @@ public static string ExtractReleaseNotes(string changeLog, string version) .Trim(); } + public static async Task FindFirstReleaseVersionPositionAsync(string changeLogFileName) + { + IReadOnlyList changelog = await File.ReadAllLinesAsync(path: changeLogFileName, encoding: Encoding.UTF8); + + for (int lineIndex = 0; lineIndex < changelog.Count; ++lineIndex) + { + string line = changelog[lineIndex]; + + if (VersionHeaderMatch.IsMatch(line)) + { + return lineIndex; + } + } + + return null; + } + private static void FindSectionForBuild(string[] text, Version? version, out int foundStart, out int foundEnd) { foundStart = -1; diff --git a/src/Credfeto.ChangeLog/ChangeLogUpdater.cs b/src/Credfeto.ChangeLog/ChangeLogUpdater.cs index e0242396..4d329d8c 100644 --- a/src/Credfeto.ChangeLog/ChangeLogUpdater.cs +++ b/src/Credfeto.ChangeLog/ChangeLogUpdater.cs @@ -7,7 +7,7 @@ namespace Credfeto.ChangeLog.Management { - public sealed class ChangeLogUpdater + public static class ChangeLogUpdater { public static async Task AddEntryAsync(string changeLogFileName, string type, string message) { diff --git a/src/Credfeto.ChangeLog/Credfeto.ChangeLog.csproj b/src/Credfeto.ChangeLog/Credfeto.ChangeLog.csproj index c33c340d..76a46d29 100644 --- a/src/Credfeto.ChangeLog/Credfeto.ChangeLog.csproj +++ b/src/Credfeto.ChangeLog/Credfeto.ChangeLog.csproj @@ -2,7 +2,7 @@ Library - net5.0 + netcoreapp3.1 true True @@ -25,6 +25,10 @@ en-GB + + + +