feat: add local quality gate scripts and documentation
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.
This commit is contained in:
@@ -74,6 +74,9 @@ ehthumbs_vista.db
|
|||||||
$RECYCLE.BIN/
|
$RECYCLE.BIN/
|
||||||
*.stackdump
|
*.stackdump
|
||||||
|
|
||||||
|
# Local quality gate logs
|
||||||
|
logs/quality/
|
||||||
|
|
||||||
# Linux desktop and trash metadata
|
# Linux desktop and trash metadata
|
||||||
.directory
|
.directory
|
||||||
.Trash-*
|
.Trash-*
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# Local Quality Gate
|
||||||
|
|
||||||
|
A single command to detect broken GDScript, formatting issues, lint problems,
|
||||||
|
parser errors, and LLM-generated garbage before committing.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- **Python 3** with `gdtoolkit` (gdscript-toolkit):
|
||||||
|
```bash
|
||||||
|
pip install gdtoolkit
|
||||||
|
```
|
||||||
|
- **Godot 4** binary on `PATH`, or set the `GODOT_BIN` environment variable
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Full project check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux / macOS / Git Bash
|
||||||
|
./tools/quality.sh
|
||||||
|
|
||||||
|
# Windows PowerShell
|
||||||
|
pwsh ./tools/quality.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fast changed-files mode
|
||||||
|
|
||||||
|
Only runs `gdformat` / `gdlint` on `.gd` files modified since the last commit.
|
||||||
|
The Godot parser check still runs on the whole project.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./tools/quality.sh --changed
|
||||||
|
pwsh ./tools/quality.ps1 -Changed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auto-format (not part of the gate)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./tools/fix_format.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## What it checks
|
||||||
|
|
||||||
|
| Check | Tool | What it detects |
|
||||||
|
|---|---|---|
|
||||||
|
| **gdformat** | `gdformat --check .` | Unformatted code (indentation, spacing) |
|
||||||
|
| **gdlint** | `gdlint .` | Lint violations (unused args, long lines, naming, etc.) |
|
||||||
|
| **godot-check** | `godot --headless --check-only` | Parser/syntax errors, missing dependencies |
|
||||||
|
| **gut** | `godot -s gut_cmdln.gd` | Failing GUT unit tests (only if addon exists) |
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
If everything passes:
|
||||||
|
|
||||||
|
```
|
||||||
|
QUALITY RESULT: PASS
|
||||||
|
|
||||||
|
gdformat PASS
|
||||||
|
gdlint PASS
|
||||||
|
godot-check PASS
|
||||||
|
gut SKIPPED
|
||||||
|
|
||||||
|
Full logs:
|
||||||
|
logs/quality/latest/
|
||||||
|
```
|
||||||
|
|
||||||
|
On failure, only relevant errors and fix suggestions are shown:
|
||||||
|
|
||||||
|
```
|
||||||
|
QUALITY RESULT: FAIL
|
||||||
|
|
||||||
|
gdformat FAIL
|
||||||
|
gdlint FAIL
|
||||||
|
godot-check PASS
|
||||||
|
gut SKIPPED
|
||||||
|
|
||||||
|
res://simulation/SimNPC.gd:42 Unused argument 'delta'
|
||||||
|
res://simulation/task_manager.gd:88 Line too long (120 > 100)
|
||||||
|
|
||||||
|
NEXT FIX:
|
||||||
|
1. Fix unused-argument in SimNPC.gd
|
||||||
|
2. Run gdformat to auto-format files
|
||||||
|
|
||||||
|
Full logs:
|
||||||
|
logs/quality/latest/
|
||||||
|
```
|
||||||
|
|
||||||
|
Full logs are always written to `logs/quality/latest/` and never dumped
|
||||||
|
to the terminal.
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
|---|---|
|
||||||
|
| `GODOT_BIN` | Path to the Godot executable (auto-detected from PATH if unset) |
|
||||||
|
|
||||||
|
## CI setup
|
||||||
|
|
||||||
|
To add this to CI, install the dependencies and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install gdtoolkit
|
||||||
|
./tools/quality.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The script exits non-zero on any failure, so it will fail the CI step.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# fix_format.sh — Auto-format all GDScript files with gdformat.
|
||||||
|
# Usage: ./tools/fix_format.sh
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
# detect gdformat
|
||||||
|
FMT=""
|
||||||
|
if command -v gdformat &>/dev/null; then
|
||||||
|
FMT="gdformat"
|
||||||
|
elif python -m gdtoolkit.formatter --help &>/dev/null 2>&1; then
|
||||||
|
FMT="python -m gdtoolkit.formatter"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$FMT" ]]; then
|
||||||
|
echo "ERROR: gdformat not found. Install: pip install gdtoolkit"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Formatting all .gd files in $ROOT ..."
|
||||||
|
$FMT .
|
||||||
|
echo "Done."
|
||||||
@@ -0,0 +1,293 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
# quality.ps1 — Local quality gate for GDScript projects (Windows).
|
||||||
|
# Usage: pwsh ./tools/quality.ps1 [-Changed] [--changed]
|
||||||
|
param([switch]$Changed)
|
||||||
|
if ($args -contains '--changed') { $Changed = $true }
|
||||||
|
|
||||||
|
$ROOT = Split-Path -Parent $PSScriptRoot
|
||||||
|
Set-Location $ROOT
|
||||||
|
|
||||||
|
$LOG = "logs/quality/latest"
|
||||||
|
New-Item -ItemType Directory -Path $LOG -Force | Out-Null
|
||||||
|
|
||||||
|
$OVERALL = $false
|
||||||
|
$ERRORS = @()
|
||||||
|
$FIXES = @()
|
||||||
|
|
||||||
|
# -- tool detection -----------------------------------------------------------
|
||||||
|
function Find-Godot {
|
||||||
|
if ($env:GODOT_BIN) { return $env:GODOT_BIN }
|
||||||
|
try { return (Get-Command godot -ErrorAction Stop).Source } catch {}
|
||||||
|
try { return (Get-Command godot4 -ErrorAction Stop).Source } catch {}
|
||||||
|
$common = @(
|
||||||
|
"$env:ProgramFiles\Godot\godot.exe",
|
||||||
|
"${env:ProgramFiles(x86)}\Godot\godot.exe",
|
||||||
|
"$env:LOCALAPPDATA\Godot\godot.exe",
|
||||||
|
"$env:USERPROFILE\AppData\Local\Godot\godot.exe"
|
||||||
|
)
|
||||||
|
foreach ($p in $common) { if (Test-Path $p) { return $p } }
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-GdFormat {
|
||||||
|
if (Get-Command gdformat -ErrorAction SilentlyContinue) { return $true }
|
||||||
|
try {
|
||||||
|
$null = & python -m gdtoolkit.formatter --help 2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) { return $true }
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
$null = & python3 -m gdtoolkit.formatter --help 2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) { return $true }
|
||||||
|
} catch {}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-GdLint {
|
||||||
|
if (Get-Command gdlint -ErrorAction SilentlyContinue) { return $true }
|
||||||
|
try {
|
||||||
|
$null = & python -m gdtoolkit.linter --help 2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) { return $true }
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
$null = & python3 -m gdtoolkit.linter --help 2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) { return $true }
|
||||||
|
} catch {}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-GdFormat {
|
||||||
|
$args = $args
|
||||||
|
if (Get-Command gdformat -ErrorAction SilentlyContinue) {
|
||||||
|
& gdformat @args 2>&1
|
||||||
|
} else {
|
||||||
|
& python -m gdtoolkit.formatter @args 2>&1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-GdLint {
|
||||||
|
$args = $args
|
||||||
|
if (Get-Command gdlint -ErrorAction SilentlyContinue) {
|
||||||
|
& gdlint @args 2>&1
|
||||||
|
} else {
|
||||||
|
& python -m gdtoolkit.linter @args 2>&1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ChangedGd {
|
||||||
|
$f = git diff --name-only HEAD -- '*.gd' 2>$null
|
||||||
|
return @($f | Where-Object { $_ -ne '' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function To-ResPath($p) {
|
||||||
|
$p = $p.Replace('\', '/')
|
||||||
|
if ($p.StartsWith($ROOT)) { $p = $p.Substring($ROOT.Length + 1) }
|
||||||
|
if ($p.StartsWith('./')) { $p = $p.Substring(2) }
|
||||||
|
return "res://$p"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-GodotWithTimeout {
|
||||||
|
param([string]$Args)
|
||||||
|
$psi = New-Object Diagnostics.ProcessStartInfo
|
||||||
|
$psi.FileName = $GODOT
|
||||||
|
$psi.Arguments = $Args
|
||||||
|
$psi.RedirectStandardOutput = $true
|
||||||
|
$psi.RedirectStandardError = $true
|
||||||
|
$psi.UseShellExecute = $false
|
||||||
|
$psi.CreateNoWindow = $true
|
||||||
|
$p = [Diagnostics.Process]::Start($psi)
|
||||||
|
$exited = $p.WaitForExit(30000)
|
||||||
|
if (-not $exited) {
|
||||||
|
$p.Kill()
|
||||||
|
$output = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
return $output + $err, $null
|
||||||
|
}
|
||||||
|
$output = $p.StandardOutput.ReadToEnd()
|
||||||
|
$err = $p.StandardError.ReadToEnd()
|
||||||
|
return ($output + $err), $p.ExitCode
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- godot --------------------------------------------------------------------
|
||||||
|
$GODOT = Find-Godot
|
||||||
|
if (-not $GODOT) {
|
||||||
|
Write-Host "ERROR: Godot binary not found. Set GODOT_BIN or add godot/godot4 to PATH."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- 1. gdformat --------------------------------------------------------------
|
||||||
|
$fmtAvail = Test-GdFormat
|
||||||
|
$fmtResult = "PASS"
|
||||||
|
Set-Content -Path "$LOG/gdformat.log" -Value "" -Encoding utf8
|
||||||
|
if (-not $fmtAvail) {
|
||||||
|
$fmtResult = "SKIPPED"
|
||||||
|
Add-Content -Path "$LOG/gdformat.log" -Value "gdformat not found -- install: pip install gdtoolkit" -Encoding utf8
|
||||||
|
} else {
|
||||||
|
if ($Changed) {
|
||||||
|
$files = Get-ChangedGd
|
||||||
|
if ($files.Count -eq 0) {
|
||||||
|
Add-Content -Path "$LOG/gdformat.log" -Value "No changed .gd files to check." -Encoding utf8
|
||||||
|
} else {
|
||||||
|
$total = 0; $bad = 0
|
||||||
|
foreach ($f in $files) {
|
||||||
|
if (-not (Test-Path $f)) { continue }
|
||||||
|
$total++
|
||||||
|
$out = Invoke-GdFormat "--check" $f
|
||||||
|
$out | Add-Content -Path "$LOG/gdformat.log" -Encoding utf8
|
||||||
|
if ($LASTEXITCODE -ne 0) { $bad++ }
|
||||||
|
}
|
||||||
|
$msg = "($bad/$total files need formatting)"
|
||||||
|
Add-Content -Path "$LOG/gdformat.log" -Value $msg -Encoding utf8
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$out = Invoke-GdFormat "--check" "."
|
||||||
|
$out | Add-Content -Path "$LOG/gdformat.log" -Encoding utf8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Select-String -SimpleMatch "would reformat" -Path "$LOG/gdformat.log" -Quiet) {
|
||||||
|
$fmtResult = "FAIL"
|
||||||
|
$lines = Select-String -SimpleMatch "would reformat" -Path "$LOG/gdformat.log" | Select-Object -ExpandProperty Line
|
||||||
|
foreach ($line in $lines) {
|
||||||
|
$p = ($line -replace '^would reformat ', '').Trim()
|
||||||
|
$rp = To-ResPath $p
|
||||||
|
$ERRORS += "$rp needs formatting"
|
||||||
|
}
|
||||||
|
$FIXES += "Run gdformat to auto-format files"
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- 2. gdlint ----------------------------------------------------------------
|
||||||
|
$lintAvail = Test-GdLint
|
||||||
|
$lintResult = "PASS"
|
||||||
|
Set-Content -Path "$LOG/gdlint.log" -Value "" -Encoding utf8
|
||||||
|
if (-not $lintAvail) {
|
||||||
|
$lintResult = "SKIPPED"
|
||||||
|
Add-Content -Path "$LOG/gdlint.log" -Value "gdlint not found -- install: pip install gdtoolkit" -Encoding utf8
|
||||||
|
} else {
|
||||||
|
if ($Changed) {
|
||||||
|
$files = Get-ChangedGd
|
||||||
|
if ($files.Count -eq 0) {
|
||||||
|
Add-Content -Path "$LOG/gdlint.log" -Value "No changed .gd files to lint." -Encoding utf8
|
||||||
|
} else {
|
||||||
|
foreach ($f in $files) {
|
||||||
|
if (-not (Test-Path $f)) { continue }
|
||||||
|
$out = Invoke-GdLint $f
|
||||||
|
$out | Add-Content -Path "$LOG/gdlint.log" -Encoding utf8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$out = Invoke-GdLint "."
|
||||||
|
$out | Add-Content -Path "$LOG/gdlint.log" -Encoding utf8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$lintLines = Select-String -Pattern '^[^ ]+:\d+:\d+:' -Path "$LOG/gdlint.log" | Select-Object -ExpandProperty Line
|
||||||
|
if ($lintLines) {
|
||||||
|
$lintResult = "FAIL"
|
||||||
|
foreach ($line in $lintLines) {
|
||||||
|
$parts = $line -split ':', 4
|
||||||
|
if ($parts.Count -lt 4) { continue }
|
||||||
|
$file = $parts[0]; $lineno = $parts[1]; $msg = $parts[3]
|
||||||
|
$rp = To-ResPath $file
|
||||||
|
$ERRORS += "$rp`:$lineno $msg"
|
||||||
|
}
|
||||||
|
foreach ($line in $lintLines) {
|
||||||
|
$parts = $line -split ':', 5
|
||||||
|
if ($parts.Count -lt 5) { continue }
|
||||||
|
$rule = $parts[3]; $fname = Split-Path -Leaf $parts[0]
|
||||||
|
$FIXES += "Fix $rule in $fname"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- 3. godot headless check --------------------------------------------------
|
||||||
|
$godotResult = "PASS"
|
||||||
|
Set-Content -Path "$LOG/godot-check.log" -Value "" -Encoding utf8
|
||||||
|
$godotArgs = "--headless --path `"$ROOT`" --quit"
|
||||||
|
$godotOutput, $godotExit = Invoke-GodotWithTimeout -Args $godotArgs
|
||||||
|
$godotOutput | Out-File -FilePath "$LOG/godot-check.log" -Encoding utf8
|
||||||
|
if ($godotExit -eq $null) {
|
||||||
|
$godotResult = "PASS"
|
||||||
|
Add-Content -Path "$LOG/godot-check.log" -Value "[timed out after 30s - project runs continuously]" -Encoding utf8
|
||||||
|
} elseif ($godotExit -eq 0) {
|
||||||
|
$godotResult = "PASS"
|
||||||
|
} else {
|
||||||
|
$godotResult = "FAIL"
|
||||||
|
$godotOutput | Select-String -Pattern 'error|warning|parse|syntax' | Select-Object -First 20 | ForEach-Object {
|
||||||
|
$ERRORS += "godot: $_"
|
||||||
|
}
|
||||||
|
$FIXES += "Fix Godot parser errors"
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- 4. GUT (optional) --------------------------------------------------------
|
||||||
|
$gutResult = "SKIPPED"
|
||||||
|
Set-Content -Path "$LOG/gut.log" -Value "" -Encoding utf8
|
||||||
|
if (Test-Path "addons/gut/gut_cmdln.gd") {
|
||||||
|
$gutArgs = "--headless --path `"$ROOT`" -s addons/gut/gut_cmdln.gd --quit"
|
||||||
|
$gutOutput, $gutExit = Invoke-GodotWithTimeout -Args $gutArgs
|
||||||
|
$gutOutput | Out-File -FilePath "$LOG/gut.log" -Encoding utf8
|
||||||
|
if ($gutExit -eq $null) {
|
||||||
|
$gutResult = "FAIL"
|
||||||
|
Add-Content -Path "$LOG/gut.log" -Value "[timed out after 30s]" -Encoding utf8
|
||||||
|
$ERRORS += "gut: GUT test timed out after 30 seconds"
|
||||||
|
$FIXES += "Fix GUT tests (timeout)"
|
||||||
|
} elseif ($gutExit -eq 0) {
|
||||||
|
$gutResult = "PASS"
|
||||||
|
} else {
|
||||||
|
$gutResult = "FAIL"
|
||||||
|
$gutOutput | Select-String -Pattern 'fail|error|assert' | Select-Object -First 20 | ForEach-Object {
|
||||||
|
$ERRORS += "gut: $_"
|
||||||
|
}
|
||||||
|
$FIXES += "Fix failing GUT tests"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# -- summary ------------------------------------------------------------------
|
||||||
|
if ($fmtResult -eq 'FAIL' -or $lintResult -eq 'FAIL' -or $godotResult -eq 'FAIL' -or $gutResult -eq 'FAIL') {
|
||||||
|
$OVERALL = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
if ($OVERALL) { Write-Host "QUALITY RESULT: FAIL" } else { Write-Host "QUALITY RESULT: PASS" }
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " gdformat $fmtResult"
|
||||||
|
Write-Host " gdlint $lintResult"
|
||||||
|
Write-Host " godot-check $godotResult"
|
||||||
|
Write-Host " gut $gutResult"
|
||||||
|
|
||||||
|
$seen = @{}
|
||||||
|
foreach ($e in $ERRORS) {
|
||||||
|
if ($seen.ContainsKey($e)) { continue }
|
||||||
|
$seen[$e] = $true
|
||||||
|
Write-Host " $e"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($OVERALL -and $FIXES.Count -gt 0) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " NEXT FIX:"
|
||||||
|
$seenFix = @{}
|
||||||
|
$idx = 1
|
||||||
|
foreach ($fix in $FIXES) {
|
||||||
|
if ($seenFix.ContainsKey($fix)) { continue }
|
||||||
|
$seenFix[$fix] = $true
|
||||||
|
Write-Host " $idx. $fix"
|
||||||
|
$idx++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Full logs:"
|
||||||
|
Write-Host " $LOG/"
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# save summary
|
||||||
|
if ($OVERALL) { $resultText = "FAIL" } else { $resultText = "PASS" }
|
||||||
|
$summary = @"
|
||||||
|
QUALITY RESULT: $resultText
|
||||||
|
|
||||||
|
gdformat $fmtResult
|
||||||
|
gdlint $lintResult
|
||||||
|
godot-check $godotResult
|
||||||
|
gut $gutResult
|
||||||
|
"@
|
||||||
|
Set-Content -Path "$LOG/summary.txt" -Value $summary -Encoding utf8
|
||||||
|
|
||||||
|
if ($OVERALL) { exit 1 } else { exit 0 }
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
#!/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
|
||||||
Reference in New Issue
Block a user