feat: add canonical changelog section extractor
This commit is contained in:
parent
ae91dff603
commit
2b66ecb1ba
4 changed files with 146 additions and 0 deletions
24
bin/changelog-section
Executable file
24
bin/changelog-section
Executable file
|
|
@ -0,0 +1,24 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
# shellcheck source=lib/changelog.sh
|
||||
source "$ROOT/lib/changelog.sh"
|
||||
|
||||
changelog="${1:-}"
|
||||
ver="${2:-}"
|
||||
if [ -z "$changelog" ] || [ -z "$ver" ]; then
|
||||
echo "usage: changelog-section <file> <version>" >&2
|
||||
exit 2
|
||||
fi
|
||||
[ -f "$changelog" ] || {
|
||||
echo "changelog-section: no such file: $changelog" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
notes="$(changelog_section "$changelog" "$ver")"
|
||||
[ -n "$notes" ] || {
|
||||
echo "changelog-section: $changelog has no section for '$ver' — the release PR stamps the Unreleased section with version + date BEFORE the tag" >&2
|
||||
exit 1
|
||||
}
|
||||
printf '%s\n' "$notes"
|
||||
22
lib/changelog.sh
Normal file
22
lib/changelog.sh
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env bash
|
||||
# This is the convergence point for box/cast's executable release-notes.sh
|
||||
# and rig's sourced release-lib.sh. Both histories matter: the publisher and
|
||||
# guards must use exactly one definition of a changelog section.
|
||||
#
|
||||
# box/cast: https://github.com/heavy-duty/box/blob/a17903f07c83aa18c0f009565e1a5442da6d0827/.github/scripts/release-notes.sh
|
||||
# rig: https://github.com/heavy-duty/rig/blob/7f8a0e08852837475505f404985a1251a2c3a8a1/.github/scripts/release-lib.sh
|
||||
|
||||
# changelog_section <file> <version>
|
||||
#
|
||||
# Print the body of exactly one changelog section. The whole second field is
|
||||
# compared as a string so dots are not regex metacharacters and 0.7.0 can
|
||||
# never select 0.7.0-rc1. Leading blank padding is omitted; blank lines after
|
||||
# the body starts are content. Empty output represents either an absent or an
|
||||
# empty section, which callers deliberately treat as the same refusal.
|
||||
changelog_section() {
|
||||
awk -v ver="$2" '
|
||||
/^## / { if (found) exit; found = ($2 == ver); next }
|
||||
found && !body && /^[[:space:]]*$/ { next }
|
||||
found { body = 1; print }
|
||||
' "$1"
|
||||
}
|
||||
74
test/changelog.test.sh
Executable file
74
test/changelog.test.sh
Executable file
|
|
@ -0,0 +1,74 @@
|
|||
#!/usr/bin/env bash
|
||||
set -u
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
# shellcheck source=test/harness.sh
|
||||
source "$ROOT/test/harness.sh"
|
||||
# shellcheck source=lib/changelog.sh
|
||||
source "$ROOT/lib/changelog.sh"
|
||||
|
||||
TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
FIXTURE="$TMP/CHANGELOG.md"
|
||||
cat >"$FIXTURE" <<'EOF'
|
||||
# Changelog
|
||||
|
||||
Preamble prose belongs to no section.
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Not yet released and never part of a version.
|
||||
|
||||
## 0.7.0 — 2026-07-20
|
||||
|
||||
### Added
|
||||
|
||||
- The seven-oh entry.
|
||||
|
||||
## 0.7.0-rc1 — 2026-07-19
|
||||
|
||||
- The release-candidate entry.
|
||||
|
||||
## 0.6.0 — 2026-07-18
|
||||
|
||||
- The six-oh entry.
|
||||
|
||||
## 0.5.0 — 2026-07-15
|
||||
|
||||
## 0.4.0
|
||||
- A date-less version heading also parses.
|
||||
EOF
|
||||
|
||||
assert_section() {
|
||||
local version="$1" expected="$2" actual
|
||||
actual="$(changelog_section "$FIXTURE" "$version")"
|
||||
[ "$actual" = "$expected" ] || {
|
||||
printf 'wanted:\n%s\ngot:\n%s\n' "$expected" "$actual"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
check "extracts one section body exactly" 0 "" assert_section 0.7.0 $'### Added\n\n- The seven-oh entry.'
|
||||
check "adjacent older section does not bleed" 0 "" assert_section 0.6.0 '- The six-oh entry.'
|
||||
check "whole match selects the rc section" 0 "" assert_section 0.7.0-rc1 '- The release-candidate entry.'
|
||||
check "Unreleased parses as a date-less heading" 0 "" assert_section Unreleased '- Not yet released and never part of a version.'
|
||||
check "date-less version heading parses" 0 "" assert_section 0.4.0 '- A date-less version heading also parses.'
|
||||
check "empty stamped section returns empty output" 0 "" assert_section 0.5.0 ""
|
||||
check "missing section returns empty output" 0 "" assert_section 9.9.9 ""
|
||||
|
||||
WRAPPER="$ROOT/bin/changelog-section"
|
||||
check "wrapper publishes the requested body" 0 "The seven-oh entry" "$WRAPPER" "$FIXTURE" 0.7.0
|
||||
check "wrapper refuses an empty section" 1 "no section for '0.5.0'" "$WRAPPER" "$FIXTURE" 0.5.0
|
||||
check "wrapper refuses an absent section" 1 "no section for '9.9.9'" "$WRAPPER" "$FIXTURE" 9.9.9
|
||||
check "wrapper explains how the release PR fixes refusal" 1 "stamps the Unreleased section" "$WRAPPER" "$FIXTURE" 9.9.9
|
||||
check "wrapper requires both arguments" 2 "usage:" "$WRAPPER"
|
||||
check "wrapper refuses a missing file" 1 "no such file" "$WRAPPER" "$TMP/missing.md" 1.0.0
|
||||
|
||||
REALISTIC="$ROOT/test/fixtures/CHANGELOG.realistic.md"
|
||||
check "realistic changelog keeps its production heading shape" 0 "A mint records its provenance" \
|
||||
"$WRAPPER" "$REALISTIC" 0.9.0
|
||||
check "realistic adjacent release remains independently extractable" 0 "Merging the release PR" \
|
||||
"$WRAPPER" "$REALISTIC" 0.8.0
|
||||
|
||||
summary
|
||||
26
test/fixtures/CHANGELOG.realistic.md
vendored
Normal file
26
test/fixtures/CHANGELOG.realistic.md
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Changelog
|
||||
|
||||
History before 0.5.0 lives in git and in the drill records.
|
||||
|
||||
## Unreleased
|
||||
|
||||
## 0.9.0 — 2026-07-21
|
||||
|
||||
### Added
|
||||
|
||||
- A mint records its provenance and reports it back.
|
||||
- CI refuses a release PR with no drill record.
|
||||
|
||||
### Fixed
|
||||
|
||||
- A release section cannot be deleted or duplicated unnoticed.
|
||||
|
||||
## 0.8.0 — 2026-07-19
|
||||
|
||||
### Added
|
||||
|
||||
- Merging the release PR is the release.
|
||||
|
||||
### Fixed
|
||||
|
||||
- The release ceremony re-arms the changelog after publishing.
|
||||
Loading…
Reference in a new issue