todo-app/scripts/bump_patch_version.sh
Krzysztof kuhy Rudnicki a494843d0a
Some checks are pending
release-apk / release (APK) (push) Waiting to run
Bump the patch version on every commit
The AUR package sources the clean vX.Y.Z tag the release workflow cuts, but
that tag only moves when pubspec.yaml's version changes by hand -- so it sat on
v1.0.0 while HEAD moved 8 commits ahead, and the package advertised a stale
tree. Bumping the patch per commit keeps that tag tracking HEAD.

Patch, not minor: minor/major are the "how much broke" signal, and
auto-incrementing them on every commit throws that away (and reaches absurd
numbers within a year).

Two guards keep the number honest, both covered by a scratch-repo run:
re-running the hook over the same staged change is idempotent, because the bump
is computed from the last COMMITTED version rather than the working tree; and a
rebase/merge/cherry-pick in progress is skipped, so replayed commits don't each
inflate it.

.git/hooks is untracked, so scripts/install_hooks.sh makes the hook
reproducible on a fresh clone -- this repo has no pre-commit framework.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aoa1nKWRrAbowz7Hfd3tgL
2026-08-09 23:03:59 +02:00

123 lines
3.8 KiB
Bash
Executable File

#!/bin/bash
# ============================================================================
# Bump the patch version in pubspec.yaml on every commit.
#
# `version: X.Y.Z+B` -> `version: X.Y.(Z+1)+B`. The +build suffix is left
# alone: CI replaces it with the commit count, so it is not ours to manage.
#
# Wired as a pre-commit hook so each commit ships its own bump. That keeps the
# release-apk workflow's clean `vX.Y.Z` tag moving with HEAD instead of
# sitting on whatever version was last hand-edited, which is what left the AUR
# package advertising a tag 8 commits behind.
#
# Deliberately NOT bumping minor: minor/major are the "how much broke" signal.
# Auto-incrementing them on every commit throws that signal away and reaches
# absurd numbers within a year.
# ============================================================================
set -euo pipefail
SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME
PUBSPEC=""
usage() {
echo "Usage: $SCRIPT_NAME [--pubspec PATH]"
echo "Options:"
echo " --pubspec PATH pubspec.yaml to bump (default: repo root's)"
echo " -h, --help Show this help"
exit 0
}
# Bumping while a rebase/merge/amend is replaying commits would inflate the
# version once per replayed commit, so skip those states entirely.
in_replay_state() {
local git_dir
git_dir="$(git rev-parse --git-dir)"
[[ -d "$git_dir/rebase-merge" || -d "$git_dir/rebase-apply" \
|| -f "$git_dir/MERGE_HEAD" || -f "$git_dir/CHERRY_PICK_HEAD" ]]
}
# Only bump when something other than pubspec.yaml is staged. A commit that
# only touches the version (or only docs the hook itself rewrote) must not
# trigger another bump, or `git commit --amend` would climb a version per
# amend.
has_non_pubspec_changes() {
local staged
staged="$(git diff --cached --name-only)"
[[ -n "$(grep -v -e '^$' -e 'pubspec\.yaml$' <<<"$staged")" ]]
}
main() {
if [[ -z "$PUBSPEC" ]]; then
PUBSPEC="$(git rev-parse --show-toplevel)/pubspec.yaml"
fi
if [[ ! -f "$PUBSPEC" ]]; then
echo "Error: no pubspec.yaml at $PUBSPEC" >&2
exit 1
fi
if in_replay_state; then
echo "bump-version: rebase/merge in progress; leaving version alone"
exit 0
fi
if ! has_non_pubspec_changes; then
echo "bump-version: nothing staged but the version itself; not bumping"
exit 0
fi
local raw semver build major minor patch next committed rel_path
raw="$(sed -nE 's/^version:[[:space:]]*(.*)$/\1/p' "$PUBSPEC" | head -1)"
if [[ -z "$raw" ]]; then
echo "Error: no 'version:' line in $PUBSPEC" >&2
exit 1
fi
# Bump relative to the LAST COMMITTED version, not the working-tree one.
# Otherwise re-running the hook over the same staged change (a hook that
# failed, got fixed, and was retried) climbs a patch per attempt instead
# of per commit.
rel_path="${PUBSPEC#"$(git rev-parse --show-toplevel)/"}"
committed="$(git show "HEAD:$rel_path" 2>/dev/null \
| sed -nE 's/^version:[[:space:]]*(.*)$/\1/p' | head -1 || true)"
[[ -n "$committed" ]] && raw="$committed"
semver="${raw%%+*}"
build=""
[[ "$raw" == *+* ]] && build="+${raw#*+}"
if [[ ! "$semver" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
echo "Error: version '$raw' is not X.Y.Z[+B]" >&2
exit 1
fi
major="${BASH_REMATCH[1]}"
minor="${BASH_REMATCH[2]}"
patch="${BASH_REMATCH[3]}"
next="$major.$minor.$((patch + 1))"
sed -i -E "s/^version:[[:space:]]*.*$/version: $next$build/" "$PUBSPEC"
git add "$PUBSPEC"
echo "bump-version: $semver -> $next"
}
while [[ $# -gt 0 ]]; do
case $1 in
--pubspec)
PUBSPEC="$2"
shift 2
;;
-h|--help)
usage
;;
*)
echo "Unknown option: $1" >&2
exit 1
;;
esac
done
main "$@"