# 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:

- Don't manually run `bump-version.sh` on conventional-commit branches
- Use release-please's `release-as` commit footer if you need to override its computed next
- Hotfix branches with bare names still trigger the check (for legitimate manual control)

## 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.