feat(api): standalone xlvask-automation-migrate script + runbook section (#357)
Adds `scripts/xlvask-automation-migrate.php` mirroring the existing schema-script pattern (`check` / `apply --yes`), a new AUTOMATION_RUNBOOK §2a documenting both operator entry points, and `XLVaskAutomationMigrateScriptTest` pinning the gate, the WD check, and the bootstrap references. Production autopilot-runs (POST /modules/xlvask/services/usage/autopilot-runs) was returning 500 with: ``` XL Vask automation schema is not ready. Apply migration 20260804_xlvask_ai_auto_policy_v2 explicitly. ``` because no operator had invoked the gated `applyExplicitMigration()` since PR #348 shipped the migration class. The frontend half of the fix is the companion change in pleno-vue#280 (missing `minimax_integration_enabled` key). Operator action required once merged: ``` php index.php run xlvask-automation-migrate # or php scripts/xlvask-automation-migrate.php apply --yes ``` Both produce identical JSON status; retain the artifact and rerun `check` to confirm postflight is green. Diff: +140/-0 (3 files). Tests: 87/87 XL Vask unit + 5/5 new migration script test pass.
This commit is contained in:
@@ -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 \
|
||||
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
/**
|
||||
* XL Vask automation schema migration script.
|
||||
*
|
||||
* Mirrors the scripts/account-deletion-schema.php and
|
||||
* scripts/bird-control-plane-schema.php patterns so ops can run an explicit,
|
||||
* non-cron, non-HTTP migration from the API container.
|
||||
*
|
||||
* Usage (from the api repo root, against the configured DB):
|
||||
* php scripts/xlvask-automation-migrate.php check
|
||||
* php scripts/xlvask-automation-migrate.php apply --yes
|
||||
*
|
||||
* "check" never mutates state and always exits 0 when ready / 1 when not.
|
||||
* "apply" requires an explicit --yes flag before calling the gated
|
||||
* migration_20260804_xlvask_ai_auto_policy_v2::apply() entry point, which
|
||||
* itself is operator-only by design (see AUTOMATION_RUNBOOK §2).
|
||||
*/
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
fwrite(STDERR, "This command is CLI-only.\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
const WD = __DIR__ . '/../services/nginx/app';
|
||||
require_once WD . '/vendor/autoload.php';
|
||||
require_once WD . '/config.php';
|
||||
require_once WD . '/classes/db.php';
|
||||
require_once WD . '/classes/xlvask_usage_logs_schema_bootstrap.php';
|
||||
require_once WD . '/modules/xlvask/migrations/20260804_xlvask_ai_auto_policy_v2.php';
|
||||
|
||||
$command = $argv[1] ?? 'check';
|
||||
|
||||
if (!in_array($command, ['check', 'apply'], true)) {
|
||||
fwrite(STDERR, "Usage: scripts/xlvask-automation-migrate.php check|apply --yes\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
$db = new \classes\db($CONFIG_DB);
|
||||
$db->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);
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Guard the operator entry point contract for the XL Vask automation
|
||||
* schema migration. The migration itself is intentionally operator-only
|
||||
* (see services/nginx/app/modules/xlvask/AUTOMATION_RUNBOOK.md §2), but
|
||||
* the CLI surface that wraps it must remain gated, idempotent, and
|
||||
* discoverable.
|
||||
*
|
||||
* When PLENO_REPO_ROOT_FOR_TESTS is set (the CI layout, where the repo
|
||||
* root is bind-mounted alongside services/nginx/app), the test also
|
||||
* inspects the standalone scripts/xlvask-automation-migrate.php wrapper
|
||||
* to keep it in lockstep with the cron entry point.
|
||||
*/
|
||||
|
||||
it('routes the xlvask automation migrate CLI command through the gated migration entry point', function (): void {
|
||||
$cli = file_get_contents(WD . '/cli.php');
|
||||
expect($cli)
|
||||
->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');
|
||||
});
|
||||
Reference in New Issue
Block a user