#!/usr/bin/env bash set -euo pipefail usage() { echo "Usage: scripts/release X.Y.Z" >&2 echo "Set RELEASE_DATE=YYYY-MM-DD to override the changelog/CITATION date." >&2 } die() { echo "release: $*" >&2 exit 1 } require_clean_tracked_tree() { git diff --quiet || die "tracked worktree changes are present" git diff --cached --quiet || die "staged changes are present" } release_branch() { local version="$1" local branch="release-${version}" local current_branch current_branch="$(git branch --show-current || true)" if [[ "$current_branch" == "$branch" ]]; then return fi if git show-ref --verify --quiet "refs/heads/${branch}"; then die "branch ${branch} already exists; switch to it or delete it first" fi git switch -c "$branch" } require_match() { local pattern="$1" local path="$2" grep -qE "$pattern" "$path" || die "${path} does not match ${pattern}" } replace_required() { local pattern="$1" local replacement="$2" local path="$3" require_match "$pattern" "$path" sed -i -E "s#${pattern}#${replacement}#" "$path" } release_update_file() { local extname="$1" local update_dir="$2" local cmake_file="$3" local version="$4" local matches old_file new_file old_name new_name shopt -s nullglob matches=("${update_dir}/${extname}--"*--unreleased.sql) shopt -u nullglob if [[ ${#matches[@]} -ne 1 ]]; then die "expected exactly one ${update_dir}/${extname}--*--unreleased.sql, found ${#matches[@]}" fi old_file="${matches[0]}" new_file="${old_file/--unreleased.sql/--${version}.sql}" old_name="$(basename "$old_file")" new_name="$(basename "$new_file")" [[ "$old_file" != "$new_file" ]] || die "refusing to rename ${old_file} to itself" [[ ! -e "$new_file" ]] || die "${new_file} already exists" git mv "$old_file" "$new_file" sed -i "s/unreleased/${version}/g" "$new_file" sed -i -E 's/[[:space:]]+$//' "$new_file" grep -qF "$old_name" "$cmake_file" || die "${cmake_file} does not reference ${old_name}" OLD_NAME="$old_name" NEW_NAME="$new_name" \ perl -0pi -e 's/\Q$ENV{OLD_NAME}\E/$ENV{NEW_NAME}/g' "$cmake_file" } changelog_previous_version() { sed -nE 's#^\[unreleased\]: .*/compare/v([0-9]+\.[0-9]+\.[0-9]+)\.\.\.HEAD$#\1#p' CHANGELOG.md } require_changelog_release_ready() { local version="$1" local previous_version previous_version="$(changelog_previous_version)" [[ -n "$previous_version" ]] || die "could not determine previous version from CHANGELOG.md [unreleased] link" if grep -qF "## [${version}]" CHANGELOG.md || grep -qF "[${version}]:" CHANGELOG.md; then die "CHANGELOG.md already has a [${version}] release entry" fi } update_changelog() { local version="$1" local release_date="$2" local previous_version tmp require_changelog_release_ready "$version" previous_version="$(changelog_previous_version)" tmp="$(mktemp)" awk -v version="$version" -v release_date="$release_date" ' BEGIN { released = 0; in_release = 0; skip_summary = 0 } !released && $0 == "## [Unreleased]" { print "## [Unreleased]" print "" print "## [" version "] - " release_date released = 1 in_release = 1 next } in_release && $0 ~ /^[[:space:]]*
[[:space:]]*$/ { skip_summary = 1 next } skip_summary { if ($0 ~ /^[[:space:]]*<\/summary>[[:space:]]*$/) skip_summary = 0 next } in_release && $0 ~ /^[[:space:]]*<\/details>[[:space:]]*$/ { next } in_release && $0 ~ /^## \[/ { in_release = 0 } { print } ' CHANGELOG.md > "$tmp" mv "$tmp" CHANGELOG.md tmp="$(mktemp)" awk -v version="$version" -v previous="v${previous_version}" ' /^\[unreleased\]: / { print "[unreleased]: https://github.com/postgis/h3-pg/compare/v" version "...HEAD" print "[" version "]: https://github.com/postgis/h3-pg/compare/" previous "...v" version next } { print } ' CHANGELOG.md > "$tmp" mv "$tmp" CHANGELOG.md } verify_update_references() { local cmake_file update_path for cmake_file in h3/CMakeLists.txt h3_postgis/CMakeLists.txt; do while IFS= read -r update_path; do [[ -f "$(dirname "$cmake_file")/${update_path}" ]] || die "${cmake_file} references missing ${update_path}" done < <(grep -oE 'sql/updates/[A-Za-z0-9_.-]+\.sql' "$cmake_file") done } verify_release_state() { local version="$1" verify_update_references if find h3/sql/updates h3_postgis/sql/updates -name '*--unreleased.sql' | grep -q .; then find h3/sql/updates h3_postgis/sql/updates -name '*--unreleased.sql' >&2 die "unreleased update files remain after release" fi if grep -R -n -E 'availability: unreleased|Since vunreleased|UPDATE TO '\''unreleased'\''|--unreleased\.sql' \ h3 h3_postgis docs/api.md CMakeLists.txt; then die "unreleased release markers remain" fi grep -q "VERSION ${version}" CMakeLists.txt || die "CMakeLists.txt project version was not updated" grep -q "set(INSTALL_VERSION \"\${PROJECT_VERSION}\")" CMakeLists.txt || die "INSTALL_VERSION is not set to PROJECT_VERSION" scripts/check-metadata git diff --check } main() { [[ $# -eq 1 ]] || { usage; exit 2; } local version="$1" local release_date="${RELEASE_DATE:-$(date +%F)}" local repo_root h3_core_version [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || die "version must be X.Y.Z" [[ "$release_date" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || die "RELEASE_DATE must be YYYY-MM-DD" repo_root="$(git rev-parse --show-toplevel)" cd "$repo_root" h3_core_version="$(sed -rnE 's/^set\(H3_CORE_VERSION ([0-9]+\.[0-9]+\.[0-9]+)\)/\1/p' CMakeLists.txt)" [[ -n "$h3_core_version" ]] || die "could not parse H3_CORE_VERSION" [[ "${version%.*}" == "${h3_core_version%.*}" ]] || die "h3-pg major.minor (${version%.*}) must match H3 core major.minor (${h3_core_version%.*})" require_changelog_release_ready "$version" require_clean_tracked_tree release_branch "$version" replace_required ' VERSION [0-9]+\.[0-9]+\.[0-9]+' " VERSION ${version}" CMakeLists.txt sed -i "/set(INSTALL_VERSION \"\${PROJECT_VERSION}\")/s/^#//g" CMakeLists.txt sed -i '/set(INSTALL_VERSION "unreleased")/s/^/#/g' CMakeLists.txt sed -i "s/availability: unreleased/availability: ${version}/g" h3/sql/install/*.sql sed -i "s/availability: unreleased/availability: ${version}/g" h3_postgis/sql/install/*.sql release_update_file h3 h3/sql/updates h3/CMakeLists.txt "$version" release_update_file h3_postgis h3_postgis/sql/updates h3_postgis/CMakeLists.txt "$version" sed -i "s/ALTER EXTENSION h3 UPDATE TO 'unreleased'/ALTER EXTENSION h3 UPDATE TO '${version}'/g" \ h3/test/sql/extension.sql h3/test/expected/extension.out sed -i -E "s/^version: v[0-9]+\.[0-9]+\.[0-9]+/version: v${version}/" CITATION.cff sed -i -E "s/^date-released: [0-9]{4}-[0-9]{2}-[0-9]{2}/date-released: ${release_date}/" CITATION.cff update_changelog "$version" "$release_date" scripts/documentation verify_release_state "$version" echo "Release branch release-${version} is prepared." echo "Next:" echo " - review and commit the release-${version} changes" echo " - push and merge the release branch" echo " - have the release manager publish GitHub release v${version} from CHANGELOG.md" echo " - run scripts/bundle and upload to PGXN" echo " - run scripts/postrelease after the release is merged" } main "$@"