Ghost versions fix · fix/pre-push-ghost-versions

Closes #1457.

The bug

Two manual bump-version.sh minor calls in the last 72 hours created tag-less version numbers:

v7.65.0   ← tagged (just now)
v7.63.1   ← tagged
v7.63.0   ← tagged
v7.61.0   ← tagged
v7.62.0   ← GHOST — my bump, release-please picked 7.63.0
v7.64.0   ← GHOST — my bump, release-please picked 7.65.0
v7.60.0   ← tagged

Root cause

Version enforcement lives in two places that must stay in lock-step:

  1. bin/git-hooks/pre-push L23 — local fail-fast
  2. .github/workflows/version-check.yml L41 — server-side "Validate: Version" gate

Both enforced manual version bumps on feat/fix/perf/refactor/* branches. Release-please (release-type: simple) computes its own next version from conventional-commit types since last tag and overwrites whatever the manual bump produced. When they disagreed, the manually-typed version became a ghost.

First attempt was naive. The initial patch only fixed the pre-push hook. The CI workflow still enforced — PR went green locally, red on GitHub. Lesson: grep for version-check and Version Bump before scoping a gate fix.

The fix (two layers)

1. Local hook — bin/git-hooks/pre-push

- if [[ "$BRANCH" =~ ^(docs|chore|ci|style|test)/ ]]; then
+ if [[ "$BRANCH" =~ ^(docs|chore|ci|style|test|feat|fix|perf|refactor)/ ]]; then
    echo "  ✓ Skipping version check for $BRANCH"
    exit 0
  fi

2. CI workflow — .github/workflows/version-check.yml

- if [[ "$BRANCH_NAME" =~ ^(docs|chore|ci|style|test)/ ]]; then
+ if [[ "$BRANCH_NAME" =~ ^(docs|chore|ci|style|test|feat|fix|perf|refactor)/ ]]; then
    echo "skip=true" >> $GITHUB_OUTPUT
    exit 0
  fi

3. Drift test — tests/unit/test-pre-push-hook.sh

New test_skip_regex_parity extracts the alternation group from both files and fails if they differ. Prevents the same two-layer split from recurring.

Release-please now owns version decisions on all conventional-commit branches. Both layers agree. Bare-named branches (hotfix-auth-bug, quick-patch) still trigger the check for legitimate manual control.

Also shipped

CONTRIBUTING.md gains a "Versioning" section documenting:

Install note

git config core.hooksPath = bin/git-hooks is already set. .git/hooks/pre-push is a symlink. Local change is live without a separate install step. CI workflow update takes effect on the next PR event.

Expected outcome

Next feat/* or fix/* PR to main: pre-push runs, sees branch prefix in the new skip list, exits 0. Server-side gate does the same. Release-please opens a PR with its computed version, no ghost number.