fix(lint): hard-fail on main when a release tag is needed

.pre-commit-hooks.yaml now exposes hooks to external consumers pinning
rev: <tag>, but nothing enforced that a tag actually gets cut when the
files it references change — relying on memory is exactly what this
repo's governance rules say to avoid for a repeatable, deterministic
check.

scripts/check-release-needed.sh hard-fails at pre-push, but only when
PRE_COMMIT_REMOTE_BRANCH (set by pre-commit's hook-impl) is
refs/heads/main: it diffs .pre-commit-hooks.yaml's referenced paths
against the last tag reachable from HEAD, and fails if either no tag
exists yet or something changed since. It's a silent no-op on every
other branch — hard-failing on feature-branch pushes mid-review would
force a premature tag on a commit that might not survive a
squash-merge, the exact risk the repo: local (vs. pinned self-
reference) decision in ADR-0014 already avoids for this repo's own
dev-time gate.

Verified against the real git pre-push hook path (not just the script
in isolation): simulated stdin matching git's pre-push protocol through
.git/hooks/pre-push, confirmed it correctly fires and fails when
targeting main with no tag, and is silent otherwise.

ADR: 0014
Refs: #87
This commit is contained in:
2026-08-09 10:20:43 +00:00
parent 1164f3abad
commit 4d018af03c
4 changed files with 172 additions and 0 deletions

56
scripts/check-release-needed.sh Executable file
View File

@@ -0,0 +1,56 @@
#!/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: <tag>` 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).
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"
# Paths whose content .pre-commit-hooks.yaml exposes to external consumers.
# Keep in sync with .pre-commit-hooks.yaml's entry: paths.
RELEASE_PATHS=(
.pre-commit-hooks.yaml
scripts/skill-size-check.sh
plugins/kyberforge/skills/skill-audit/scripts
plugins/kyberforge/skills/skill-audit/assets/vale
plugins/kyberforge/skills/agent-audit/scripts
plugins/kyberforge/skills/agent-audit/assets/vale
)
LAST_TAG="$(git describe --tags --abbrev=0 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
EXISTING_PATHS=()
for p in "${RELEASE_PATHS[@]}"; do
[[ -e "$p" ]] && EXISTING_PATHS+=("$p")
done
if [[ ${#EXISTING_PATHS[@]} -eq 0 ]]; then
exit 0
fi
CHANGED="$(git diff --name-only "$LAST_TAG"..HEAD -- "${EXISTING_PATHS[@]}" 2>/dev/null || true)"
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