From adcba3d37345004489db82a5f1c9f6a813bcd137 Mon Sep 17 00:00:00 2001 From: codex-bot-andresmgsl <304681515+codex-bot-andresmgsl@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:22:38 +0000 Subject: [PATCH 1/2] feat: bound changelog fragment entries --- BUILDER.md | 5 ++++- CHANGELOG.md | 5 ++++- changelog.d/167.md | 1 + lib/changelog.sh | 41 ++++++++++++++++++++++++++++++++++++ test/changelog.test.sh | 48 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 changelog.d/167.md diff --git a/BUILDER.md b/BUILDER.md index 95c3d0d..32ed0d1 100644 --- a/BUILDER.md +++ b/BUILDER.md @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index ca4ed8e..4936289 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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/.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 diff --git a/changelog.d/167.md b/changelog.d/167.md new file mode 100644 index 0000000..a4730ce --- /dev/null +++ b/changelog.d/167.md @@ -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). diff --git a/lib/changelog.sh b/lib/changelog.sh index a484857..3a859c1 100644 --- a/lib/changelog.sh +++ b/lib/changelog.sh @@ -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 ' /^### / { diff --git a/test/changelog.test.sh b/test/changelog.test.sh index 8754407..2486993 100755 --- a/test/changelog.test.sh +++ b/test/changelog.test.sh @@ -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() { -- 2.45.2 From 056a38cd731e09a54ea41d5d3c201d99511ef3ea Mon Sep 17 00:00:00 2001 From: codex-bot-andresmgsl <304681515+codex-bot-andresmgsl@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:23:16 +0000 Subject: [PATCH 2/2] test: exercise entry bound through guard --- test/changelog-armed.test.sh | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/changelog-armed.test.sh b/test/changelog-armed.test.sh index 07124a2..b3bb30d 100644 --- a/test/changelog-armed.test.sh +++ b/test/changelog-armed.test.sh @@ -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 -- 2.45.2