|
| 1 | +#!/usr/bin/env bash |
| 2 | +# extract-changelog.sh <version> [changelog-file] |
| 3 | +# |
| 4 | +# Extracts the [Unreleased] block from CHANGELOG.md, prints its content to |
| 5 | +# stdout (for use as a GitHub release body), and rewrites the changelog: |
| 6 | +# - renames ## [Unreleased] → ## [<version>] — <today> |
| 7 | +# - inserts a fresh empty ## [Unreleased] block above it |
| 8 | +# |
| 9 | +# Usage: |
| 10 | +# body=$(bash .github/extract-changelog.sh v1.2.3) |
| 11 | +# body=$(bash .github/extract-changelog.sh v1.2.3 path/to/CHANGELOG.md) |
| 12 | +# |
| 13 | +# Exit codes: |
| 14 | +# 0 success |
| 15 | +# 1 bad usage or no [Unreleased] section found |
| 16 | + |
| 17 | +set -euo pipefail |
| 18 | + |
| 19 | +VERSION="${1:-}" |
| 20 | +CHANGELOG="${2:-CHANGELOG.md}" |
| 21 | + |
| 22 | +if [[ -z "$VERSION" ]]; then |
| 23 | + echo "usage: extract-changelog.sh <version> [changelog-file]" >&2 |
| 24 | + exit 1 |
| 25 | +fi |
| 26 | + |
| 27 | +if [[ ! -f "$CHANGELOG" ]]; then |
| 28 | + echo "error: $CHANGELOG not found" >&2 |
| 29 | + exit 1 |
| 30 | +fi |
| 31 | + |
| 32 | +TODAY=$(date -u +%Y-%m-%d) |
| 33 | + |
| 34 | +# --------------------------------------------------------------------------- |
| 35 | +# Extract the body of the [Unreleased] section. |
| 36 | +# The section starts on the line after "## [Unreleased]" and ends just before |
| 37 | +# the next "## [" heading or end-of-file. |
| 38 | +# --------------------------------------------------------------------------- |
| 39 | +body=$(awk ' |
| 40 | + /^## \[Unreleased\]/ { in_section=1; next } |
| 41 | + in_section && /^## \[/ { exit } |
| 42 | + in_section { print } |
| 43 | +' "$CHANGELOG") |
| 44 | + |
| 45 | +if [[ -z "$(echo "$body" | tr -d '[:space:]-')" ]]; then |
| 46 | + echo "error: no content found under ## [Unreleased] in $CHANGELOG" >&2 |
| 47 | + exit 1 |
| 48 | +fi |
| 49 | + |
| 50 | +# Trim leading/trailing blank lines and trailing horizontal rules (---) that |
| 51 | +# are changelog section separators, not part of the release notes. |
| 52 | +body=$(echo "$body" | \ |
| 53 | + sed -e '/./,$!d' \ |
| 54 | + -e 's/[[:space:]]*$//' | \ |
| 55 | + awk ' |
| 56 | + { lines[NR] = $0 } |
| 57 | + END { |
| 58 | + # Strip trailing blank lines and bare "---" separators. |
| 59 | + last = NR |
| 60 | + while (last > 0 && (lines[last] ~ /^[[:space:]]*$/ || lines[last] ~ /^---[[:space:]]*$/)) { |
| 61 | + last-- |
| 62 | + } |
| 63 | + for (i = 1; i <= last; i++) print lines[i] |
| 64 | + } |
| 65 | + ') |
| 66 | + |
| 67 | +# --------------------------------------------------------------------------- |
| 68 | +# Rewrite the changelog in-place. |
| 69 | +# The existing "## [Unreleased]" line (and everything in its section up to but |
| 70 | +# not including the next "## [" heading) is replaced with: |
| 71 | +# 1. A fresh empty ## [Unreleased] block (just the heading + blank line) |
| 72 | +# 2. A --- separator |
| 73 | +# 3. The versioned heading with today's date |
| 74 | +# 4. The original section content (minus the trailing ---) |
| 75 | +# --------------------------------------------------------------------------- |
| 76 | +tmp=$(mktemp) |
| 77 | + |
| 78 | +awk -v version="$VERSION" -v today="$TODAY" ' |
| 79 | + # State: 0 = before unreleased, 1 = inside unreleased, 2 = after unreleased |
| 80 | + state == 0 && /^## \[Unreleased\]/ { |
| 81 | + # Emit the new empty Unreleased block. |
| 82 | + print "## [Unreleased]" |
| 83 | + print "" |
| 84 | + print "---" |
| 85 | + print "" |
| 86 | + # Emit the versioned heading in place of ## [Unreleased]. |
| 87 | + print "## [" version "] \xe2\x80\x94 " today |
| 88 | + state = 1 |
| 89 | + next |
| 90 | + } |
| 91 | + state == 1 && /^## \[/ { |
| 92 | + # We have left the old unreleased section; switch to passthrough. |
| 93 | + state = 2 |
| 94 | + } |
| 95 | + # Print every line that is not the old "## [Unreleased]" heading itself. |
| 96 | + state != 0 || !/^## \[Unreleased\]/ { print } |
| 97 | +' "$CHANGELOG" > "$tmp" |
| 98 | + |
| 99 | +mv "$tmp" "$CHANGELOG" |
| 100 | + |
| 101 | +# --------------------------------------------------------------------------- |
| 102 | +# Print the body to stdout — captured by the caller as the release body. |
| 103 | +# --------------------------------------------------------------------------- |
| 104 | +printf '%s\n' "$body" |
0 commit comments