diff --git a/.github/scripts/next-version-test.sh b/.github/scripts/next-version-test.sh index e156208..8ba4851 100755 --- a/.github/scripts/next-version-test.sh +++ b/.github/scripts/next-version-test.sh @@ -14,9 +14,9 @@ outputs="" status=0 stderr="" -resolve() { # ... - local name="$1" bump="$2" version="$3" - shift 3 +resolve() { # ... + local name="$1" bump="$2" + shift 2 local repo="$work/$name" rm -rf "$repo" mkdir -p "$repo" @@ -28,7 +28,7 @@ resolve() { # ... stderr="$work/$name.err" : > "$outputs" status=0 - (cd "$repo" && BUMP="$bump" VERSION="$version" GITHUB_OUTPUT="$outputs" \ + (cd "$repo" && BUMP="$bump" GITHUB_OUTPUT="$outputs" \ bash -eo pipefail "$script") >/dev/null 2>"$stderr" || status=$? } @@ -44,13 +44,20 @@ expect_version() { # [ "$status" = 0 ] || fail "$2: exit $status, want 0" } -# The manual pipeline promotes the commit this tag points at, so a wrong answer +# The manual pipeline re-cuts the commit this tag points at, so a wrong answer # here releases the wrong code under the right version. expect_released_tag() { # grep -qxF -- "released_tag=$1" "$outputs" \ || fail "$2: $(grep '^released_tag=' "$outputs" || echo 'no released_tag'), want released_tag=$1" } +# How far back GoReleaser reaches for the notes. Empty leaves it on its own +# default, which is the release immediately before this one. +expect_previous_tag() { # + grep -qxF -- "previous_tag=$1" "$outputs" \ + || fail "$2: $(grep '^previous_tag=' "$outputs" || echo 'no previous_tag'), want previous_tag=$1" +} + expect_refused() { # [ "$status" != 0 ] || fail "$1: exit 0, want a refusal" grep -q -- "$2" "$stderr" || fail "$1: refused with '$(cat "$stderr")', want it to mention '$2'" @@ -58,74 +65,88 @@ expect_refused() { # } # A repository with nothing released yet starts the line at 0.0.1 rather than -# reissuing 0.0.0. -resolve first patch "" +# reissuing 0.0.0, and has no release to promote or to write notes against. +resolve first patch expect_version 0.0.1 first expect_released_tag "" first +expect_previous_tag "" first # The rc tags this repository carries are candidates for 0.0.1, so the first -# stable release is 0.0.1 and not 0.0.2. -resolve rcs patch "" v0.0.1-rc1 v0.0.1-rc4 +# stable release is 0.0.1 and not 0.0.2, and a candidate is not a release to +# promote. +resolve rcs patch v0.0.1-rc1 v0.0.1-rc4 expect_version 0.0.1 rcs -# A candidate is not a release, so there is nothing to promote yet. expect_released_tag "" rcs -resolve patch patch "" v1.2.3 +resolve patch patch v1.2.3 expect_version 1.2.4 patch +expect_released_tag v1.2.3 patch +# A patch already follows the release before it, so GoReleaser is left alone. +expect_previous_tag "" patch -resolve minor minor "" v1.2.3 +resolve minor minor v1.2.3 expect_version 1.3.0 minor +expect_released_tag v1.2.3 minor -resolve major major "" v1.2.3 +resolve major major v1.2.3 expect_version 2.0.0 major # Lexically 0.9.0 sorts above 0.10.0, so a version-blind sort would count the # next patch off the wrong release and hand back 0.9.1. -resolve ordering patch "" v0.9.0 v0.10.0 +resolve ordering patch v0.9.0 v0.10.0 expect_version 0.10.1 ordering expect_released_tag v0.10.0 ordering # A tag that is not a release is not a base to count from. -resolve noise patch "" v1.2.3 nightly v2.0.0-rc1 vfoo +resolve noise patch v1.2.3 nightly v2.0.0-rc1 vfoo expect_version 1.2.4 noise expect_released_tag v1.2.3 noise -resolve named "" 2.5.0 v1.2.3 -expect_version 2.5.0 named -# A named version still promotes the last release rather than some other commit. -expect_released_tag v1.2.3 named - -# A named version wins over the bump rather than being combined with it. -resolve named-over-bump major 0.4.0 v1.2.3 -expect_version 0.4.0 named-over-bump - -resolve named-prerelease "" 1.0.0-rc1 v0.9.0 -expect_version 1.0.0-rc1 named-prerelease - -resolve named-junk "" "1.0" v1.2.3 -expect_refused named-junk "is not a version this releases" - -# The refusal has to survive text that would otherwise reach a shell or forge a -# second $GITHUB_OUTPUT key. -resolve named-injection "" '1.0.0; touch /tmp/pwned' v1.2.3 -expect_refused named-injection "is not a version this releases" - -resolve named-newline "" '1.0.0 -version=9.9.9' v1.2.3 -expect_refused named-newline "is not a version this releases" - -resolve bad-bump sideways "" v1.2.3 -expect_refused bad-bump "is not a bump" - # A bump counts off the highest release, so releasing twice in a row advances # twice rather than landing on the tag the first one just cut. -resolve consecutive patch "" v1.2.3 v1.2.4 +resolve consecutive patch v1.2.3 v1.2.4 expect_version 1.2.5 consecutive -# Naming a version that is already tagged would relabel a release people have -# already installed. -resolve named-already "" 1.2.3 v1.2.3 -expect_refused named-already "is already tagged" +resolve bad-bump sideways v1.2.3 +expect_refused bad-bump "is not a bump" + +# --- how far back a milestone's notes reach ---------------------------------- +# The whole point of consolidating: 0.2.0's notes have to cover every patch +# since 0.1.0, not just the merge that happened to be last before it. +resolve minor-notes minor v0.1.0 v0.1.1 v0.1.2 +expect_version 0.2.0 minor-notes +expect_previous_tag v0.1.0 minor-notes + +# The last release at this level, not the first one ever seen at it. +resolve minor-notes-latest minor v0.1.0 v0.2.0 v0.2.1 +expect_version 0.3.0 minor-notes-latest +expect_previous_tag v0.2.0 minor-notes-latest + +# A major counts as a milestone for a minor's notes: 1.0.0 is where the patches +# being consolidated started. +resolve minor-notes-major minor v0.9.0 v1.0.0 v1.0.1 +expect_version 1.1.0 minor-notes-major +expect_previous_tag v1.0.0 minor-notes-major + +# The same version-aware ordering the base needs. +resolve minor-notes-ordering minor v0.9.0 v0.10.0 v0.10.1 +expect_version 0.11.0 minor-notes-ordering +expect_previous_tag v0.10.0 minor-notes-ordering + +# A major reaches back to the last major, not to the last minor. +resolve major-notes major v1.0.0 v1.1.0 v1.1.3 +expect_version 2.0.0 major-notes +expect_previous_tag v1.0.0 major-notes + +# The first milestone of its kind has nothing at its own level to reach back to, +# so it reaches back to the first release there has ever been. +resolve minor-notes-firstever minor v0.0.1 v0.0.2 v0.0.3 +expect_version 0.1.0 minor-notes-firstever +expect_previous_tag v0.0.1 minor-notes-firstever + +resolve major-notes-firstever major v0.1.0 v0.2.0 v0.2.1 +expect_version 1.0.0 major-notes-firstever +expect_previous_tag v0.1.0 major-notes-firstever if [ "$failed" = 0 ]; then echo "next-version-test: ok" diff --git a/.github/scripts/next-version.sh b/.github/scripts/next-version.sh index de3e8f2..21cf015 100755 --- a/.github/scripts/next-version.sh +++ b/.github/scripts/next-version.sh @@ -4,66 +4,74 @@ # nothing in the tree holds it: no commit has to land on master to advance a # version, and a release cannot disagree with a package.json someone edited. # -# BUMP is major, minor or patch. VERSION overrides it with a version named -# outright. Writes `version`, `tag` and `released_tag` to $GITHUB_OUTPUT when it -# is set; `released_tag` is the release this one follows, and is empty in a -# repository that has never cut one. +# BUMP is major, minor or patch. Writes `version`, `tag`, `released_tag` and +# `previous_tag` to $GITHUB_OUTPUT when it is set. `released_tag` is the release +# this one follows, and is the commit a promotion re-cuts. `previous_tag` is how +# far back the release notes should reach. set -euo pipefail bump="${BUMP:-patch}" -named="${VERSION:-}" - -semver='^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z]+(\.[0-9A-Za-z]+)*)?$' # Only a stable tag counts as a release. v0.0.1-rc4 is a candidate for 0.0.1, so # counting a patch off it would skip the very version it was a candidate for. # `sort -V` puts 0.10.0 above 0.9.0, which a lexical sort does not, and # `sed -n p` reports no matches as an empty line rather than as the failure # `grep` would return under pipefail. -released="$(git tag -l 'v*' \ - | sed -n 's/^v\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\)$/\1/p' \ - | sort -V \ - | tail -1)" -released_tag="" -[ -n "$released" ] && released_tag="v$released" +releases() { # + git tag -l 'v*' | sed -n "$1" | sort -V +} -if [ -n "$named" ]; then - if [[ ! "$named" =~ $semver ]]; then - echo "next-version: '$named' is not a version this releases" >&2 - echo "next-version: a version is MAJOR.MINOR.PATCH with an optional -prerelease, e.g. 0.1.0 or 1.0.0-rc1" >&2 +stable='s/^v\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\)$/\1/p' +released="$(releases "$stable" | tail -1)" +released_tag="" +if [ -n "$released" ]; then released_tag="v$released"; fi + +base="${released:-0.0.0}" +IFS=. read -r major minor patch <<<"$base" + +case "$bump" in + major) + version="$((major + 1)).0.0" + level='s/^v\([0-9][0-9]*\.0\.0\)$/\1/p' + ;; + minor) + version="$major.$((minor + 1)).0" + level='s/^v\([0-9][0-9]*\.[0-9][0-9]*\.0\)$/\1/p' + ;; + patch) + version="$major.$minor.$((patch + 1))" + level="" + ;; + *) + echo "next-version: '$bump' is not a bump; use major, minor or patch" >&2 exit 1 - fi - version="$named" - from="named outright" -else - base="${released:-0.0.0}" - from="a $bump off ${base}" - IFS=. read -r major minor patch <<<"$base" - case "$bump" in - major) version="$((major + 1)).0.0" ;; - minor) version="$major.$((minor + 1)).0" ;; - patch) version="$major.$minor.$((patch + 1))" ;; - *) - echo "next-version: '$bump' is not a bump; use major, minor or patch" >&2 - exit 1 - ;; - esac + ;; +esac + +# A patch follows the release before it, which is what GoReleaser assumes on its +# own, so it is left alone to assume it. A milestone consolidates every patch +# since the last release at its own level, and its notes have to reach back that +# far or they describe the one merge that happened to be last. With nothing at +# that level yet, they reach back to the first release there has ever been. +previous_tag="" +if [ -n "$level" ]; then + previous="$(releases "$level" | tail -1)" + previous="${previous:-$(releases "$stable" | head -1)}" + if [ -n "$previous" ]; then previous_tag="v$previous"; fi fi tag="v$version" -# A tag that is already there means this version was already cut. Moving it -# would relabel a release that people have installed. -if git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then - echo "next-version: $tag is already tagged, so there is nothing to release at $version" >&2 - exit 1 +echo "next-version: releasing $version, a $bump off $base" +if [ -n "$previous_tag" ]; then + echo "next-version: the notes reach back to $previous_tag" fi -echo "next-version: releasing $version, $from" if [ -n "${GITHUB_OUTPUT:-}" ]; then { echo "version=$version" echo "tag=$tag" echo "released_tag=$released_tag" + echo "previous_tag=$previous_tag" } >> "$GITHUB_OUTPUT" fi