#!/usr/bin/env bashset -euo pipefailusage() {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: $*" >&2exit 1}require_command() {command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"}confirm() {local answerif [[ "$assume_yes" == true ]]; thenreturn 0firead -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 EmeraldGold Gray Green Indigo Ivory Lavender Lime Magenta Maroon NavyOchre Olive Orange Peach Pink Plum Purple Red Rose RubySapphire Scarlet Silver Tan Teal Turquoise Violet White Yellow)local animals=(Alpaca Badger Bear Beaver Bison Boa Bobcat Buffalo Camel CapybaraCat Cheetah Cobra Condor Cougar Coyote Crane Crow Deer DolphinDonkey Eagle Elephant Falcon Ferret Finch Fox Gecko Giraffe GoatGoldfish Goose Heron Hippo Horse Ibex Iguana Jaguar Koala LemurLeopard Lion Llama Lobster Lynx Mantis Mole Moose Narwhal OctopusOtter Owl Oyster Panda Panther Parrot Pelican Penguin Pigeon PumaRabbit Raven Rhino Seal Shark Sheep Sloth Sparrow Squid Swan TigerToad Toucan Turtle Walrus Whale Wolf Wombat Yak Zebra)local candidatelocal attempts=0while (( attempts < 200 )); docandidate="${colors[RANDOM % ${#colors[@]}]} ${animals[RANDOM % ${#animals[@]}]}"if ! title_exists "$candidate"; thenprintf '%s\n' "$candidate"return 0fiattempts=$((attempts + 1))donedie "could not generate an unused release title"}dry_run=falseassume_yes=falsewhile (( $# > 0 )); docase "$1" in--dry-run)dry_run=trueshift;;--yes)assume_yes=trueshift;;-h|--help)usageexit 0;;--)shiftbreak;;-*)die "unknown option: $1";;*)break;;esacdone(( $# >= 1 && $# <= 2 )) || {usage >&2exit 1}version=$1requested_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 gitrequire_command ghrequire_command batsrepo_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; thendie "tag already exists: $version"fiif gh release view "$version" >/dev/null 2>&1; thendie "GitHub release already exists: $version"ficurrent_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_titletitle_exists "$title" && die "release title is already in use: $title"elsetitle=$(generate_title)fiechoecho "Release plan"echo " Version: $current_version -> $version"echo " Title: $title"echo " Branch: $branch"echo " Notes: GitHub-generated draft"echoif [[ "$dry_run" == true ]]; thenecho "Running bats test..."bats testechoecho "Dry run complete; no files, commits, tags, or releases were changed."exit 0ficonfirm "Prepare the release commit and tag?" || die "cancelled"sed -i.bak "s/^# V.*$/# V${version}/" bash-preexec.shrm -f bash-preexec.sh.bakecho "Running bats test..."if ! bats test; thensed -i.bak "s/^# V.*$/# V${current_version}/" bash-preexec.shrm -f bash-preexec.sh.bakdie "tests failed; restored bash-preexec.sh"figit diff --checkgit add -- bash-preexec.shgit commit -m "Preparing to tag v${version}"git tag "$version"if ! confirm "Push $branch and tag $version, then create the draft GitHub release?"; thenechoecho "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 0figit push --atomic origin "$branch" "refs/tags/$version"echoecho "Creating draft GitHub release..."if release_url=$(gh release create "$version" \--verify-tag \--title "$title" \--generate-notes \--draft); thenecho "Draft release created: $release_url"elseecho "The commit and tag were pushed, but the draft release could not be created." >&2echo "Retry with:" >&2echo " gh release create $version --verify-tag --title \"$title\" --generate-notes --draft" >&2exit 1fi