99e5641127
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
215 lines
9.5 KiB
Bash
Executable File
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
|