|
| 1 | +#!/usr/bin/env bash |
| 2 | +# |
| 3 | +# Generate git-cliff-style release notes from Conventional Commits since the |
| 4 | +# previous v* tag. Output is Markdown on stdout. |
| 5 | +# |
| 6 | +# Usage: scripts/release-notes.sh <new-tag> [owner/repo] |
| 7 | +# e.g. scripts/release-notes.sh v1.17.4 sprisa/opencode |
| 8 | +# |
| 9 | +# Commit subjects are grouped by type (feat, fix, ...). Squash-merged PR refs |
| 10 | +# like "(#123)" are turned into links, and every entry links to its commit - |
| 11 | +# mirroring what GitHub's --generate-notes does, but driven by commits so it |
| 12 | +# works with a direct-to-main workflow. |
| 13 | +set -euo pipefail |
| 14 | + |
| 15 | +TAG="${1:?usage: release-notes.sh <new-tag> [owner/repo]}" |
| 16 | +REPO="${2:-${RELEASE_REPO:-sprisa/opencode}}" |
| 17 | +REPO_URL="https://github.com/${REPO}" |
| 18 | +VERSION="${TAG#v}" # image version without the leading "v" |
| 19 | + |
| 20 | +# Previous release tag (newest v* tag that isn't the one we're cutting). |
| 21 | +PREV="$(git tag --list 'v*' --sort=-v:refname | grep -vx "$TAG" | head -n1 || true)" |
| 22 | +if [ -n "$PREV" ]; then |
| 23 | + RANGE="${PREV}..HEAD" |
| 24 | +else |
| 25 | + RANGE="HEAD" # first release: include all history |
| 26 | +fi |
| 27 | + |
| 28 | +tmp="$(mktemp -d)" |
| 29 | +trap 'rm -rf "$tmp"' EXIT |
| 30 | + |
| 31 | +# Conventional Commit subject: type(scope)?!?: description |
| 32 | +cc_re='^([a-zA-Z]+)(\(([^)]*)\))?(!)?:[[:space:]]+(.*)$' |
| 33 | + |
| 34 | +# Bucket each commit into $tmp/<type>. |
| 35 | +while IFS="$(printf '\t')" read -r subject hash; do |
| 36 | + [ -n "$subject" ] || continue |
| 37 | + |
| 38 | + if [[ "$subject" =~ $cc_re ]]; then |
| 39 | + type="$(printf '%s' "${BASH_REMATCH[1]}" | tr '[:upper:]' '[:lower:]')" |
| 40 | + scope="${BASH_REMATCH[3]}" |
| 41 | + bang="${BASH_REMATCH[4]}" |
| 42 | + desc="${BASH_REMATCH[5]}" |
| 43 | + else |
| 44 | + type="other"; scope=""; bang=""; desc="$subject" |
| 45 | + fi |
| 46 | + |
| 47 | + # Only keep known types in their bucket; everything else -> other. |
| 48 | + case "$type" in |
| 49 | + feat|fix|perf|refactor|docs|style|test|build|ci|chore|revert) ;; |
| 50 | + *) type="other" ;; |
| 51 | + esac |
| 52 | + |
| 53 | + # Turn "(#123)" PR references into links. |
| 54 | + desc="$(printf '%s' "$desc" | sed -E "s@\(#([0-9]+)\)@([#\1](${REPO_URL}/pull/\1))@g")" |
| 55 | + |
| 56 | + line="-" |
| 57 | + [ -n "$scope" ] && line="$line **${scope}:**" |
| 58 | + line="$line ${desc}" |
| 59 | + [ -n "$bang" ] && line="$line **(breaking)**" |
| 60 | + line="$line ([\`${hash}\`](${REPO_URL}/commit/${hash}))" |
| 61 | + |
| 62 | + printf '%s\n' "$line" >> "$tmp/$type" |
| 63 | +# The trailing `printf '\n'` appends a newline: git's --pretty=format omits one |
| 64 | +# after the last commit, so otherwise `read` would drop the final entry. |
| 65 | +done < <(git log --no-merges --reverse --pretty=format:'%s%x09%h' "$RANGE"; printf '\n') |
| 66 | + |
| 67 | +# Emit one group (if it has any entries). Args: <type> <emoji heading> |
| 68 | +emit() { |
| 69 | + if [ -s "$tmp/$1" ]; then |
| 70 | + printf '### %s\n\n' "$2" |
| 71 | + cat "$tmp/$1" |
| 72 | + printf '\n' |
| 73 | + fi |
| 74 | +} |
| 75 | + |
| 76 | +# Install instructions for this exact version, up top. |
| 77 | +printf '### 📦 Install\n\n' |
| 78 | +printf '```bash\n' |
| 79 | +printf 'docker run -it -p 4096:4096 -v $(pwd):/home/opencode sprisa/opencode:%s\n' "$VERSION" |
| 80 | +printf '```\n\n' |
| 81 | + |
| 82 | +# Emit groups in a fixed, git-cliff-like order. |
| 83 | +emit feat "🚀 Features" |
| 84 | +emit fix "🐛 Bug Fixes" |
| 85 | +emit perf "⚡ Performance" |
| 86 | +emit refactor "🚜 Refactor" |
| 87 | +emit docs "📚 Documentation" |
| 88 | +emit style "🎨 Styling" |
| 89 | +emit test "🧪 Testing" |
| 90 | +emit build "📦 Build" |
| 91 | +emit ci "⚙️ CI" |
| 92 | +emit chore "🧹 Miscellaneous Tasks" |
| 93 | +emit revert "◀️ Revert" |
| 94 | +emit other "🔗 Other" |
| 95 | + |
| 96 | +# Footer: compare link (or commit list for the very first release). |
| 97 | +if [ -n "$PREV" ]; then |
| 98 | + printf '**Full Changelog**: %s/compare/%s...%s\n' "$REPO_URL" "$PREV" "$TAG" |
| 99 | +else |
| 100 | + printf '**Full Changelog**: %s/commits/%s\n' "$REPO_URL" "$TAG" |
| 101 | +fi |
0 commit comments