#!/usr/bin/env bash # Local CI runner. Runs lint / static analysis / tests for both subprojects, # builds the ui asset bundle, and verifies docker compose images build. # # Designed for a host with Docker installed but no PHP/Composer/Node toolchain: # every PHP and Node command runs inside an ephemeral container. # # Usage: # ./scripts/ci.sh # # Env vars: # DB_DRIVERS space-separated list, default "sqlite mysql". # api test stage runs once per driver. mysql is skipped # with a warning if no MySQL is reachable on the host. set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$REPO_ROOT" DB_DRIVERS="${DB_DRIVERS:-sqlite mysql}" PHP_IMAGE="composer:2" NODE_IMAGE="node:20-alpine" UID_GID="$(id -u):$(id -g)" banner() { printf '\n\033[1;36m==> %s\033[0m\n' "$*" } note() { printf '\033[1;33m[note]\033[0m %s\n' "$*" } ok() { printf '\033[1;32m%s\033[0m\n' "$*" } fail() { printf '\033[1;31m[fail]\033[0m %s\n' "$*" >&2 exit 1 } # Run a command inside a one-off PHP/Composer container, mounted at /app. # Args: run_php() { local dir="$1"; shift docker run --rm \ -u "$UID_GID" \ -v "$REPO_ROOT/$dir":/app \ -v "$HOME/.composer-cache":/tmp \ -w /app \ -e COMPOSER_HOME=/tmp/composer \ -e COMPOSER_CACHE_DIR=/tmp/composer-cache \ -e XDG_CONFIG_HOME=/tmp \ "$@" } # Run a command inside a one-off Node container. run_node() { local dir="$1"; shift docker run --rm \ -u "$UID_GID" \ -v "$REPO_ROOT/$dir":/app \ -w /app \ -e HOME=/tmp \ "$NODE_IMAGE" "$@" } mkdir -p "$HOME/.composer-cache" # ---------- 1. api ---------- banner "api: composer install" run_php api "$PHP_IMAGE" composer install --no-interaction --prefer-dist banner "api: phpstan" run_php api "$PHP_IMAGE" composer stan banner "api: php-cs-fixer (dry-run)" run_php api "$PHP_IMAGE" composer cs for driver in $DB_DRIVERS; do if [ "$driver" = "mysql" ]; then # No MySQL reachable from the host in M01; skip gracefully. Once # later milestones add real DB-touching tests, replace this with a # proper reachability probe. note "api: skipping phpunit for driver=mysql (no MySQL configured for local CI in M01)" continue fi banner "api: phpunit (DB_DRIVER=$driver)" run_php api -e "DB_DRIVER=$driver" "$PHP_IMAGE" composer test done # ---------- 2. ui ---------- banner "ui: composer install" run_php ui "$PHP_IMAGE" composer install --no-interaction --prefer-dist banner "ui: phpstan" run_php ui "$PHP_IMAGE" composer stan banner "ui: php-cs-fixer (dry-run)" run_php ui "$PHP_IMAGE" composer cs banner "ui: phpunit" run_php ui "$PHP_IMAGE" composer test # ---------- 3. ui frontend build ---------- banner "ui: npm ci" if [ -f ui/package-lock.json ]; then run_node ui npm ci --no-audit --no-fund else note "ui/package-lock.json not present — using npm install to bootstrap" run_node ui npm install --no-audit --no-fund fi banner "ui: npm run build" run_node ui npm run build if [ ! -f ui/public/assets/app.css ]; then fail "ui/public/assets/app.css was not produced by the tailwind build" fi # ---------- 4. docker compose build ---------- banner "docker compose build" docker compose build ok "CI OK"