From 69410723bd4f5572a089f2c45c95a53433dd7c6b Mon Sep 17 00:00:00 2001 From: codex-bot-andresmgsl <304681515+codex-bot-andresmgsl@users.noreply.github.com> Date: Thu, 23 Jul 2026 23:43:20 +0000 Subject: [PATCH] fix: reject entry-less release notes --- .github/workflows/release-exercise.yml | 24 +++++++++++++++++-- .github/workflows/release.yml | 10 ++++---- test/release-chain.test.sh | 33 ++++++++++++++++++++++---- 3 files changed, 57 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release-exercise.yml b/.github/workflows/release-exercise.yml index e75940a..28ee10f 100644 --- a/.github/workflows/release-exercise.yml +++ b/.github/workflows/release-exercise.yml @@ -171,12 +171,32 @@ jobs: run: | # shellcheck source=/dev/null . "$CEREMONY_DIR/lib/changelog.sh" - changelog_section CHANGELOG.md "$VER" > "$RUNNER_TEMP/notes.md" - if [ ! -s "$RUNNER_TEMP/notes.md" ]; then + if ! diagnosis="$(changelog_section_problem CHANGELOG.md "$VER")"; then echo "CHANGELOG.md has no '## $VER' section at the merge commit — the ceremony PR must stamp it; refusing to publish an empty release" >&2 + printf '%s\n' "$diagnosis" >&2 exit 1 fi + changelog_section CHANGELOG.md "$VER" > "$RUNNER_TEMP/notes.md" cat "$RUNNER_TEMP/notes.md" + - name: an entry-less stamped fixture is refused by the notes predicate + working-directory: ${{ runner.temp }}/fixture + env: + VER: ${{ steps.facts.outputs.ver }} + run: | + cp CHANGELOG.md "$RUNNER_TEMP/CHANGELOG.good.md" + awk -v ver="$VER" ' + /^## / { in_section = ($2 == ver) } + in_section && /^[[:space:]]*[-*][[:space:]]/ { next } + { print } + ' "$RUNNER_TEMP/CHANGELOG.good.md" > CHANGELOG.md + # shellcheck source=/dev/null + . "$CEREMONY_DIR/lib/changelog.sh" + if diagnosis="$(changelog_section_problem CHANGELOG.md "$VER")"; then + echo "entry-less stamped section unexpectedly passed" >&2 + exit 1 + fi + printf '%s\n' "$diagnosis" | grep -F "section '$VER' has no entries" + cp "$RUNNER_TEMP/CHANGELOG.good.md" CHANGELOG.md - name: the chain must land where the fixture says it lands env: CEREMONY: ${{ steps.decide.outputs.ceremony }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6e5ce98..bcfbc14 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -198,11 +198,12 @@ jobs: run: | # shellcheck source=/dev/null . "$CEREMONY_DIR/lib/changelog.sh" - changelog_section CHANGELOG.md "$VER" > "$RUNNER_TEMP/notes.md" - if [ ! -s "$RUNNER_TEMP/notes.md" ]; then + if ! diagnosis="$(changelog_section_problem CHANGELOG.md "$VER")"; then echo "CHANGELOG.md has no '## $VER' section at the merge commit — the ceremony PR must stamp it; refusing to publish an empty release" >&2 + printf '%s\n' "$diagnosis" >&2 exit 1 fi + changelog_section CHANGELOG.md "$VER" > "$RUNNER_TEMP/notes.md" cat "$RUNNER_TEMP/notes.md" - name: nothing may exist yet — no tag, no release (re-runs refuse loudly) if: steps.decide.outputs.ceremony == 'yes' @@ -342,11 +343,12 @@ jobs: run: | # shellcheck source=/dev/null . "$CEREMONY_DIR/lib/changelog.sh" - changelog_section CHANGELOG.md "$VER" > "$RUNNER_TEMP/notes.md" - if [ ! -s "$RUNNER_TEMP/notes.md" ]; then + if ! diagnosis="$(changelog_section_problem CHANGELOG.md "$VER")"; then echo "CHANGELOG.md has no '## $VER' section — stamp the Unreleased section in the release PR before tagging; refusing to publish an empty release" >&2 + printf '%s\n' "$diagnosis" >&2 exit 1 fi + changelog_section CHANGELOG.md "$VER" > "$RUNNER_TEMP/notes.md" cat "$RUNNER_TEMP/notes.md" - name: artifact hook — the consumer's own release-artifact action # After the tag exists (it fired this door), before the publish — diff --git a/test/release-chain.test.sh b/test/release-chain.test.sh index 5973353..a83348b 100644 --- a/test/release-chain.test.sh +++ b/test/release-chain.test.sh @@ -89,11 +89,11 @@ chain() { *ceremony=yes*) # shellcheck source=lib/changelog.sh . "$ROOT/lib/changelog.sh" - notes="$(changelog_section CHANGELOG.md "$ver")" - if [ -z "$notes" ]; then - echo "chain: the changelog section for $ver is empty" >&2 + diagnosis="$(changelog_section_problem CHANGELOG.md "$ver")" || { + printf 'chain: %s\n' "$diagnosis" >&2 exit 1 - fi + } + notes="$(changelog_section CHANGELOG.md "$ver")" printf 'notes: %s\n' "$notes" ;; esac @@ -105,6 +105,31 @@ check "the ceremony merge decides ceremony=yes" 0 "ceremony=yes" \ check "the notes are the stamped section's prose" 0 \ "notes: - The entry this release ships." chain "$MERGE_SHA" "$BASE_SHA" +# The same ceremony facts with an entry-less stamped section must stop at the +# notes door, before any tag or release mutation could run. +git -C "$TMP/repo" reset -q --hard "$BASE_SHA" +printf '0.7.0\n' >"$TMP/repo/VERSION" +cat >"$TMP/repo/CHANGELOG.md" <<'EOF' +# Changelog + +## Unreleased + +## 0.7.0 — 2026-07-21 + +### Added + +## 0.6.8 — 2026-07-01 + +- An older entry. +EOF +git -C "$TMP/repo" add VERSION CHANGELOG.md +git -C "$TMP/repo" commit -qm "release: entry-less 0.7.0" +EMPTY_MERGE_SHA="$(git -C "$TMP/repo" rev-parse HEAD)" +check "the notes door refuses an entry-less stamped section" 1 \ + "section '0.7.0' has no entries" chain "$EMPTY_MERGE_SHA" "$BASE_SHA" + +git -C "$TMP/repo" reset -q --hard "$MERGE_SHA" + # The same chain on an ordinary merge: -dev, unchanged — a green NOTICE # no-op that never consults the API (the stub would refuse a release view). printf 'ordinary work\n' >"$TMP/repo/notes.txt"