Bump the patch version on every commit
Some checks are pending
release-apk / release (APK) (push) Waiting to run

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
This commit is contained in:
Krzysztof Rudnicki 2026-08-09 23:03:59 +02:00
parent cc389ccb6e
commit a494843d0a
3 changed files with 157 additions and 1 deletions

View File

@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
version: 1.0.1+1
environment:
sdk: ^3.12.2

122
scripts/bump_patch_version.sh Executable file
View File

@ -0,0 +1,122 @@
#!/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 "$@"

34
scripts/install_hooks.sh Executable file
View File

@ -0,0 +1,34 @@
#!/bin/bash
# ============================================================================
# Install this repo's git hooks.
#
# .git/hooks/ is not tracked, so a fresh clone has no hooks. Run this once
# after cloning. Unlike habit_stack and diet-guard, this repo does not use the
# pre-commit framework (its quality gate is CI), so the hook is a plain script.
# ============================================================================
set -euo pipefail
REPO_ROOT="$(git rev-parse --show-toplevel)"
readonly REPO_ROOT
readonly HOOK="$REPO_ROOT/.git/hooks/pre-commit"
main() {
if [[ -e "$HOOK" ]] && ! grep -q 'bump_patch_version.sh' "$HOOK"; then
echo "Error: $HOOK exists and is not ours; refusing to clobber it." >&2
exit 1
fi
cat > "$HOOK" <<'HOOK_BODY'
#!/bin/bash
# Installed by scripts/install_hooks.sh — see that file.
set -euo pipefail
exec bash "$(git rev-parse --show-toplevel)/scripts/bump_patch_version.sh"
HOOK_BODY
chmod 755 "$HOOK"
echo "Installed pre-commit hook: $HOOK"
}
main "$@"