name: CI on: push: branches: [main] pull_request: concurrency: group: ci-${{ gitea.workflow }}-${{ gitea.ref }} cancel-in-progress: true env: APP_ENV: testing APP_KEY: base64:NXm/6jIyFcDGHoMKGc5QZuSaq0dRZFYPg1Isuy1fNvE= APP_LOCALE: pt_BR APP_FALLBACK_LOCALE: pt_BR APP_TIMEZONE: America/Sao_Paulo BCRYPT_ROUNDS: 4 CACHE_STORE: database DB_CONNECTION: pgsql # Service hostname on the per-job network (act_runner with empty # container.network). Do not publish host :5432/:8000 — parallel jobs collide. DB_HOST: postgres DB_PORT: 5432 DB_DATABASE: amare_test DB_USERNAME: amare DB_PASSWORD: secret QUEUE_CONNECTION: database SESSION_DRIVER: database jobs: static: name: static runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - run: cp .env.example .env - uses: shivammathur/setup-php@v2 with: php-version: "8.4" extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd coverage: none - run: composer validate --strict - run: composer install --no-interaction --prefer-dist - run: composer pint:check - run: composer phpstan - run: composer audit # audit-level=high: package.json has no runtime `dependencies` today (npm # itself reports prod:1, 0 vulnerabilities), so this is currently a # vacuous forward guard rather than an active protection. `high` is # chosen deliberately over `low`/`moderate` so that once a real runtime # JS dependency is added, the gate flags exploitable issues without # becoming noisy on every transitive dev-only advisory. - run: npm audit --omit=dev --audit-level=high unit: name: unit runs-on: ubuntu-latest # Postgres is required here (not just in `feature`) because the # Domain/Application coverage gate below runs the Feature suite too: the # App\Application\Queries\Marketing\* classes are only exercised through # Feature (HTTP) tests today, so a Unit-only coverage run would undercount # them well under the 80% threshold. services: postgres: image: postgres:17 env: POSTGRES_DB: amare_test POSTGRES_USER: amare POSTGRES_PASSWORD: secret options: >- --health-cmd "pg_isready -U amare -d amare_test" --health-interval 5s --health-timeout 5s --health-retries 10 steps: - uses: actions/checkout@v5 - run: cp .env.example .env - uses: shivammathur/setup-php@v2 with: php-version: "8.4" extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd coverage: pcov - run: composer install --no-interaction --prefer-dist - run: npm ci - run: npm run build - run: composer test:unit - run: php artisan migrate --force # Domain/Application coverage gate (SPEC.md L2351, 80% minimum). Scoped # via phpunit.coverage.xml rather than editing the project-wide # phpunit.xml block, which stays covering all of app/ for # every other test/coverage invocation. app/Domain currently holds only # the DomainModule placeholder (zero executable lines), so in practice # this gates app/Application until Domain gains real logic. - run: composer test:coverage feature: name: feature runs-on: ubuntu-latest services: postgres: image: postgres:17 env: POSTGRES_DB: amare_test POSTGRES_USER: amare POSTGRES_PASSWORD: secret options: >- --health-cmd "pg_isready -U amare -d amare_test" --health-interval 5s --health-timeout 5s --health-retries 10 steps: - uses: actions/checkout@v5 - run: cp .env.example .env - uses: shivammathur/setup-php@v2 with: php-version: "8.4" extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd coverage: none - run: composer install --no-interaction --prefer-dist - run: npm ci - run: npm run build - run: php artisan migrate --force - run: composer test:feature browser: name: browser runs-on: ubuntu-latest services: postgres: image: postgres:17 env: POSTGRES_DB: amare_test POSTGRES_USER: amare POSTGRES_PASSWORD: secret options: >- --health-cmd "pg_isready -U amare -d amare_test" --health-interval 5s --health-timeout 5s --health-retries 10 steps: - uses: actions/checkout@v5 - run: cp .env.example .env - uses: shivammathur/setup-php@v2 with: php-version: "8.4" extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd coverage: none - run: composer install --no-interaction --prefer-dist - run: npm ci - run: npm run build - run: npx playwright install chromium --with-deps - run: php artisan migrate --force - run: php artisan storage:link - name: Build application image run: docker build -t amare-app:ci . - name: Run browser tests against FrankenPHP container run: | # Join the per-job network (act_runner creates one when # container.network is empty). No host -p: parallel jobs would # collide on :8000/:5432; DNS names work on the job network. # Container --name is global on the shared docker.sock host — # include run id or leftovers from cancelled jobs Conflict. JOB_CID="$(hostname)" JOB_NET="$(docker inspect -f '{{range $k, $_ := .NetworkSettings.Networks}}{{println $k}}{{end}}' "$JOB_CID" | head -n1)" test -n "$JOB_NET" WEB_NAME="amare-web-${GITHUB_RUN_ID:-$$}" docker rm -f "$WEB_NAME" 2>/dev/null || true docker run -d --name "$WEB_NAME" \ --network "$JOB_NET" \ --network-alias amare-web \ -e APP_ENV=testing \ -e APP_KEY="${APP_KEY}" \ -e APP_URL=http://amare-web:8000 \ -e APP_LOCALE=pt_BR \ -e APP_FALLBACK_LOCALE=pt_BR \ -e APP_TIMEZONE=America/Sao_Paulo \ -e DB_CONNECTION=pgsql \ -e DB_HOST=postgres \ -e DB_PORT=5432 \ -e DB_DATABASE=amare_test \ -e DB_USERNAME=amare \ -e DB_PASSWORD=secret \ -e SESSION_DRIVER=database \ -e CACHE_STORE=database \ -e QUEUE_CONNECTION=database \ -v "${GITHUB_WORKSPACE}/storage/app/public:/app/storage/app/public" \ amare-app:ci cleanup() { docker rm -f "$WEB_NAME" >/dev/null 2>&1 || true; } trap cleanup EXIT for i in $(seq 1 30); do if curl -fsS http://amare-web:8000/up; then break fi sleep 2 done curl -fsS http://amare-web:8000/up APP_URL=http://amare-web:8000 ./vendor/bin/pest --testsuite=Browser - name: Collect failure diagnostics if: failure() run: | mkdir -p artifacts/browser WEB_NAME="amare-web-${GITHUB_RUN_ID:-$$}" docker logs "$WEB_NAME" > artifacts/browser/container.log 2>&1 || true cp -R storage/logs artifacts/browser/app-logs 2>/dev/null || true - name: Upload browser failure artifacts if: failure() uses: actions/upload-artifact@v4 with: name: browser-failure-artifacts path: artifacts/browser if-no-files-found: ignore container: name: container runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: Build production image run: docker build -t amare-app:ci . - name: Verify container healthcheck and storage link run: | # Same per-job network as the step container — no host :8000 # publish (collides when capacity > 1). Unique --name: docker.sock # is shared across jobs; leftovers from cancelled runs Conflict. JOB_CID="$(hostname)" JOB_NET="$(docker inspect -f '{{range $k, $_ := .NetworkSettings.Networks}}{{println $k}}{{end}}' "$JOB_CID" | head -n1)" test -n "$JOB_NET" HEALTH_NAME="amare-health-${GITHUB_RUN_ID:-$$}" docker rm -f "$HEALTH_NAME" 2>/dev/null || true docker run -d --name "$HEALTH_NAME" \ --network "$JOB_NET" \ --network-alias amare-health \ -e APP_ENV=production \ -e APP_KEY="${{ env.APP_KEY }}" \ -e APP_URL=http://amare-health:8000 \ -e APP_DEBUG=false \ -e DB_CONNECTION=pgsql \ -e DB_HOST=127.0.0.1 \ -e DB_PORT=5432 \ -e DB_DATABASE=amare \ -e DB_USERNAME=amare \ -e DB_PASSWORD=secret \ amare-app:ci cleanup() { docker rm -f "$HEALTH_NAME" >/dev/null 2>&1 || true; } trap cleanup EXIT for i in $(seq 1 30); do if curl -fsS http://amare-health:8000/up; then docker exec "$HEALTH_NAME" test -L /app/public/storage exit 0 fi sleep 2 done docker logs "$HEALTH_NAME" exit 1