From 12bffa44b88bbb28617f9b0de1c6d742431f0f07 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sun, 9 Aug 2026 23:04:17 +0200 Subject: [PATCH] Bump the patch version on every commit The AUR package sources a clean vX.Y.Z tag, which only moves when the pubspec version changes by hand -- so the packaged version drifts behind HEAD. Bumping the patch per commit keeps the tag tracking HEAD. The Flutter app lives in app/, so the hook is pointed at app/pubspec.yaml explicitly. Patch, not minor: minor/major are the "how much broke" signal, and auto-incrementing them on every commit throws that away. Two guards keep the number honest: re-running the hook over the same staged change is idempotent (the bump is computed from the last COMMITTED version, not the working tree), and a rebase/merge/cherry-pick in progress is skipped so replayed commits don't each inflate it. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Aoa1nKWRrAbowz7Hfd3tgL --- .pre-commit-config.yaml | 10 +++ scripts/bump_patch_version.sh | 122 ++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100755 scripts/bump_patch_version.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8dd87b2..9e4c746 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -142,6 +142,16 @@ repos: - repo: local hooks: + # Every commit ships its own patch bump, so the release workflow's clean + # vX.Y.Z tag tracks HEAD instead of whatever version was last hand-edited. + # The Flutter app lives in app/, so point the script at its pubspec. + - id: bump-patch-version + name: Bump patch version in app/pubspec.yaml + entry: bash scripts/bump_patch_version.sh --pubspec app/pubspec.yaml + language: system + pass_filenames: false + always_run: true + require_serial: true - id: shellcheck name: shellcheck entry: bash -c 'printf "%s\0" "$@" | xargs -0 -n 40 shellcheck --severity=warning' -- diff --git a/scripts/bump_patch_version.sh b/scripts/bump_patch_version.sh new file mode 100755 index 0000000..c1b1994 --- /dev/null +++ b/scripts/bump_patch_version.sh @@ -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 "$@"