Build the release tag body from the version's CHANGELOG section

The tag body now carries the version's CHANGELOG.md section, so the release page reads as release notes.

scripts/tag-release.sh created the tag with -m "Release $ref". The catalog release component reads the annotated tag body and passes it to the release page with no template of its own, which is why standards/release requires an annotated tag: the body is load-bearing. v1.2.0 shows the result, a release page whose whole body is the string Release v1.2.0. The other three tags read well because each was written by hand at tag time, so the defect only bites when the script is used.

scripts/release-notes.sh extracts the section and refuses rather than emitting a stub, on three states: no section for the version in VERSION, a section still sitting under ## [Unreleased], and a section whose body is blank. The refusal for the middle case prints the heading to rename it to. Refusing early matters because a release page that says nothing can only be corrected by deleting and re-pushing the tag, which standards/release already calls destructive to anyone who fetched it first.

--cleanup=whitespace is load-bearing. git tag defaults to strip, which drops every line opening with # as commentary and would take each ### Added and ### Fixed heading with it. Both modes were read back through git tag -l --format='%(contents)' before choosing.

On the v prefix: VERSION holds v1.3.0 and the heading is ## [1.3.0] -- 2026-08-24. The prefix is stripped explicitly and the heading is matched with index() against a fixed string, so brackets and dots stay literal and the closing bracket keeps [1.3.0] from selecting [1.3.01].

just validate-release-notes joins the validate gate, so a section that will not extract fails the merge request that wrote it. Verified able to fail: the section boundary and the exact-match guard were each broken by hand and the validator caught both. The awk is POSIX-plain and was exercised under busybox on alpine:3.24, the image the validate job runs on. The full tag path was demonstrated in a throwaway clone, so no tag exists here.

Two staleness items in standards/release/SKILL.md are deliberately out of scope: it names the component pipeline/release-create and says it has not shipped, and it documents the transport as CI_COMMIT_TAG_MESSAGE. Both want a follow-up.

Merge request reports

Loading
Loading