mirror-rcaloras-bash-preexec/script/release
Ryan Caloras 3281b825cb Add release automation script
Add script/release to update the version, run tests, create and push
the release commit and tag, and draft GitHub release notes.

Usage:
  script/release --dry-run 0.7.0
  script/release 0.7.0
  script/release 0.7.0 "Teal Otter"
2026-08-22 22:46:31 -04:00

222 lines
6.4 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage: script/release [--dry-run] [--yes] VERSION ["Color Animal"]
Prepare a release, push it, and create draft release notes on GitHub.
Options:
--dry-run Validate the release and run tests without changing anything.
--yes Skip confirmation prompts.
-h, --help Show this help.
If no title is supplied, an unused Color Animal title is generated.
EOF
}
die() {
echo "release: $*" >&2
exit 1
}
require_command() {
command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"
}
confirm() {
local answer
if [[ "$assume_yes" == true ]]; then
return 0
fi
read -r -p "$1 [y/N] " answer || return 1
[[ "$answer" == "y" || "$answer" == "Y" || "$answer" == "yes" || "$answer" == "YES" ]]
}
title_exists() {
printf '%s\n' "$existing_titles" | grep -Fxi -- "$1" >/dev/null 2>&1
}
generate_title() {
local colors=(
Amber Aqua Black Blue Bronze Brown Coral Crimson Cyan Emerald
Gold Gray Green Indigo Ivory Lavender Lime Magenta Maroon Navy
Ochre Olive Orange Peach Pink Plum Purple Red Rose Ruby
Sapphire Scarlet Silver Tan Teal Turquoise Violet White Yellow
)
local animals=(
Alpaca Badger Bear Beaver Bison Boa Bobcat Buffalo Camel Capybara
Cat Cheetah Cobra Condor Cougar Coyote Crane Crow Deer Dolphin
Donkey Eagle Elephant Falcon Ferret Finch Fox Gecko Giraffe Goat
Goldfish Goose Heron Hippo Horse Ibex Iguana Jaguar Koala Lemur
Leopard Lion Llama Lobster Lynx Mantis Mole Moose Narwhal Octopus
Otter Owl Oyster Panda Panther Parrot Pelican Penguin Pigeon Puma
Rabbit Raven Rhino Seal Shark Sheep Sloth Sparrow Squid Swan Tiger
Toad Toucan Turtle Walrus Whale Wolf Wombat Yak Zebra
)
local candidate
local attempts=0
while (( attempts < 200 )); do
candidate="${colors[RANDOM % ${#colors[@]}]} ${animals[RANDOM % ${#animals[@]}]}"
if ! title_exists "$candidate"; then
printf '%s\n' "$candidate"
return 0
fi
attempts=$((attempts + 1))
done
die "could not generate an unused release title"
}
dry_run=false
assume_yes=false
while (( $# > 0 )); do
case "$1" in
--dry-run)
dry_run=true
shift
;;
--yes)
assume_yes=true
shift
;;
-h|--help)
usage
exit 0
;;
--)
shift
break
;;
-*)
die "unknown option: $1"
;;
*)
break
;;
esac
done
(( $# >= 1 && $# <= 2 )) || {
usage >&2
exit 1
}
version=$1
requested_title=${2:-}
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]] || \
die "version must look like 0.7.0 or 0.7.0-rc.1"
require_command git
require_command gh
require_command bats
repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || die "not inside a Git repository"
cd "$repo_root"
[[ -f bash-preexec.sh ]] || die "run this from the bash-preexec repository"
[[ -z "$(git status --porcelain)" ]] || die "working tree is not clean"
branch=$(git symbolic-ref --quiet --short HEAD) || die "releases cannot be made from a detached HEAD"
default_branch=$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null || true)
default_branch=${default_branch#origin/}
default_branch=${default_branch:-master}
[[ "$branch" == "$default_branch" ]] || \
die "switch to the default branch ($default_branch); currently on $branch"
echo "Fetching the latest branch and tags from origin..."
git fetch --quiet origin "$branch" --tags
[[ "$(git rev-parse HEAD)" == "$(git rev-parse "refs/remotes/origin/$branch")" ]] || \
die "$branch must exactly match origin/$branch before releasing"
if git rev-parse --quiet --verify "refs/tags/$version" >/dev/null; then
die "tag already exists: $version"
fi
if gh release view "$version" >/dev/null 2>&1; then
die "GitHub release already exists: $version"
fi
current_versions=$(sed -n 's/^# V\(.*\)$/\1/p' bash-preexec.sh)
[[ -n "$current_versions" && "$current_versions" != *$'\n'* ]] || \
die "expected exactly one '# V...' version marker in bash-preexec.sh"
current_version=$current_versions
[[ "$current_version" != "$version" ]] || die "bash-preexec.sh is already at version $version"
existing_titles=$(gh release list --limit 1000 --json name --jq '.[].name')
if [[ -n "$requested_title" ]]; then
[[ "$requested_title" =~ ^[[:alpha:]]+[[:space:]][[:alpha:]]+$ ]] || \
die "release title must have the form 'Color Animal'"
title=$requested_title
title_exists "$title" && die "release title is already in use: $title"
else
title=$(generate_title)
fi
echo
echo "Release plan"
echo " Version: $current_version -> $version"
echo " Title: $title"
echo " Branch: $branch"
echo " Notes: GitHub-generated draft"
echo
if [[ "$dry_run" == true ]]; then
echo "Running bats test..."
bats test
echo
echo "Dry run complete; no files, commits, tags, or releases were changed."
exit 0
fi
confirm "Prepare the release commit and tag?" || die "cancelled"
sed -i.bak "s/^# V.*$/# V${version}/" bash-preexec.sh
rm -f bash-preexec.sh.bak
echo "Running bats test..."
if ! bats test; then
sed -i.bak "s/^# V.*$/# V${current_version}/" bash-preexec.sh
rm -f bash-preexec.sh.bak
die "tests failed; restored bash-preexec.sh"
fi
git diff --check
git add -- bash-preexec.sh
git commit -m "Preparing to tag v${version}"
git tag "$version"
if ! confirm "Push $branch and tag $version, then create the draft GitHub release?"; then
echo
echo "Release commit and tag were created locally but not pushed."
echo "To finish later:"
echo " git push --atomic origin $branch refs/tags/$version"
echo " gh release create $version --verify-tag --title \"$title\" --generate-notes --draft"
exit 0
fi
git push --atomic origin "$branch" "refs/tags/$version"
echo
echo "Creating draft GitHub release..."
if release_url=$(gh release create "$version" \
--verify-tag \
--title "$title" \
--generate-notes \
--draft); then
echo "Draft release created: $release_url"
else
echo "The commit and tag were pushed, but the draft release could not be created." >&2
echo "Retry with:" >&2
echo " gh release create $version --verify-tag --title \"$title\" --generate-notes --draft" >&2
exit 1
fi