feat: bound changelog fragment entries at 300 characters #168

Closed
codex-bot-andresmgsl wants to merge 2 commits from build/167-changelog-entry-bound into main
6 changed files with 117 additions and 2 deletions

View file

@ -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: `- `
bullets, and in a grouped repo the `### Added` / `### Changed` /
`### 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
guard still refuses anything that deletes a shipped heading.
- Follow the repo's conventions file and match the code you touch. Tests are

View file

@ -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
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
(`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

1
changelog.d/167.md Normal file
View 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).

View file

@ -104,6 +104,9 @@ changelog_fragments() {
# a smuggled one would split the published section;
# - at least one bullet: a heading is not an entry — the rule the
# 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:
# the dangling grouped heading #98 taught us to refuse.
changelog_fragment_problem() {
@ -125,6 +128,44 @@ changelog_fragment_problem() {
return 1
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="$(
awk '
/^### / {

View file

@ -391,6 +391,25 @@ check "fragment mode quotes malformed-fragment diagnosis and file" 1 \
"fragment 'changelog.d/notes.md' is not named for its issue" \
in_tree fragments-bad-name
fragment_tree fragments-overlong 1.2.4-dev <<'EOF'
# Changelog
## 1.2.3 — 2026-07-20
- The shipped entry.
EOF
{
printf -- '- '
awk 'BEGIN { printf "%0301d", 0 }'
printf '\n'
} >"$TMP/fragments-overlong/changelog.d/167.md"
check "fragment mode refuses a 301-character entry with the complete diagnosis" 1 \
"fragment 'changelog.d/167.md' has an overlong entry" \
in_tree fragments-overlong
check "fragment mode names the measured length, bound, and split fix" 1 \
"301 characters, bound 300 — split it into multiple '- ' entries in this same fragment" \
in_tree fragments-overlong
fragment_tree fragments-dangling-group 1.2.4-dev <<'EOF'
# Changelog

View file

@ -259,6 +259,54 @@ check "fragment predicate: a dangling grouped heading is refused, heading named"
"has an empty heading: '### Added'" \
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) ----------------------------------------------------
assert_assemble() {