28a2e42c41
Add quality.sh (bash), quality.ps1 (PowerShell), and fix_format.sh for automated GDScript quality checking. Includes: - gdformat --check (formatting gate) - gdlint (lint rules) - godot --headless project validation (with 30s timeout) - Optional GUT test support - --changed mode for checking only modified .gd files - Compact summary output with NEXT FIX suggestions - Full logs saved to logs/quality/latest/ Add docs/local_quality_gate.md with setup and usage instructions.
243 lines
6.7 KiB
Bash
243 lines
6.7 KiB
Bash
#!/usr/bin/env bash
|
|
# quality.sh — Local quality gate for GDScript projects.
|
|
# Usage: ./tools/quality.sh [--changed]
|
|
# --changed Only check changed .gd files (gdformat/gdlint only)
|
|
set -uo pipefail
|
|
IFS=$'\n\t'
|
|
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
cd "$ROOT" || exit 1
|
|
LOG="logs/quality/latest"
|
|
mkdir -p "$LOG"
|
|
|
|
CHANGED=false
|
|
[[ "${1:-}" == "--changed" ]] && CHANGED=true
|
|
OVERALL=false
|
|
ERRORS=()
|
|
FIXES=()
|
|
|
|
# -- tool detection -----------------------------------------------------------
|
|
find_godot() {
|
|
if [[ -n "${GODOT_BIN:-}" ]]; then echo "$GODOT_BIN"; return 0; fi
|
|
for c in godot godot4; do command -v "$c" &>/dev/null && echo "$c" && return 0; done
|
|
return 1
|
|
}
|
|
|
|
find_gdformat() {
|
|
if command -v gdformat &>/dev/null; then echo "gdformat"; return 0; fi
|
|
if python -m gdtoolkit.formatter --help &>/dev/null 2>&1; then echo "python -m gdtoolkit.formatter"; return 0; fi
|
|
return 1
|
|
}
|
|
|
|
find_gdlint() {
|
|
if command -v gdlint &>/dev/null; then echo "gdlint"; return 0; fi
|
|
if python -m gdtoolkit.linter --help &>/dev/null 2>&1; then echo "python -m gdtoolkit.linter"; return 0; fi
|
|
return 1
|
|
}
|
|
|
|
changed_gd() {
|
|
git diff --name-only HEAD -- '*.gd' 2>/dev/null || true
|
|
}
|
|
|
|
res_path() {
|
|
local p="$1"
|
|
p="${p//\\//}"
|
|
if [[ "$p" == "$ROOT"* ]]; then p="${p#"$ROOT"/}"; fi
|
|
p="${p#./}"
|
|
echo "res://$p"
|
|
}
|
|
|
|
contains_element() {
|
|
local e; for e in "${@:2}"; do [[ "$e" == "$1" ]] && return 0; done; return 1
|
|
}
|
|
|
|
need_timeout() {
|
|
command -v timeout &>/dev/null
|
|
}
|
|
|
|
# -- godot --------------------------------------------------------------------
|
|
GODOT="$(find_godot)" || true
|
|
if [[ -z "$GODOT" ]]; then
|
|
echo "ERROR: Godot binary not found. Set GODOT_BIN or add godot/godot4 to PATH."
|
|
exit 1
|
|
fi
|
|
|
|
# -- 1. gdformat --------------------------------------------------------------
|
|
fmt_tool=$(find_gdformat) || true
|
|
fmt_result="PASS"
|
|
: > "$LOG/gdformat.log"
|
|
if [[ -z "$fmt_tool" ]]; then
|
|
fmt_result="SKIPPED"
|
|
echo "gdformat not found — install: pip install gdtoolkit" > "$LOG/gdformat.log"
|
|
else
|
|
if $CHANGED; then
|
|
files=$(changed_gd)
|
|
if [[ -z "$files" ]]; then
|
|
echo "No changed .gd files to check." > "$LOG/gdformat.log"
|
|
else
|
|
total=0; bad=0
|
|
while IFS= read -r f; do
|
|
[[ -f "$f" ]] || continue
|
|
total=$((total+1))
|
|
if ! $fmt_tool --check "$f" >> "$LOG/gdformat.log" 2>&1; then
|
|
bad=$((bad+1))
|
|
fi
|
|
done <<< "$files"
|
|
echo "($bad/$total files need formatting)" >> "$LOG/gdformat.log"
|
|
fi
|
|
else
|
|
$fmt_tool --check . >> "$LOG/gdformat.log" 2>&1 || true
|
|
fi
|
|
fi
|
|
|
|
if grep -q "would reformat" "$LOG/gdformat.log" 2>/dev/null; then
|
|
fmt_result="FAIL"
|
|
while IFS= read -r line; do
|
|
line="${line#would reformat }"
|
|
rp=$(res_path "$line")
|
|
ERRORS+=("$rp needs formatting")
|
|
done < <(grep "would reformat" "$LOG/gdformat.log")
|
|
FIXES+=("Run gdformat to auto-format files")
|
|
fi
|
|
|
|
# -- 2. gdlint ----------------------------------------------------------------
|
|
lint_tool=$(find_gdlint) || true
|
|
lint_result="PASS"
|
|
: > "$LOG/gdlint.log"
|
|
if [[ -z "$lint_tool" ]]; then
|
|
lint_result="SKIPPED"
|
|
echo "gdlint not found — install: pip install gdtoolkit" > "$LOG/gdlint.log"
|
|
else
|
|
if $CHANGED; then
|
|
files=$(changed_gd)
|
|
if [[ -z "$files" ]]; then
|
|
echo "No changed .gd files to lint." > "$LOG/gdlint.log"
|
|
else
|
|
while IFS= read -r f; do
|
|
[[ -f "$f" ]] || continue
|
|
$lint_tool "$f" >> "$LOG/gdlint.log" 2>&1 || true
|
|
done <<< "$files"
|
|
fi
|
|
else
|
|
$lint_tool . >> "$LOG/gdlint.log" 2>&1 || true
|
|
fi
|
|
fi
|
|
|
|
if grep -qE "^[^ ]+:[0-9]+:" "$LOG/gdlint.log" 2>/dev/null; then
|
|
lint_result="FAIL"
|
|
while IFS= read -r line; do
|
|
[[ "$line" =~ ^([^:]+):([0-9]+):([0-9]+):[^:]+:(.*) ]] || continue
|
|
file="${BASH_REMATCH[1]}"
|
|
lineno="${BASH_REMATCH[2]}"
|
|
msg="${BASH_REMATCH[4]}"
|
|
msg="${msg## }"
|
|
rp=$(res_path "$file")
|
|
ERRORS+=("$rp:$lineno $msg")
|
|
done < <(grep -E "^[^ ]+:[0-9]+:" "$LOG/gdlint.log")
|
|
|
|
while IFS= read -r line; do
|
|
[[ "$line" =~ ^([^:]+):([0-9]+):([0-9]+):([^:]+):(.*) ]] || continue
|
|
rule="${BASH_REMATCH[4]}"
|
|
fname=$(basename "${BASH_REMATCH[1]}")
|
|
FIXES+=("Fix $rule in $fname")
|
|
done < <(grep -E "^[^ ]+:[0-9]+:" "$LOG/gdlint.log")
|
|
fi
|
|
|
|
# -- 3. godot headless check --------------------------------------------------
|
|
godot_result="PASS"
|
|
: > "$LOG/godot-check.log"
|
|
GODOT_CMD="$GODOT --headless --path \"$ROOT\" --check-only --quit"
|
|
if need_timeout; then
|
|
if timeout 30 $GODOT_CMD >> "$LOG/godot-check.log" 2>&1; then
|
|
godot_result="PASS"
|
|
else
|
|
ec=$?
|
|
if [[ $ec -eq 124 ]]; then
|
|
godot_result="PASS"
|
|
echo "[timed out after 30s — project runs continuously]" >> "$LOG/godot-check.log"
|
|
else
|
|
godot_result="FAIL"
|
|
fi
|
|
fi
|
|
else
|
|
if $GODOT --headless --path "$ROOT" --quit >> "$LOG/godot-check.log" 2>&1; then
|
|
godot_result="PASS"
|
|
else
|
|
godot_result="FAIL"
|
|
fi
|
|
fi
|
|
if [[ "$godot_result" == "FAIL" ]]; then
|
|
grep -iE "error|warning|parse|syntax" "$LOG/godot-check.log" 2>/dev/null | head -20 | while IFS= read -r errline; do
|
|
ERRORS+=("godot: $errline")
|
|
done
|
|
FIXES+=("Fix Godot parser errors")
|
|
fi
|
|
|
|
# -- 4. GUT (optional) --------------------------------------------------------
|
|
gut_result="SKIPPED"
|
|
: > "$LOG/gut.log"
|
|
if [[ -f "addons/gut/gut_cmdln.gd" ]]; then
|
|
if $GODOT --headless --path "$ROOT" -s addons/gut/gut_cmdln.gd >> "$LOG/gut.log" 2>&1; then
|
|
gut_result="PASS"
|
|
else
|
|
gut_result="FAIL"
|
|
grep -iE "fail|error|assert" "$LOG/gut.log" 2>/dev/null | head -20 | while IFS= read -r errline; do
|
|
ERRORS+=("gut: $errline")
|
|
done
|
|
FIXES+=("Fix failing GUT tests")
|
|
fi
|
|
fi
|
|
|
|
# -- summary ------------------------------------------------------------------
|
|
if [[ "$fmt_result" == "FAIL" || "$lint_result" == "FAIL" || "$godot_result" == "FAIL" || "$gut_result" == "FAIL" ]]; then
|
|
OVERALL=true
|
|
fi
|
|
|
|
echo ""
|
|
if $OVERALL; then
|
|
echo "QUALITY RESULT: FAIL"
|
|
else
|
|
echo "QUALITY RESULT: PASS"
|
|
fi
|
|
echo ""
|
|
echo " gdformat $fmt_result"
|
|
echo " gdlint $lint_result"
|
|
echo " godot-check $godot_result"
|
|
echo " gut $gut_result"
|
|
|
|
seen_err=()
|
|
for e in "${ERRORS[@]}"; do
|
|
contains_element "$e" "${seen_err[@]}" && continue
|
|
seen_err+=("$e")
|
|
echo " $e"
|
|
done
|
|
|
|
if $OVERALL && [[ ${#FIXES[@]} -gt 0 ]]; then
|
|
echo ""
|
|
echo " NEXT FIX:"
|
|
seen_fix=()
|
|
idx=1
|
|
for fix in "${FIXES[@]}"; do
|
|
contains_element "$fix" "${seen_fix[@]}" && continue
|
|
seen_fix+=("$fix")
|
|
echo " $idx. $fix"
|
|
idx=$((idx+1))
|
|
done
|
|
fi
|
|
|
|
echo ""
|
|
echo "Full logs:"
|
|
echo " $LOG/"
|
|
echo ""
|
|
|
|
{
|
|
echo "QUALITY RESULT: $($OVERALL && echo FAIL || echo PASS)"
|
|
echo ""
|
|
echo " gdformat $fmt_result"
|
|
echo " gdlint $lint_result"
|
|
echo " godot-check $godot_result"
|
|
echo " gut $gut_result"
|
|
} > "$LOG/summary.txt"
|
|
|
|
$OVERALL && exit 1 || exit 0
|