From 9d148ca2419d2243489731287dc8019cfe117bd9 Mon Sep 17 00:00:00 2001 From: nala Date: Tue, 20 Oct 2026 15:07:54 +0200 Subject: [PATCH] Preservar diagnostico mais recente dos programadores e isolar fixtures unitarias --- .../Commands/CheckDevelopmentEnvironment.php | 79 +++++++++++++++++-- deploy/homologacao/pipeline.py | 5 +- 2 files changed, 78 insertions(+), 6 deletions(-) diff --git a/app/Console/Commands/CheckDevelopmentEnvironment.php b/app/Console/Commands/CheckDevelopmentEnvironment.php index 240becbd..1acc5478 100644 --- a/app/Console/Commands/CheckDevelopmentEnvironment.php +++ b/app/Console/Commands/CheckDevelopmentEnvironment.php @@ -5,15 +5,24 @@ namespace App\Console\Commands; use Illuminate\Console\Command; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; +use Illuminate\Support\Sleep; +use Throwable; class CheckDevelopmentEnvironment extends Command { - protected $signature = 'portal:dev-check'; + protected $signature = 'portal:dev-check {--wait=0 : Segundos para aguardar uma falha transitória do PostgreSQL}'; protected $description = 'Verifica o arranque local sem alterar dados ou mostrar credenciais'; public function handle(): int { + $wait = filter_var($this->option('wait'), FILTER_VALIDATE_INT, ['options' => ['min_range' => 0, 'max_range' => 3600]]); + if ($wait === false) { + $this->error('--wait deve ser um inteiro entre 0 e 3600 segundos.'); + + return self::INVALID; + } + $checks = [ 'PHP >= 8.4.1' => version_compare(PHP_VERSION, '8.4.1', '>='), 'Driver PostgreSQL activo' => extension_loaded('pdo_pgsql'), @@ -25,14 +34,17 @@ class CheckDevelopmentEnvironment extends Command ]; if ($checks['Driver PostgreSQL activo'] && $checks['Base PostgreSQL configurada']) { + $checks['Ligação à base de dados'] = $this->checkConnection($wait); + } + + if ($checks['Ligação à base de dados'] ?? false) { try { - DB::selectOne('SELECT 1'); - $checks['Ligação à base de dados'] = true; $checks['Extensão PostGIS instalada'] = (bool) DB::selectOne("SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'postgis') AS installed")->installed; $checks['Tabelas principais instaladas'] = collect(['migrations', 'users', 'sectors', 'opportunities', 'sessions', 'cache', 'jobs']) ->every(fn (string $table): bool => Schema::hasTable($table)); - } catch (\Throwable) { - $checks['Ligação à base de dados'] = false; + } catch (Throwable) { + $checks['Esquema da base de dados verificável'] = false; + $this->warn('A ligação foi estabelecida, mas a verificação do esquema falhou. Verifique as permissões e a preparação QA.'); } } @@ -48,4 +60,61 @@ class CheckDevelopmentEnvironment extends Command return self::SUCCESS; } + + private function checkConnection(int $wait): bool + { + $deadline = microtime(true) + $wait; + + while (true) { + try { + DB::selectOne('SELECT 1'); + + return true; + } catch (Throwable $error) { + [$reason, $retryable] = $this->connectionFailure($error); + $remaining = $deadline - microtime(true); + if (! $retryable || $remaining <= 0) { + $this->warn($reason); + if ($retryable) { + $this->warn('Inicie Docker Desktop e execute ./deploy/local/start.ps1. Consulte os registos com docker compose --env-file .env -f deploy/local/compose.yaml logs --tail 40 postgres.'); + } + + return false; + } + + $this->line('[AGUARDA] '.$reason.' Nova tentativa; prazo restante: '.(int) ceil($remaining).' s.'); + DB::purge(); + Sleep::for(min(5, $remaining))->seconds(); + } + } + } + + /** @return array{string, bool} */ + private function connectionFailure(Throwable $error): array + { + // Inspect the chain internally; never print exception messages, DSNs or credentials. + $messages = ''; + do { + $messages .= ' '.strtolower($error->getMessage()).' '.strtolower((string) $error->getCode()); + $error = $error->getPrevious(); + } while ($error !== null); + + if (str_contains($messages, '28p01') || str_contains($messages, '28000') + || str_contains($messages, 'password authentication failed') || str_contains($messages, 'no pg_hba.conf entry')) { + return ['Autenticação PostgreSQL recusada. Verifique as credenciais e as regras de acesso; aguardar não corrige este erro.', false]; + } + if (str_contains($messages, '3d000') || preg_match('/database .* does not exist/', $messages)) { + return ['A base PostgreSQL configurada não existe. Verifique a configuração e a preparação QA.', false]; + } + if (str_contains($messages, '57p03') || str_contains($messages, 'recovery mode') + || str_contains($messages, 'not yet accepting connections') || str_contains($messages, 'database system is starting up')) { + return ['PostgreSQL em arranque ou recuperação; aguarde a conclusão sem remover o volume.', true]; + } + if (preg_match('/\b08[0-9a-z]{3}\b/', $messages) || str_contains($messages, 'connection refused') + || str_contains($messages, '57p01') || str_contains($messages, '57p02')) { + return ['PostgreSQL indisponível. Verifique se Docker Desktop e o contentor local estão iniciados.', true]; + } + + return ['Falha PostgreSQL não transitória. Verifique a configuração e os registos do servidor.', false]; + } } diff --git a/deploy/homologacao/pipeline.py b/deploy/homologacao/pipeline.py index 52d18d86..e7a481b1 100644 --- a/deploy/homologacao/pipeline.py +++ b/deploy/homologacao/pipeline.py @@ -202,7 +202,10 @@ class Delivery: self.run(['npm','run','build'],cwd=release,env=env) self.run(['npm','run','types:check'],cwd=release,env=env) self.run([self.c['php'],'tools/portal-diagnostics-deployment-guard.php'],cwd=release,env=env) - self.run([self.c['php'],'vendor/bin/phpunit','--configuration=phpunit.xml','--testsuite','Unit','--do-not-cache-result'],cwd=release,env=env) + # Unit fixtures mock DB/mail and select their own feature settings. + # Do not inject the destination's enabled-module flags into those mocks. + unit_env={k:v for k,v in env.items() if not k.startswith(('PORTAL_','INVESTMOZ_'))} + self.run([self.c['php'],'vendor/bin/phpunit','--configuration=phpunit.xml','--testsuite','Unit','--do-not-cache-result'],cwd=release,env=unit_env) tests=sorted((release/'tests/frontend').glob('*.test.mjs')) self.run(['node','--test',*tests],cwd=release,env=env) # Final production vendor from the same lock, before linking any real configuration.