ci.sh 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. #!/usr/bin/env bash
  2. # Local CI runner. Runs lint / static analysis / tests for both subprojects,
  3. # builds the ui asset bundle, and verifies docker compose images build.
  4. #
  5. # Designed for a host with Docker installed but no PHP/Composer/Node toolchain:
  6. # every PHP and Node command runs inside an ephemeral container.
  7. #
  8. # Usage:
  9. # ./scripts/ci.sh
  10. #
  11. # Env vars:
  12. # DB_DRIVERS space-separated list, default "sqlite mysql".
  13. # api test stage runs once per driver. mysql is skipped
  14. # with a warning if no MySQL is reachable on the host.
  15. set -euo pipefail
  16. REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
  17. cd "$REPO_ROOT"
  18. DB_DRIVERS="${DB_DRIVERS:-sqlite mysql}"
  19. PHP_IMAGE="composer:2"
  20. NODE_IMAGE="node:20-alpine"
  21. UID_GID="$(id -u):$(id -g)"
  22. banner() {
  23. printf '\n\033[1;36m==> %s\033[0m\n' "$*"
  24. }
  25. note() {
  26. printf '\033[1;33m[note]\033[0m %s\n' "$*"
  27. }
  28. ok() {
  29. printf '\033[1;32m%s\033[0m\n' "$*"
  30. }
  31. fail() {
  32. printf '\033[1;31m[fail]\033[0m %s\n' "$*" >&2
  33. exit 1
  34. }
  35. # Run a command inside a one-off PHP/Composer container, mounted at /app.
  36. # Args: <subproject-dir> <env-args> <command...>
  37. run_php() {
  38. local dir="$1"; shift
  39. docker run --rm \
  40. -u "$UID_GID" \
  41. -v "$REPO_ROOT/$dir":/app \
  42. -v "$HOME/.composer-cache":/tmp \
  43. -w /app \
  44. -e COMPOSER_HOME=/tmp/composer \
  45. -e COMPOSER_CACHE_DIR=/tmp/composer-cache \
  46. -e XDG_CONFIG_HOME=/tmp \
  47. "$@"
  48. }
  49. # Run a command inside a one-off Node container.
  50. run_node() {
  51. local dir="$1"; shift
  52. docker run --rm \
  53. -u "$UID_GID" \
  54. -v "$REPO_ROOT/$dir":/app \
  55. -w /app \
  56. -e HOME=/tmp \
  57. "$NODE_IMAGE" "$@"
  58. }
  59. mkdir -p "$HOME/.composer-cache"
  60. # ---------- 1. api ----------
  61. banner "api: composer install"
  62. run_php api "$PHP_IMAGE" composer install --no-interaction --prefer-dist
  63. banner "api: phpstan"
  64. run_php api "$PHP_IMAGE" composer stan
  65. banner "api: php-cs-fixer (dry-run)"
  66. run_php api "$PHP_IMAGE" composer cs
  67. for driver in $DB_DRIVERS; do
  68. if [ "$driver" = "mysql" ]; then
  69. # No MySQL reachable from the host in M01; skip gracefully. Once
  70. # later milestones add real DB-touching tests, replace this with a
  71. # proper reachability probe.
  72. note "api: skipping phpunit for driver=mysql (no MySQL configured for local CI in M01)"
  73. continue
  74. fi
  75. banner "api: phpunit (DB_DRIVER=$driver)"
  76. run_php api -e "DB_DRIVER=$driver" "$PHP_IMAGE" composer test
  77. done
  78. # ---------- 2. ui ----------
  79. banner "ui: composer install"
  80. run_php ui "$PHP_IMAGE" composer install --no-interaction --prefer-dist
  81. banner "ui: phpstan"
  82. run_php ui "$PHP_IMAGE" composer stan
  83. banner "ui: php-cs-fixer (dry-run)"
  84. run_php ui "$PHP_IMAGE" composer cs
  85. banner "ui: phpunit"
  86. run_php ui "$PHP_IMAGE" composer test
  87. # ---------- 3. ui frontend build ----------
  88. banner "ui: npm ci"
  89. if [ -f ui/package-lock.json ]; then
  90. run_node ui npm ci --no-audit --no-fund
  91. else
  92. note "ui/package-lock.json not present — using npm install to bootstrap"
  93. run_node ui npm install --no-audit --no-fund
  94. fi
  95. banner "ui: npm run build"
  96. run_node ui npm run build
  97. if [ ! -f ui/public/assets/app.css ]; then
  98. fail "ui/public/assets/app.css was not produced by the tailwind build"
  99. fi
  100. # ---------- 4. docker compose build ----------
  101. banner "docker compose build"
  102. docker compose build
  103. ok "CI OK"