Compare commits
6 Commits
c5ecfeebde
...
8d9e15ca44
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d9e15ca44 | ||
|
|
f96458344a | ||
|
|
cdfe75f360 | ||
|
|
eb7d2798f1 | ||
|
|
29ca9e81ad | ||
|
|
c4f643c39b |
@@ -186,12 +186,20 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
recommended_tag="$(${RUN_COMMAND} --recommend --root .)"
|
${RUN_COMMAND} --help >/dev/null
|
||||||
case "$recommended_tag" in
|
|
||||||
v*.*.*)
|
recommend_stderr="$(mktemp)"
|
||||||
|
if ${RUN_COMMAND} --recommend --root . >/dev/null 2>"${recommend_stderr}"; then
|
||||||
|
echo "Expected --recommend to fail on the tagged release checkout" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
recommend_error="$(cat "${recommend_stderr}")"
|
||||||
|
case "${recommend_error}" in
|
||||||
|
*"unreleased section is empty"*)
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Unexpected recommended tag: $recommended_tag" >&2
|
echo "Unexpected recommend failure output: ${recommend_error}" >&2
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -201,5 +209,6 @@ jobs:
|
|||||||
echo
|
echo
|
||||||
echo "- Release tag: ${TAG_NAME}"
|
echo "- Release tag: ${TAG_NAME}"
|
||||||
echo "- Asset: ${asset_name}"
|
echo "- Asset: ${asset_name}"
|
||||||
echo "- Recommended tag: ${recommended_tag}"
|
echo "- Binary executed successfully via --help."
|
||||||
|
echo "- --recommend failed as expected on the tagged checkout because Unreleased is empty."
|
||||||
} >> "$GITHUB_STEP_SUMMARY"
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|||||||
14
changelog.md
14
changelog.md
@@ -9,6 +9,20 @@ A `### Breaking` section is used in addition to Keep a Changelog's standard sect
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Breaking
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Prepare now recreates the standard `Unreleased` section headers after promoting notes into a tagged release entry.
|
||||||
|
- First-release recommendation remains `v1.0.0` when no prior releases exist in the changelog.
|
||||||
|
- Do Release smoke validation now expects `--recommend` to fail on tagged release checkouts where `Unreleased` is intentionally empty.
|
||||||
|
|
||||||
## [0.1.0] - 2026-03-20
|
## [0.1.0] - 2026-03-20
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const (
|
|||||||
defaultVersionFile = "release-version"
|
defaultVersionFile = "release-version"
|
||||||
defaultVersionExpr = `^\s*([^\r\n]+)\s*$`
|
defaultVersionExpr = `^\s*([^\r\n]+)\s*$`
|
||||||
defaultChangelog = "changelog.md"
|
defaultChangelog = "changelog.md"
|
||||||
|
defaultUnreleasedTemplate = "### Breaking\n\n### Added\n\n### Changed\n\n### Removed\n\n### Fixed\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
var releasedSectionRe = regexp.MustCompile(`(?m)^## \[(\d+\.\d+\.\d+)\] - `)
|
var releasedSectionRe = regexp.MustCompile(`(?m)^## \[(\d+\.\d+\.\d+)\] - `)
|
||||||
@@ -95,8 +96,8 @@ func Prepare(rootDir, version, releaseDate string, options Options) error {
|
|||||||
// - minor: Unreleased contains Added entries
|
// - minor: Unreleased contains Added entries
|
||||||
// - patch: all other cases
|
// - patch: all other cases
|
||||||
//
|
//
|
||||||
// When no previous release is present in the changelog, the base version is
|
// When no previous release is present in the changelog, the first
|
||||||
// treated as 0.0.0.
|
// recommendation is always v1.0.0.
|
||||||
func RecommendedTag(rootDir string, options Options) (string, error) {
|
func RecommendedTag(rootDir string, options Options) (string, error) {
|
||||||
resolved, err := resolveOptions(options)
|
resolved, err := resolveOptions(options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -104,6 +105,7 @@ func RecommendedTag(rootDir string, options Options) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var currentVersion string
|
var currentVersion string
|
||||||
|
isFirstRelease := false
|
||||||
if options.VersionFile != "" {
|
if options.VersionFile != "" {
|
||||||
currentVersion, err = readCurrentVersion(rootDir, resolved)
|
currentVersion, err = readCurrentVersion(rootDir, resolved)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -116,6 +118,7 @@ func RecommendedTag(rootDir string, options Options) (string, error) {
|
|||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
currentVersion = "0.0.0"
|
currentVersion = "0.0.0"
|
||||||
|
isFirstRelease = true
|
||||||
} else {
|
} else {
|
||||||
currentVersion = version
|
currentVersion = version
|
||||||
}
|
}
|
||||||
@@ -131,8 +134,12 @@ func RecommendedTag(rootDir string, options Options) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isFirstRelease {
|
||||||
|
return "v1.0.0", nil
|
||||||
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case strings.Contains(unreleasedBody, "### Breaking"), sectionHasEntries(unreleasedBody, "Removed"):
|
case sectionHasEntries(unreleasedBody, "Breaking"), sectionHasEntries(unreleasedBody, "Removed"):
|
||||||
parsed.major++
|
parsed.major++
|
||||||
parsed.minor = 0
|
parsed.minor = 0
|
||||||
parsed.patch = 0
|
parsed.patch = 0
|
||||||
@@ -226,7 +233,7 @@ func updateChangelog(rootDir, version, releaseDate, changelogPath string) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.TrimSpace(unreleasedBody) == "" {
|
if !unreleasedHasEntries(unreleasedBody) {
|
||||||
return fmt.Errorf("unreleased section is empty")
|
return fmt.Errorf("unreleased section is empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,7 +243,7 @@ func updateChangelog(rootDir, version, releaseDate, changelogPath string) error
|
|||||||
newSection += "\n"
|
newSection += "\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
updated := text[:afterHeader] + "\n" + newSection + text[nextSectionStart:]
|
updated := text[:afterHeader] + "\n" + defaultUnreleasedTemplate + "\n" + newSection + text[nextSectionStart:]
|
||||||
repoURL, ok := deriveRepositoryURL(rootDir)
|
repoURL, ok := deriveRepositoryURL(rootDir)
|
||||||
if ok {
|
if ok {
|
||||||
updated = addChangelogLinks(updated, repoURL)
|
updated = addChangelogLinks(updated, repoURL)
|
||||||
@@ -269,13 +276,25 @@ func readUnreleasedBody(rootDir, changelogPath string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.TrimSpace(unreleasedBody) == "" {
|
if !unreleasedHasEntries(unreleasedBody) {
|
||||||
return "", fmt.Errorf("unreleased section is empty")
|
return "", fmt.Errorf("unreleased section is empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
return unreleasedBody, nil
|
return unreleasedBody, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func unreleasedHasEntries(unreleasedBody string) bool {
|
||||||
|
for _, line := range strings.Split(unreleasedBody, "\n") {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if trimmed == "" || strings.HasPrefix(trimmed, "### ") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func readChangelogState(rootDir, changelogPath string) (string, string, int, int, string, error) {
|
func readChangelogState(rootDir, changelogPath string) (string, string, int, int, string, error) {
|
||||||
path := filepath.Join(rootDir, changelogPath)
|
path := filepath.Join(rootDir, changelogPath)
|
||||||
contents, err := os.ReadFile(path)
|
contents, err := os.ReadFile(path)
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ func (s *PrepareSuite) TestPrepare_UpdatesVersionAndPromotesUnreleasedNotes() {
|
|||||||
|
|
||||||
changelogBytes, err := os.ReadFile(filepath.Join(s.rootDir, "changelog.md"))
|
changelogBytes, err := os.ReadFile(filepath.Join(s.rootDir, "changelog.md"))
|
||||||
require.NoError(s.T(), err)
|
require.NoError(s.T(), err)
|
||||||
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))
|
require.Equal(s.T(), "# Changelog\n\n## [Unreleased]\n\n### Breaking\n\n### Added\n\n### Changed\n\n### Removed\n\n### Fixed\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() {
|
func (s *PrepareSuite) TestPrepare_ReturnsErrorWhenUnreleasedSectionMissing() {
|
||||||
@@ -81,11 +81,23 @@ func (s *PrepareSuite) TestPrepare_ReturnsErrorWhenUnreleasedSectionIsEmpty() {
|
|||||||
require.ErrorContains(s.T(), err, "unreleased section is empty")
|
require.ErrorContains(s.T(), err, "unreleased section is empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PrepareSuite) TestRecommendedTag_UsesMajorBumpWhenBreakingHeadingExists() {
|
func (s *PrepareSuite) TestRecommendedTag_UsesMinorBumpWhenBreakingHeadingIsEmpty() {
|
||||||
tag, err := vociferate.RecommendedTag(s.rootDir, vociferate.Options{})
|
tag, err := vociferate.RecommendedTag(s.rootDir, vociferate.Options{})
|
||||||
|
|
||||||
require.NoError(s.T(), err)
|
require.NoError(s.T(), err)
|
||||||
require.Equal(s.T(), "v2.0.0", tag)
|
require.Equal(s.T(), "v1.2.0", tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PrepareSuite) TestRecommendedTag_ReturnsErrorWhenUnreleasedHasOnlyTemplateHeadings() {
|
||||||
|
require.NoError(s.T(), os.WriteFile(
|
||||||
|
filepath.Join(s.rootDir, "changelog.md"),
|
||||||
|
[]byte("# Changelog\n\n## [Unreleased]\n\n### Breaking\n\n### Added\n\n### Changed\n\n### Removed\n\n### Fixed\n\n## [1.1.6] - 2017-12-20\n"),
|
||||||
|
0o644,
|
||||||
|
))
|
||||||
|
|
||||||
|
_, err := vociferate.RecommendedTag(s.rootDir, vociferate.Options{})
|
||||||
|
|
||||||
|
require.ErrorContains(s.T(), err, "unreleased section is empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PrepareSuite) TestRecommendedTag_UsesPatchBumpForFixOnlyChanges() {
|
func (s *PrepareSuite) TestRecommendedTag_UsesPatchBumpForFixOnlyChanges() {
|
||||||
@@ -245,6 +257,8 @@ func (s *PrepareSuite) TestPrepare_UsesGitHrafnXYZEnvironmentForChangelogLinks()
|
|||||||
changelog := string(changelogBytes)
|
changelog := string(changelogBytes)
|
||||||
|
|
||||||
require.Contains(s.T(), changelog, "## [Unreleased]\n")
|
require.Contains(s.T(), changelog, "## [Unreleased]\n")
|
||||||
|
require.Contains(s.T(), changelog, "### Changed\n")
|
||||||
|
require.Contains(s.T(), changelog, "### Removed\n")
|
||||||
require.Contains(s.T(), changelog, "## [1.1.7] - 2026-03-20")
|
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, "## [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, "[Unreleased]: https://git.hrafn.xyz/aether/vociferate/src/branch/main")
|
||||||
@@ -264,6 +278,8 @@ func (s *PrepareSuite) TestPrepare_UsesGitHubEnvironmentForChangelogLinks() {
|
|||||||
changelog := string(changelogBytes)
|
changelog := string(changelogBytes)
|
||||||
|
|
||||||
require.Contains(s.T(), changelog, "## [Unreleased]\n")
|
require.Contains(s.T(), changelog, "## [Unreleased]\n")
|
||||||
|
require.Contains(s.T(), changelog, "### Changed\n")
|
||||||
|
require.Contains(s.T(), changelog, "### Removed\n")
|
||||||
require.Contains(s.T(), changelog, "## [1.1.7] - 2026-03-20")
|
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, "## [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, "[Unreleased]: https://github.com/aether/vociferate/src/branch/main")
|
||||||
|
|||||||
Reference in New Issue
Block a user