diff --git a/scripts/php-ci-test.sh b/scripts/php-ci-test.sh index 35f93202..66c703f2 100644 --- a/scripts/php-ci-test.sh +++ b/scripts/php-ci-test.sh @@ -151,6 +151,7 @@ tar \ docker-compose.prod.standalone.yml \ scripts/bird-control-plane-auto-activate.php \ scripts/bird-control-plane-bootstrap-local.sh \ + scripts/xlvask-automation-migrate.php \ services/coolify/api/start.sh \ services/php/Dockerfile \ services/php/php-fpm-pool.conf \ diff --git a/scripts/xlvask-automation-migrate.php b/scripts/xlvask-automation-migrate.php new file mode 100755 index 00000000..407f7401 --- /dev/null +++ b/scripts/xlvask-automation-migrate.php @@ -0,0 +1,54 @@ +#!/usr/bin/env php +connect(); + +if ($command === 'apply') { + if (($argv[2] ?? '') !== '--yes') { + fwrite(STDERR, "Refusing schema mutation without: apply --yes\n"); + exit(2); + } + $status = \classes\xlvask_usage_logs_schema_bootstrap::applyExplicitMigration(); +} else { + $status = \classes\xlvask_usage_logs_schema_bootstrap::migrationStatus(); +} + +fwrite(STDOUT, json_encode($status, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . PHP_EOL); +exit((bool)($status['ready'] ?? false) ? 0 : 1); diff --git a/services/nginx/app/modules/xlvask/AUTOMATION_RUNBOOK.md b/services/nginx/app/modules/xlvask/AUTOMATION_RUNBOOK.md index 6f813c2c..1eef9eec 100644 --- a/services/nginx/app/modules/xlvask/AUTOMATION_RUNBOOK.md +++ b/services/nginx/app/modules/xlvask/AUTOMATION_RUNBOOK.md @@ -35,6 +35,26 @@ Use the controlled database migration procedure to invoke only `preflight()` output. Review the additive SQL and backup/restore point, approve the exact SHA, run it once, retain the returned status, and rerun readiness. Do not invoke `applyExplicitMigration()` from a request, worker, cron task, or application startup. If preflight reports multiple legacy execute runs in `queued`, `running`, or `retry_wait`, stop. Reconcile those runs through a separately approved operational procedure; the migration never auto-resolves or modifies the conflicting run records. +### 2a. Operator entry points + +There are two equivalent ways to apply the migration from a privileged +container with the configured DB credentials. Both call the same gated +`migration_20260804_xlvask_ai_auto_policy_v2::apply()` entry point and +produce identical status output. Pick whichever fits the workflow. + +``` +# Option A — standalone script (mirrors scripts/account-deletion-schema.php) +php scripts/xlvask-automation-migrate.php check # read-only preflight +php scripts/xlvask-automation-migrate.php apply --yes # apply, gated by --yes + +# Option B — CLI dispatcher inside index.php (defines WD + composes bootstrap) +php index.php run xlvask-automation-migrate # preflight, applies if !ready +``` + +Both exit 0 when `ready=true` and 1 otherwise. Always retain the JSON +status artifact for the audit log and rerun `check` to confirm the +postflight is green. + ## 3. WashId uniqueness Inspect normalized duplicate WashIds. Resolve conflicts through an independently approved data procedure. Only then use the guarded uniqueness activation with the exact typed phrase. Recheck the generated normalized column and unique index before any automatic action. diff --git a/services/nginx/app/tests/Unit/XLVask/XLVaskAutomationMigrateScriptTest.php b/services/nginx/app/tests/Unit/XLVask/XLVaskAutomationMigrateScriptTest.php new file mode 100644 index 00000000..338ee2f2 --- /dev/null +++ b/services/nginx/app/tests/Unit/XLVask/XLVaskAutomationMigrateScriptTest.php @@ -0,0 +1,65 @@ +toContain("case 'xlvask-automation-migrate':") + ->toContain("require_once 'cron/EnsureXLVaskAutomationSchema.php'"); +}); + +it('keeps the cron entry point gated by the WD constant and the migration class', function (): void { + $cron = file_get_contents(WD . '/cron/EnsureXLVaskAutomationSchema.php'); + expect($cron) + ->toContain("if (!defined('WD'))") + ->toContain('migration_20260804_xlvask_ai_auto_policy_v2::preflight') + ->toContain('migration_20260804_xlvask_ai_auto_policy_v2::apply') + ->toContain('xlvask_usage_logs_schema_bootstrap::applyWashIdUniquenessMigration'); +}); + +it('keeps the migration class operator-only and references the bootstrap entry point', function (): void { + $migration = file_get_contents(WD . '/modules/xlvask/migrations/20260804_xlvask_ai_auto_policy_v2.php'); + expect($migration) + ->toContain('operator-invoked') + ->toContain('xlvask_usage_logs_schema_bootstrap::applyExplicitMigration') + ->toContain('xlvask_usage_logs_schema_bootstrap::migrationStatus'); +}); + +it('documents both operator entry points in the XL Vask automation runbook', function (): void { + $runbook = file_get_contents(WD . '/modules/xlvask/AUTOMATION_RUNBOOK.md'); + expect($runbook) + ->toContain('## 2. Explicit schema migration') + ->toContain('## 2a. Operator entry points') + ->toContain('scripts/xlvask-automation-migrate.php') + ->toContain("php index.php run xlvask-automation-migrate"); +}); + +it('keeps the standalone xlvask automation migration script gated and idempotent when the repo root is mounted', function (): void { + $repoRoot = getenv('PLENO_REPO_ROOT_FOR_TESTS'); + if ($repoRoot === false || $repoRoot === '') { + expect(true)->toBeTrue(); // covered by CI; local docker lacks the repo-root bind mount + return; + } + + $scriptPath = realpath($repoRoot . '/scripts/xlvask-automation-migrate.php'); + expect($scriptPath)->not->toBeFalse(); + + $source = file_get_contents($scriptPath); + expect($source) + ->toContain("if (PHP_SAPI !== 'cli')") + ->toContain("Refusing schema mutation without: apply --yes") + ->toContain('xlvask_usage_logs_schema_bootstrap::applyExplicitMigration') + ->toContain('xlvask_usage_logs_schema_bootstrap::migrationStatus'); +});