Files
dsh-slopo agent ffefc42e01 feat: derive the release bump from conventional commit subjects
The bump was decided by `[major]` / `[minor]` markers alone, so a range of
ordinary `fix:` commits and a range of `feat:` commits released the same way,
and the version number said nothing about what changed.

Read the intent from the subject instead, keeping the explicit markers as
overrides for the cases a subject cannot carry:

  `feat` as its own word   minor   0.1.0 -> 0.2.0
  `!:` or BREAKING         major   0.1.0 -> 1.0.0
  anything else            patch   0.1.0 -> 0.1.1

`feat` has to be a word, so "Fix the feature flag leak" is a patch rather than
a minor. Only the subject line is read; the body is not.

Adds `npm run release:notes`, which prints the notes a push would publish, and
AGENTS.md, which states the rules where an agent will read them.
2026-09-26 06:38:08 +01:00

200 lines
7.0 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Tests for the release script, in throwaway git repositories.
#
# These paths only occur at moments you cannot stage by hand — a repository with
# no tags, a range that asks for no release, a version whose patch number is
# already two digits — and getting them wrong publishes a wrong version or no
# release at all. Run with: bash .gitea/scripts/release.test.sh
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
RELEASE="$HERE/release.sh"
PASS=0
FAIL=0
# A fresh repository with one commit, optionally tagged.
new_repo() {
local dir
dir="$(mktemp -d)"
cd "$dir"
git init -q
git config user.email test@example.com
git config user.name Test
printf '{\n "name": "thing",\n "version": "%s"\n}\n' "${1:-0.1.0}" > package.json
git add -A
git commit -qm "Scaffold the thing"
echo "$dir"
}
commit() {
git commit -q --allow-empty -m "$1"
}
tag() {
git tag "$1"
}
check() {
local label="$1" expected="$2" actual="$3"
if [ "$expected" = "$actual" ]; then
PASS=$((PASS + 1))
printf ' ok %s\n' "$label"
else
FAIL=$((FAIL + 1))
printf ' FAIL %s\n expected: %s\n actual: %s\n' "$label" "$expected" "$actual"
fi
}
check_contains() {
local label="$1" needle="$2" haystack="$3"
if printf '%s' "$haystack" | grep -qF -- "$needle"; then
PASS=$((PASS + 1))
printf ' ok %s\n' "$label"
else
FAIL=$((FAIL + 1))
printf ' FAIL %s\n missing: %s\n in: %s\n' "$label" "$needle" "$haystack"
fi
}
check_absent() {
local label="$1" needle="$2" haystack="$3"
if printf '%s' "$haystack" | grep -qF -- "$needle"; then
FAIL=$((FAIL + 1))
printf ' FAIL %s\n should not contain: %s\n' "$label" "$needle"
else
PASS=$((PASS + 1))
printf ' ok %s\n' "$label"
fi
}
echo "semver arithmetic"
check "patch" "0.1.1" "$(bash "$RELEASE" apply-bump 0.1.0 patch)"
check "minor" "0.2.0" "$(bash "$RELEASE" apply-bump 0.1.5 minor)"
check "major" "1.0.0" "$(bash "$RELEASE" apply-bump 0.9.9 major)"
check "patch to 10" "0.1.10" "$(bash "$RELEASE" apply-bump 0.1.9 patch)"
check "minor to 10" "0.10.0" "$(bash "$RELEASE" apply-bump 0.9.3 minor)"
check "prerelease" "0.1.1" "$(bash "$RELEASE" apply-bump 0.1.0-rc.1 patch)"
echo
echo "first release with no tags"
new_repo 0.2.0 >/dev/null
check "version is the declared one" "0.2.0" "$(bash "$RELEASE" next-version patch)"
check "no tag yet" "" "$(bash "$RELEASE" last-tag)"
cd /;
echo
echo "subsequent releases move forward from the tag"
new_repo 0.1.0 >/dev/null
tag v0.1.3
check "patch" "0.1.4" "$(bash "$RELEASE" next-version patch)"
check "minor" "0.2.0" "$(bash "$RELEASE" next-version minor)"
check "major" "1.0.0" "$(bash "$RELEASE" next-version major)"
commit "Fix the thing"
check "bump defaults to patch" "patch" "$(bash "$RELEASE" bump "v0.1.3..HEAD")"
commit "Add a capability [minor]"
check "marker raises the bump" "minor" "$(bash "$RELEASE" bump "v0.1.3..HEAD")"
check "and the version follows" "0.2.0" "$(bash "$RELEASE" next-version "$(bash "$RELEASE" bump "v0.1.3..HEAD")")"
commit "Rewrite everything [major]"
check "major outranks minor" "major" "$(bash "$RELEASE" bump "v0.1.3..HEAD")"
cd /;
echo
echo "the subject decides the bump"
new_repo 0.1.0 >/dev/null
tag v0.1.0
check "prose is a patch" "patch" "$(bash "$RELEASE" bump "v0.1.0..HEAD")"
commit "feat: add the recall index"
check "feat is a minor" "minor" "$(bash "$RELEASE" bump "v0.1.0..HEAD")"
check "so 0.1 -> 0.2" "0.2.0" "$(bash "$RELEASE" next-version "$(bash "$RELEASE" bump "v0.1.0..HEAD")")"
cd /;
new_repo 0.1.0 >/dev/null
tag v0.1.0
commit "feat(scope): add the scope filter"
check "a scoped feat counts" "minor" "$(bash "$RELEASE" bump "v0.1.0..HEAD")"
cd /;
new_repo 0.1.0 >/dev/null
tag v0.1.0
commit "Fix the feature flag leak"
check "feat inside a word is not a feature" "no" "$(bash "$RELEASE" is-feature "Fix the feature flag leak")"
check "so it stays a patch" "patch" "$(bash "$RELEASE" bump "v0.1.0..HEAD")"
cd /;
new_repo 0.1.0 >/dev/null
tag v0.1.0
commit "feat!: move the index to sqlite"
check "a bang is breaking" "yes" "$(bash "$RELEASE" is-breaking "feat!: move the index to sqlite")"
check "so it is a major" "major" "$(bash "$RELEASE" bump "v0.1.0..HEAD")"
check "and 0.1 -> 1.0" "1.0.0" "$(bash "$RELEASE" next-version "$(bash "$RELEASE" bump "v0.1.0..HEAD")")"
NOTES="$(bash "$RELEASE" notes "v0.1.0..HEAD")"
check_contains "breaking gets a section" "### Breaking" "$NOTES"
check_contains "and is listed there" "- move the index to sqlite" "$NOTES"
check_absent "not under Added" "### Added" "$NOTES"
cd /;
echo
echo "skip release"
new_repo 0.1.0 >/dev/null
tag v0.1.0
commit "Update the README [skip release]"
check "skip is detected" "yes" "$(bash "$RELEASE" wants-skip "v0.1.0..HEAD")"
check_absent "skipped work is not in the notes" "Update the README" "$(bash "$RELEASE" notes "v0.1.0..HEAD")"
commit "Fix a real bug"
check "a later real change does not un-skip" "yes" "$(bash "$RELEASE" wants-skip "v0.1.0..HEAD")"
check "so nothing is released" "no" "$(bash "$RELEASE" should-release "v0.1.0..HEAD")"
cd /;
echo
echo "notes only cover the range"
new_repo 0.1.0 >/dev/null
tag v0.1.0
commit "Fix the leaked handle"
commit "Add the status card"
NOTES="$(bash "$RELEASE" notes "v0.1.0..HEAD")"
check_contains "includes the new work" "Add the status card" "$NOTES"
check_contains "includes the fix" "Fix the leaked handle" "$NOTES"
check_absent "excludes released work" "Scaffold the thing" "$NOTES"
check "a range with real work releases" "yes" "$(bash "$RELEASE" should-release "v0.1.0..HEAD")"
cd /;
echo
echo "conventional commits are grouped, plain ones are not"
new_repo 0.1.0 >/dev/null
tag v0.1.0
commit "feat: add the release workflow"
commit "fix: stop the runner tagging twice"
commit "docs: explain the markers"
NOTES="$(bash "$RELEASE" notes "v0.1.0..HEAD")"
check_contains "grouped heading" "### Added" "$NOTES"
check_contains "grouped fix" "### Fixed" "$NOTES"
check_contains "prefix stripped" "- add the release workflow" "$NOTES"
check_absent "no raw prefix" "feat: add" "$NOTES"
cd /;
echo
new_repo 0.1.0 >/dev/null
tag v0.1.0
commit "Background only the warm-up, and use the framework's own parameter name"
commit "Make the settings card behave like the other cards"
NOTES="$(bash "$RELEASE" notes "v0.1.0..HEAD")"
check_absent "prose commits get no invented headings" "### Added" "$NOTES"
check_contains "and stay as bullets" "- Make the settings card behave" "$NOTES"
cd /;
echo
echo "an empty range says so rather than printing nothing"
new_repo 0.1.0 >/dev/null
tag v0.1.0
check "empty range" "_No changes._" "$(bash "$RELEASE" notes "v0.1.0..HEAD")"
# The release commit is itself a push to master; without this the next run would
# publish a release describing nothing.
check "an empty range releases nothing" "no" "$(bash "$RELEASE" should-release "v0.1.0..HEAD")"
cd /;
echo
printf 'passed %d, failed %d\n' "$PASS" "$FAIL"
[ "$FAIL" -eq 0 ]