|
| 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