Fix API CI failures
This commit is contained in:
@@ -157,6 +157,7 @@ jobs:
|
||||
env:
|
||||
COMPOSE_FILE: docker-compose.yml:.github/docker-compose.ci.yml
|
||||
COMPOSE_PROJECT_NAME: edge-gateway-backend-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
COMPOSE_PROFILES: dev
|
||||
TRAEFIK_WEB_PORT: "18080"
|
||||
TRAEFIK_WEBSECURE_PORT: "18443"
|
||||
TRAEFIK_WEBSECURE_STAGING_PORT: "18433"
|
||||
@@ -235,11 +236,8 @@ jobs:
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Prune stale Docker networks
|
||||
run: docker network prune -f || true
|
||||
|
||||
- name: Boot local stack
|
||||
run: docker compose -f docker-compose.yml -f .github/docker-compose.ci.yml up -d traefik redis mysql-debug edge-broker php1 php2 php3 php4 php5 caddy
|
||||
run: sh scripts/ci-docker-compose-up.sh traefik redis mysql-debug edge-broker php1 php2 php3 php4 php5 caddy
|
||||
|
||||
- name: Sync PHP app checkout
|
||||
run: >
|
||||
|
||||
+4
-1
@@ -13,4 +13,7 @@
|
||||
.env.old
|
||||
/.tmp/
|
||||
/.env.staging
|
||||
/services/nginx/app/storage/replication-bootstrap.json
|
||||
/services/nginx/app/storage/replication-bootstrap.json
|
||||
/.env_old_2
|
||||
/.openclaw/
|
||||
/services/nginx/app/build/phpstan/
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
if [ "$#" -eq 0 ]; then
|
||||
echo "Usage: $0 <service> [service ...]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
compose_files="${CI_DOCKER_COMPOSE_FILES:--f docker-compose.yml -f .github/docker-compose.ci.yml}"
|
||||
lock_file="${CI_DOCKER_LOCK_FILE:-/tmp/pleno-api-ci-docker-compose-up.lock}"
|
||||
max_attempts="${CI_DOCKER_UP_RETRIES:-${PHP_CI_DOCKER_RETRIES:-3}}"
|
||||
export COMPOSE_PROFILES="${COMPOSE_PROFILES:-dev}"
|
||||
|
||||
compose_up() {
|
||||
attempt=1
|
||||
while :; do
|
||||
docker network prune -f >/dev/null 2>&1 || true
|
||||
|
||||
if docker compose $compose_files up -d "$@"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
status="$?"
|
||||
docker compose $compose_files down -v --remove-orphans >/dev/null 2>&1 || true
|
||||
|
||||
if [ "$attempt" -ge "$max_attempts" ]; then
|
||||
return "$status"
|
||||
fi
|
||||
|
||||
sleep_seconds=$((attempt * 5))
|
||||
echo "Docker compose up failed with status $status; retrying in ${sleep_seconds}s (attempt $((attempt + 1))/$max_attempts)." >&2
|
||||
sleep "$sleep_seconds"
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
}
|
||||
|
||||
if command -v flock >/dev/null 2>&1; then
|
||||
(
|
||||
flock 9
|
||||
compose_up "$@"
|
||||
) 9>"$lock_file"
|
||||
else
|
||||
echo "flock is not available; running Docker compose startup without a host lock." >&2
|
||||
compose_up "$@"
|
||||
fi
|
||||
@@ -18,6 +18,7 @@ cd "$repo_root"
|
||||
compose_files="-f docker-compose.yml -f .github/docker-compose.ci.yml"
|
||||
project_suffix="$(date +%s)-$$"
|
||||
export COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-php-local-${suite}-${project_suffix}}"
|
||||
export COMPOSE_PROFILES="${COMPOSE_PROFILES:-dev}"
|
||||
|
||||
log_dir=".tmp/ci-logs/$suite"
|
||||
mkdir -p "$log_dir"
|
||||
@@ -84,10 +85,6 @@ composer_install() {
|
||||
'cd /var/www/html && composer install --no-interaction --prefer-source --no-progress'
|
||||
}
|
||||
|
||||
prune_unused_docker_networks() {
|
||||
docker network prune -f >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
configure_ci_docker_subnet() {
|
||||
if [ -n "${CI_DOCKER_SUBNET:-}" ]; then
|
||||
return
|
||||
@@ -119,8 +116,7 @@ cleanup() {
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
configure_ci_docker_subnet
|
||||
prune_unused_docker_networks
|
||||
retry_command "${PHP_CI_DOCKER_RETRIES:-3}" docker compose $compose_files up -d redis mysql-debug php1
|
||||
sh scripts/ci-docker-compose-up.sh redis mysql-debug php1
|
||||
|
||||
docker compose $compose_files exec -T php1 sh -lc '
|
||||
set -eu
|
||||
|
||||
@@ -314,7 +314,9 @@ it('creates updates lists and deactivates scoped employees without exposing raw
|
||||
|
||||
expect($deactivated->data()['active'] ?? true)->toBeFalse();
|
||||
$userRow = api_test_runtime()->queryOne('SELECT `password`, `group_id`, `deleted_at` FROM `users` WHERE `id` = ' . $employeeId);
|
||||
expect($userRow['password'] ?? 'not-null')->toBeNull();
|
||||
expect($userRow)->not->toBeNull();
|
||||
expect(array_key_exists('password', $userRow ?? []))->toBeTrue();
|
||||
expect($userRow['password'])->toBeNull();
|
||||
expect((int)($userRow['group_id'] ?? -1))->toBe(0);
|
||||
expect($userRow['deleted_at'] ?? null)->not->toBeNull();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
it('services broker transport before replaying queued outbox items', function (): void {
|
||||
$agentSource = (string)file_get_contents(app_path('resources/edge-gateway-agent/agent.php'));
|
||||
$runOffset = strpos($agentSource, 'public function run(): void');
|
||||
expect($runOffset)->not->toBeFalse();
|
||||
|
||||
$loopSource = substr($agentSource, (int)$runOffset, 1800);
|
||||
$configureOffset = strpos($loopSource, '$this->configureBrokerClient();');
|
||||
$pumpOffset = strpos($loopSource, '$this->pumpBrokerTransport();');
|
||||
$flushOffset = strpos($loopSource, '$this->flushOutbox();');
|
||||
|
||||
expect($configureOffset)->not->toBeFalse()
|
||||
->and($pumpOffset)->not->toBeFalse()
|
||||
->and($flushOffset)->not->toBeFalse()
|
||||
->and((int)$configureOffset)->toBeLessThan((int)$pumpOffset)
|
||||
->and((int)$pumpOffset)->toBeLessThan((int)$flushOffset);
|
||||
});
|
||||
|
||||
it('bounds stale outbox replay so it cannot monopolize the gateway loop', function (): void {
|
||||
$agentSource = (string)file_get_contents(app_path('resources/edge-gateway-agent/agent.php'));
|
||||
|
||||
expect($agentSource)->toContain('private const OUTBOX_REPLAY_BATCH_LIMIT = 3;')
|
||||
->and($agentSource)->toContain('private const OUTBOX_REPLAY_TIMEOUT_SECONDS = 3;')
|
||||
->and($agentSource)->toContain('private const OUTBOX_OPERATION_COMPLETE_REPLAY_TIMEOUT_SECONDS = 10;')
|
||||
->and($agentSource)->toContain('private const OUTBOX_REPLAY_FAILURE_COOLDOWN_SECONDS = 15;')
|
||||
->and($agentSource)->toContain('private int $lastOutboxFailureAt = 0;')
|
||||
->and($agentSource)->toContain('$this->stateStore->queuedItems(self::OUTBOX_REPLAY_BATCH_LIMIT)')
|
||||
->and($agentSource)->toContain('private function shouldSkipOutboxReplay(): bool')
|
||||
->and($agentSource)->toContain('$this->lastOutboxFailureAt = time();')
|
||||
->and($agentSource)->toContain('? self::OUTBOX_OPERATION_COMPLETE_REPLAY_TIMEOUT_SECONDS')
|
||||
->and($agentSource)->toContain(': self::OUTBOX_REPLAY_TIMEOUT_SECONDS;');
|
||||
});
|
||||
|
||||
it('uses a short control-plane timeout for self-serve machine signals before queueing', function (): void {
|
||||
$agentSource = (string)file_get_contents(app_path('resources/edge-gateway-agent/agent.php'));
|
||||
|
||||
expect($agentSource)->toContain('private const MACHINE_SIGNAL_TIMEOUT_SECONDS = 3;')
|
||||
->and($agentSource)->toContain('private function sendControlPlaneEvent(string $endpoint, array $payload, string $type, int $timeoutSeconds = 20): bool')
|
||||
->and($agentSource)->toContain("'machine_signal',\n self::MACHINE_SIGNAL_TIMEOUT_SECONDS");
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
it('wires self-serve machine signals through the broker control plane', function (): void {
|
||||
$routeSource = (string)file_get_contents(app_path('routes/edgeGatewaysRoute.php'));
|
||||
$agentSource = (string)file_get_contents(app_path('resources/edge-gateway-agent/agent.php'));
|
||||
$machineSignalSource = (string)file_get_contents(app_path('modules/selfserve/classes/selfserve_machine_signal.php'));
|
||||
|
||||
expect($routeSource)->toContain("'/edge-agent/internal/gateways/{id}/selfserve/machine-signal'")
|
||||
->and($routeSource)->toContain('private function handleBrokerSelfserveMachineSignal(): void')
|
||||
->and($routeSource)->toContain('recordBrokerEdgeGatewaySignal($gatewayId, $payload)')
|
||||
->and($agentSource)->toContain("'type' => 'MACHINE_SIGNAL'")
|
||||
->and($agentSource)->toContain('#/edge-agent/gateways/\d+/selfserve/machine-signal$#')
|
||||
->and($machineSignalSource)->toContain('public function recordBrokerEdgeGatewaySignal(int $gatewayId, array $payload): array')
|
||||
->and($machineSignalSource)->toContain('private function recordEdgeGatewaySignalForDepartment(int $gatewayId, int $departmentId, array $payload): array');
|
||||
});
|
||||
Reference in New Issue
Block a user