feat: bound changelog fragment entries at 300 characters #168
6 changed files with 117 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: `- `
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
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;
|
||||
# - 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 '
|
||||
/^### / {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue