Files
RedBear-OS/local/scripts/post-checkout-version-sync.sh
T
vasilito 99e5641127 feat: release-bump pipeline + external graphics version sync
Pipeline (3 operator asks):
- bump-release.sh: canonical orchestrator (forks + sources + external)
- upgrade-forks.sh --to=<tag>: rebase forks with diverged-mode guard
- bump-graphics-recipes.sh: map-driven group-aware graphics bumps
- check-external-versions.sh: drift checker for Qt6/KF6/Plasma/Mesa/Wayland
- refresh-fork-upstream-map.sh: append-only map updater with --check
- post-checkout-version-sync.sh + install-git-hooks.sh: opt-in branch hook
- external_version_lib.py: shared version-parsing/bumping library
- external-upstream-map.toml: ~80 external package entries
- bump-fork.sh: deprecated (REDBEAR_I_KNOW_BUMP_FORK_IS_DEPRECATED=1)
- RELEASE-BUMP-WORKFLOW.md: operator runbook

Quality fixes (8 defects from two independent audits):
- blake2b stable cache keys (was hash(), non-portable)
- atomic cache writes via os.replace
- version_sort_key pre-release demotion (was sorting after finals)
- apply_ver_transform re.error tolerance
- grep || true (pipefail abort)
- cd failure detection in upgrade-forks
- sed URL escape (injection hardening)
- refresh-fork-upstream-map last-row drop fix

Doc cleanup:
- Archive 5 obsolete plans to local/docs/archived/
- Remove 14 stale/superseded docs
- Update 18 docs to reference bump-release.sh and fix inbound links
- TOOLS.md drift fixes
2026-07-18 14:45:41 +09:00

215 lines
9.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# post-checkout-version-sync.sh — Optional post-checkout hook for Red Bear OS.
#
# Purpose:
# When an operator switches to a release branch whose name is an anchored
# semver (e.g. `0.3.1`, `0.3.2`), automatically synchronise every Cat 1
# (in-house) and Cat 2 (upstream fork) crate version label to match that
# branch. This is the label-only half of a release bump — it does NOT
# touch fork source content, regenerate lockfiles, or commit anything.
#
# Why label-only:
# Labels are cheap and mechanical (sed rewrites performed by
# sync-versions.sh). Source upgrades (rebasing a fork onto a newer upstream
# tag) and lockfile regen are expensive and operator-judgement steps, so
# they remain explicit (`bump-release.sh --with-sources --with-external`
# and `sync-versions.sh --regen`). See
# local/docs/RELEASE-BUMP-WORKFLOW.md for the full release model.
#
# Git post-checkout contract (git invokes this hook with three arguments):
# $1 = previous HEAD sha
# $2 = new HEAD sha
# $3 = 1 for a branch checkout, 0 for a file checkout
#
# Install (operator opt-in — NEVER auto-installed):
# cp local/scripts/post-checkout-version-sync.sh .git/hooks/post-checkout
# chmod +x .git/hooks/post-checkout
#
# Or via the installer (preferred):
# ./local/scripts/install-git-hooks.sh # post-checkout only
# ./local/scripts/install-git-hooks.sh --all # + pre-push + commit-msg
#
# Bypass for a single checkout:
# REDBEAR_NO_AUTO_SYNC=1 git checkout 0.3.2
#
# Hard guarantees (a failing hook MUST NEVER block a checkout):
# - Always exits 0. Any internal failure degrades to a stderr note + exit 0.
# - No network access. No git mutations (no commit / branch / push / stash).
# - No lockfile regeneration (NEVER calls `sync-versions.sh --regen`).
# - Does not run on non-semver branches (master, submodule/*, recovered/*,
# detached HEAD).
# - Does not run inside an in-progress rebase / cherry-pick / merge.
# - Does not run when the working tree is dirty (warns + skips).
# - Total runtime budget: < ~5 seconds on a warm tree.
#
# See: local/docs/HOOKS.md, local/docs/RELEASE-BUMP-WORKFLOW.md,
# local/AGENTS.md § "Version conventions — two categories".
# NOTE: deliberately `set -uo pipefail` and NOT `-e`. A post-checkout hook
# must never abort mid-way and leave the operator staring at a non-zero exit
# that blocks the checkout. Every fallible step is either guarded or wrapped
# so that failure degrades to an exit 0 with a stderr note.
set -uo pipefail
# ---------------------------------------------------------------------------
# Locate the repo root. We prefer `git rev-parse --show-toplevel` over a
# dirname-relative derivation because git places the hook under `.git/hooks/`
# which may itself be a gitdir pointer (worktrees, linked checkouts).
# ---------------------------------------------------------------------------
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [[ -z "$ROOT" ]]; then
# Not inside a git work tree — nothing to do.
exit 0
fi
# Git passes exactly three arguments to post-checkout. If invoked manually
# without them, treat $3 as unknown (skip the branch-flag guard).
BRANCH_FLAG="${3:-}"
# ---------------------------------------------------------------------------
# Guard (a): branch checkouts only. $3 == 1 means a branch checkout;
# $3 == 0 means a file checkout (e.g. `git checkout -- path`). Silently
# skip file checkouts — there is no branch context to sync against.
# ---------------------------------------------------------------------------
if [[ "$BRANCH_FLAG" != "1" ]]; then
exit 0
fi
# ---------------------------------------------------------------------------
# Guard (b): the new branch name MUST be an anchored semver
# `^[0-9]+\.[0-9]+\.[0-9]+$`. This silently excludes master, main,
# submodule/* branches, recovered/* branches, and detached HEAD.
# ---------------------------------------------------------------------------
BRANCH="$(git branch --show-current 2>/dev/null || true)"
if [[ -z "$BRANCH" ]]; then
# Detached HEAD — no branch name to anchor a version against.
exit 0
fi
if ! printf '%s' "$BRANCH" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
# Non-semver branch (master, submodule/*, recovered/*, feature/*, …).
# Silently skip — version labels are only meaningful on release branches.
exit 0
fi
# ---------------------------------------------------------------------------
# Guard (c): never run inside an in-progress rebase / cherry-pick / merge.
# Touching version labels mid-sequence would corrupt the operator's
# in-flight history rewrite. Silently skip in all such states.
# ---------------------------------------------------------------------------
GIT_DIR="$(git rev-parse --git-dir 2>/dev/null || true)"
if [[ -n "$GIT_DIR" ]]; then
# Resolve relative to the worktree root for linked worktrees.
[[ "$GIT_DIR" = /* ]] || GIT_DIR="$ROOT/$GIT_DIR"
for marker in rebase-merge rebase-apply CHERRY_PICK_HEAD MERGE_HEAD; do
if [[ -e "$GIT_DIR/$marker" ]]; then
# An interactive operation is in progress — stay out of the way.
exit 0
fi
done
fi
# ---------------------------------------------------------------------------
# Guard (d): honour the explicit opt-out env var.
# ---------------------------------------------------------------------------
if [[ -n "${REDBEAR_NO_AUTO_SYNC:-}" ]]; then
exit 0
fi
# ---------------------------------------------------------------------------
# Guard (e): working tree MUST be clean. A bump rewrites Cargo.toml files in
# place; running it over uncommitted operator edits would silently swallow
# those edits into the bump. Warn + skip instead.
# ---------------------------------------------------------------------------
if ! git -C "$ROOT" diff --quiet --ignore-submodules HEAD 2>/dev/null \
|| [[ -n "$(git -C "$ROOT" status --porcelain 2>/dev/null)" ]]; then
echo "post-checkout-version-sync: skipping version auto-sync: uncommitted changes present" >&2
echo " (commit or stash, then run: ./local/scripts/sync-versions.sh --no-regen)" >&2
exit 0
fi
# ---------------------------------------------------------------------------
# All guards passed. Compare the root cookbook Cargo.toml `[package] version`
# against the branch version. They are the canonical indicator of whether a
# label sync is needed: sync-versions.sh Phase 0 keeps them in lock-step,
# so if the root is already at the branch version, every Cat 1/Cat 2 label
# is already correct too.
# ---------------------------------------------------------------------------
ROOT_CARGO="$ROOT/Cargo.toml"
if [[ ! -f "$ROOT_CARGO" ]]; then
# Not a Red Bear OS checkout (no root Cargo.toml). Stay silent.
exit 0
fi
CURRENT_VERSION=""
if grep -qE '^\[package\]' "$ROOT_CARGO" 2>/dev/null; then
# Must match sync-versions.sh Phase 0's extraction so both scripts agree.
CURRENT_VERSION="$(awk '
/^\[package\]/ { in_pkg=1; next }
/^\[/ { in_pkg=0 }
in_pkg && /^version[[:space:]]*=/ { print; exit }
' "$ROOT_CARGO" 2>/dev/null | sed 's/.*"\(.*\)".*/\1/' || true)"
fi
if [[ "$CURRENT_VERSION" == "$BRANCH" ]]; then
# Labels already synced for this branch — nothing to do.
exit 0
fi
# ---------------------------------------------------------------------------
# Drift detected. Run the label-only sync (NEVER --regen — lockfile regen is
# an explicit operator step). sync-versions.sh is the single executor of
# suffix-only label rewrites; this hook never duplicates that sed logic.
# ---------------------------------------------------------------------------
SYNC="$ROOT/local/scripts/sync-versions.sh"
if [[ ! -x "$SYNC" ]]; then
echo "post-checkout-version-sync: $SYNC not found or not executable — skipping label sync" >&2
echo " run manually: ./local/scripts/sync-versions.sh --no-regen" >&2
exit 0
fi
echo "post-checkout-version-sync: branch '$BRANCH' root Cargo.toml at '$CURRENT_VERSION' -> '$BRANCH'"
echo "post-checkout-version-sync: running sync-versions.sh --no-regen (labels only, no lockfile regen)"
# Run label sync. Capture output so the operator sees a concise summary.
# We do NOT propagate a non-zero exit: even if sync fails, the checkout
# itself must not be blocked.
SYNC_OUTPUT="$("$SYNC" --no-regen 2>&1)" || true
if [[ -n "$SYNC_OUTPUT" ]]; then
printf '%s\n' "$SYNC_OUTPUT"
fi
echo "post-checkout-version-sync: label sync complete for branch '$BRANCH'."
# ---------------------------------------------------------------------------
# Follow-up hint. The hook handles ONLY mechanical label rewrites. The
# expensive half of a release bump — fork source upgrades to newer upstream
# tags and external desktop-stack version refresh — is the operator's
# explicit decision and is never triggered automatically.
# ---------------------------------------------------------------------------
cat >&2 <<EOF
post-checkout-version-sync: follow-up steps (NOT run automatically):
- source upgrades (Cat 2 forks to newer upstream tags):
./local/scripts/bump-release.sh --with-sources --with-external
- lockfile regeneration (after labels settle):
./local/scripts/sync-versions.sh --regen
- check-only verification:
./local/scripts/sync-versions.sh --check
See local/docs/RELEASE-BUMP-WORKFLOW.md for the full runbook.
EOF
# Always exit 0. A hook that blocks the checkout is strictly worse than a
# hook that no-ops; git treats a non-zero exit from post-checkout as a
# warning and clutters the operator's terminal on every checkout.
exit 0