fix(pre-commit): keep key order in pretty-format-json so apm-owned JSON survives
pretty-format-json sorts object keys by default, but apm emits insertion order and `apm audit --ci` diffs its output byte-for-byte. A file in the formatter's scope therefore drifts on every commit with an empty git diff. Pass --no-sort-keys so `.claude/settings.json` and `.claude/apm-hooks.json` round-trip untouched and drop them from the exclude. marketplace.json stays excluded: it carries literal em dashes the formatter re-escapes to —. tests/test-pretty-json-tool-owned.sh runs the repo's real autofix hooks over copies of the four tracked apm-owned files and fails if any is rewritten, so losing the flag fails a test instead of surfacing as drift. Refs: #102 Co-Authored-By: Claude Code <[email protected]> Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
This commit is contained in:
1 parent
3ff0741857
commit
c2c56ff948
4 files changed
+156
-50
No files matched your search
Executable
+118
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env bash
|
||||
# The autofixing pre-commit hooks must leave apm's own output byte-identical.
|
||||
#
|
||||
# apm owns four tracked files, and `apm audit --ci` diffs them byte-for-byte
|
||||
# against a replayed install. If an autofixing hook rewrites any of them, the
|
||||
# pre-push gate reports drift on a file that has no git diff. This suite runs
|
||||
# the repo's real autofix hooks, with the real args and the real `exclude:` from
|
||||
# .pre-commit-config.yaml, over copies of those four files and asserts nothing
|
||||
# changes. A file the config excludes passes trivially; a file it leaves in scope
|
||||
# must survive the formatter untouched, which is what fails if `--no-sort-keys`
|
||||
# is ever dropped.
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
|
||||
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
|
||||
|
||||
# exit 77 (automake convention; run-tests.sh renders it as SKIPPED, and
|
||||
# --strict turns that into a setup error at the pre-push gate).
|
||||
command -v git > /dev/null 2>&1 || { echo "SKIP: git is required"; exit 77; }
|
||||
command -v pre-commit > /dev/null 2>&1 || { echo "SKIP: pre-commit is required to run pretty-format-json"; exit 77; }
|
||||
command -v python3 > /dev/null 2>&1 || { echo "SKIP: python3 is required to read .pre-commit-config.yaml"; exit 77; }
|
||||
python3 -c 'import yaml' > /dev/null 2>&1 || { echo "SKIP: PyYAML is required to read .pre-commit-config.yaml"; exit 77; }
|
||||
|
||||
TOOL_OWNED=(
|
||||
.claude/settings.json
|
||||
.claude/apm-hooks.json
|
||||
.claude-plugin/marketplace.json
|
||||
apm.lock.yaml
|
||||
)
|
||||
HOOK_IDS="pretty-format-json,end-of-file-fixer,trailing-whitespace"
|
||||
|
||||
WORK="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
# Derive the fixture config from the real one so the args under test are the
|
||||
# repo's, not a copy that can drift. `stages:` is dropped so the hooks run under
|
||||
# a plain `pre-commit run`.
|
||||
if ! python3 - "$REPO_ROOT/.pre-commit-config.yaml" "$HOOK_IDS" "$WORK/pre-commit-config.yaml" << 'PY'
|
||||
import sys
|
||||
import yaml
|
||||
|
||||
src, ids, dest = sys.argv[1], set(sys.argv[2].split(",")), sys.argv[3]
|
||||
cfg = yaml.safe_load(open(src))
|
||||
repos = []
|
||||
for repo in cfg["repos"]:
|
||||
hooks = [dict(h) for h in repo.get("hooks", []) if h["id"] in ids]
|
||||
if not hooks:
|
||||
continue
|
||||
for h in hooks:
|
||||
h.pop("stages", None)
|
||||
repos.append({"repo": repo["repo"], "rev": repo["rev"], "hooks": hooks})
|
||||
found = {h["id"] for r in repos for h in r["hooks"]}
|
||||
missing = ids - found
|
||||
if missing:
|
||||
sys.exit("hooks not found in .pre-commit-config.yaml: " + ", ".join(sorted(missing)))
|
||||
yaml.safe_dump({"repos": repos}, open(dest, "w"))
|
||||
PY
|
||||
then
|
||||
echo "ERROR: could not derive the fixture config"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# pre-commit clones a hook repo once and caches it; without the cache this
|
||||
# would need the network, which is a setup problem, not a regression.
|
||||
HOOK_REPO="$(python3 -c 'import yaml,sys; print(yaml.safe_load(open(sys.argv[1]))["repos"][0]["repo"])' "$WORK/pre-commit-config.yaml")"
|
||||
HOOK_REV="$(python3 -c 'import yaml,sys; print(yaml.safe_load(open(sys.argv[1]))["repos"][0]["rev"])' "$WORK/pre-commit-config.yaml")"
|
||||
CACHE_DB="${PRE_COMMIT_HOME:-${XDG_CACHE_HOME:-$HOME/.cache}/pre-commit}/db.db"
|
||||
if ! python3 - "$CACHE_DB" "$HOOK_REPO" "$HOOK_REV" << 'PY' > /dev/null 2>&1
|
||||
import sqlite3, sys
|
||||
db, repo, rev = sys.argv[1:4]
|
||||
row = sqlite3.connect(db).execute("select 1 from repos where repo=? and ref=?", (repo, rev)).fetchone()
|
||||
sys.exit(0 if row else 1)
|
||||
PY
|
||||
then
|
||||
echo "SKIP: $HOOK_REPO@$HOOK_REV is not in the pre-commit cache; run the hooks once with network access"
|
||||
exit 77
|
||||
fi
|
||||
|
||||
mkdir -p "$WORK/repo"
|
||||
cd "$WORK/repo"
|
||||
git init -q .
|
||||
git config user.email [email protected]
|
||||
git config user.name test
|
||||
cp "$WORK/pre-commit-config.yaml" .pre-commit-config.yaml
|
||||
for f in "${TOOL_OWNED[@]}"; do
|
||||
mkdir -p "$(dirname "$f")"
|
||||
cp "$REPO_ROOT/$f" "$f"
|
||||
done
|
||||
git add -A
|
||||
|
||||
echo ""
|
||||
echo "--- autofix hooks leave apm-owned files byte-identical ---"
|
||||
RC=0
|
||||
pre-commit run --all-files > "$WORK/hooks.out" 2>&1 || RC=$?
|
||||
|
||||
for f in "${TOOL_OWNED[@]}"; do
|
||||
if cmp -s "$REPO_ROOT/$f" "$f"; then
|
||||
pass "$f is unchanged by $HOOK_IDS"
|
||||
else
|
||||
fail "$f was rewritten by an autofixing hook -- apm audit --ci would report drift"
|
||||
diff "$REPO_ROOT/$f" "$f" | sed 's/^/ /' | head -20 || true
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$RC" -eq 0 ]]; then
|
||||
pass "pre-commit exits 0 over the tool-owned files"
|
||||
else
|
||||
fail "pre-commit exited $RC over the tool-owned files"
|
||||
sed 's/^/ /' "$WORK/hooks.out"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Results: $PASS passed, $FAIL failed"
|
||||
[[ "$FAIL" -eq 0 ]]
|
||||
Reference in new issue
Block a user