From dd51b06f3adfa05af37ef0ed8c6111d77fc6b829 Mon Sep 17 00:00:00 2001 From: dan-claude-bot Date: Sat, 18 Jul 2026 20:54:39 +0000 Subject: [PATCH] feat(release): publish the tagged version's changelog section (#83) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On a bare X.Y.Z tag push (the 0.6.0 tag set the no-'v' precedent), release.yml asserts the tag names the tree's own VERSION — a mismatch fails loudly and creates NOTHING — then publishes the GitHub release with that version's CHANGELOG.md section as the body: the curated prose, not the generated PR list. No assets, because for a pure-bash tree GitHub's source tarball for the tag IS the package. The extraction lives in .github/scripts/release-notes.sh, a file of its own so test/release.sh drives the same code against fixtures and the real changelog — it refuses a missing or empty section, so a tag whose release ritual was skipped fails before anything is created. Co-Authored-By: Claude Fable 5 --- .github/scripts/release-notes.sh | 30 ++++++++++++++++++++++++ .github/workflows/release.yml | 39 ++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 .github/scripts/release-notes.sh create mode 100644 .github/workflows/release.yml diff --git a/.github/scripts/release-notes.sh b/.github/scripts/release-notes.sh new file mode 100644 index 0000000..06bce9b --- /dev/null +++ b/.github/scripts/release-notes.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail + +# release-notes.sh [] — print exactly 's +# section of the changelog: every line between its '## ' +# header and the next '## '. This is what release.yml hands to +# 'gh release create', so the release notes are the curated prose we wrote, +# not the PR list GitHub would generate (#83). Fails loudly when the section +# is missing or empty — a tag without its changelog section is a release +# ritual skipped, and an empty release body would paper over it. +# +# A file of its own (not inlined in release.yml) so test/release.sh drives +# the same extraction against fixtures and the real CHANGELOG.md. + +ver="${1:-}" +changelog="${2:-CHANGELOG.md}" +[ -n "$ver" ] || { echo "usage: release-notes.sh []" >&2; exit 2; } +[ -f "$changelog" ] || { echo "release-notes: no such file: $changelog" >&2; exit 1; } + +# $2 of a section header ('## 0.6.0 — 2026-07-18') is the bare version — +# compared WHOLE, so 0.6.0 can never match a 0.6.0-rc1 section (or vice +# versa), and no regex-escaping of dots. sed drops the blank padding under +# the header; the command substitution eats the trailing blanks. +notes="$(awk -v ver="$ver" ' + /^## / { grab = ($2 == ver); next } + grab { print } +' "$changelog" | sed '/./,$!d')" + +[ -n "$notes" ] || { echo "release-notes: $changelog has no section for '$ver' — the release PR stamps the Unreleased section with version + date BEFORE the tag (#83)" >&2; exit 1; } +printf '%s\n' "$notes" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b24398d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,39 @@ +name: release +# The release publisher (#83), on a bare X.Y.Z tag push (the 0.6.0 tag set +# the precedent — no 'v' prefix). Two facts, then one act: the tag must name +# the tree's own VERSION (a mismatch fails loudly and creates NOTHING — a +# wrong release is worse than a missing one), and the release body is that +# version's CHANGELOG.md section (.github/scripts/release-notes.sh, shared +# with test/release.sh) — the curated prose, not the generated PR list. No +# assets are uploaded: for a pure-bash tree, GitHub's source tarball for the +# tag IS the package, and install.sh downloads exactly that. +on: + push: + tags: ["[0-9]*.[0-9]*.[0-9]*"] + +permissions: + contents: write # gh release create + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: the tag must name the tree's VERSION + run: | + ver="$(cat VERSION)" + if [ "$GITHUB_REF_NAME" != "$ver" ]; then + echo "tag '$GITHUB_REF_NAME' does not match VERSION '$ver' — creating nothing." >&2 + echo "A release is a PR, then a tag (#83): the release PR bumps VERSION and stamps the changelog; the tag goes on its MERGE commit. Delete this tag and re-tag the right commit." >&2 + exit 1 + fi + - name: release notes — the version's own CHANGELOG.md section + # release-notes.sh fails loudly on a missing/empty section, which + # fails the release here — before anything is created. + run: | + bash .github/scripts/release-notes.sh "$GITHUB_REF_NAME" > "$RUNNER_TEMP/notes.md" + cat "$RUNNER_TEMP/notes.md" + - name: create the release + env: + GH_TOKEN: ${{ github.token }} + run: gh release create "$GITHUB_REF_NAME" --verify-tag --title "$GITHUB_REF_NAME" --notes-file "$RUNNER_TEMP/notes.md"