#!/usr/bin/env bash set -euo pipefail # Hard-fails only when pushing to main: if any file covered by .pre-commit-hooks.yaml # (the external git-hook/CI contract, see ADR-0014) changed since the last tag, # a release must be cut before landing on main, or external consumers pinning # `rev: ` silently miss the change. Pre-commit sets PRE_COMMIT_REMOTE_BRANCH # for pre-push hooks; on every other branch (feature work mid-review) this is a # silent no-op — pushing WIP commits there must not be blocked on cutting a # premature tag (see ADR-0014's repo: local vs pinned self-reference decision). # # Known gap: this only fires on a local `git push` through pre-commit's pre-push # hook. A PR merged via Gitea's merge button (server-side, no local push) or a # CI runner invoking `pre-commit run --hook-stage pre-push` directly does not set # PRE_COMMIT_REMOTE_BRANCH and will not trigger this check — closing that # requires a server-side CI job, which this repo does not have yet. TARGET_BRANCH="refs/heads/main" if [[ "${PRE_COMMIT_REMOTE_BRANCH:-}" != "$TARGET_BRANCH" ]]; then exit 0 fi REPO_ROOT="$(git rev-parse --show-toplevel)" cd "$REPO_ROOT" HOOKS_MANIFEST=".pre-commit-hooks.yaml" if [[ ! -f "$HOOKS_MANIFEST" ]]; then exit 0 fi # Derive release-relevant paths from .pre-commit-hooks.yaml's own entry: lines # instead of hand-maintaining a parallel list — the manifest is the single # source of truth for what external consumers actually pull at a pinned rev, # so a hook added/removed/renamed there can't silently drift out of sync here. # Everything is derived from tokens[0], the hook's script: pre-commit prefixes # only entry[0] with the hook-repo clone path, so any later token that looks # like a path resolves against the *consuming* repo and can never name a file # this repo ships. A hook's bundled data therefore has to be self-located # relative to the script — vale-wrap.sh reads its own # /../assets/vale/.vale.ini plus the sibling styles/ tree — which # makes /../assets release-relevant alongside the script itself. # The ../ is normalised by stripping a path component rather than with # `realpath -m`, which is a GNU-only extension. Two guards keep the derivation # from inventing paths: a bundle root of "." is skipped, because a script in a # top-level directory (scripts/skill-size-check.sh) would derive the repo's own # shared assets/, which no hook owns and whose churn must not demand a release; # and the directory is added only when it exists, since a hook that bundles # nothing must not contribute a pathspec matching nothing. RELEASE_PATHS=("$HOOKS_MANIFEST") while IFS= read -r entry; do read -ra tokens <<< "$entry" [[ ${#tokens[@]} -eq 0 ]] && continue RELEASE_PATHS+=("${tokens[0]}") bundle_root="$(dirname "$(dirname "${tokens[0]}")")" if [[ "$bundle_root" != "." && -d "$bundle_root/assets" ]]; then RELEASE_PATHS+=("$bundle_root/assets") fi done < <(sed -n 's/^[[:space:]]*entry:[[:space:]]*//p' "$HOOKS_MANIFEST") # Only vX.Y.Z release tags count as a baseline — an incidental checkpoint or # experiment tag reachable from HEAD must not shift the diff baseline. LAST_TAG="$(git describe --tags --abbrev=0 --match 'v[0-9]*.[0-9]*.[0-9]*' 2>/dev/null || true)" if [[ -z "$LAST_TAG" ]]; then echo "FAIL: no release tag exists yet, but .pre-commit-hooks.yaml already exposes hooks to external consumers." >&2 echo " Fix: cut the first release tag (e.g. v1.0.0) before this lands on main." >&2 exit 1 fi # No -e/existence filtering: a path deleted since $LAST_TAG is exactly the case # that must be caught (external consumers pinning the old tag would hit a # missing file), and `git diff` reports deletions fine without it existing at # HEAD. A git failure (e.g. a shallow clone missing $LAST_TAG's history) must # fail closed, not be swallowed into an empty, falsely-clean diff. if ! CHANGED="$(git diff --name-only "$LAST_TAG"..HEAD -- "${RELEASE_PATHS[@]}")"; then echo "FAIL: could not diff $LAST_TAG..HEAD to check for release-relevant changes (see git error above)." >&2 echo " Fix: ensure full tag history is available (e.g. git fetch --unshallow) and retry." >&2 exit 1 fi if [[ -n "$CHANGED" ]]; then echo "FAIL: files covered by .pre-commit-hooks.yaml changed since $LAST_TAG:" >&2 echo "$CHANGED" | sed 's/^/ /' >&2 echo " Fix: cut a new release tag — external consumers pinning rev: $LAST_TAG would miss this change." >&2 exit 1 fi