From 11e0059d240ce2926a1f6c3932817591d0f4b512 Mon Sep 17 00:00:00 2001 From: s-b-e-n-s-o-n <80784472+s-b-e-n-s-o-n@users.noreply.github.com> Date: Thu, 21 May 2026 14:47:51 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20chore(ci):=20auto-stamp=20releas?= =?UTF-8?q?e-notes=20heading=20+=20add=20Full=20Changelog=20link?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The release-cut "Generate release notes" step copied the CHANGELOG section verbatim. For pre-releases cut before the CHANGELOG was stamped, the [Unreleased] fallback leaked a literal "## [Unreleased]" heading (no version, no date) into the published GitHub Release — this hit rc.17, rc.18, rc.19, and rc.25. - When the [Unreleased] fallback is used, rewrite the section heading in the generated notes to "## [] — " so a cut can never again publish an unstamped heading. CHANGELOG.md itself is still stamped separately at release time. - Prepend a "Full Changelog" compare link (previous tag → this tag) to every release's notes, resolved from the newest existing release. --- .github/workflows/release-cut.yml | 32 +++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/.github/workflows/release-cut.yml b/.github/workflows/release-cut.yml index 1e6fcba0..1a779d0d 100644 --- a/.github/workflows/release-cut.yml +++ b/.github/workflows/release-cut.yml @@ -551,15 +551,19 @@ jobs: env: IS_PRERELEASE: ${{ steps.tag.outputs.is_prerelease }} RELEASE_TAG: ${{ steps.next.outputs.release_tag }} + REPO: ${{ github.repository }} + GH_TOKEN: ${{ github.token }} run: | set -euo pipefail notes_path="dist/release-notes-${RELEASE_TAG}.md" entry_path="$(mktemp)" missing_heading="## [${RELEASE_TAG#v}] - YYYY-MM-DD" + used_unreleased_fallback=false if ! node scripts/extract-changelog-entry.mjs --version "${RELEASE_TAG}" --file CHANGELOG.md > "${entry_path}"; then if [ "${IS_PRERELEASE}" = "true" ] && node scripts/extract-changelog-entry.mjs --version "Unreleased" --file CHANGELOG.md > "${entry_path}" 2>/dev/null; then echo "Using [Unreleased] changelog section for pre-release ${RELEASE_TAG}." + used_unreleased_fallback=true else rm -f "${entry_path}" "${notes_path}" echo "::error::Release notes generation failed: CHANGELOG entry missing for ${RELEASE_TAG}. Add heading '${missing_heading}' and retry release." @@ -571,9 +575,37 @@ jobs: exit 1 fi fi + + # The [Unreleased] fallback means the CHANGELOG was not stamped before + # the cut. Rewrite the section heading in the generated notes so the + # published release shows the version and date instead of a literal + # "[Unreleased]" — a cut can never ship an unstamped heading. The + # CHANGELOG.md file itself is stamped separately at release time. + if [ "${used_unreleased_fallback}" = "true" ]; then + stamped_heading="## [${RELEASE_TAG#v}] — $(date -u +%Y-%m-%d)" + stamped_path="$(mktemp)" + awk -v h="${stamped_heading}" \ + 'NR == 1 && /^## \[Unreleased\][[:space:]]*$/ { print h; next } { print }' \ + "${entry_path}" > "${stamped_path}" + mv "${stamped_path}" "${entry_path}" + fi + + # Resolve the previous release for a Full Changelog compare link. The + # current tag is not pushed yet, so the newest existing release is the + # predecessor; the select() guards re-runs after a partial release. + prev_tag="$(gh release list --repo "${REPO}" --limit 100 \ + --json tagName,createdAt \ + --jq 'sort_by(.createdAt) | reverse | map(.tagName) + | map(select(. != env.RELEASE_TAG)) | .[0] // empty' \ + 2>/dev/null || true)" + { echo "# ${RELEASE_TAG}" echo "" + if [ -n "${prev_tag}" ]; then + echo "**Full Changelog**: https://github.com/${REPO}/compare/${prev_tag}...${RELEASE_TAG}" + echo "" + fi cat "${entry_path}" } > "${notes_path}" rm -f "${entry_path}"