#!/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 $script:PreviousLocation = Get-Location Set-Location $ROOT $LOG = "logs/quality/latest" New-Item -ItemType Directory -Path $LOG -Force | Out-Null $GODOT_PROFILE = Join-Path $ROOT "logs/quality/godot_profile" New-Item -ItemType Directory -Path $GODOT_PROFILE -Force | Out-Null $OVERALL = $false $ERRORS = @() $FIXES = @() $OWNED_GDSCRIPT_ROOTS = @("player", "simulation", "tests", "tools", "world") $CROSS_PLATFORM_PLUGIN_FILES = @( "addons/terrain_3d/terrain.gdextension", "addons/terrain_3d/bin/libterrain.macos.debug.framework/libterrain.macos.debug", "addons/terrain_3d/bin/libterrain.macos.release.framework/libterrain.macos.release", "addons/terrain_3d/bin/libterrain.windows.debug.x86_64.dll", "addons/terrain_3d/bin/libterrain.windows.release.x86_64.dll" ) $REQUIRED_GODOT_SERIES = "4.7" $RUNNING_ON_MACOS = [Runtime.InteropServices.RuntimeInformation]::IsOSPlatform( [Runtime.InteropServices.OSPlatform]::OSX ) function Exit-Quality { param([int]$ExitCode) Set-Location $script:PreviousLocation exit $ExitCode } # -- tool detection ----------------------------------------------------------- function ConvertTo-ConsoleGodotPath($Path) { if (-not $Path) { return $null } $separator = [IO.Path]::DirectorySeparatorChar $normalized = $Path.Replace([char]47, $separator).Replace([char]92, $separator).Trim('"') if ($normalized.EndsWith(".exe")) { $consolePath = $normalized -replace '\.exe$', '_console.exe' if (Test-Path $consolePath) { return $consolePath } } if (Test-Path $normalized) { return $normalized } return $null } function Find-ProjectGodot { $metadata = Join-Path $ROOT ".godot/editor/project_metadata.cfg" if (-not (Test-Path $metadata)) { return $null } $line = Select-String -Path $metadata -Pattern '^executable_path=' | Select-Object -First 1 if (-not $line) { return $null } $rawPath = ($line.Line -replace '^executable_path=', '').Trim().Trim('"') return ConvertTo-ConsoleGodotPath $rawPath } function Find-Godot { if ($env:GODOT_BIN) { return (ConvertTo-ConsoleGodotPath $env:GODOT_BIN) } $projectGodot = Find-ProjectGodot if ($projectGodot) { return $projectGodot } foreach ($name in @("godot", "godot4")) { $command = Get-Command $name -ErrorAction SilentlyContinue if (-not $command) { continue } $candidate = ConvertTo-ConsoleGodotPath $command.Source if ($candidate) { return $candidate } } $common = @() if ($env:ProgramFiles) { $common += Join-Path $env:ProgramFiles "Godot/godot.exe" } if (${env:ProgramFiles(x86)}) { $common += Join-Path ${env:ProgramFiles(x86)} "Godot/godot.exe" } if ($env:LOCALAPPDATA) { $common += Join-Path $env:LOCALAPPDATA "Godot/godot.exe" $common += Join-Path $env:LOCALAPPDATA "Programs/Godot/godot.exe" } if ($env:USERPROFILE) { $common += Join-Path $env:USERPROFILE "scoop/apps/godot/current/godot.exe" } foreach ($path in $common) { $candidate = ConvertTo-ConsoleGodotPath $path if ($candidate) { return $candidate } } $searchRoots = @() if ($env:ProgramFiles) { $searchRoots += Join-Path $env:ProgramFiles "Godot" } if (${env:ProgramFiles(x86)}) { $searchRoots += Join-Path ${env:ProgramFiles(x86)} "Godot" } if ($env:LOCALAPPDATA) { $searchRoots += Join-Path $env:LOCALAPPDATA "Godot" $searchRoots += Join-Path $env:LOCALAPPDATA "Programs/Godot" } foreach ($searchRoot in $searchRoots) { $bundled = Get-ChildItem $searchRoot -Filter "Godot*_console.exe" -File -ErrorAction SilentlyContinue | Sort-Object Name -Descending | Select-Object -First 1 if ($bundled) { return $bundled.FullName } } return $null } function Resolve-GdTool { param( [string]$ExecutableName, [string]$ModuleName ) $localTool = Join-Path $ROOT ".venv/Scripts/$ExecutableName.exe" if (Test-Path $localTool) { return [pscustomobject]@{ Command = $localTool; Prefix = @() } } $directTool = Get-Command $ExecutableName -ErrorAction SilentlyContinue if ($directTool) { return [pscustomobject]@{ Command = $directTool.Source; Prefix = @() } } $localPython = Join-Path $ROOT ".venv/Scripts/python.exe" $pythonCandidates = @( [pscustomobject]@{ Command = $localPython; Prefix = @() }, [pscustomobject]@{ Command = "python"; Prefix = @() }, [pscustomobject]@{ Command = "python3"; Prefix = @() }, [pscustomobject]@{ Command = "py"; Prefix = @("-3") } ) foreach ($candidate in $pythonCandidates) { $isLocalPython = $candidate.Command -eq $localPython if ($isLocalPython) { if (-not (Test-Path $candidate.Command)) { continue } $command = $candidate.Command } else { $resolved = Get-Command $candidate.Command -ErrorAction SilentlyContinue if (-not $resolved) { continue } $command = $resolved.Source } $prefix = @($candidate.Prefix) $null = @(& $command @prefix -m $ModuleName --help 2>&1) if ($LASTEXITCODE -eq 0) { return [pscustomobject]@{ Command = $command; Prefix = $prefix + @("-m", $ModuleName) } } } return $null } $script:GdFormatRunner = $null $script:GdLintRunner = $null function Test-GdFormat { $script:GdFormatRunner = Resolve-GdTool "gdformat" "gdtoolkit.formatter" return $null -ne $script:GdFormatRunner } function Test-GdLint { $script:GdLintRunner = Resolve-GdTool "gdlint" "gdtoolkit.linter" return $null -ne $script:GdLintRunner } $script:GdToolExitCode = 0 function Invoke-GdFormat { $command = $script:GdFormatRunner.Command $prefix = @($script:GdFormatRunner.Prefix) & $command @prefix @args 2>&1 $script:GdToolExitCode = $LASTEXITCODE } function Invoke-GdLint { $command = $script:GdLintRunner.Command $prefix = @($script:GdLintRunner.Prefix) & $command @prefix @args 2>&1 $script:GdToolExitCode = $LASTEXITCODE } function Get-ChangedGd { $changed = @(git diff --name-only HEAD -- '*.gd' 2>$null) $untracked = @(git ls-files --others --exclude-standard -- '*.gd' 2>$null) return @( (@($changed) + @($untracked)) | Where-Object { $_ -ne '' -and -not $_.Replace('\', '/').StartsWith('addons/') } | Sort-Object -Unique ) } function To-ResPath($p) { $p = $p.Replace('\', '/') $normalizedRoot = $ROOT.Replace('\', '/').TrimEnd('/') $rootPrefix = "$normalizedRoot/" if ($p.StartsWith($rootPrefix, [StringComparison]::OrdinalIgnoreCase)) { $p = $p.Substring($rootPrefix.Length) } if ($p.StartsWith('./')) { $p = $p.Substring(2) } return "res://$p" } function Test-AllowedScenarioDiagnostic { param([string]$Diagnostic) # Godot 4.7 can fail to read the system certificate store on macOS when HOME # is intentionally isolated. Scenario tests do not perform network access. if ($script:RUNNING_ON_MACOS -and $Diagnostic -ceq 'ERROR: Condition "ret != noErr" is true. Returning: ""') { return $true } # Pinned Terrain3D 1.0.2 invokes this Godot 4.7 compatibility method while # loading its vendored GDExtension. Remove this exception with that vendor fix. if ($Diagnostic -ceq 'WARNING: instance_reset_physics_interpolation() is deprecated.') { return $true } # Godot 4.7's dummy headless renderer retains exactly one DummyShader RID at # process teardown. A different RID type or count remains a gate failure. if ($Diagnostic -ceq "ERROR: 1 RID allocations of type 'N13RendererDummy15MaterialStorage11DummyShaderE' were leaked at exit.") { return $true } return $false } function Invoke-GodotWithTimeout { param( [string]$ArgumentString, [int]$TimeoutSeconds = 30 ) $psi = New-Object Diagnostics.ProcessStartInfo $psi.FileName = $GODOT $psi.Arguments = $ArgumentString $psi.RedirectStandardOutput = $true $psi.RedirectStandardError = $true $psi.UseShellExecute = $false $psi.CreateNoWindow = $true $previousAppData = $env:APPDATA $previousLocalAppData = $env:LOCALAPPDATA $env:APPDATA = $GODOT_PROFILE $env:LOCALAPPDATA = $GODOT_PROFILE try { $p = [Diagnostics.Process]::Start($psi) } catch { return "Failed to start Godot: $($_.Exception.Message)", $null } finally { $env:APPDATA = $previousAppData $env:LOCALAPPDATA = $previousLocalAppData } $outputTask = $p.StandardOutput.ReadToEndAsync() $errorTask = $p.StandardError.ReadToEndAsync() $exited = $p.WaitForExit($TimeoutSeconds * 1000) if (-not $exited) { $p.Kill() $p.WaitForExit() $output = $outputTask.Result $err = $errorTask.Result return ($output + $err), $null } $output = $outputTask.Result $err = $errorTask.Result 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-Quality 1 } $godotVersionOutput = @(& $GODOT --version 2>&1) $godotVersionExit = $LASTEXITCODE if ($godotVersionExit -ne 0 -or $godotVersionOutput.Count -eq 0) { Write-Host "ERROR: Could not query the Godot version from '$GODOT'." Exit-Quality 1 } $GODOT_VERSION = ([string]$godotVersionOutput[0]).Trim() $requiredVersionPattern = '^' + [regex]::Escape($REQUIRED_GODOT_SERIES) + '([.-]|$)' if ($GODOT_VERSION -notmatch $requiredVersionPattern) { Write-Host "ERROR: Godot $REQUIRED_GODOT_SERIES.x is required; found '$GODOT_VERSION'." Exit-Quality 1 } # -- 0. Cross-platform plugin payload ----------------------------------------- $platformAssetsResult = "PASS" Set-Content -Path "$LOG/platform-assets.log" -Value "" -Encoding utf8 foreach ($pluginFile in $CROSS_PLATFORM_PLUGIN_FILES) { if (-not (Test-Path $pluginFile -PathType Leaf)) { $platformAssetsResult = "FAIL" Add-Content -Path "$LOG/platform-assets.log" -Value "Missing $pluginFile" -Encoding utf8 $ERRORS += "platform-assets: missing res://$pluginFile" } } if ($platformAssetsResult -eq "FAIL") { $FIXES += "Restore the complete pinned Terrain3D 1.0.2 package" } # -- 1. Godot import bootstrap ------------------------------------------------ $importResult = "PASS" Set-Content -Path "$LOG/godot-import.log" -Value "" -Encoding utf8 $classCache = Join-Path $ROOT ".godot/global_script_class_cache.cfg" $hasProjectClasses = (Test-Path $classCache) -and (Select-String -Path $classCache -SimpleMatch '"class": &"SimNPC"' -Quiet) $hasGutClasses = (Test-Path $classCache) -and (Select-String -Path $classCache -SimpleMatch '"class": &"GutTest"' -Quiet) $changedGd = @(Get-ChangedGd) $needsImport = (-not $hasProjectClasses) -or ((Test-Path "addons/gut/gut_cmdln.gd") -and (-not $hasGutClasses)) -or ($changedGd.Count -gt 0) if ($needsImport) { $importArgs = "--headless --path `"$ROOT`" --import" $importOutput, $importExit = Invoke-GodotWithTimeout -ArgumentString $importArgs -TimeoutSeconds 60 $importOutput | Add-Content -Path "$LOG/godot-import.log" -Encoding utf8 if ($importExit -eq $null -or $importExit -ne 0) { $importResult = "FAIL" if ($importExit -eq $null) { Add-Content -Path "$LOG/godot-import.log" -Value "[timed out after 60s]" -Encoding utf8 } } } $hasProjectClasses = (Test-Path $classCache) -and (Select-String -Path $classCache -SimpleMatch '"class": &"SimNPC"' -Quiet) $hasGutClasses = (Test-Path $classCache) -and (Select-String -Path $classCache -SimpleMatch '"class": &"GutTest"' -Quiet) if (-not $hasProjectClasses -or ((Test-Path "addons/gut/gut_cmdln.gd") -and (-not $hasGutClasses))) { $importResult = "FAIL" } $importLoadError = Select-String -Path "$LOG/godot-import.log" -Pattern 'SCRIPT ERROR:|Parse Error:|Failed to load script' -Quiet if ($importLoadError) { $importResult = "FAIL" } if ($importResult -eq "FAIL") { $ERRORS += "godot-import: failed to build the global class cache" $FIXES += "Fix Godot import errors before running headless checks" } # -- 2. gdformat -------------------------------------------------------------- $fmtAvail = Test-GdFormat $fmtResult = "PASS" $fmtFailed = $false Set-Content -Path "$LOG/gdformat.log" -Value "" -Encoding utf8 if (-not $fmtAvail) { $fmtResult = "FAIL" Add-Content -Path "$LOG/gdformat.log" -Value "gdformat not found -- install requirements-dev.txt (see docs/local_quality_gate.md)" -Encoding utf8 $ERRORS += "gdformat: required formatter not found" $FIXES += "Install the pinned dev tools from requirements-dev.txt" } 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 $formatExit = $script:GdToolExitCode $out | Add-Content -Path "$LOG/gdformat.log" -Encoding utf8 if ($formatExit -ne 0) { $bad++; $fmtFailed = $true } } $msg = "($bad/$total files need formatting)" Add-Content -Path "$LOG/gdformat.log" -Value $msg -Encoding utf8 } } else { $formatArgs = @("--check") + $OWNED_GDSCRIPT_ROOTS $out = Invoke-GdFormat @formatArgs $formatExit = $script:GdToolExitCode $out | Add-Content -Path "$LOG/gdformat.log" -Encoding utf8 if ($formatExit -ne 0) { $fmtFailed = $true } } } if ($fmtFailed) { $fmtResult = "FAIL" } $formatDiagnostics = Select-String -SimpleMatch "would reformat" -Path "$LOG/gdformat.log" -Quiet if ($formatDiagnostics) { $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" } elseif ($fmtFailed) { $ERRORS += "gdformat: formatter exited non-zero; see $LOG/gdformat.log" $FIXES += "Fix the gdformat error before continuing" } # -- 3. gdlint ---------------------------------------------------------------- $lintAvail = Test-GdLint $lintResult = "PASS" $lintFailed = $false Set-Content -Path "$LOG/gdlint.log" -Value "" -Encoding utf8 if (-not $lintAvail) { $lintResult = "FAIL" Add-Content -Path "$LOG/gdlint.log" -Value "gdlint not found -- install requirements-dev.txt (see docs/local_quality_gate.md)" -Encoding utf8 $ERRORS += "gdlint: required linter not found" $FIXES += "Install the pinned dev tools from requirements-dev.txt" } 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 $lintExit = $script:GdToolExitCode $out | Add-Content -Path "$LOG/gdlint.log" -Encoding utf8 if ($lintExit -ne 0) { $lintFailed = $true } } } } else { $out = Invoke-GdLint @OWNED_GDSCRIPT_ROOTS $lintExit = $script:GdToolExitCode $out | Add-Content -Path "$LOG/gdlint.log" -Encoding utf8 if ($lintExit -ne 0) { $lintFailed = $true } } } if ($lintFailed) { $lintResult = "FAIL" } $lintLines = Select-String -Pattern '^[^ ]+:\d+:\d+:' -Path "$LOG/gdlint.log" | Select-Object -ExpandProperty Line if ($lintLines) { 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" } } elseif ($lintFailed) { $ERRORS += "gdlint: linter exited non-zero; see $LOG/gdlint.log" $FIXES += "Fix the gdlint error before continuing" } # -- 4. godot headless check -------------------------------------------------- $godotResult = "PASS" Set-Content -Path "$LOG/godot-check.log" -Value "" -Encoding utf8 $godotArgs = "--headless --path `"$ROOT`" --script res://tests/simulation_definitions_test.gd" $godotOutput, $godotExit = Invoke-GodotWithTimeout -ArgumentString $godotArgs $godotOutput | Out-File -FilePath "$LOG/godot-check.log" -Encoding utf8 if ($godotExit -eq $null) { $godotResult = "FAIL" Add-Content -Path "$LOG/godot-check.log" -Value "[timed out after 30s]" -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" } $godotLoadError = Select-String -Path "$LOG/godot-check.log" -Pattern 'SCRIPT ERROR:|Parse Error:|Failed to load script' -Quiet if ($godotLoadError) { $godotResult = "FAIL" $ERRORS += "godot: project check reported a script load or parse error" $FIXES += "Fix Godot parser errors" } # -- 5. project scenario tests ------------------------------------------------ $scenarioResult = "PASS" Set-Content -Path "$LOG/scenarios.log" -Value "" -Encoding utf8 $scenarioTests = Get-ChildItem "tests" -Filter "*_test.gd" | Sort-Object Name foreach ($test in $scenarioTests) { Add-Content -Path "$LOG/scenarios.log" -Value "[RUN] $($test.Name)" -Encoding utf8 $testArgs = "--headless --path `"$ROOT`" --script res://tests/$($test.Name)" $testOutput, $testExit = Invoke-GodotWithTimeout -ArgumentString $testArgs $testOutput | Add-Content -Path "$LOG/scenarios.log" -Encoding utf8 if ($testExit -eq $null -or $testExit -ne 0) { $scenarioResult = "FAIL" $ERRORS += "scenario: $($test.Name) failed or timed out" } } $compatibilityTest = "jajce_presentation_quality_test.gd" $compatibilityLabel = "$compatibilityTest (gl_compatibility)" Add-Content -Path "$LOG/scenarios.log" -Value "[RUN] $compatibilityLabel" -Encoding utf8 $compatibilityArgs = "--headless --rendering-method gl_compatibility --path `"$ROOT`" --script res://tests/$compatibilityTest" $compatibilityOutput, $compatibilityExit = Invoke-GodotWithTimeout -ArgumentString $compatibilityArgs $compatibilityOutput | Add-Content -Path "$LOG/scenarios.log" -Encoding utf8 if ($compatibilityExit -eq $null -or $compatibilityExit -ne 0) { $scenarioResult = "FAIL" $ERRORS += "scenario: $compatibilityLabel failed or timed out" } $unexpectedScenarioDiagnostics = @() $currentScenario = "unknown scenario" foreach ($rawLine in Get-Content -Path "$LOG/scenarios.log") { if ($rawLine.StartsWith('[RUN] ')) { $currentScenario = $rawLine.Substring(6) continue } $diagnostic = $rawLine.Trim() if ($diagnostic -cnotmatch '^(?:(?:SCRIPT )?ERROR|WARNING):') { continue } if (-not (Test-AllowedScenarioDiagnostic $diagnostic)) { $unexpectedScenarioDiagnostics += "$currentScenario`: $diagnostic" } } if ($unexpectedScenarioDiagnostics.Count -gt 0) { $scenarioResult = "FAIL" $ERRORS += "scenario: Godot reported $($unexpectedScenarioDiagnostics.Count) unallowlisted ERROR/WARNING diagnostic(s)" foreach ($diagnostic in $unexpectedScenarioDiagnostics | Select-Object -First 20) { $ERRORS += "scenario: $diagnostic" } } $scenarioLoadError = Select-String -Path "$LOG/scenarios.log" -Pattern 'SCRIPT ERROR:|Parse Error:|Failed to load script' -Quiet if ($scenarioLoadError) { $scenarioResult = "FAIL" $ERRORS += "scenario: Godot reported a script load or parse error" } if ($scenarioResult -eq "FAIL") { $FIXES += "Fix failing project scenario tests" } # -- 6. GUT ------------------------------------------------------------------- $gutResult = "PASS" Set-Content -Path "$LOG/gut.log" -Value "" -Encoding utf8 if (-not (Test-Path "addons/gut/gut_cmdln.gd")) { $gutResult = "FAIL" $ERRORS += "gut: pinned addon is missing" $FIXES += "Restore addons/gut from pinned GUT v9.7.1" } else { $gutArgs = "--headless --path `"$ROOT`" -s addons/gut/gut_cmdln.gd -gexit -gdisable_colors" $gutOutput, $gutExit = Invoke-GodotWithTimeout -ArgumentString $gutArgs $gutOutput | Add-Content -Path "$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" } $gutLoadError = Select-String -Path "$LOG/gut.log" -Pattern 'Some GUT class_names have not been imported|SCRIPT ERROR:|Parse Error:|Failed to load script' -Quiet if ($gutLoadError) { $gutResult = "FAIL" $ERRORS += "gut: addon or unit tests failed to load" $FIXES += "Run Godot 4.7 headless import and fix GUT load errors" } } # -- summary ------------------------------------------------------------------ if ($platformAssetsResult -eq 'FAIL' -or $importResult -eq 'FAIL' -or $fmtResult -eq 'FAIL' -or $lintResult -eq 'FAIL' -or $godotResult -eq 'FAIL' -or $scenarioResult -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 "Godot: $GODOT_VERSION" Write-Host "" Write-Host " platform-assets $platformAssetsResult" Write-Host " godot-import $importResult" Write-Host " gdformat $fmtResult" Write-Host " gdlint $lintResult" Write-Host " godot-check $godotResult" Write-Host " scenarios $scenarioResult" 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 Godot: $GODOT_VERSION platform-assets $platformAssetsResult godot-import $importResult gdformat $fmtResult gdlint $lintResult godot-check $godotResult scenarios $scenarioResult gut $gutResult "@ Set-Content -Path "$LOG/summary.txt" -Value $summary -Encoding utf8 if ($OVERALL) { Exit-Quality 1 } else { Exit-Quality 0 }