Closes the 'Unknown column invoice_email in SELECT' production failure mode (TRU-77) by: - New GET /admin/schema-check endpoint — returns 503 with explicit list of missing columns if any are absent (instead of a generic 500) - scripts/run-schema-bootstraps.php — auto-discovers and runs every *_schema_bootstrap class on the live database (additive, idempotent) - scripts/schema-health-check.php — CLI tool for the same check, used by deploy pipelines - New Pest contract test SchemaHealthCheckTest — verifies the test DB has every required users column and the schema-check endpoint works - deploy.yml: pre-deploy step runs the bootstrap runner, smoke test also runs the schema check, Slack alert on failure
65 lines
1.8 KiB
PHP
65 lines
1.8 KiB
PHP
#!/usr/bin/env php
|
|
<?php
|
|
/**
|
|
* Pre-deploy schema bootstrap runner.
|
|
*
|
|
* Loads and runs every `*_schema_bootstrap` class so the production
|
|
* database has all the columns the current code expects. Each
|
|
* bootstrap is additive and idempotent — safe to run on every deploy.
|
|
*
|
|
* Run via:
|
|
* php scripts/run-schema-bootstraps.php
|
|
*
|
|
* Used in .github/workflows/deploy.yml as a pre-deploy step.
|
|
*
|
|
* When you add a new *_schema_bootstrap class, you don't need to
|
|
* edit this file — the runner auto-discovers any class whose name
|
|
* ends in `_schema_bootstrap`.
|
|
*/
|
|
|
|
namespace scripts;
|
|
|
|
// Load the app entry point so $db is wired up the same way as in
|
|
// normal request handling.
|
|
$index = __DIR__ . '/../services/nginx/app/index.php';
|
|
if (!file_exists($index)) {
|
|
fwrite(STDERR, "Cannot find app entry point at {$index}\n");
|
|
exit(2);
|
|
}
|
|
require_once $index;
|
|
|
|
$classesDir = __DIR__ . '/../services/nginx/app/classes';
|
|
$bootstraps = glob($classesDir . '/*_schema_bootstrap.php');
|
|
if (!$bootstraps) {
|
|
fwrite(STDERR, "No *_schema_bootstrap.php files found in {$classesDir}\n");
|
|
exit(0);
|
|
}
|
|
|
|
$ran = 0;
|
|
$skipped = 0;
|
|
foreach ($bootstraps as $file) {
|
|
require_once $file;
|
|
$base = basename($file, '.php');
|
|
$class = "classes\\{$base}";
|
|
if (!class_exists($class)) {
|
|
fwrite(STDERR, " [skip] {$base}: class not found\n");
|
|
$skipped++;
|
|
continue;
|
|
}
|
|
if (!method_exists($class, 'ensureSchema')) {
|
|
fwrite(STDERR, " [skip] {$base}: no ensureSchema() method\n");
|
|
$skipped++;
|
|
continue;
|
|
}
|
|
try {
|
|
$class::ensureSchema();
|
|
echo " [ok] {$base}\n";
|
|
$ran++;
|
|
} catch (\Throwable $e) {
|
|
fwrite(STDERR, " [FAIL] {$base}: " . $e->getMessage() . "\n");
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
echo "Schema bootstraps complete: {$ran} ran, {$skipped} skipped.\n";
|