Merge pull request #153 from copenhagentruckwash/backend-coolify-runner

Backend coolify runner
This commit is contained in:
Jeppe B
2026-04-21 22:19:21 +02:00
committed by GitHub
6 changed files with 324 additions and 153 deletions
+63 -138
View File
@@ -7,7 +7,8 @@ on:
jobs:
unit:
name: Unit (required)
runs-on: ubuntu-latest
# Match the labels exposed by the Coolify-managed GitHub runner.
runs-on: [self-hosted, Linux, X64, default]
steps:
- name: Checkout
@@ -21,25 +22,26 @@ jobs:
- name: Check AI workflow sync
run: node scripts/sync-ai-workflow.mjs --check
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: mysqli, curl, openssl, json, redis, xdebug
coverage: xdebug
ini-values: variables_order=EGPCS,xdebug.mode=coverage
- name: Materialize compose env files
env:
COMPOSE_ENV: ${{ secrets.COMPOSE_ENV }}
COMPOSE_ENV_STAGING: ${{ secrets.COMPOSE_ENV_STAGING }}
run: |
set -euo pipefail
if [ -z "${COMPOSE_ENV}" ]; then
echo "Required GitHub secret COMPOSE_ENV is not configured." >&2
exit 1
fi
if [ -z "${COMPOSE_ENV_STAGING}" ]; then
echo "Required GitHub secret COMPOSE_ENV_STAGING is not configured." >&2
exit 1
fi
printf '%s\n' "$COMPOSE_ENV" > .env
printf '%s\n' "$COMPOSE_ENV_STAGING" > .env.staging
chmod +x scripts/ci/run-php-suite.sh
- name: Resolve dependencies
working-directory: services/nginx/app
run: composer update --no-interaction --prefer-dist
- name: Run unit tests
working-directory: services/nginx/app
run: composer test:unit
- name: Generate coverage report
working-directory: services/nginx/app
run: composer test:coverage
- name: Run unit suite in php1
run: scripts/ci/run-php-suite.sh unit
- name: Upload coverage artifact
if: ${{ github.event_name == 'pull_request' }}
@@ -53,7 +55,7 @@ jobs:
edge-agent:
name: Edge Agent (required)
runs-on: ubuntu-latest
runs-on: [self-hosted, Linux, X64, default]
steps:
- name: Checkout
@@ -76,7 +78,7 @@ jobs:
edge-broker:
name: Edge Broker (required)
runs-on: ubuntu-latest
runs-on: [self-hosted, Linux, X64, default]
steps:
- name: Checkout
@@ -121,137 +123,60 @@ jobs:
integration:
name: Integration (advisory)
runs-on: ubuntu-latest
runs-on: [self-hosted, Linux, X64, default]
continue-on-error: true
services:
redis:
image: redis:7
ports:
- 6379:6379
mysql:
image: mysql:8
env:
MYSQL_DATABASE: app_test
MYSQL_ROOT_PASSWORD: root
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping -h 127.0.0.1 -proot"
--health-interval=10s
--health-timeout=5s
--health-retries=10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: mysqli, curl, openssl, json, redis
ini-values: variables_order=EGPCS
- name: Resolve dependencies
working-directory: services/nginx/app
run: composer update --no-interaction --prefer-dist
- name: Run integration tests
working-directory: services/nginx/app
- name: Materialize compose env files
env:
RUN_INTEGRATION_TESTS: '1'
USE_ENV: 'true'
DEBUG: '0'
ENCRYPTION_KEY: test-key
CORS: '*'
CONFIG_TIMEZONE: Europe/Copenhagen
ECONOMIC_API_APP_ACCESS_GRANT: test
ECONOMIC_API_APP_ACCESS_GRANT2: test
ECONOMIC_API_APP_SECRET_TOKEN: test
WORDPRESS_STATIC_TOKEN: ''
EMAIL_WASH_CERTIFICATE_TOKEN: ''
WORDPRESS_API_URL: http://localhost
MINIO_ENDPOINT: ''
MINIO_ACCESS_KEY: ''
MINIO_SECRET_KEY: ''
SLACK_DEFAULT_WEBHOOK: ''
REDIS_CONFIG_HOST: 127.0.0.1
REDIS_CONFIG_DATABASE: '0'
REDIS_CONFIG_PASSWORD: ''
REDIS_CONFIG_PORT: '6379'
CONFIG_DB_HOST: 127.0.0.1
CONFIG_DB_USER: root
CONFIG_DB_PASSWORD: root
CONFIG_DB_DATABASE: app_test
CONFIG_DB_PORT: '3306'
run: composer test:integration
COMPOSE_ENV: ${{ secrets.COMPOSE_ENV }}
COMPOSE_ENV_STAGING: ${{ secrets.COMPOSE_ENV_STAGING }}
run: |
set -euo pipefail
if [ -z "${COMPOSE_ENV}" ]; then
echo "Required GitHub secret COMPOSE_ENV is not configured." >&2
exit 1
fi
if [ -z "${COMPOSE_ENV_STAGING}" ]; then
echo "Required GitHub secret COMPOSE_ENV_STAGING is not configured." >&2
exit 1
fi
printf '%s\n' "$COMPOSE_ENV" > .env
printf '%s\n' "$COMPOSE_ENV_STAGING" > .env.staging
chmod +x scripts/ci/run-php-suite.sh
- name: Run integration suite in php1
run: scripts/ci/run-php-suite.sh integration
api:
name: API (advisory)
runs-on: ubuntu-latest
runs-on: [self-hosted, Linux, X64, default]
continue-on-error: true
services:
redis:
image: redis:7
ports:
- 6379:6379
mysql:
image: mysql:8
env:
MYSQL_DATABASE: app_test
MYSQL_ROOT_PASSWORD: root
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping -h 127.0.0.1 -proot"
--health-interval=10s
--health-timeout=5s
--health-retries=10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: mysqli, curl, openssl, json, redis
ini-values: variables_order=EGPCS
- name: Resolve dependencies
working-directory: services/nginx/app
run: composer update --no-interaction --prefer-dist
- name: Run API tests
working-directory: services/nginx/app
- name: Materialize compose env files
env:
RUN_API_TESTS: '1'
API_TEST_BOOTSTRAP_SCHEMA: '1'
USE_ENV: 'true'
DEBUG: '0'
ENCRYPTION_KEY: test-key
CORS: '*'
CONFIG_TIMEZONE: Europe/Copenhagen
ECONOMIC_API_APP_ACCESS_GRANT: test
ECONOMIC_API_APP_ACCESS_GRANT2: test
ECONOMIC_API_APP_SECRET_TOKEN: test
WORDPRESS_STATIC_TOKEN: ''
EMAIL_WASH_CERTIFICATE_TOKEN: ''
WORDPRESS_API_URL: http://localhost
MINIO_ENDPOINT: ''
MINIO_ACCESS_KEY: ''
MINIO_SECRET_KEY: ''
SLACK_DEFAULT_WEBHOOK: ''
REDIS_CONFIG_HOST: 127.0.0.1
REDIS_CONFIG_DATABASE: '0'
REDIS_CONFIG_PASSWORD: ''
REDIS_CONFIG_PORT: '6379'
CONFIG_DB_HOST: 127.0.0.1
CONFIG_DB_USER: root
CONFIG_DB_PASSWORD: root
CONFIG_DB_DATABASE: app_test
CONFIG_DB_PORT: '3306'
run: composer test:api
COMPOSE_ENV: ${{ secrets.COMPOSE_ENV }}
COMPOSE_ENV_STAGING: ${{ secrets.COMPOSE_ENV_STAGING }}
run: |
set -euo pipefail
if [ -z "${COMPOSE_ENV}" ]; then
echo "Required GitHub secret COMPOSE_ENV is not configured." >&2
exit 1
fi
if [ -z "${COMPOSE_ENV_STAGING}" ]; then
echo "Required GitHub secret COMPOSE_ENV_STAGING is not configured." >&2
exit 1
fi
printf '%s\n' "$COMPOSE_ENV" > .env
printf '%s\n' "$COMPOSE_ENV_STAGING" > .env.staging
chmod +x scripts/ci/run-php-suite.sh
- name: Run API suite in php1
run: scripts/ci/run-php-suite.sh api
+2 -2
View File
@@ -16,9 +16,9 @@ Backend API for Copenhagen Truck Wash services.
- PHP 8.2 CLI (optional, for host-side testing)
### Local Development
To bring up the minimal development stack (Traefik, Redis, MySQL debug DB, Caddy, and one PHP worker):
To bring up the minimal development stack (Traefik, Redis, MySQL debug DB, Edge Broker, Caddy, and one PHP worker):
```powershell
docker compose up -d traefik redis mysql-debug php1 caddy
docker compose up -d traefik redis mysql-debug edge-broker php1 caddy
```
The API is accessible at:
+32 -1
View File
@@ -80,6 +80,16 @@ services:
retries: 10
start_period: 20s
edge-broker:
build:
context: .
dockerfile: services/edge-broker/Dockerfile
container_name: edge-broker
environment:
EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev}
ports:
- "4300:4300"
caddy:
image: caddy:2.7.6-alpine
container_name: caddy
@@ -196,11 +206,14 @@ services:
container_name: php1
depends_on:
- redis
- edge-broker
command: ["php-fpm"]
env_file:
- .env
environment:
AUTO_COMPOSER_INSTALL: "true"
AUTO_COMPOSER_INSTALL: "${AUTO_COMPOSER_INSTALL:-true}"
EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300}
EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev}
volumes:
- ./services/nginx/app:/var/www/html
- ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro
@@ -213,11 +226,14 @@ services:
container_name: php2
depends_on:
- redis
- edge-broker
command: ["php-fpm"]
env_file:
- .env
environment:
AUTO_COMPOSER_INSTALL: "false"
EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300}
EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev}
volumes:
- ./services/nginx/app:/var/www/html
- ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro
@@ -230,11 +246,14 @@ services:
container_name: php3
depends_on:
- redis
- edge-broker
command: ["php-fpm"]
env_file:
- .env
environment:
AUTO_COMPOSER_INSTALL: "false"
EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300}
EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev}
volumes:
- ./services/nginx/app:/var/www/html
- ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro
@@ -247,11 +266,14 @@ services:
container_name: php4
depends_on:
- redis
- edge-broker
command: ["php-fpm"]
env_file:
- .env
environment:
AUTO_COMPOSER_INSTALL: "false"
EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300}
EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev}
volumes:
- ./services/nginx/app:/var/www/html
- ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro
@@ -264,11 +286,14 @@ services:
container_name: php5
depends_on:
- redis
- edge-broker
command: ["php-fpm"]
env_file:
- .env
environment:
AUTO_COMPOSER_INSTALL: "false"
EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300}
EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev}
volumes:
- ./services/nginx/app:/var/www/html
- ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro
@@ -281,11 +306,14 @@ services:
container_name: php-staging
depends_on:
- redis-staging
- edge-broker
command: ["php-fpm"]
env_file:
- .env.staging
environment:
AUTO_COMPOSER_INSTALL: "false"
EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300}
EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev}
volumes:
- ./services/nginx/app:/var/www/html
- ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro
@@ -298,11 +326,14 @@ services:
container_name: php-cron
depends_on:
- redis
- edge-broker
command: ["sh", "-c", "while true; do php index.php run cron; sleep 60; done"]
env_file:
- .env
environment:
AUTO_COMPOSER_INSTALL: "false"
EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300}
EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev}
volumes:
- ./services/nginx/app:/var/www/html
- ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro
+184
View File
@@ -0,0 +1,184 @@
#!/usr/bin/env bash
set -euo pipefail
suite="${1:-}"
if [[ -z "$suite" ]]; then
echo "Usage: $0 <unit|integration|api>" >&2
exit 1
fi
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$repo_root"
if [[ -f .env ]]; then
set -a
# shellcheck disable=SC1091
. ./.env
set +a
fi
export AUTO_COMPOSER_INSTALL=false
cleanup() {
docker compose down -v --remove-orphans || true
}
wait_for_container_running() {
local name="$1"
local status=""
for _ in $(seq 1 90); do
status="$(docker inspect -f '{{.State.Status}}' "$name" 2>/dev/null || true)"
if [[ "$status" == "running" ]]; then
return 0
fi
sleep 1
done
docker logs "$name" || true
echo "Container $name did not reach running state." >&2
exit 1
}
wait_for_container_healthy() {
local name="$1"
local status=""
for _ in $(seq 1 90); do
status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$name" 2>/dev/null || true)"
if [[ "$status" == "healthy" || "$status" == "running" ]]; then
return 0
fi
sleep 2
done
docker logs "$name" || true
echo "Container $name did not reach a healthy state." >&2
exit 1
}
docker_exec_php1() {
docker exec "$@" php1 sh -lc "cd /var/www/html && ${PHP_COMMAND}"
}
run_php_command() {
local -a env_args=()
while [[ $# -gt 0 ]]; do
env_args+=(-e "$1")
shift
done
docker exec "${env_args[@]}" php1 sh -lc "cd /var/www/html && ${PHP_COMMAND}"
}
sync_php_app_into_container() {
docker exec php1 sh -lc 'mkdir -p /var/www/html && find /var/www/html -mindepth 1 -maxdepth 1 -exec rm -rf {} +'
docker cp "${repo_root}/services/nginx/app/." php1:/var/www/html/
}
install_php_dependencies() {
PHP_COMMAND="composer install --no-interaction --prefer-dist"
docker_exec_php1
docker exec php1 sh -lc '
module_dir="/var/www/html/modules/washcertificates"
if [ -f "$module_dir/composer.json" ]; then
cd "$module_dir"
composer install --no-interaction --prefer-dist
fi
'
}
docker compose down -v --remove-orphans || true
trap cleanup EXIT
services=(redis edge-broker php1)
if [[ "$suite" != "unit" ]]; then
services+=(mysql-debug)
fi
docker compose up -d --build "${services[@]}"
wait_for_container_running php1
wait_for_container_healthy redis
if [[ "$suite" != "unit" ]]; then
wait_for_container_healthy mysql-debug
fi
sync_php_app_into_container
install_php_dependencies
case "$suite" in
unit)
PHP_COMMAND="composer test:unit"
docker_exec_php1
PHP_COMMAND="composer test:coverage"
docker_exec_php1
mkdir -p "${repo_root}/services/nginx/app/build/logs"
docker cp php1:/var/www/html/build/logs/clover.xml "${repo_root}/services/nginx/app/build/logs/clover.xml" 2>/dev/null || true
;;
integration)
PHP_COMMAND="composer test:integration"
run_php_command \
"RUN_INTEGRATION_TESTS=1" \
"USE_ENV=true" \
"DEBUG=0" \
"ENCRYPTION_KEY=test-key" \
"CORS=*" \
"CONFIG_TIMEZONE=Europe/Copenhagen" \
"ECONOMIC_API_APP_ACCESS_GRANT=test" \
"ECONOMIC_API_APP_ACCESS_GRANT2=test" \
"ECONOMIC_API_APP_SECRET_TOKEN=test" \
"WORDPRESS_STATIC_TOKEN=" \
"EMAIL_WASH_CERTIFICATE_TOKEN=" \
"WORDPRESS_API_URL=http://localhost" \
"MINIO_ENDPOINT=" \
"MINIO_ACCESS_KEY=" \
"MINIO_SECRET_KEY=" \
"SLACK_DEFAULT_WEBHOOK=" \
"REDIS_CONFIG_HOST=redis" \
"REDIS_CONFIG_DATABASE=0" \
"REDIS_CONFIG_PASSWORD=" \
"REDIS_CONFIG_PORT=6379" \
"CONFIG_DB_TARGET=debug" \
"CONFIG_DB_DEBUG_HOST=mysql-debug" \
"CONFIG_DB_DEBUG_USER=root" \
"CONFIG_DB_DEBUG_PASSWORD=${CONFIG_DB_DEBUG_PASSWORD:-debug_root_password}" \
"CONFIG_DB_DEBUG_DATABASE=${CONFIG_DB_DEBUG_DATABASE:-nnks_db_debug}" \
"CONFIG_DB_DEBUG_PORT=3306"
;;
api)
PHP_COMMAND="composer test:api"
run_php_command \
"RUN_API_TESTS=1" \
"API_TEST_BOOTSTRAP_SCHEMA=1" \
"USE_ENV=true" \
"DEBUG=0" \
"ENCRYPTION_KEY=test-key" \
"CORS=*" \
"CONFIG_TIMEZONE=Europe/Copenhagen" \
"ECONOMIC_API_APP_ACCESS_GRANT=test" \
"ECONOMIC_API_APP_ACCESS_GRANT2=test" \
"ECONOMIC_API_APP_SECRET_TOKEN=test" \
"WORDPRESS_STATIC_TOKEN=" \
"EMAIL_WASH_CERTIFICATE_TOKEN=" \
"WORDPRESS_API_URL=http://localhost" \
"MINIO_ENDPOINT=" \
"MINIO_ACCESS_KEY=" \
"MINIO_SECRET_KEY=" \
"SLACK_DEFAULT_WEBHOOK=" \
"REDIS_CONFIG_HOST=redis" \
"REDIS_CONFIG_DATABASE=0" \
"REDIS_CONFIG_PASSWORD=" \
"REDIS_CONFIG_PORT=6379" \
"CONFIG_DB_TARGET=debug" \
"CONFIG_DB_DEBUG_HOST=mysql-debug" \
"CONFIG_DB_DEBUG_USER=root" \
"CONFIG_DB_DEBUG_PASSWORD=${CONFIG_DB_DEBUG_PASSWORD:-debug_root_password}" \
"CONFIG_DB_DEBUG_DATABASE=${CONFIG_DB_DEBUG_DATABASE:-nnks_db_debug}" \
"CONFIG_DB_DEBUG_PORT=3306"
;;
*)
echo "Unsupported suite: $suite" >&2
exit 1
;;
esac
+41 -10
View File
@@ -137,6 +137,7 @@ function buildTransportHeartbeatState(brokerState = {}) {
status: "ONLINE",
metadata: {
command_transport: brokerConnected ? "BROKER_FAST_PATH" : "API_POLLING",
shell_transport: brokerConnected ? "BROKER_FAST_PATH" : "API_POLLING",
broker_connected: brokerConnected,
broker_url: brokerState.url || null,
broker_last_error: brokerState.lastError || null,
@@ -1761,22 +1762,22 @@ export async function startAgent({
});
config.configPath = configPath;
const intervalMs = Number(config.heartbeatIntervalSeconds || 15) * 1000;
const commandPollTimeoutSeconds = Number(config.commandPollTimeoutSeconds || 20);
const commandPollRetryDelayMs = Number(config.commandPollRetryDelayMs || 1000);
const commandPollTimeoutSeconds = Number(config.commandPollTimeoutSeconds ?? 20);
const commandPollRetryDelayMs = Number(config.commandPollRetryDelayMs ?? 1000);
const shellActionPollTimeoutSeconds = Number(config.shellActionPollTimeoutSeconds ?? 20);
const shellActionPollRetryDelayMs = Number(config.shellActionPollRetryDelayMs ?? 1000);
const brokerReconnectDelayMs = Number(config.brokerReconnectDelayMs || DEFAULT_BROKER_RECONNECT_DELAY_MS);
let stopped = false;
let cpuSnapshot = null;
let lastHeartbeatLatencyMs = null;
void createShellBridgeImpl;
let brokerBridge = null;
const shell = {
open: async () => {},
input: () => {},
resize: () => {},
close: () => {},
dispose: () => {},
const shellEventPublisher = createShellEventPublisher(config, fetchImpl);
const sendShellMessage = (message) => {
shellEventPublisher.publish(message);
brokerBridge?.send(message);
};
const shell = createShellBridgeImpl(sendShellMessage);
brokerBridge = createBrokerBridge({
config,
shell,
@@ -1841,8 +1842,37 @@ export async function startAgent({
}
};
const runShellActionPollLoop = async () => {
while (!stopped) {
try {
if (brokerBridge?.state?.connected) {
await new Promise((resolve) => setTimeout(resolve, shellActionPollRetryDelayMs));
continue;
}
const action = await pollShellActionJob(config, fetchImpl, shellActionPollTimeoutSeconds);
if (stopped) {
break;
}
if (!action) {
continue;
}
await processPolledShellAction(config, action, shell, fetchImpl);
} catch {
if (stopped) {
break;
}
await new Promise((resolve) => setTimeout(resolve, shellActionPollRetryDelayMs));
}
}
};
await sendTransportHeartbeat();
const commandPollPromise = runCommandPollLoop();
const shellActionPollPromise = runShellActionPollLoop();
const timer = setInterval(() => {
sendTransportHeartbeat().catch(() => {});
@@ -1854,7 +1884,8 @@ export async function startAgent({
clearInterval(timer);
brokerBridge?.stop();
shell.dispose();
await Promise.allSettled([commandPollPromise]);
await Promise.allSettled([commandPollPromise, shellActionPollPromise]);
await shellEventPublisher.drain();
};
return {
+2 -2
View File
@@ -43,8 +43,8 @@ RUN set -eux; \
pdo_mysql \
mysqli \
zip; \
pecl install imagick-3.7.0; \
docker-php-ext-enable imagick; \
pecl install imagick-3.7.0 redis xdebug; \
docker-php-ext-enable imagick redis xdebug; \
apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false $PHPIZE_DEPS; \
rm -rf /var/lib/apt/lists/*