Restore API startup by isolating Bird activation (#334)

Keep Bird activation outside the PHP-FPM/nginx startup path so Bird configuration failures cannot make the core API unavailable. Retain guarded explicit activation and regression coverage.
This commit is contained in:
Jeppe B
2026-07-29 22:41:19 +02:00
committed by GitHub
parent a442e70744
commit 4587bdfb06
3 changed files with 23 additions and 19 deletions
-3
View File
@@ -2,8 +2,5 @@
set -e set -e
mkdir -p /run/nginx mkdir -p /run/nginx
if [ "${CONFIG_DB_TARGET:-live}" = "live" ]; then
php /var/www/html/scripts/bird-control-plane-auto-activate.php
fi
php-fpm -D php-fpm -D
exec nginx -g "daemon off;" exec nginx -g "daemon off;"
+19 -12
View File
@@ -44,15 +44,14 @@ deletion, physical gate actions, arbitrary recipients, attachments, or a
generic proxy. Outbound references are durably reserved before a provider generic proxy. Outbound references are durably reserved before a provider
request; an ambiguous outcome must be inspected through request; an ambiguous outcome must be inspected through
`/bird/control-plane/v1/messages/by-reference` and is never blindly retried. `/bird/control-plane/v1/messages/by-reference` and is never blindly retried.
Credentials and write switches can only be changed by the guarded CLI startup Credentials and write switches can only be changed by the guarded CLI
path or its explicit break-glass command, never by a web request. activation command, never by a web request.
### Schema deployment and preflight ### Schema deployment and preflight
The durable webhook and outbound-action ledgers use checked-in schema version The durable webhook and outbound-action ledgers use checked-in schema version
`1`. Schema changes are never run from a web request or worker. Production `1`. Schema changes are never run from a web request, worker, or core API
startup applies and verifies the schema automatically; these commands remain startup. Use these commands for deployment preflight and recovery:
available for manual preflight and break-glass recovery:
```bash ```bash
php scripts/bird-control-plane-schema.php check php scripts/bird-control-plane-schema.php check
@@ -64,15 +63,21 @@ php scripts/bird-control-plane-schema.php check
publishes read-only `schema: {ready, version, expectedVersion, missing}` state. publishes read-only `schema: {ready, version, expectedVersion, missing}` state.
Ledger-dependent reads, webhook ingestion, messages, and operational actions Ledger-dependent reads, webhook ingestion, messages, and operational actions
fail closed with HTTP `503` and code `bird_schema_not_ready` until the preflight fail closed with HTTP `503` and code `bird_schema_not_ready` until the preflight
is ready. Production auto-activation applies and verifies the schema before is ready. Explicit activation applies and verifies the schema before enabling
enabling any Bird write switch; the manual command remains a diagnostic and any Bird write switch.
break-glass path.
### Fail-closed production auto-activation ### Fail-closed production activation
The Coolify production container runs activation before PHP-FPM or nginx. It Bird activation is deliberately separate from PHP-FPM and nginx startup, so a
first transactionally disables bootstrap readiness and all three write Bird provider or configuration failure cannot make the core API unavailable.
switches. It then canonicalizes the existing legacy workspace/channel Run the guarded activation explicitly in the deployed API container:
```bash
php /var/www/html/scripts/bird-control-plane-auto-activate.php
```
The command first transactionally disables bootstrap readiness and all three
write switches. It then canonicalizes the existing legacy workspace/channel
configuration, applies and checks schema version `1`, validates configured configuration, applies and checks schema version `1`, validates configured
channels and conversations, resolves the access-key participant, pins the channels and conversations, resolves the access-key participant, pins the
public webhook URL to public webhook URL to
@@ -80,6 +85,8 @@ public webhook URL to
verifies webhooks while writes remain dark. Only then does one transaction verifies webhooks while writes remain dark. Only then does one transaction
enable bootstrap readiness, the Control Plane, outbound messaging, and enable bootstrap readiness, the Control Plane, outbound messaging, and
operational-action switches, followed by final readiness checks. operational-action switches, followed by final readiness checks.
Any failure returns a non-zero exit code and re-disables the Bird capabilities
without stopping or restarting the core API.
It preserves existing valid credentials; otherwise it generates distinct It preserves existing valid credentials; otherwise it generates distinct
48-byte random Control Plane and webhook secrets inside the container. The 48-byte random Control Plane and webhook secrets inside the container. The
webhook key remains backend-only. webhook key remains backend-only.
@@ -398,16 +398,16 @@ it('accepts staged activation only while every write capability remains disabled
expect(bird_control_plane_auto_activation::stagedActivationReady($status))->toBeFalse(); expect(bird_control_plane_auto_activation::stagedActivationReady($status))->toBeFalse();
}); });
it('wires fail-closed startup and a pinned local bootstrap without plaintext exposure', function (): void { it('keeps Bird activation outside core API startup and pins local bootstrap without plaintext exposure', function (): void {
$repoRoot = getenv('PLENO_REPO_ROOT_FOR_TESTS'); $repoRoot = getenv('PLENO_REPO_ROOT_FOR_TESTS');
expect($repoRoot)->toBeString()->not->toBe(''); expect($repoRoot)->toBeString()->not->toBe('');
$start = file_get_contents($repoRoot . '/services/coolify/api/start.sh'); $start = file_get_contents($repoRoot . '/services/coolify/api/start.sh');
$local = file_get_contents($repoRoot . '/scripts/bird-control-plane-bootstrap-local.sh'); $local = file_get_contents($repoRoot . '/scripts/bird-control-plane-bootstrap-local.sh');
$route = file_get_contents(app_path('routes/birdControlPlaneRoute.php')); $route = file_get_contents(app_path('routes/birdControlPlaneRoute.php'));
expect($start)->toContain('bird-control-plane-auto-activate.php') expect($start)->not->toContain('bird-control-plane-auto-activate.php')
->and(strpos($start, 'bird-control-plane-auto-activate.php')) ->and($start)->toContain('php-fpm -D')
->toBeLessThan(strpos($start, 'php-fpm -D')) ->and($start)->toContain('exec nginx')
->and($local)->toContain( ->and($local)->toContain(
"bootstrap_url='https://api.truckwash.io:4433/bird/control-plane/v1/bootstrap'" "bootstrap_url='https://api.truckwash.io:4433/bird/control-plane/v1/bootstrap'"
) )