feat: add check-no-upstream-edits.sh + bats tests (enforces I1)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Executable
+34
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Enforce Invariant I1: no upstream-tracked file is ever edited.
|
||||||
|
# Exits 0 on clean state, 1 on violation.
|
||||||
|
#
|
||||||
|
# Catches BOTH committed drift (commits unique to HEAD vs upstream/master)
|
||||||
|
# AND working-tree drift (uncommitted local edits to tracked files).
|
||||||
|
#
|
||||||
|
# Spec: sethlabels-docs/specs/2026-04-29-packaging-design.md §5.5 (I1, F1)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Allowlist: files/dirs sethLabels is permitted to add or modify.
|
||||||
|
# `.gitignore` is the one upstream-file exception (called out in spec §2).
|
||||||
|
allowed_pattern='^\.gitignore$|^\.claude/|^scripts/|^packaging/|^sethlabels-docs/|^tests-impl/|^README\.sethlabels\.md$|^CLAUDE\.md$|^IDEA\.md$|^DECISIONS\.md$'
|
||||||
|
|
||||||
|
committed=$(git diff --name-only upstream/master..HEAD 2>/dev/null || true)
|
||||||
|
working=$(git diff --name-only HEAD 2>/dev/null || true)
|
||||||
|
|
||||||
|
all_changes=$(printf "%s\n%s\n" "$committed" "$working" | sort -u | sed '/^$/d')
|
||||||
|
|
||||||
|
if [ -z "$all_changes" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
violations=$(echo "$all_changes" | grep -vE "$allowed_pattern" || true)
|
||||||
|
|
||||||
|
if [ -n "$violations" ]; then
|
||||||
|
echo "ERROR: strict-zero policy violated. The following upstream files have been modified:"
|
||||||
|
echo "$violations"
|
||||||
|
echo ""
|
||||||
|
echo "See sethlabels-docs/specs/2026-04-29-packaging-design.md §I1."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
#!/usr/bin/env bats
|
||||||
|
|
||||||
|
# Tests for scripts/check-no-upstream-edits.sh
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||||
|
SCRIPT="$REPO_ROOT/scripts/check-no-upstream-edits.sh"
|
||||||
|
TMP_REPO=""
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown() {
|
||||||
|
if [ -n "$TMP_REPO" ] && [ -d "$TMP_REPO" ]; then
|
||||||
|
rm -rf "$TMP_REPO"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Helpers ---
|
||||||
|
|
||||||
|
# Build a minimal disposable repo that mimics the sethLabels structure with a
|
||||||
|
# local "upstream/master" ref. Returns its path via stdout.
|
||||||
|
make_test_repo() {
|
||||||
|
local tmp=$(mktemp -d)
|
||||||
|
cd "$tmp"
|
||||||
|
git init -q -b master
|
||||||
|
git config user.email "test@test"
|
||||||
|
git config user.name "test"
|
||||||
|
# Pretend-upstream files
|
||||||
|
echo "upstream content" > UPSTREAM_FILE.md
|
||||||
|
echo "real source" > glabels-source.cpp
|
||||||
|
git add UPSTREAM_FILE.md glabels-source.cpp
|
||||||
|
git commit -m "upstream base" -q
|
||||||
|
# Create a local "upstream/master" ref pointing here
|
||||||
|
git update-ref refs/remotes/upstream/master HEAD
|
||||||
|
# Create a feature branch for sethLabels content
|
||||||
|
git checkout -q -b main
|
||||||
|
echo "$tmp"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Tests ---
|
||||||
|
|
||||||
|
@test "script exists and is executable" {
|
||||||
|
[ -x "$SCRIPT" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "exits 0 on clean state with only allowlisted committed changes" {
|
||||||
|
TMP_REPO=$(make_test_repo)
|
||||||
|
cd "$TMP_REPO"
|
||||||
|
mkdir -p scripts packaging sethlabels-docs .claude/handoffs
|
||||||
|
echo "test" > CLAUDE.md
|
||||||
|
echo "test" > scripts/something.sh
|
||||||
|
echo "test" > packaging/x.env
|
||||||
|
echo "test" > sethlabels-docs/spec.md
|
||||||
|
echo "" >> .gitignore
|
||||||
|
git add -A
|
||||||
|
git commit -m "sethLabels additions" -q
|
||||||
|
|
||||||
|
run "$SCRIPT"
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ -z "$output" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "exits 1 when an upstream file is committed-modified" {
|
||||||
|
TMP_REPO=$(make_test_repo)
|
||||||
|
cd "$TMP_REPO"
|
||||||
|
echo "evil edit" >> glabels-source.cpp
|
||||||
|
git add glabels-source.cpp
|
||||||
|
git commit -m "BAD: edit upstream file" -q
|
||||||
|
|
||||||
|
run "$SCRIPT"
|
||||||
|
[ "$status" -eq 1 ]
|
||||||
|
[[ "$output" == *"glabels-source.cpp"* ]]
|
||||||
|
[[ "$output" == *"strict-zero"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "exits 1 when an upstream file has uncommitted working-tree edits" {
|
||||||
|
TMP_REPO=$(make_test_repo)
|
||||||
|
cd "$TMP_REPO"
|
||||||
|
echo "uncommitted evil edit" >> glabels-source.cpp
|
||||||
|
|
||||||
|
run "$SCRIPT"
|
||||||
|
[ "$status" -eq 1 ]
|
||||||
|
[[ "$output" == *"glabels-source.cpp"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "exits 0 when only .gitignore is modified (allowlisted)" {
|
||||||
|
TMP_REPO=$(make_test_repo)
|
||||||
|
cd "$TMP_REPO"
|
||||||
|
echo "*.tmp" >> .gitignore
|
||||||
|
git add .gitignore
|
||||||
|
git commit -m "extend gitignore" -q
|
||||||
|
|
||||||
|
run "$SCRIPT"
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "exits 0 when only CLAUDE.md / IDEA.md / DECISIONS.md / README.sethlabels.md are added" {
|
||||||
|
TMP_REPO=$(make_test_repo)
|
||||||
|
cd "$TMP_REPO"
|
||||||
|
echo "x" > CLAUDE.md
|
||||||
|
echo "x" > IDEA.md
|
||||||
|
echo "x" > DECISIONS.md
|
||||||
|
echo "x" > README.sethlabels.md
|
||||||
|
git add -A
|
||||||
|
git commit -m "add sethLabels root docs" -q
|
||||||
|
|
||||||
|
run "$SCRIPT"
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user