Skip to content

Commit df7a936

Browse files
committed
[meta] add release-notes.sh, the release notes generator
The `release` target has invoked `./release-notes.sh` since 0b8ed9f, but the script itself was never committed, so the "Tests: release process" workflow fails on a fresh checkout.
1 parent f0b0c6b commit df7a936

1 file changed

Lines changed: 153 additions & 0 deletions

File tree

release-notes.sh

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
#!/bin/sh
2+
3+
# Generates release notes for an nvm release, in the format used in the
4+
# annotated release tags and version bump commits.
5+
#
6+
# Usage: ./release-notes.sh <new-version> [<prev-ref>] [<end-ref>]
7+
# <new-version> the version being released, e.g. `v0.40.7` (the leading `v` is optional)
8+
# <prev-ref> list commits since this ref; defaults to the most recent `vX.Y.Z` tag
9+
# <end-ref> list commits up to this ref; defaults to HEAD
10+
#
11+
# Commits from <prev-ref> to <end-ref> are grouped into sections by the leading
12+
# `[bracket]` tag of their subject line, in `git log` order (newest first).
13+
# Category tags ([New], [Fix], ...) are stripped from the bullet text;
14+
# context tags ([readme], [actions], [meta], ...) are kept.
15+
# Commits without a recognized tag land under "Misc", with a warning on stderr.
16+
#
17+
# Each bullet gets a ` (#123)` reference to the pull request that introduced
18+
# the commit, looked up via `gh` from GitHub's commit->PR association, unless
19+
# the subject already ends with one. Set NVM_RELEASE_NOTES_SKIP_PRS to a
20+
# nonempty value to skip the lookup (references will be omitted).
21+
22+
NEW_VERSION="${1-}"
23+
PREV_REF="${2-}"
24+
END_REF="${3:-HEAD}"
25+
26+
if [ -z "${NEW_VERSION}" ]; then
27+
printf >&2 'Usage: %s <new-version> [<prev-ref>] [<end-ref>]\n' "$0"
28+
exit 2
29+
fi
30+
case "${NEW_VERSION}" in
31+
v*) ;;
32+
*) NEW_VERSION="v${NEW_VERSION}" ;;
33+
esac
34+
35+
if [ -z "${PREV_REF}" ]; then
36+
PREV_REF="$(git describe --abbrev=0 --tags --match 'v[0-9]*.[0-9]*.[0-9]*')" || {
37+
printf >&2 'release-notes: failed to determine the previous version tag.\n'
38+
exit 1
39+
}
40+
fi
41+
for REF in "${PREV_REF}" "${END_REF}"; do
42+
git rev-parse --quiet --verify "${REF}^{commit}" >/dev/null || {
43+
# shellcheck disable=SC2016
44+
printf >&2 'release-notes: `%s` is not a valid ref.\n' "${REF}"
45+
exit 1
46+
}
47+
done
48+
49+
LOOKUP_PRS=1
50+
if [ -n "${NVM_RELEASE_NOTES_SKIP_PRS-}" ]; then
51+
LOOKUP_PRS=0
52+
elif ! command -v gh >/dev/null 2>&1; then
53+
# shellcheck disable=SC2016
54+
printf >&2 'release-notes: `gh` is not available; PR references will be omitted.\n'
55+
LOOKUP_PRS=0
56+
fi
57+
58+
# the repo to look up commit->PR associations in, e.g. `nvm-sh/nvm`
59+
REPO_SLUG="${GH_REPO-}"
60+
if [ -z "${REPO_SLUG}" ]; then
61+
ORIGIN_URL="$(git remote get-url origin 2>/dev/null)" || ORIGIN_URL=''
62+
case "${ORIGIN_URL}" in
63+
*github.com[:/]*) REPO_SLUG="$(printf '%s' "${ORIGIN_URL}" | sed 's#^.*github\.com[:/]##;s#\.git$##')" ;;
64+
esac
65+
fi
66+
: "${REPO_SLUG:=nvm-sh/nvm}"
67+
68+
TAB="$(printf '\t')"
69+
70+
COMMITS_FILE="$(mktemp)" || exit 1
71+
GH_ERR_FILE="$(mktemp)" || exit 1
72+
trap 'rm -f "${COMMITS_FILE}" "${GH_ERR_FILE}"' EXIT
73+
74+
# each line: <section key> TAB <sha> TAB <bullet text>, in git log order
75+
git log --no-merges --format='%H %s' "${PREV_REF}..${END_REF}" | while IFS=' ' read -r SHA SUBJECT; do
76+
LOWERED="$(printf '%s' "${SUBJECT}" | tr '[:upper:]' '[:lower:]')"
77+
STRIPPED="${SUBJECT#\[*\] }"
78+
case "${LOWERED}" in
79+
v[0-9]*.[0-9]*.[0-9]*)
80+
case "${SUBJECT}" in
81+
*' '*) # not a bump commit, just a subject that starts with a version
82+
printf >&2 'release-notes: no category prefix, filing under Misc: %s\n' "${SUBJECT}"
83+
KEY='misc' TEXT="${SUBJECT}"
84+
;;
85+
*) continue ;; # a version bump commit
86+
esac
87+
;;
88+
'[new] '*) KEY='new' TEXT="${STRIPPED}" ;;
89+
'[fix] '* | '[patch] '*) KEY='fix' TEXT="${STRIPPED}" ;;
90+
'[refactor] '*) KEY='refactor' TEXT="${STRIPPED}" ;;
91+
'[shellcheck] '*) KEY='refactor' TEXT="${SUBJECT}" ;;
92+
'[robustness] '*) KEY='robustness' TEXT="${STRIPPED}" ;;
93+
'[perf] '* | '[performance] '*) KEY='perf' TEXT="${STRIPPED}" ;;
94+
'[dockerfile] '*) KEY='dockerfile' TEXT="${STRIPPED}" ;;
95+
'[docs] '* | '[doc] '*) KEY='docs' TEXT="${STRIPPED}" ;;
96+
'[readme] '*) KEY='docs' TEXT="${SUBJECT}" ;;
97+
'[meta] '* | '[security] '* | '[dev deps] '*)
98+
KEY='misc' TEXT="${SUBJECT}" ;;
99+
'[tests] '* | '[test] '*) KEY='tests' TEXT="${STRIPPED}" ;;
100+
'[actions] '* | '[debug] '*) KEY='tests' TEXT="${SUBJECT}" ;;
101+
*)
102+
printf >&2 'release-notes: no category prefix, filing under Misc: %s\n' "${SUBJECT}"
103+
KEY='misc' TEXT="${SUBJECT}"
104+
;;
105+
esac
106+
printf '%s\t%s\t%s\n' "${KEY}" "${SHA}" "${TEXT}"
107+
done > "${COMMITS_FILE}"
108+
109+
if ! [ -s "${COMMITS_FILE}" ]; then
110+
printf >&2 'release-notes: no commits found in %s..HEAD.\n' "${PREV_REF}"
111+
fi
112+
113+
print_section() {
114+
if ! grep -q "^$1${TAB}" "${COMMITS_FILE}"; then
115+
return 0
116+
fi
117+
printf '\n%s\n\n' "$2"
118+
grep "^$1${TAB}" "${COMMITS_FILE}" | while IFS="${TAB}" read -r _KEY SHA TEXT; do
119+
REF=''
120+
case "${TEXT}" in
121+
*'(#'[0-9]*')') ;; # the subject already carries a reference
122+
*)
123+
if [ "${LOOKUP_PRS}" = 1 ]; then
124+
PR="$(gh api "repos/${REPO_SLUG}/commits/${SHA}/pulls" --jq '.[0].number // empty' 2>"${GH_ERR_FILE}")" || {
125+
if grep -q 'No commit found' "${GH_ERR_FILE}"; then
126+
PR='' # not pushed to GitHub yet, so it can not have come from a PR
127+
else
128+
printf >&2 'release-notes: failed to look up the PR for commit %s:\n' "${SHA}"
129+
cat >&2 "${GH_ERR_FILE}"
130+
exit 1
131+
fi
132+
}
133+
case "${PR}" in
134+
'' | *[!0-9]*) ;;
135+
*) REF=" (#${PR})" ;;
136+
esac
137+
fi
138+
;;
139+
esac
140+
printf ' - %s%s\n' "${TEXT}" "${REF}"
141+
done || exit 1
142+
}
143+
144+
printf '%s\n' "${NEW_VERSION}"
145+
print_section 'new' 'New Stuff' &&
146+
print_section 'fix' 'Bug Fixes' &&
147+
print_section 'refactor' 'Refactors' &&
148+
print_section 'robustness' 'Robustness' &&
149+
print_section 'perf' 'Performance' &&
150+
print_section 'dockerfile' 'Dockerfile' &&
151+
print_section 'docs' 'Docs' &&
152+
print_section 'misc' 'Misc' &&
153+
print_section 'tests' 'Tests'

0 commit comments

Comments
 (0)