feat: bound changelog fragment entries
This commit is contained in:
parent
9008e03b03
commit
adcba3d373
5 changed files with 98 additions and 2 deletions
|
|
@ -141,7 +141,10 @@ triage bug, and the move is to say so on the issue, not to guess.
|
||||||
cross-repo) — the exact prose that will be published, nothing else: `- `
|
cross-repo) — the exact prose that will be published, nothing else: `- `
|
||||||
bullets, and in a grouped repo the `### Added` / `### Changed` /
|
bullets, and in a grouped repo the `### Added` / `### Changed` /
|
||||||
`### Fixed` headings inside the fragment, creating a rarer kind only when
|
`### Fixed` headings inside the fragment, creating a rarer kind only when
|
||||||
a change genuinely is one. Never edit `CHANGELOG.md` for an entry — the
|
a change genuinely is one. Each entry is at most 300 characters after
|
||||||
|
wrapped lines are joined and whitespace is collapsed; a genuinely long
|
||||||
|
change ships as several short `- ` entries in the same fragment, never one
|
||||||
|
long entry. Never edit `CHANGELOG.md` for an entry — the
|
||||||
release PR assembles the section from the fragments (#112); the monotonic
|
release PR assembles the section from the fragments (#112); the monotonic
|
||||||
guard still refuses anything that deletes a shipped heading.
|
guard still refuses anything that deletes a shipped heading.
|
||||||
- Follow the repo's conventions file and match the code you touch. Tests are
|
- Follow the repo's conventions file and match the code you touch. Tests are
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,10 @@ published verbatim as that release's body (lib/changelog.sh extracts it),
|
||||||
so entries say what changed, cite the issue, and stop. Entries arrive as
|
so entries say what changed, cite the issue, and stop. Entries arrive as
|
||||||
fragments — one `changelog.d/<issue>.md` per PR, never an edit to this
|
fragments — one `changelog.d/<issue>.md` per PR, never an edit to this
|
||||||
file — and the release PR assembles them into the next section here
|
file — and the release PR assembles them into the next section here
|
||||||
(`bin/changelog-assemble`, #112).
|
(`bin/changelog-assemble`, #112). Each entry is at most 300 characters after
|
||||||
|
wrapped lines are joined and whitespace is collapsed; a genuinely long
|
||||||
|
change ships as several short `- ` entries in the same fragment, never one
|
||||||
|
long entry.
|
||||||
|
|
||||||
## 0.3.0 — 2026-07-24
|
## 0.3.0 — 2026-07-24
|
||||||
|
|
||||||
|
|
|
||||||
1
changelog.d/167.md
Normal file
1
changelog.d/167.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
- Bound changelog entries at 300 normalized characters in the shared fragment validator, with boundary, wrapping, grouping, and forward-only coverage; document splitting genuinely long changes into several short entries (#167).
|
||||||
|
|
@ -104,6 +104,9 @@ changelog_fragments() {
|
||||||
# a smuggled one would split the published section;
|
# a smuggled one would split the published section;
|
||||||
# - at least one bullet: a heading is not an entry — the rule the
|
# - at least one bullet: a heading is not an entry — the rule the
|
||||||
# publisher enforces at release time, moved onto the PR;
|
# publisher enforces at release time, moved onto the PR;
|
||||||
|
# - at most 300 characters per normalized entry: prose drift is refused
|
||||||
|
# where it is written (#167), with wrapped continuation lines joined and
|
||||||
|
# whitespace collapsed before measuring;
|
||||||
# - no '### ' heading without a bullet before the next heading or EOF:
|
# - no '### ' heading without a bullet before the next heading or EOF:
|
||||||
# the dangling grouped heading #98 taught us to refuse.
|
# the dangling grouped heading #98 taught us to refuse.
|
||||||
changelog_fragment_problem() {
|
changelog_fragment_problem() {
|
||||||
|
|
@ -125,6 +128,44 @@ changelog_fragment_problem() {
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
problem="$(
|
||||||
|
awk '
|
||||||
|
function inspect( normalized, preview) {
|
||||||
|
if (entry == "") return
|
||||||
|
normalized = entry
|
||||||
|
gsub(/[[:space:]]+/, " ", normalized)
|
||||||
|
sub(/^ /, "", normalized)
|
||||||
|
sub(/ $/, "", normalized)
|
||||||
|
if (length(normalized) > 300) {
|
||||||
|
preview = substr(normalized, 1, 60)
|
||||||
|
print length(normalized) "\t" preview
|
||||||
|
entry = ""
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/^[[:space:]]*[-*][[:space:]]/ {
|
||||||
|
inspect()
|
||||||
|
entry = $0
|
||||||
|
sub(/^[[:space:]]*[-*][[:space:]]+/, "", entry)
|
||||||
|
next
|
||||||
|
}
|
||||||
|
/^### / {
|
||||||
|
inspect()
|
||||||
|
entry = ""
|
||||||
|
next
|
||||||
|
}
|
||||||
|
entry != "" { entry = entry " " $0 }
|
||||||
|
END { inspect() }
|
||||||
|
' "$file"
|
||||||
|
)"
|
||||||
|
if [ -n "$problem" ]; then
|
||||||
|
local length preview
|
||||||
|
IFS="$(printf '\t')" read -r length preview <<<"$problem"
|
||||||
|
printf "fragment '%s' has an overlong entry '%s…': %s characters, bound 300 — split it into multiple '- ' entries in this same fragment\n" \
|
||||||
|
"$file" "$preview" "$length"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
problem="$(
|
problem="$(
|
||||||
awk '
|
awk '
|
||||||
/^### / {
|
/^### / {
|
||||||
|
|
|
||||||
|
|
@ -259,6 +259,54 @@ check "fragment predicate: a dangling grouped heading is refused, heading named"
|
||||||
"has an empty heading: '### Added'" \
|
"has an empty heading: '### Added'" \
|
||||||
changelog_fragment_problem "$PF/22.md"
|
changelog_fragment_problem "$PF/22.md"
|
||||||
|
|
||||||
|
entry_of_length() {
|
||||||
|
local count="$1"
|
||||||
|
awk -v count="$count" 'BEGIN {
|
||||||
|
printf "%0*d\n", count, 0
|
||||||
|
}'
|
||||||
|
}
|
||||||
|
|
||||||
|
printf -- '- %s\n' "$(entry_of_length 300)" >"$PF/23.md"
|
||||||
|
check "fragment predicate: a 300-character entry passes" 0 "" \
|
||||||
|
changelog_fragment_problem "$PF/23.md"
|
||||||
|
|
||||||
|
printf -- '- %s\n' "$(entry_of_length 301)" >"$PF/24.md"
|
||||||
|
check "fragment predicate: a 301-character entry is refused with actionable detail" 1 \
|
||||||
|
"301 characters, bound 300 — split it into multiple '- ' entries in this same fragment" \
|
||||||
|
changelog_fragment_problem "$PF/24.md"
|
||||||
|
check "fragment predicate: an overlong diagnosis previews the entry" 1 \
|
||||||
|
"000000000000000000000000000000000000000000000000000000000000…" \
|
||||||
|
changelog_fragment_problem "$PF/24.md"
|
||||||
|
|
||||||
|
{
|
||||||
|
printf -- '- %s\n' "$(entry_of_length 200)"
|
||||||
|
printf -- '- %s\n' "$(entry_of_length 200)"
|
||||||
|
} >"$PF/25.md"
|
||||||
|
check "fragment predicate: several bounded entries may total over 300 characters" 0 "" \
|
||||||
|
changelog_fragment_problem "$PF/25.md"
|
||||||
|
|
||||||
|
{
|
||||||
|
printf -- '- %s\n' "$(entry_of_length 60)"
|
||||||
|
printf ' %s\n' "$(entry_of_length 60)"
|
||||||
|
printf ' %s\n' "$(entry_of_length 60)"
|
||||||
|
printf ' %s\n' "$(entry_of_length 67)"
|
||||||
|
} >"$PF/26.md"
|
||||||
|
check "fragment predicate: a roughly 250-character entry may wrap over four lines" 0 "" \
|
||||||
|
changelog_fragment_problem "$PF/26.md"
|
||||||
|
|
||||||
|
{
|
||||||
|
printf '### Added\n\n'
|
||||||
|
printf -- '- %s\n\n' "$(entry_of_length 300)"
|
||||||
|
printf '### Fixed\n\n'
|
||||||
|
printf -- '* %s\n' "$(entry_of_length 301)"
|
||||||
|
} >"$PF/27.md"
|
||||||
|
check "fragment predicate: grouped headings are not counted and grouped bullets are bounded" 1 \
|
||||||
|
"301 characters, bound 300" \
|
||||||
|
changelog_fragment_problem "$PF/27.md"
|
||||||
|
|
||||||
|
check "section predicate: published over-bound entries remain unvalidated" 0 "" \
|
||||||
|
changelog_section_problem "$ROOT/CHANGELOG.md" 0.3.0
|
||||||
|
|
||||||
# --- the assembler (#114) ----------------------------------------------------
|
# --- the assembler (#114) ----------------------------------------------------
|
||||||
|
|
||||||
assert_assemble() {
|
assert_assemble() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue