diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a0eb2de..c33215e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -159,6 +159,72 @@ jobs: pattern: binary-* path: downloaded/ + - name: Resolve release notes from CHANGELOG.md + id: notes + env: + TAG: ${{ needs.tag.outputs.tag }} + run: | + set -eu + + # Extract the block under `## []` up to the next `## [` heading. + extract_section() { + awk -v h="$1" ' + $0 ~ "^## \\[" h "\\]" { found=1; next } + found && /^## \[/ { exit } + found { print } + ' CHANGELOG.md + } + + has_content() { printf '%s' "$1" | grep -Eq '[^[:space:]]'; } + + # The tag arrives as vX.Y.Z; CHANGELOG convention is [X.Y.Z] (no v). + TAG_STRIPPED="${TAG#v}" + + body="" + for candidate in "$TAG" "$TAG_STRIPPED"; do + body=$(extract_section "$candidate") + if has_content "$body"; then break; fi + done + + if ! has_content "$body"; then + # Promote [Unreleased] → [TAG_STRIPPED] — YYYY-MM-DD and insert a fresh empty Unreleased. + unreleased=$(extract_section 'Unreleased') + if ! has_content "$unreleased"; then + echo "::error::CHANGELOG.md has neither a '## [${TAG}]' / '## [${TAG_STRIPPED}]' section nor a non-empty '## [Unreleased]' section." + echo "::error::Add a '## [${TAG_STRIPPED}] — YYYY-MM-DD' section (or populate Unreleased) before releasing." + exit 1 + fi + + echo "Promoting [Unreleased] → [${TAG_STRIPPED}] in CHANGELOG.md" + date_iso=$(date -u +%Y-%m-%d) + python3 - <> "$GITHUB_OUTPUT" + - name: Assemble versioned binaries + SHA256SUMS env: TAG: ${{ needs.tag.outputs.tag }} @@ -201,19 +267,11 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ needs.tag.outputs.tag }} + NOTES_BODY: ${{ steps.notes.outputs.body }} run: | set -eu - # Draft first so we can get the auto-generated body, then edit - # the body to append the cosign Verify footer before publishing. - gh release create "$TAG" \ - --title "$TAG" \ - --generate-notes \ - --draft \ - dist/docsiq-* dist/SHA256SUMS dist/SHA256SUMS.sig dist/SHA256SUMS.pem - - body=$(gh release view "$TAG" --json body -q .body) { - printf '%s\n\n' "$body" + printf '%s\n\n' "$NOTES_BODY" printf '### Verify\n\n' printf 'All artifacts are signed with [cosign](https://github.com/sigstore/cosign) keyless via Sigstore.\n\n' printf '```sh\n' @@ -226,7 +284,10 @@ jobs: printf '```\n' } > release-notes.md - gh release edit "$TAG" --notes-file release-notes.md --draft=false + gh release create "$TAG" \ + --title "$TAG" \ + --notes-file release-notes.md \ + dist/docsiq-* dist/SHA256SUMS dist/SHA256SUMS.sig dist/SHA256SUMS.pem - name: Generate SLSA build provenance id: attest diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b7d49e..5dc2c71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Each release ships signed binaries (cosign keyless + Rekor), a signed `SHA256SUMS`, and SLSA build provenance. +> **Contributors:** add bullets under `## [Unreleased]` as part of any +> PR worth mentioning in release notes. When the release workflow runs, +> it promotes `[Unreleased]` → `[vX.Y.Z] — YYYY-MM-DD` automatically and +> uses that section as the GitHub release body. If no non-empty +> `[Unreleased]` section exists at release time, the workflow fails. + ## [Unreleased] ### Added