fix(lint): harden check-release-needed.sh, script the vale-style sync
A review of PR #85's last two commits (1164f3a,4d018af) found the new release-gate script fails open in four separate ways, and the new drift check for the duplicated Vale styles only ever detects drift after a human already hand-edited both copies out of sync. check-release-needed.sh: - The `-e` existence filter dropped a RELEASE_PATHS entry from the diff pathspec once it was deleted from the tree, so deleting a path exposed via .pre-commit-hooks.yaml since the last tag passed the gate clean — exactly the breakage the gate exists to catch. git diff reports deletions fine without an existence check; the filter is gone. - `git diff ... 2>/dev/null || true` turned any git failure (a shallow clone missing the tag's objects, a corrupted ref) into an empty, falsely-clean diff. The diff result is no longer swallowed: a failure now hard-fails with the underlying git error visible. - RELEASE_PATHS was a hand-maintained array duplicating .pre-commit-hooks.yaml's entry: paths with only a comment holding them in sync, and was already over-broad (it swept in validate.sh / validate-provenance.sh, which no hook entry references). It's now parsed straight from .pre-commit-hooks.yaml's entry: lines at runtime, so it can't drift from the manifest and only tracks what a hook actually exposes. - `git describe --tags --abbrev=0` accepted any tag reachable from HEAD as the diff baseline, not just release tags. Added `--match 'v[0-9]*.[0-9]*.[0-9]*'` so an incidental checkpoint tag can't shift the baseline and mask a real release-relevant change. check-vale-style-sync.sh still only detects drift between skill-audit's and agent-audit's duplicated vale-wrap.sh/styles/Kyberforge copies (both copies must exist independently per the plugin's no-cross-skill- path packaging rule — a symlink would break at install time). Added scripts/sync-vale-styles.sh to regenerate skill-audit's copy from agent-audit's canonical one on demand, and pointed the sync check's failure message at it, so fixing drift is one command instead of a hand diff across two files. Also recorded, rather than silently left unfixed: check-release-needed.sh only fires on a local `git push` through pre-commit's pre-push hook — a PR merged via Gitea's merge button, or CI invoking `pre-commit run --hook-stage pre-push` directly, never sets PRE_COMMIT_REMOTE_BRANCH and skips the gate entirely. Closing that needs a server-side CI job this repo doesn't have yet; documented as a known limitation in ADR-0014 rather than papered over. Separately, LESSONS.md's "a clean check can mean nothing ran" entry was marked **Graduated** without ever being promoted per the repo's own graduation rule (3+ instances → a standing doc, marked `[graduated → target file]`). Actually promoted it into core/instructions/testing.md and fixed the marker. tests/test-check-release-needed.sh gained 4 regression tests, one per check-release-needed.sh fix above, each verified to fail against the pre-fix script and pass against the current one. Verification: bash tests/run-tests.sh (11 scripts + 125 bats, all passing), pre-commit run --all-files, and pre-commit run --all-files --hook-stage pre-push all clean. ADR: 0014
This commit is contained in:
@@ -8,6 +8,12 @@ set -euo pipefail
|
||||
# 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"
|
||||
|
||||
@@ -18,18 +24,34 @@ 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
|
||||
)
|
||||
HOOKS_MANIFEST=".pre-commit-hooks.yaml"
|
||||
|
||||
LAST_TAG="$(git describe --tags --abbrev=0 2>/dev/null || true)"
|
||||
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.
|
||||
# Each entry is "<script> [--config <path>] [...]"; the script itself and the
|
||||
# directory holding any --config target (vale-wrap.sh needs its .vale.ini's
|
||||
# sibling styles/ tree, not just the ini file) are release-relevant.
|
||||
RELEASE_PATHS=("$HOOKS_MANIFEST")
|
||||
while IFS= read -r entry; do
|
||||
read -ra tokens <<< "$entry"
|
||||
[[ ${#tokens[@]} -eq 0 ]] && continue
|
||||
RELEASE_PATHS+=("${tokens[0]}")
|
||||
for ((i = 1; i < ${#tokens[@]}; i++)); do
|
||||
if [[ "${tokens[i]}" == "--config" && -n "${tokens[i + 1]:-}" ]]; then
|
||||
RELEASE_PATHS+=("$(dirname "${tokens[i + 1]}")")
|
||||
fi
|
||||
done
|
||||
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
|
||||
@@ -37,17 +59,17 @@ if [[ -z "$LAST_TAG" ]]; then
|
||||
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
|
||||
# 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
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user