From a7edf699a14d978ea194db73567adf965231c63b Mon Sep 17 00:00:00 2001 From: Aaron Sachs <898627+asachs01@users.noreply.github.com> Date: Fri, 4 Sep 2026 12:19:21 +0000 Subject: [PATCH] feat(release): use extracted CHANGELOG notes instead of --generate-notes PUBLISH mode's gh release create used GitHub's own --generate-notes auto-summary, which diverges from the curated notes semantic-release already wrote to CHANGELOG.md during PREPARE mode (CodeRabbit catch, task_1788457898992, deferred to a fast-follow). scripts/extract-release-notes.mjs pulls the released version's section out of CHANGELOG.md and FAILS OPEN by design (boss's requirement): any read/parse failure or empty section exits 1, and the workflow step falls back to --generate-notes rather than ever blocking a publish over a cosmetic notes gap. Byte-identical to the canary (node-datto-rmm#77, murph-reviewed): the regex bug murph caught there (single-# minor/major headings not recognized as section boundaries) is already fixed in this version -- re-validated against this repo's own real CHANGELOG.md before pushing. --- .github/workflows/release.yml | 16 ++++++- scripts/extract-release-notes.mjs | 70 +++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 scripts/extract-release-notes.mjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8fd13d0..f839365 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -169,6 +169,13 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: npm publish + # Uses the same curated notes semantic-release already committed to + # CHANGELOG.md during PREPARE mode, instead of GitHub's --generate-notes + # auto-summary (CodeRabbit catch, task_1788457898992: the two diverge in + # content). scripts/extract-release-notes.mjs FAILS OPEN by design + # (boss's requirement) -- any read/parse failure or an empty section + # exits 1, and this step falls back to --generate-notes rather than + # ever blocking a publish over a cosmetic notes gap. - name: "Publish: create GitHub release" if: steps.mode.outputs.mode == 'publish' && steps.mode.outputs.release_exists == 'false' env: @@ -176,7 +183,14 @@ jobs: run: | set -euo pipefail VERSION="${{ steps.mode.outputs.version }}" - gh release create "v${VERSION}" --title "v${VERSION}" --generate-notes + NOTES_FILE="$(mktemp)" + if node scripts/extract-release-notes.mjs "${VERSION}" > "${NOTES_FILE}" && [ -s "${NOTES_FILE}" ]; then + echo "Using extracted CHANGELOG.md notes for v${VERSION}." + gh release create "v${VERSION}" --title "v${VERSION}" --notes-file "${NOTES_FILE}" + else + echo "::warning::extract-release-notes.mjs failed or produced empty output for v${VERSION} -- falling back to --generate-notes" + gh release create "v${VERSION}" --title "v${VERSION}" --generate-notes + fi # --- PREPARE mode: steady state, check for new releasable work --- diff --git a/scripts/extract-release-notes.mjs b/scripts/extract-release-notes.mjs new file mode 100644 index 0000000..5ab756f --- /dev/null +++ b/scripts/extract-release-notes.mjs @@ -0,0 +1,70 @@ +#!/usr/bin/env node +/* global process, console */ +// Extracts one version's release-notes section out of CHANGELOG.md so +// PUBLISH mode's `gh release create` can use the same curated notes PREPARE +// mode already committed, instead of GitHub's generic --generate-notes +// summary (CodeRabbit catch, task_1788457898992: the two diverge in +// content -- CHANGELOG.md carries semantic-release's real notes, +// --generate-notes is GitHub's own auto-summary from commit/PR titles). +// +// FAILS OPEN, deliberately (boss's requirement): any read/parse failure or +// an empty result prints an error to stderr and exits 1. The caller must +// treat a non-zero exit as "fall back to --generate-notes" -- cosmetic +// release notes must never block a PUBLISH. + +import { readFileSync } from 'node:fs'; + +const version = process.argv[2]; +if (!version) { + console.error('usage: extract-release-notes.mjs '); + process.exit(1); +} + +let changelog; +try { + changelog = readFileSync('CHANGELOG.md', 'utf8'); +} catch (err) { + console.error(`could not read CHANGELOG.md: ${err.message}`); + process.exit(1); +} + +// semantic-release-changelog headings look like: +// ## [1.0.12](https://github.com/.../compare/v1.0.11...v1.0.12) (2026-09-03) +// for a patch, but a single '#' for a minor/major release, e.g.: +// # [2.0.0](https://github.com/.../compare/v1.0.2...v2.0.0) (2026-07-28) +// (murph, task_1788458278413: matching only '##' both fails to find a +// minor/major version's own section, AND fails to recognize one as the +// boundary that closes a preceding patch section -- reproduced against +// node-crewhu's real CHANGELOG.md, where extracting 2.0.1 silently +// swallowed all of the following 2.0.0 section, including its BREAKING +// CHANGES block, into 2.0.1's "notes"). Match 1 or 2 leading '#'s so both +// heading levels are recognized as section boundaries. +const lines = changelog.split('\n'); +const headingRe = /^#{1,2} \[([^\]]+)\]/; +let start = -1; +let end = lines.length; + +for (let i = 0; i < lines.length; i++) { + const m = lines[i].match(headingRe); + if (!m) continue; + if (start === -1 && m[1] === version) { + start = i + 1; // section body starts after the heading line + } else if (start !== -1) { + end = i; // next heading closes the section + break; + } +} + +if (start === -1) { + console.error(`no CHANGELOG.md section found for version ${version}`); + process.exit(1); +} + +const section = lines.slice(start, end).join('\n').trim(); + +if (!section) { + console.error(`CHANGELOG.md section for ${version} is empty`); + process.exit(1); +} + +console.log(section);