Addresses all blocking + minor items from PR #136 review. Architectural change (T2/H3 — the review's main complaint): generate_brands was previously a separate Rust binary that produced a TOML artifact, with its stamp written "on behalf" by import_economics.py at the end of its own run. Reviewers flagged the invisible coupling: two sources of truth in a system designed to have one, and no way to tell from the stamp that one "generator" was really a subroutine of the other. import_economics now invokes tooling/generate-brands as the first step of its main() flow, before opening its own DB connection. The TOML artefact is still produced and still committed (useful for diff-review of brand changes), but there's now one pipeline owner. The meta table carries two rows (import_economics, generate_atlas) not three; the Rust binary's source SHA folds into import_economics' stamp via IMPORT_ECONOMICS_SOURCES. A MIGRATION_SQL DELETE cleans up pre-merge DBs that still have the orphan generate_brands row. Other review items addressed in-line: H1 generate_atlas._write_stamp no longer commits — transaction ownership stays with the caller (matches import_economics pattern). Stamp + atlas data now commit atomically; a failed stamp rolls back the atlas data rather than leaving a stamp-missing-data intermediate. H2 _file_sha1 (in both import_economics, generate_atlas, check-systems-db-stamp) raises FileNotFoundError on missing sources instead of silently contributing an empty-bytes hash. A ghost-SHA convergence could otherwise produce vacuous "fresh" passes. H4 pre-push no-meta-table warning rephrased — was "run after next regeneration", now "run now if this DB was generated by you". T1 asset-pipeline.md determinism claim softened: the stamp is deterministic (same source → same recorded SHA), the DB binary is not (generated_at + SQLite rowids/freelist churn). T3 asset-pipeline.md gains a "migration escape hatch" section naming MIGRATION_SQL in import_economics.py as the only sanctioned path for direct writes, and forbidding hand-run sqlite-exec / one-off patch scripts / SQLite-GUI edits. T4 Makefile regen-db now runs as a single shell with `set -e`. A failure in one generator halts the pipeline immediately, preventing the "stale data, fresh stamp" state where a later step stamped a DB whose earlier step had failed. import_economics' exit code 2 (coverage gate warning) remains explicitly tolerated. T5 pre-push stamp check now runs on a branch's first push too — compares against origin/main instead of origin/$BRANCH, closing the gap where a new branch could ship a stale DB via the first push. T6 check-systems-db-stamp fails closed on unknown generator_names in meta — a future branch adding a new generator without registering it in GENERATOR_SOURCES will now be rejected, not silently skipped. T7 /pr-push watch list gains a mutual cross-reference comment with GENERATOR_SOURCES in check-systems-db-stamp, plus the missing names.rs source file, so the two lists cannot silently drift. Follow-up tickets created: #887 T8 decisions-orphan-tickets CLI — surfaces tickets whose decision_ref points at a non-existent D-record. #888 T9 meta.schema_version monotonic semver — for savegame migration lineage in Phase 5+ (SHA comparison can't be ordered). Verified: make regen-db end-to-end — OK make check-systems-db — OK, 2 generator(s) up to date STALE detection — OK, verified by touching generate_atlas.py /pr-push watch list — OK, flags this branch's changed sources decision show D-159 — OK, structured output with tickets + refs Refs: #855 #856 #857 #858 #859 PR #136 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
183 lines
7.5 KiB
Bash
Executable File
183 lines
7.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Pre-push hook: lint GDScript and Rust before pushing.
|
|
# Installed via: git config core.hooksPath .config/hooks
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
|
ERRORS=0
|
|
|
|
echo "pre-push: running lint checks..."
|
|
|
|
# --- Detect which directories have changes vs remote ---
|
|
BRANCH=$(git branch --show-current)
|
|
REMOTE_REF="origin/$BRANCH"
|
|
if git rev-parse --verify "$REMOTE_REF" >/dev/null 2>&1; then
|
|
CLIENT_CHANGED=$(git diff --name-only "$REMOTE_REF"..HEAD -- client/ 2>/dev/null | wc -l)
|
|
SERVER_CHANGED=$(git diff --name-only "$REMOTE_REF"..HEAD -- server/ 2>/dev/null | wc -l)
|
|
TOOLING_CHANGED=$(git diff --name-only "$REMOTE_REF"..HEAD -- tooling/ pyproject.toml 2>/dev/null | wc -l)
|
|
else
|
|
# New branch or no remote ref — fall through to directory checks
|
|
CLIENT_CHANGED=1
|
|
SERVER_CHANGED=1
|
|
TOOLING_CHANGED=1
|
|
fi
|
|
|
|
# --- GDScript parse check (headless Godot) ---
|
|
GODOT="${GODOT:-godot}"
|
|
if [ "$CLIENT_CHANGED" -eq 0 ]; then
|
|
echo "pre-push: no client/ changes — skipping GDScript checks"
|
|
elif command -v "$GODOT" >/dev/null 2>&1 && [ -d "$REPO_ROOT/client/.godot" ]; then
|
|
echo "pre-push: checking GDScript (parse)..."
|
|
SCRIPT_ERRORS=$("$GODOT" --headless --path "$REPO_ROOT/client" --quit 2>&1 | grep -ci "SCRIPT ERROR" || true)
|
|
if [ "$SCRIPT_ERRORS" -gt 0 ]; then
|
|
echo "pre-push: FAIL — $SCRIPT_ERRORS GDScript error(s) found"
|
|
"$GODOT" --headless --path "$REPO_ROOT/client" --quit 2>&1 | grep -i "SCRIPT ERROR"
|
|
ERRORS=$((ERRORS + 1))
|
|
else
|
|
echo "pre-push: GDScript parse — OK"
|
|
fi
|
|
else
|
|
echo "pre-push: skipping GDScript parse (no .godot/ import — run Godot once to enable)"
|
|
fi
|
|
|
|
# --- GDScript lint (gdlint static analysis) — advisory only until codebase is clean ---
|
|
if [ "$CLIENT_CHANGED" -gt 0 ] && command -v gdlint >/dev/null 2>&1 && [ -d "$REPO_ROOT/client/scripts" ]; then
|
|
echo "pre-push: checking GDScript (gdlint — advisory)..."
|
|
LINT_COUNT=$(gdlint "$REPO_ROOT/client/scripts/" "$REPO_ROOT/client/ui/" 2>&1 | grep -c "Error:" || true)
|
|
if [ "$LINT_COUNT" -gt 0 ]; then
|
|
echo "pre-push: gdlint — $LINT_COUNT issue(s) (advisory, not blocking)"
|
|
else
|
|
echo "pre-push: gdlint — OK"
|
|
fi
|
|
fi
|
|
|
|
# --- GDScript format check (gdformat) — advisory only until codebase is clean ---
|
|
if [ "$CLIENT_CHANGED" -gt 0 ] && command -v gdformat >/dev/null 2>&1 && [ -d "$REPO_ROOT/client/scripts" ]; then
|
|
echo "pre-push: checking GDScript (gdformat — advisory)..."
|
|
FORMAT_COUNT=$(gdformat --check "$REPO_ROOT/client/scripts/" "$REPO_ROOT/client/ui/" 2>&1 | grep -c "would reformat" || true)
|
|
if [ "$FORMAT_COUNT" -gt 0 ]; then
|
|
echo "pre-push: gdformat — $FORMAT_COUNT file(s) need formatting (advisory, not blocking)"
|
|
else
|
|
echo "pre-push: gdformat — OK"
|
|
fi
|
|
fi
|
|
|
|
# --- Rust lint (clippy + fmt) ---
|
|
if [ "$SERVER_CHANGED" -eq 0 ]; then
|
|
echo "pre-push: no server/ changes — skipping Rust checks"
|
|
elif command -v cargo >/dev/null 2>&1 && [ -d "$REPO_ROOT/server" ]; then
|
|
# fmt only needs source files — always safe to run
|
|
echo "pre-push: checking Rust (fmt)..."
|
|
if ! (cd "$REPO_ROOT/server" && cargo fmt --check 2>&1); then
|
|
ERRORS=$((ERRORS + 1))
|
|
else
|
|
echo "pre-push: fmt — OK"
|
|
fi
|
|
|
|
# clippy needs a build — skip if target/ doesn't exist (cold worktree)
|
|
if [ -d "$REPO_ROOT/server/target" ]; then
|
|
echo "pre-push: checking Rust (clippy)..."
|
|
if ! (cd "$REPO_ROOT/server" && cargo clippy -- -D warnings 2>&1); then
|
|
ERRORS=$((ERRORS + 1))
|
|
else
|
|
echo "pre-push: clippy — OK"
|
|
fi
|
|
else
|
|
echo "pre-push: skipping clippy (no target/ — run 'cargo build' once to enable)"
|
|
fi
|
|
|
|
# --- Rust dependency audit (cargo deny) — requires deny.toml config ---
|
|
if command -v cargo-deny >/dev/null 2>&1 && [ -f "$REPO_ROOT/server/deny.toml" ]; then
|
|
echo "pre-push: checking Rust (cargo deny)..."
|
|
if ! (cd "$REPO_ROOT/server" && cargo deny check 2>&1); then
|
|
ERRORS=$((ERRORS + 1))
|
|
else
|
|
echo "pre-push: cargo deny — OK"
|
|
fi
|
|
fi
|
|
else
|
|
echo "pre-push: WARNING — cargo not found or server/ missing, skipping Rust lint"
|
|
fi
|
|
|
|
# --- Python lint (ruff) ---
|
|
if [ "$TOOLING_CHANGED" -eq 0 ]; then
|
|
echo "pre-push: no tooling/ changes — skipping Python lint"
|
|
elif command -v ruff >/dev/null 2>&1 && [ -d "$REPO_ROOT/tooling" ]; then
|
|
echo "pre-push: checking Python (ruff)..."
|
|
if ! (cd "$REPO_ROOT" && ruff check tooling/ 2>&1); then
|
|
ERRORS=$((ERRORS + 1))
|
|
else
|
|
echo "pre-push: ruff — OK"
|
|
fi
|
|
else
|
|
echo "pre-push: skipping Python lint (ruff not found — install with: pip install 'ruff>=0.9')"
|
|
fi
|
|
|
|
# --- JSON syntax validation ---
|
|
if git rev-parse --verify "$REMOTE_REF" >/dev/null 2>&1; then
|
|
JSON_FILES=$(git diff --name-only "$REMOTE_REF"..HEAD -- '*.json' 2>/dev/null || true)
|
|
else
|
|
JSON_FILES=$(git ls-files '*.json')
|
|
fi
|
|
if [ -n "$JSON_FILES" ]; then
|
|
echo "pre-push: checking JSON syntax..."
|
|
JSON_FAIL=0
|
|
while IFS= read -r f; do
|
|
if [ -f "$REPO_ROOT/$f" ] && ! python3 -m json.tool "$REPO_ROOT/$f" >/dev/null 2>&1; then
|
|
echo " FAIL: $f"
|
|
JSON_FAIL=$((JSON_FAIL + 1))
|
|
fi
|
|
done <<< "$JSON_FILES"
|
|
if [ "$JSON_FAIL" -gt 0 ]; then
|
|
echo "pre-push: FAIL — $JSON_FAIL JSON file(s) have syntax errors"
|
|
ERRORS=$((ERRORS + 1))
|
|
else
|
|
echo "pre-push: JSON — OK ($(echo "$JSON_FILES" | wc -l) file(s))"
|
|
fi
|
|
else
|
|
echo "pre-push: no JSON changes — skipping"
|
|
fi
|
|
|
|
# --- systems.db stamp check (#857) ---
|
|
# If the branch touches server/data/systems.db and the meta stamp does not
|
|
# match current generator sources, reject the push. Prevents pushing a
|
|
# stale DB snapshot where generator source was modified but the DB was not
|
|
# regenerated.
|
|
#
|
|
# Runs whenever systems.db was modified in ANY branch commit vs. main —
|
|
# including on a branch's very first push (review T5: the previous version
|
|
# skipped the check for new branches because it compared against origin/$BRANCH,
|
|
# which didn't exist yet, leaving a gap where a stale DB could ship via the
|
|
# first push). We compare against origin/main — which always exists — so the
|
|
# check covers the first-push case.
|
|
DB_IN_PUSH=$(git diff --name-only origin/main...HEAD -- server/data/systems.db 2>/dev/null | wc -l)
|
|
if [ "$DB_IN_PUSH" -gt 0 ] && [ -f "$REPO_ROOT/tooling/check-systems-db-stamp" ]; then
|
|
echo "pre-push: checking systems.db stamp..."
|
|
rc=0
|
|
python3 "$REPO_ROOT/tooling/check-systems-db-stamp" || rc=$?
|
|
if [ "$rc" -eq 1 ]; then
|
|
# rc=1 means stale / unknown generator / missing source; message on stderr
|
|
echo " Fix: run 'make regen-db' then stage server/data/systems.db"
|
|
echo " Or use /pr-push — it handles regen automatically before pushing."
|
|
ERRORS=$((ERRORS + 1))
|
|
elif [ "$rc" -eq 2 ]; then
|
|
# rc=2 means no meta table — treat as unstamped, warn but don't block.
|
|
# This is legitimate immediately after the meta table is introduced;
|
|
# the next `make regen-db` will populate it (H4).
|
|
echo "pre-push: WARNING — systems.db has no meta stamp — run 'make regen-db' now if this DB was generated by you"
|
|
else
|
|
echo "pre-push: systems.db stamp — OK"
|
|
fi
|
|
else
|
|
echo "pre-push: systems.db not in push — skipping stamp check"
|
|
fi
|
|
|
|
if [ "$ERRORS" -gt 0 ]; then
|
|
echo ""
|
|
echo "pre-push: $ERRORS check(s) failed. Push aborted."
|
|
echo " Fix the errors above, then try again."
|
|
exit 1
|
|
fi
|
|
|
|
echo "pre-push: all checks passed."
|