From 8ef72006347eae7758bf3a0d6b455997154ee2bc Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sun, 7 Jun 2026 02:11:27 +0900 Subject: [PATCH 1/3] Update GitHub release notes template --- .codex/workflows/release-changelog.md | 36 ++++++++-- .github/workflows/release.yml | 1 - .../+release-notes-template.changed.md | 15 +++++ tests/CodeIndex.Tests/ChangelogToolTests.cs | 65 ++++++++++++++++--- tests/CodeIndex.Tests/ReleaseWorkflowTests.cs | 11 ++++ tools/CodeIndex.Changelog/Program.cs | 32 +++++++-- 6 files changed, 138 insertions(+), 22 deletions(-) create mode 100644 changelog.d/unreleased/+release-notes-template.changed.md diff --git a/.codex/workflows/release-changelog.md b/.codex/workflows/release-changelog.md index 4653f3b4f8..faf9aab1c6 100644 --- a/.codex/workflows/release-changelog.md +++ b/.codex/workflows/release-changelog.md @@ -51,17 +51,41 @@ received new fragments after the last `prepare` run. ## GitHub release notes -The release workflow publishes the curated `CHANGELOG.md` section for the tag, -not GitHub's generated commit summary. Before creating the release it runs: +The release workflow publishes a fixed GitHub release-note template instead of +GitHub's generated commit summary. Before creating the release it runs: ```bash dotnet run --project tools/CodeIndex.Changelog -- release-notes --version 1.17.0 ``` -The command extracts the matching English and 日本語 `### [1.17.0]` blocks from -`CHANGELOG.md` and fails if either section is missing or both are empty. This -means the release-preparation PR must land before the `v*` tag is pushed. The -workflow keeps GitHub-generated notes only as an explicit +The command validates that the matching English and 日本語 `### [1.17.0]` +blocks exist in `CHANGELOG.md`, fails if both are empty, reads the previous +release version from the `[1.17.0]` compare-link footer, and emits: + +````md +## What's Changed + +Full Changelog: https://github.com/Widthdom/CodeIndex/compare/v1.16.0...v1.17.0 + +## Install or update + +Homebrew: + +```bash +brew install widthdom/tap/codeindex +brew upgrade widthdom/tap/codeindex +``` + +NuGet: + +```bash +dotnet tool install -g cdidx +dotnet tool update -g cdidx +``` +```` + +This means the release-preparation PR must land before the `v*` tag is pushed. +The workflow keeps GitHub-generated notes only as an explicit `workflow_dispatch` fallback via `allow_generated_notes`. ## Compare-link footer diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f3dd0fcb51..f17129ec28 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -542,7 +542,6 @@ jobs: set -euo pipefail version="${TAG_NAME#v}" dotnet run --project tools/CodeIndex.Changelog -- release-notes --version "${version}" > release-notes.md - cat release-install-notes.md >> release-notes.md - name: Create GitHub release env: diff --git a/changelog.d/unreleased/+release-notes-template.changed.md b/changelog.d/unreleased/+release-notes-template.changed.md new file mode 100644 index 0000000000..53f4770523 --- /dev/null +++ b/changelog.d/unreleased/+release-notes-template.changed.md @@ -0,0 +1,15 @@ +--- +category: changed +affected: + - tools/CodeIndex.Changelog/Program.cs + - .github/workflows/release.yml + - .codex/workflows/release-changelog.md +--- + +## English + +- **GitHub release notes now use the install-focused template** — `release-notes` now emits the `What's Changed` compare link plus Homebrew and NuGet install/update commands, using the prepared changelog footer to resolve the previous release version. + +## 日本語 + +- **GitHub Release notes が install 重視のテンプレートを使うようになりました** — `release-notes` は `What's Changed` の compare link と Homebrew / NuGet の install/update command を出力し、前回リリースバージョンは prepare 済み changelog footer から解決します。 diff --git a/tests/CodeIndex.Tests/ChangelogToolTests.cs b/tests/CodeIndex.Tests/ChangelogToolTests.cs index afcd0da276..9853aed9a1 100644 --- a/tests/CodeIndex.Tests/ChangelogToolTests.cs +++ b/tests/CodeIndex.Tests/ChangelogToolTests.cs @@ -285,7 +285,7 @@ public void PrepareFailureBeforeFragmentDeletionRollsBackReleaseFiles() } [Fact] - public void RenderReleaseNotesExtractsMatchingEnglishAndJapaneseSections() + public void RenderReleaseNotesUsesGitHubTemplateWithCompareFooter() { using var scope = new TestRepositoryScope(); scope.WriteFile("CHANGELOG.md", SampleChangelog); @@ -301,16 +301,65 @@ public void RenderReleaseNotesExtractsMatchingEnglishAndJapaneseSections() var notes = tool.RenderReleaseNotes(new Version(1, 17, 0)); - Assert.StartsWith("## CodeIndex v1.17.0", notes, StringComparison.Ordinal); - Assert.Contains("### English", notes); - Assert.Contains("English release note", notes); - Assert.Contains("Existing English unreleased note", notes); - Assert.Contains("### 日本語", notes); - Assert.Contains("Japanese release note", notes); - Assert.Contains("Existing Japanese unreleased note", notes); + Assert.Equal(""" + ## What's Changed + + Full Changelog: https://github.com/Widthdom/CodeIndex/compare/v1.16.0...v1.17.0 + + ## Install or update + + Homebrew: + + ```bash + brew install widthdom/tap/codeindex + brew upgrade widthdom/tap/codeindex + ``` + + NuGet: + + ```bash + dotnet tool install -g cdidx + dotnet tool update -g cdidx + ``` + """.Replace("\r\n", "\n") + "\n", notes.Replace("\r\n", "\n")); + Assert.DoesNotContain("English release note", notes); + Assert.DoesNotContain("Japanese release note", notes); Assert.DoesNotContain("[Unreleased]:", notes); } + [Fact] + public void RenderReleaseNotesRequiresTargetCompareFooter() + { + using var scope = new TestRepositoryScope(); + scope.WriteFile("CHANGELOG.md", """ + # Changelog + + All notable changes to this project will be documented in this file. + + ## English + + ### [1.17.0] - 2026-05-01 + + #### Fixed + - English release note. + + ## 日本語 + + ### [1.17.0] - 2026-05-01 + + #### 修正 + - Japanese release note. + + [Unreleased]: https://github.com/Widthdom/CodeIndex/compare/v1.17.0...HEAD + [1.16.0]: https://github.com/Widthdom/CodeIndex/compare/v1.15.3...v1.16.0 + """); + + var tool = new ChangelogTool(scope.Root); + var ex = Assert.Throws(() => tool.RenderReleaseNotes(new Version(1, 17, 0))); + + Assert.Contains("CHANGELOG.md is missing compare-link footer for v1.17.0", ex.Message); + } + [Fact] public void CheckFragmentsRejectsCategoryMismatch() { diff --git a/tests/CodeIndex.Tests/ReleaseWorkflowTests.cs b/tests/CodeIndex.Tests/ReleaseWorkflowTests.cs index aebcd24a51..0a0808b8b4 100644 --- a/tests/CodeIndex.Tests/ReleaseWorkflowTests.cs +++ b/tests/CodeIndex.Tests/ReleaseWorkflowTests.cs @@ -117,6 +117,17 @@ public void ReleaseWorkflow_ValidatesNuGetVersionBeforePublishing() Assert.DoesNotContain("--skip-duplicate", workflow); } + [Fact] + public void ReleaseWorkflow_UsesChangelogToolForTemplatedReleaseNotes() + { + var workflow = File.ReadAllText(Path.Combine(GetRepositoryRoot(), ".github", "workflows", "release.yml")); + + Assert.Contains("dotnet run --project tools/CodeIndex.Changelog -- release-notes", workflow); + Assert.Contains("--notes-file release-notes.md", workflow); + Assert.Contains("--notes-file release-install-notes.md", workflow); + Assert.DoesNotContain("cat release-install-notes.md >> release-notes.md", workflow); + } + // Issue #2756: NuGet emits the core-properties OPC part with a random // *.psmdcp entry name, so two otherwise identical pack runs can produce // different .nupkg/.snupkg bytes. The release workflow normalizes that diff --git a/tools/CodeIndex.Changelog/Program.cs b/tools/CodeIndex.Changelog/Program.cs index db5d09d5b1..c71e82807a 100644 --- a/tools/CodeIndex.Changelog/Program.cs +++ b/tools/CodeIndex.Changelog/Program.cs @@ -298,18 +298,36 @@ public string RenderReleaseNotes(Version targetVersion) if (englishBlock.BodyLines.Count == 0 && japaneseBlock.BodyLines.Count == 0) throw new ChangelogException($"CHANGELOG.md release notes for v{targetVersion} are empty."); + var targetVersionText = targetVersion.ToString(); + var compareEntry = changelog.FooterEntries.FirstOrDefault(entry => + !entry.IsTagLink && + entry.Label == targetVersionText && + entry.TargetVersion == targetVersionText); + if (compareEntry is null) + throw new ChangelogException($"CHANGELOG.md is missing compare-link footer for v{targetVersion}."); + var output = new List { - $"## CodeIndex v{targetVersion}", + "## What's Changed", + string.Empty, + $"Full Changelog: https://github.com/Widthdom/CodeIndex/compare/v{compareEntry.BaseVersion}...v{targetVersion}", + string.Empty, + "## Install or update", + string.Empty, + "Homebrew:", + string.Empty, + "```bash", + "brew install widthdom/tap/codeindex", + "brew upgrade widthdom/tap/codeindex", + "```", string.Empty, - "### English", + "NuGet:", string.Empty, + "```bash", + "dotnet tool install -g cdidx", + "dotnet tool update -g cdidx", + "```", }; - output.AddRange(englishBlock.BodyLines); - output.Add(string.Empty); - output.Add("### 日本語"); - output.Add(string.Empty); - output.AddRange(japaneseBlock.BodyLines); return string.Join('\n', output).TrimEnd() + Environment.NewLine; } From e8f9bd4acf21f075bbebc1f05c73495904456b39 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sun, 7 Jun 2026 03:37:28 +0900 Subject: [PATCH 2/3] Increase CI test session timeout --- TESTING_GUIDE.md | 2 +- .../+ci-test-session-timeout.internal.md | 15 +++++++++++++++ tests/CodeIndex.Tests/CiWorkflowTests.cs | 2 +- tests/CodeIndex.Tests/CodeIndex.Tests.runsettings | 2 +- 4 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 changelog.d/unreleased/+ci-test-session-timeout.internal.md diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md index 520dd01c9d..fd31a63374 100644 --- a/TESTING_GUIDE.md +++ b/TESTING_GUIDE.md @@ -26,7 +26,7 @@ Use the full suite by default. Use targeted filters only while iterating locally - These test-only packages are separate from the production dependency rule in `src/CodeIndex`, which still allows only `Microsoft.Data.Sqlite` at runtime. - `FsCheck.Xunit` is reserved for property-based tests that assert universal invariants (never-throws contracts, idempotence, "output is parseable by downstream consumer") across randomly generated inputs. Use it to complement, not replace, the example-based `[Fact]` / `[Theory]` tests — pick FsCheck when the property is a universally quantified claim, and an example test when a specific concrete case is the contract. - Test parallelism: enabled by default across independent test classes. Tests that touch process-global state such as SQLite pool resets, environment variables, or current-directory overrides must use an explicit non-parallel collection, and tests that swap `Console.Out` / `Console.Error` must lock on `TestConsoleLock.Gate`. -- CI runs the test project through `tests/CodeIndex.Tests/CodeIndex.Tests.runsettings`, enables VSTest blame crash and hang collection, applies a 30-minute session timeout plus 60-second xUnit long-running diagnostics, and reruns the suite once after an initial failure. If the retry passes, CI uploads `TestResults/flaky-retry.txt` with the TRX and blame artifacts so the run is treated as suspect instead of silently trusted. +- CI runs the test project through `tests/CodeIndex.Tests/CodeIndex.Tests.runsettings`, enables VSTest blame crash and hang collection, applies a 45-minute session timeout plus 60-second xUnit long-running diagnostics, and reruns the suite once after an initial failure. If the retry passes, CI uploads `TestResults/flaky-retry.txt` with the TRX and blame artifacts so the run is treated as suspect instead of silently trusted. ## Test Layout diff --git a/changelog.d/unreleased/+ci-test-session-timeout.internal.md b/changelog.d/unreleased/+ci-test-session-timeout.internal.md new file mode 100644 index 0000000000..79a886a2be --- /dev/null +++ b/changelog.d/unreleased/+ci-test-session-timeout.internal.md @@ -0,0 +1,15 @@ +--- +category: internal +affected: + - tests/CodeIndex.Tests/CodeIndex.Tests.runsettings + - tests/CodeIndex.Tests/CiWorkflowTests.cs + - TESTING_GUIDE.md +--- + +## English + +- **CI test sessions now allow 45 minutes** — the shared VSTest runsettings timeout now has enough headroom for the Windows net9 full suite when runner variance pushes execution past 30 minutes. + +## 日本語 + +- **CI test session の上限を45分にしました** — Windows net9 の full suite が runner のばらつきで30分を超える場合に備え、共有 VSTest runsettings の timeout に余裕を持たせました。 diff --git a/tests/CodeIndex.Tests/CiWorkflowTests.cs b/tests/CodeIndex.Tests/CiWorkflowTests.cs index d3a97f1ed7..56065e72cb 100644 --- a/tests/CodeIndex.Tests/CiWorkflowTests.cs +++ b/tests/CodeIndex.Tests/CiWorkflowTests.cs @@ -28,7 +28,7 @@ public void Runsettings_DefinesSessionTimeoutAndXunitLongRunningDiagnostics() var document = XDocument.Load(path); Assert.Equal( - "1800000", + "2700000", document.Root?.Element("RunConfiguration")?.Element("TestSessionTimeout")?.Value); Assert.Equal( "60", diff --git a/tests/CodeIndex.Tests/CodeIndex.Tests.runsettings b/tests/CodeIndex.Tests/CodeIndex.Tests.runsettings index a5bf7b8d39..b840081622 100644 --- a/tests/CodeIndex.Tests/CodeIndex.Tests.runsettings +++ b/tests/CodeIndex.Tests/CodeIndex.Tests.runsettings @@ -2,7 +2,7 @@ ./TestResults - 1800000 + 2700000 60 From 9b1fb0495104da3991cd993c5331894c68df001d Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sun, 7 Jun 2026 04:59:45 +0900 Subject: [PATCH 3/3] Use GitHub latest release for release notes base --- .codex/workflows/release-changelog.md | 9 +++-- .github/workflows/release.yml | 22 +++++++++- .../+release-notes-template.changed.md | 4 +- tests/CodeIndex.Tests/ChangelogToolTests.cs | 11 ++--- tests/CodeIndex.Tests/ReleaseWorkflowTests.cs | 7 ++++ tools/CodeIndex.Changelog/Program.cs | 40 +++++++++++-------- 6 files changed, 65 insertions(+), 28 deletions(-) diff --git a/.codex/workflows/release-changelog.md b/.codex/workflows/release-changelog.md index faf9aab1c6..4de1a17928 100644 --- a/.codex/workflows/release-changelog.md +++ b/.codex/workflows/release-changelog.md @@ -52,15 +52,16 @@ received new fragments after the last `prepare` run. ## GitHub release notes The release workflow publishes a fixed GitHub release-note template instead of -GitHub's generated commit summary. Before creating the release it runs: +GitHub's generated commit summary. Before creating the release, it asks GitHub +Releases for the latest non-draft, non-prerelease tag, excludes the target tag +itself for idempotent reruns, and passes that existing release version to: ```bash -dotnet run --project tools/CodeIndex.Changelog -- release-notes --version 1.17.0 +dotnet run --project tools/CodeIndex.Changelog -- release-notes --version 1.17.0 --previous-version 1.16.0 ``` The command validates that the matching English and 日本語 `### [1.17.0]` -blocks exist in `CHANGELOG.md`, fails if both are empty, reads the previous -release version from the `[1.17.0]` compare-link footer, and emits: +blocks exist in `CHANGELOG.md`, fails if both are empty, and emits: ````md ## What's Changed diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f17129ec28..15c32e62e2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -537,11 +537,31 @@ jobs: - name: Write curated release notes if: ${{ !inputs.allow_generated_notes }} env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG_NAME: ${{ inputs.tag_name || github.ref_name }} run: | set -euo pipefail version="${TAG_NAME#v}" - dotnet run --project tools/CodeIndex.Changelog -- release-notes --version "${version}" > release-notes.md + previous_tag="$( + gh release list \ + --exclude-drafts \ + --exclude-pre-releases \ + --limit 20 \ + --json tagName \ + --jq ".[] | select(.tagName != \"${TAG_NAME}\") | .tagName" \ + | sed -n '1p' + )" + if [ -z "${previous_tag}" ]; then + echo "No previous non-draft, non-prerelease GitHub release was found before ${TAG_NAME}." >&2 + exit 1 + fi + if [[ ! "${previous_tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Latest GitHub release tag is not a v-prefixed SemVer version: ${previous_tag}" >&2 + exit 1 + fi + + previous_version="${previous_tag#v}" + dotnet run --project tools/CodeIndex.Changelog -- release-notes --version "${version}" --previous-version "${previous_version}" > release-notes.md - name: Create GitHub release env: diff --git a/changelog.d/unreleased/+release-notes-template.changed.md b/changelog.d/unreleased/+release-notes-template.changed.md index 53f4770523..321a4741f8 100644 --- a/changelog.d/unreleased/+release-notes-template.changed.md +++ b/changelog.d/unreleased/+release-notes-template.changed.md @@ -8,8 +8,8 @@ affected: ## English -- **GitHub release notes now use the install-focused template** — `release-notes` now emits the `What's Changed` compare link plus Homebrew and NuGet install/update commands, using the prepared changelog footer to resolve the previous release version. +- **GitHub release notes now use the install-focused template** — `release-notes` now emits the `What's Changed` compare link plus Homebrew and NuGet install/update commands, using the previous version supplied by the release workflow from the latest non-draft, non-prerelease GitHub Release. ## 日本語 -- **GitHub Release notes が install 重視のテンプレートを使うようになりました** — `release-notes` は `What's Changed` の compare link と Homebrew / NuGet の install/update command を出力し、前回リリースバージョンは prepare 済み changelog footer から解決します。 +- **GitHub Release notes が install 重視のテンプレートを使うようになりました** — `release-notes` は `What's Changed` の compare link と Homebrew / NuGet の install/update command を出力し、前回リリースバージョンは release workflow が最新の draft / prerelease ではない GitHub Release から渡します。 diff --git a/tests/CodeIndex.Tests/ChangelogToolTests.cs b/tests/CodeIndex.Tests/ChangelogToolTests.cs index 9853aed9a1..d2908d9695 100644 --- a/tests/CodeIndex.Tests/ChangelogToolTests.cs +++ b/tests/CodeIndex.Tests/ChangelogToolTests.cs @@ -285,7 +285,7 @@ public void PrepareFailureBeforeFragmentDeletionRollsBackReleaseFiles() } [Fact] - public void RenderReleaseNotesUsesGitHubTemplateWithCompareFooter() + public void RenderReleaseNotesUsesProvidedPreviousVersion() { using var scope = new TestRepositoryScope(); scope.WriteFile("CHANGELOG.md", SampleChangelog); @@ -299,7 +299,7 @@ public void RenderReleaseNotesUsesGitHubTemplateWithCompareFooter() var tool = new ChangelogTool(scope.Root); tool.Prepare(new Version(1, 17, 0), new DateOnly(2026, 5, 1), writeChanges: true); - var notes = tool.RenderReleaseNotes(new Version(1, 17, 0)); + var notes = tool.RenderReleaseNotes(new Version(1, 17, 0), new Version(1, 16, 0)); Assert.Equal(""" ## What's Changed @@ -328,7 +328,7 @@ dotnet tool update -g cdidx } [Fact] - public void RenderReleaseNotesRequiresTargetCompareFooter() + public void RenderReleaseNotesDoesNotRequireTargetCompareFooter() { using var scope = new TestRepositoryScope(); scope.WriteFile("CHANGELOG.md", """ @@ -355,9 +355,10 @@ All notable changes to this project will be documented in this file. """); var tool = new ChangelogTool(scope.Root); - var ex = Assert.Throws(() => tool.RenderReleaseNotes(new Version(1, 17, 0))); + var notes = tool.RenderReleaseNotes(new Version(1, 17, 0), new Version(1, 16, 0)); - Assert.Contains("CHANGELOG.md is missing compare-link footer for v1.17.0", ex.Message); + Assert.Contains("Full Changelog: https://github.com/Widthdom/CodeIndex/compare/v1.16.0...v1.17.0", notes); + Assert.DoesNotContain("CHANGELOG.md is missing compare-link footer", notes); } [Fact] diff --git a/tests/CodeIndex.Tests/ReleaseWorkflowTests.cs b/tests/CodeIndex.Tests/ReleaseWorkflowTests.cs index 0a0808b8b4..cb379223cc 100644 --- a/tests/CodeIndex.Tests/ReleaseWorkflowTests.cs +++ b/tests/CodeIndex.Tests/ReleaseWorkflowTests.cs @@ -122,7 +122,14 @@ public void ReleaseWorkflow_UsesChangelogToolForTemplatedReleaseNotes() { var workflow = File.ReadAllText(Path.Combine(GetRepositoryRoot(), ".github", "workflows", "release.yml")); + Assert.Contains("gh release list", workflow); + Assert.Contains("--exclude-drafts", workflow); + Assert.Contains("--exclude-pre-releases", workflow); + Assert.Contains("select(.tagName != \\\"${TAG_NAME}\\\")", workflow); + Assert.Contains("No previous non-draft, non-prerelease GitHub release was found", workflow); + Assert.Contains("Latest GitHub release tag is not a v-prefixed SemVer version", workflow); Assert.Contains("dotnet run --project tools/CodeIndex.Changelog -- release-notes", workflow); + Assert.Contains("--previous-version \"${previous_version}\"", workflow); Assert.Contains("--notes-file release-notes.md", workflow); Assert.Contains("--notes-file release-install-notes.md", workflow); Assert.DoesNotContain("cat release-install-notes.md >> release-notes.md", workflow); diff --git a/tools/CodeIndex.Changelog/Program.cs b/tools/CodeIndex.Changelog/Program.cs index c71e82807a..a5036c2371 100644 --- a/tools/CodeIndex.Changelog/Program.cs +++ b/tools/CodeIndex.Changelog/Program.cs @@ -45,8 +45,8 @@ public static int Main(string[] args) } case "release-notes": { - var options = ParseOptions(args[1..], requireDate: false); - Console.Out.Write(tool.RenderReleaseNotes(options.Version)); + var options = ParseOptions(args[1..], requireDate: false, requirePreviousVersion: true); + Console.Out.Write(tool.RenderReleaseNotes(options.Version, options.PreviousVersion!)); return 0; } default: @@ -68,7 +68,7 @@ private static void PrintUsage() Console.Out.WriteLine(" dotnet run --project tools/CodeIndex.Changelog -- check"); Console.Out.WriteLine(" dotnet run --project tools/CodeIndex.Changelog -- prepare --version X.Y.Z --date YYYY-MM-DD"); Console.Out.WriteLine(" dotnet run --project tools/CodeIndex.Changelog -- render --version X.Y.Z --date YYYY-MM-DD"); - Console.Out.WriteLine(" dotnet run --project tools/CodeIndex.Changelog -- release-notes --version X.Y.Z"); + Console.Out.WriteLine(" dotnet run --project tools/CodeIndex.Changelog -- release-notes --version Y.Y.Y --previous-version X.X.X"); Console.Out.WriteLine(); Console.Out.WriteLine("Limits:"); Console.Out.WriteLine($" unreleased fragments: {ChangelogTool.MaxFragmentCount}"); @@ -77,9 +77,10 @@ private static void PrintUsage() Console.Out.WriteLine($" version.json size: {ChangelogTool.MaxVersionJsonBytes} bytes"); } - private static ParsedOptions ParseOptions(string[] args, bool requireDate) + private static ParsedOptions ParseOptions(string[] args, bool requireDate, bool requirePreviousVersion = false) { Version? version = null; + Version? previousVersion = null; DateOnly? releaseDate = null; for (var i = 0; i < args.Length; i++) @@ -103,6 +104,15 @@ private static ParsedOptions ParseOptions(string[] args, bool requireDate) continue; } + if (arg == "--previous-version") + { + if (i + 1 >= args.Length) + throw new ChangelogException("Missing value for --previous-version."); + + previousVersion = Version.Parse(args[++i]); + continue; + } + throw new ChangelogException($"Unknown option '{arg}'."); } @@ -112,7 +122,10 @@ private static ParsedOptions ParseOptions(string[] args, bool requireDate) if (requireDate && releaseDate is null) throw new ChangelogException("Missing required option --date."); - return new ParsedOptions(version, releaseDate ?? default); + if (requirePreviousVersion && previousVersion is null) + throw new ChangelogException("Missing required option --previous-version."); + + return new ParsedOptions(version, releaseDate ?? default, previousVersion); } private static string FindRepositoryRoot() @@ -147,7 +160,7 @@ private static string FindRepositoryRoot() return null; } - private sealed record ParsedOptions(Version Version, DateOnly ReleaseDate); + private sealed record ParsedOptions(Version Version, DateOnly ReleaseDate, Version? PreviousVersion); } public sealed class ChangelogTool @@ -283,8 +296,11 @@ public PrepareResult Prepare(Version targetVersion, DateOnly releaseDate, bool w return new PrepareResult(summary.ToString().TrimEnd(), writeChanges ? null : updatedChangelog); } - public string RenderReleaseNotes(Version targetVersion) + public string RenderReleaseNotes(Version targetVersion, Version previousVersion) { + if (previousVersion.CompareTo(targetVersion) >= 0) + throw new ChangelogException($"Previous version v{previousVersion} must be older than target version v{targetVersion}."); + var changelogPath = Path.Combine(_repositoryRoot, "CHANGELOG.md"); var changelogText = ReadAllTextBounded(changelogPath, _repositoryRoot, MaxChangelogBytes).Replace("\r\n", "\n", StringComparison.Ordinal); var changelog = ParsedChangelog.Parse(changelogText); @@ -298,19 +314,11 @@ public string RenderReleaseNotes(Version targetVersion) if (englishBlock.BodyLines.Count == 0 && japaneseBlock.BodyLines.Count == 0) throw new ChangelogException($"CHANGELOG.md release notes for v{targetVersion} are empty."); - var targetVersionText = targetVersion.ToString(); - var compareEntry = changelog.FooterEntries.FirstOrDefault(entry => - !entry.IsTagLink && - entry.Label == targetVersionText && - entry.TargetVersion == targetVersionText); - if (compareEntry is null) - throw new ChangelogException($"CHANGELOG.md is missing compare-link footer for v{targetVersion}."); - var output = new List { "## What's Changed", string.Empty, - $"Full Changelog: https://github.com/Widthdom/CodeIndex/compare/v{compareEntry.BaseVersion}...v{targetVersion}", + $"Full Changelog: https://github.com/Widthdom/CodeIndex/compare/v{previousVersion}...v{targetVersion}", string.Empty, "## Install or update", string.Empty,