diff --git a/internal/vociferate/vociferate.go b/internal/vociferate/vociferate.go index 936eb92..8c6a69c 100644 --- a/internal/vociferate/vociferate.go +++ b/internal/vociferate/vociferate.go @@ -25,6 +25,7 @@ var releasedSectionRe = regexp.MustCompile(`(?m)^## \[(\d+\.\d+\.\d+)\] - `) var linkedReleasedSectionRe = regexp.MustCompile(`(?m)^## \[(\d+\.\d+\.\d+)\](?:\([^\n)]*\))? - `) var unreleasedHeadingRe = regexp.MustCompile(`(?m)^## \[Unreleased\](?:\([^\n)]*\))?\n`) var releaseHeadingRe = regexp.MustCompile(`(?m)^## \[(\d+\.\d+\.\d+)\](?:\([^\n)]*\))? - `) +var refLinkLineRe = regexp.MustCompile(`^\[[^\]]+\]: \S`) type Options struct { // VersionFile is the path to the file that stores the current version, @@ -412,19 +413,38 @@ func addChangelogLinks(text, repoURL string) string { return text } - mainURL := repoURL + "/src/branch/main" - text = unreleasedHeadingRe.ReplaceAllString(text, fmt.Sprintf("## [Unreleased](%s)\n", mainURL)) - + // Normalize headings to plain format, stripping any existing inline links. + text = unreleasedHeadingRe.ReplaceAllString(text, "## [Unreleased]\n") text = releaseHeadingRe.ReplaceAllStringFunc(text, func(match string) string { parts := releaseHeadingRe.FindStringSubmatch(match) if len(parts) < 2 { return match } version := parts[1] - return fmt.Sprintf("## [%s](%s/releases/tag/v%s) - ", version, repoURL, version) + return fmt.Sprintf("## [%s] - ", version) }) - return text + // Strip any trailing reference link block (blank lines followed by ref link lines). + lines := strings.Split(strings.TrimRight(text, "\n"), "\n") + cutAt := len(lines) + for i := len(lines) - 1; i >= 0; i-- { + if strings.TrimSpace(lines[i]) == "" || refLinkLineRe.MatchString(lines[i]) { + cutAt = i + } else { + break + } + } + text = strings.Join(lines[:cutAt], "\n") + "\n" + + // Build and append reference link definitions. + linkDefs := []string{fmt.Sprintf("[Unreleased]: %s/src/branch/main", repoURL)} + for _, match := range releasedSectionRe.FindAllStringSubmatch(text, -1) { + if len(match) >= 2 { + linkDefs = append(linkDefs, fmt.Sprintf("[%s]: %s/releases/tag/v%s", match[1], repoURL, match[1])) + } + } + + return strings.TrimRight(text, "\n") + "\n\n" + strings.Join(linkDefs, "\n") + "\n" } func parseSemver(version string) (semver, error) { diff --git a/internal/vociferate/vociferate_test.go b/internal/vociferate/vociferate_test.go index 2ed1016..0324f9f 100644 --- a/internal/vociferate/vociferate_test.go +++ b/internal/vociferate/vociferate_test.go @@ -54,7 +54,7 @@ func (s *PrepareSuite) TestPrepare_UpdatesVersionAndPromotesUnreleasedNotes() { changelogBytes, err := os.ReadFile(filepath.Join(s.rootDir, "changelog.md")) require.NoError(s.T(), err) - require.Equal(s.T(), "# Changelog\n\n## [Unreleased](https://git.hrafn.xyz/aether/vociferate/src/branch/main)\n\n## [1.1.7](https://git.hrafn.xyz/aether/vociferate/releases/tag/v1.1.7) - 2026-03-20\n\n### Breaking\n\n### Added\n\n- New thing.\n\n### Fixed\n\n- Old thing.\n\n## [1.1.6](https://git.hrafn.xyz/aether/vociferate/releases/tag/v1.1.6) - 2017-12-20\n\n### Fixed\n\n- Historical note.\n", string(changelogBytes)) + require.Equal(s.T(), "# Changelog\n\n## [Unreleased]\n\n## [1.1.7] - 2026-03-20\n\n### Breaking\n\n### Added\n\n- New thing.\n\n### Fixed\n\n- Old thing.\n\n## [1.1.6] - 2017-12-20\n\n### Fixed\n\n- Historical note.\n\n[Unreleased]: https://git.hrafn.xyz/aether/vociferate/src/branch/main\n[1.1.7]: https://git.hrafn.xyz/aether/vociferate/releases/tag/v1.1.7\n[1.1.6]: https://git.hrafn.xyz/aether/vociferate/releases/tag/v1.1.6\n", string(changelogBytes)) } func (s *PrepareSuite) TestPrepare_ReturnsErrorWhenUnreleasedSectionMissing() { @@ -244,9 +244,12 @@ func (s *PrepareSuite) TestPrepare_UsesGitHrafnXYZEnvironmentForChangelogLinks() require.NoError(s.T(), readErr) changelog := string(changelogBytes) - require.Contains(s.T(), changelog, "## [Unreleased](https://git.hrafn.xyz/aether/vociferate/src/branch/main)") - require.Contains(s.T(), changelog, "## [1.1.7](https://git.hrafn.xyz/aether/vociferate/releases/tag/v1.1.7) - 2026-03-20") - require.Contains(s.T(), changelog, "## [1.1.6](https://git.hrafn.xyz/aether/vociferate/releases/tag/v1.1.6) - 2017-12-20") + require.Contains(s.T(), changelog, "## [Unreleased]\n") + require.Contains(s.T(), changelog, "## [1.1.7] - 2026-03-20") + require.Contains(s.T(), changelog, "## [1.1.6] - 2017-12-20") + require.Contains(s.T(), changelog, "[Unreleased]: https://git.hrafn.xyz/aether/vociferate/src/branch/main") + require.Contains(s.T(), changelog, "[1.1.7]: https://git.hrafn.xyz/aether/vociferate/releases/tag/v1.1.7") + require.Contains(s.T(), changelog, "[1.1.6]: https://git.hrafn.xyz/aether/vociferate/releases/tag/v1.1.6") } func (s *PrepareSuite) TestPrepare_UsesGitHubEnvironmentForChangelogLinks() { @@ -260,7 +263,10 @@ func (s *PrepareSuite) TestPrepare_UsesGitHubEnvironmentForChangelogLinks() { require.NoError(s.T(), readErr) changelog := string(changelogBytes) - require.Contains(s.T(), changelog, "## [Unreleased](https://github.com/aether/vociferate/src/branch/main)") - require.Contains(s.T(), changelog, "## [1.1.7](https://github.com/aether/vociferate/releases/tag/v1.1.7) - 2026-03-20") - require.Contains(s.T(), changelog, "## [1.1.6](https://github.com/aether/vociferate/releases/tag/v1.1.6) - 2017-12-20") + require.Contains(s.T(), changelog, "## [Unreleased]\n") + require.Contains(s.T(), changelog, "## [1.1.7] - 2026-03-20") + require.Contains(s.T(), changelog, "## [1.1.6] - 2017-12-20") + require.Contains(s.T(), changelog, "[Unreleased]: https://github.com/aether/vociferate/src/branch/main") + require.Contains(s.T(), changelog, "[1.1.7]: https://github.com/aether/vociferate/releases/tag/v1.1.7") + require.Contains(s.T(), changelog, "[1.1.6]: https://github.com/aether/vociferate/releases/tag/v1.1.6") }