#!/usr/bin/env sh set -eu ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) cd "$ROOT_DIR" ABOUT_FILE="$ROOT_DIR/iti/__about__.py" COPIER_FILE="$ROOT_DIR/copier.yml" COPIER_TEMPLATE_FILE="$ROOT_DIR/copier-template/pyproject.toml.jinja" README_FILE="$ROOT_DIR/README.md" die() { printf '%s\n' "$*" >&2 exit 1 } parse_version() { raw=$1 version=${raw#v} old_ifs=$IFS IFS=. set -- $version IFS=$old_ifs if [ "$#" -ne 3 ]; then die "invalid version: $raw" fi for part in "$@"; do case $part in ''|*[!0-9]*) die "invalid version: $raw" ;; esac done printf '%s %s %s\n' "$1" "$2" "$3" } normalize_version() { set -- $(parse_version "$1") printf '%s.%s.%s\n' "$1" "$2" "$3" } bump_patch() { set -- $(parse_version "$1") printf '%s.%s.%s\n' "$1" "$2" "$(( $3 + 1 ))" } version_is_newer() { current_version=$1 target_version=$2 current_parts=$(parse_version "$current_version") set -- $current_parts current_major=$1 current_minor=$2 current_patch=$3 target_parts=$(parse_version "$target_version") set -- $target_parts target_major=$1 target_minor=$2 target_patch=$3 if [ "$target_major" -gt "$current_major" ]; then return 0 fi if [ "$target_major" -lt "$current_major" ]; then return 1 fi if [ "$target_minor" -gt "$current_minor" ]; then return 0 fi if [ "$target_minor" -lt "$current_minor" ]; then return 1 fi [ "$target_patch" -gt "$current_patch" ] } read_current_version() { current_version=$(sed -n 's/^__version__[[:space:]]*=[[:space:]]*"\([^"]*\)".*$/\1/p' "$ABOUT_FILE" | sed -n '1p') [ -n "$current_version" ] || die "version not found in $ABOUT_FILE" printf '%s\n' "$current_version" } replace_first_line_with_prefix() { file=$1 prefix=$2 new_line=$3 tmp_file=$(mktemp) if awk -v prefix="$prefix" -v new="$new_line" ' BEGIN { done = 0 } index($0, prefix) == 1 && !done { print new done = 1 next } { print } END { if (!done) exit 1 } ' "$file" >"$tmp_file"; then mv "$tmp_file" "$file" else rm -f "$tmp_file" die "line not updated in $file" fi } replace_first_line_in_section_with_prefix() { file=$1 section_header=$2 prefix=$3 new_line=$4 tmp_file=$(mktemp) if awk -v section="$section_header" -v prefix="$prefix" -v new="$new_line" ' BEGIN { in_section = 0; done = 0 } $0 == section { in_section = 1; print; next } in_section && /^[^[:space:]]/ { in_section = 0 } in_section && !done && index($0, prefix) == 1 { print new done = 1 next } { print } END { if (!done) exit 1 } ' "$file" >"$tmp_file"; then mv "$tmp_file" "$file" else rm -f "$tmp_file" die "line not updated in $file" fi } write_about_version() { target_version=$1 replace_first_line_with_prefix "$ABOUT_FILE" "__version__ = " "__version__ = \"$target_version\"" } write_copier_framework_tag() { target_version=$1 replace_first_line_in_section_with_prefix "$COPIER_FILE" "framework_tag:" " default: v" " default: v$target_version" } write_copier_system_tag() { target_version=$1 replace_first_line_in_section_with_prefix "$COPIER_FILE" "system_tag:" " default: v" " default: v$target_version" } write_copier_template_version() { target_version=$1 replace_first_line_with_prefix "$COPIER_TEMPLATE_FILE" "version = " "version = \"$target_version\"" } write_readme_dependency_tag() { target_version=$1 replace_first_line_with_prefix "$README_FILE" ' "iti-flask @ git+https://git.noahlan.cn/iti-framework/iTi-Flask.git@v' ' "iti-flask @ git+https://git.noahlan.cn/iti-framework/iTi-Flask.git@v'"$target_version"'",' } write_readme_release_examples() { target_version=$1 replace_first_line_with_prefix "$README_FILE" './iti.sh release v' "./iti.sh release v$target_version" replace_first_line_with_prefix "$README_FILE" 'iti.cmd release v' "iti.cmd release v$target_version" } ensure_clean_tree() { if [ -n "$(git status --porcelain)" ]; then die "working tree is not clean" fi } ensure_branch() { if branch=$(git symbolic-ref --quiet --short HEAD 2>/dev/null); then [ -n "$branch" ] || die "release requires a branch checkout" printf '%s\n' "$branch" else die "release requires a branch checkout" fi } ensure_tag_absent() { tag=$1 if git rev-parse --verify --quiet "refs/tags/$tag" >/dev/null; then die "tag already exists: $tag" fi } main() { version_arg=${1:-} current_version=$(read_current_version) if [ -n "$version_arg" ]; then target_version=$(normalize_version "$version_arg") else target_version=$(bump_patch "$current_version") fi target_tag="v$target_version" if [ "$target_version" = "$current_version" ]; then die "version already set to $target_version" fi if ! version_is_newer "$current_version" "$target_version"; then die "target version must be newer than $current_version" fi ensure_clean_tree branch=$(ensure_branch) ensure_tag_absent "$target_tag" uv run pytest -q write_about_version "$target_version" write_copier_framework_tag "$target_version" write_copier_system_tag "$target_version" write_copier_template_version "$target_version" write_readme_dependency_tag "$target_version" write_readme_release_examples "$target_version" git add iti/__about__.py copier.yml copier-template/pyproject.toml.jinja README.md git commit -m "chore: release $target_tag" git tag -a "$target_tag" -m "release $target_tag" git push origin "$branch" "$target_tag" printf 'released %s\n' "$target_tag" } main "$@"