diff options
Diffstat (limited to 'scripts')
| -rwxr-xr-x | scripts/sync-check.sh | 72 | ||||
| -rw-r--r-- | scripts/tests/sync-check.bats | 86 |
2 files changed, 158 insertions, 0 deletions
diff --git a/scripts/sync-check.sh b/scripts/sync-check.sh new file mode 100755 index 0000000..8fb9ab0 --- /dev/null +++ b/scripts/sync-check.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# +# sync-check.sh — verify canonical claude-templates/.ai/ matches the .ai/ mirror. +# +# Usage: +# scripts/sync-check.sh # exit 0 if synced, 1 if drift +# scripts/sync-check.sh --fix # rsync canonical → mirror, then re-check +# +# Runs as a pre-commit hook (githooks/pre-commit) to catch drift before +# commit, and as a manual check via `make sync-check`. +# +# The three synced paths are: +# claude-templates/.ai/protocols.org ↔ .ai/protocols.org +# claude-templates/.ai/workflows/ ↔ .ai/workflows/ +# claude-templates/.ai/scripts/ ↔ .ai/scripts/ +# +# Source of truth is the canonical (claude-templates/) side. The mirror +# exists so rulesets-as-a-project has a working copy. Drift in either +# direction is a defect — both have to land in the same commit. + +set -euo pipefail + +if ! repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || [ -z "$repo_root" ]; then + echo "sync-check: not inside a git checkout" >&2 + exit 2 +fi + +canonical="$repo_root/claude-templates/.ai" +mirror="$repo_root/.ai" + +if [ ! -d "$canonical" ] || [ ! -d "$mirror" ]; then + echo "sync-check: not a rulesets-shaped repo (missing claude-templates/.ai or .ai)" >&2 + exit 2 +fi + +paths=(protocols.org workflows scripts) + +check_drift() { + local drift=0 + for relpath in "${paths[@]}"; do + if ! diff -rq "$canonical/$relpath" "$mirror/$relpath" >/dev/null 2>&1; then + echo "drift: claude-templates/.ai/$relpath ↔ .ai/$relpath" >&2 + diff -rq "$canonical/$relpath" "$mirror/$relpath" 2>&1 | head -20 >&2 + drift=1 + fi + done + return "$drift" +} + +if check_drift; then + exit 0 +fi + +if [ "${1:-}" = "--fix" ]; then + echo "" >&2 + echo "sync-check --fix: syncing canonical → mirror..." >&2 + rsync -a "$canonical/protocols.org" "$mirror/protocols.org" + rsync -a --delete "$canonical/workflows/" "$mirror/workflows/" + rsync -a --delete "$canonical/scripts/" "$mirror/scripts/" + if check_drift; then + echo "sync-check --fix: resolved." >&2 + echo "Re-stage the synced files and retry the commit." >&2 + exit 0 + else + echo "sync-check --fix: drift persists after sync. Inspect manually." >&2 + exit 1 + fi +fi + +echo "" >&2 +echo "Run 'scripts/sync-check.sh --fix' (or 'make sync-check FIX=1') to resolve." >&2 +exit 1 diff --git a/scripts/tests/sync-check.bats b/scripts/tests/sync-check.bats new file mode 100644 index 0000000..df775b3 --- /dev/null +++ b/scripts/tests/sync-check.bats @@ -0,0 +1,86 @@ +#!/usr/bin/env bats +# +# Tests for scripts/sync-check.sh +# +# Sandboxes a fake rulesets-shaped git repo under $BATS_TMPDIR, drops the +# real script in via PATH override, and exercises clean / drift / --fix +# behavior. + +setup() { + SANDBOX="$BATS_TEST_TMPDIR/repo" + SCRIPT_SRC="$BATS_TEST_DIRNAME/../sync-check.sh" + mkdir -p "$SANDBOX/scripts" "$SANDBOX/claude-templates/.ai/workflows" \ + "$SANDBOX/claude-templates/.ai/scripts" "$SANDBOX/.ai/workflows" \ + "$SANDBOX/.ai/scripts" + cp "$SCRIPT_SRC" "$SANDBOX/scripts/sync-check.sh" + chmod +x "$SANDBOX/scripts/sync-check.sh" + cd "$SANDBOX" + git init -q + git config user.email test@example.com + git config user.name test + echo "protocols content" > claude-templates/.ai/protocols.org + echo "protocols content" > .ai/protocols.org + echo "workflow a" > claude-templates/.ai/workflows/a.org + echo "workflow a" > .ai/workflows/a.org + echo "script b" > claude-templates/.ai/scripts/b.sh + echo "script b" > .ai/scripts/b.sh +} + +@test "clean tree: exit 0" { + run scripts/sync-check.sh + [ "$status" -eq 0 ] +} + +@test "drift in protocols.org: exit 1, names the file" { + echo "drifted" > .ai/protocols.org + run scripts/sync-check.sh + [ "$status" -eq 1 ] + [[ "$output" == *"protocols.org"* ]] +} + +@test "drift in workflows/: exit 1, names workflows" { + echo "workflow a modified" > .ai/workflows/a.org + run scripts/sync-check.sh + [ "$status" -eq 1 ] + [[ "$output" == *"workflows"* ]] +} + +@test "drift in scripts/: exit 1, names scripts" { + echo "script b modified" > .ai/scripts/b.sh + run scripts/sync-check.sh + [ "$status" -eq 1 ] + [[ "$output" == *"scripts"* ]] +} + +@test "drift with --fix: syncs and exits 0" { + echo "drifted" > .ai/protocols.org + run scripts/sync-check.sh --fix + [ "$status" -eq 0 ] + run cat .ai/protocols.org + [ "$output" = "protocols content" ] +} + +@test "extra file in mirror: --delete removes it" { + echo "stale" > .ai/workflows/stale.org + run scripts/sync-check.sh --fix + [ "$status" -eq 0 ] + [ ! -f .ai/workflows/stale.org ] +} + +@test "missing canonical: exit 2 with clear error" { + rm -rf claude-templates + run scripts/sync-check.sh + [ "$status" -eq 2 ] + [[ "$output" == *"not a rulesets-shaped repo"* ]] +} + +@test "outside a git checkout: exit 2" { + cd "$BATS_TEST_TMPDIR" + mkdir not-a-repo + cp "$SCRIPT_SRC" not-a-repo/sync-check.sh + chmod +x not-a-repo/sync-check.sh + cd not-a-repo + run ./sync-check.sh + [ "$status" -eq 2 ] + [[ "$output" == *"not inside a git checkout"* ]] +} |
