name: E2E on: push: pull_request: jobs: playwright: name: Playwright runs-on: ubuntu-latest env: APP_ENV: test APP_DEBUG: "1" DATABASE_URL: "postgresql://app:app@127.0.0.1:5432/app?serverVersion=18&charset=utf8" MESSENGER_TRANSPORT_DSN: "in-memory://" MAILER_DSN: "null://null" APP_DEV_USER_EMAIL: "e2e@plmail.test" APP_DEV_USER_PASSWORD: "e2e-password-change-me" services: postgres: image: postgres:18 env: POSTGRES_USER: app POSTGRES_PASSWORD: app POSTGRES_DB: app ports: - 5432:5432 options: >- --health-cmd "pg_isready -U app" --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@v4 - name: Setup PHP extension cache id: php-extension-cache uses: shivammathur/cache-extensions@v1 with: php-version: "8.4" extensions: ctype, iconv, pcntl, pdo_pgsql, pgsql, intl, sodium, mbstring, xml, curl, zip key: php-extensions-v1 - name: Cache PHP extensions uses: actions/cache@v4 with: path: ${{ steps.php-extension-cache.outputs.dir }} key: ${{ steps.php-extension-cache.outputs.key }} restore-keys: ${{ steps.php-extension-cache.outputs.key }} - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: "8.4" extensions: ctype, iconv, pcntl, pdo_pgsql, pgsql, intl, sodium, mbstring, xml, curl, zip coverage: none tools: composer:v2 - name: Get Composer cache directory id: composer-cache run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT" - name: Cache Composer dependencies uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: composer-${{ hashFiles('**/composer.lock') }} restore-keys: composer- - name: Install PHP dependencies run: composer install --no-interaction --prefer-dist --no-progress - name: Install Symfony CLI run: curl -sS https://get.symfony.com/cli/installer | bash && sudo mv ~/.symfony*/bin/symfony /usr/local/bin/symfony - name: Setup Node uses: actions/setup-node@v4 with: # From .nvmrc rather than pinned here. The two drifted — # .nvmrc said 22, this said 20, and the machine the suite was # actually developed on ran 24 — so "works locally, fails in # CI" had three plausible causes instead of none. node-version-file: .nvmrc cache: npm - name: Install Node dependencies run: npm ci - name: Resolve Playwright version id: playwright-version run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT" - name: Cache Playwright browsers id: playwright-cache uses: actions/cache@v4 with: path: ~/.cache/ms-playwright key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} - name: Install Playwright browsers if: steps.playwright-cache.outputs.cache-hit != 'true' run: npx playwright install --with-deps chromium # Browsers came from cache; the apt libs they link against did not. - name: Install Playwright system dependencies if: steps.playwright-cache.outputs.cache-hit == 'true' run: npx playwright install-deps chromium # Tailwind scans the templates, so they belong in the key alongside # the importmap and the sources themselves. - name: Cache frontend build id: frontend-cache uses: actions/cache@v4 with: path: | public/assets assets/vendor key: frontend-${{ runner.os }}-${{ hashFiles('importmap.php', 'package-lock.json', 'assets/**', 'templates/**') }} - name: Build frontend assets if: steps.frontend-cache.outputs.cache-hit != 'true' run: | php bin/console importmap:install php bin/console tailwind:build # Compiled rather than served through PHP. Without this every # asset request falls through to a full Symfony kernel boot — # /login alone pulls 56 of them, and Playwright gives every # test a cold cache. It was about 40% of the whole suite. php bin/console asset-map:compile - name: Prepare database run: | php bin/console doctrine:database:create --if-not-exists --no-interaction php bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration # The Playwright suite seeds its own per-worker users, so this is # only what PHPUnit needs: the default user, and the admin the # controller tests look for. That admin used to be created by a # browser spec, which meant PHPUnit silently skipped ~50 tests # whenever it ran first — which is exactly the order used here. - name: Seed fixture users run: | php bin/console app:test:seed-user php bin/console app:test:seed-user --admin --email=e2e-admin@plmail.test --password=e2e-admin-password # Needs the compiled test container for phpstan-symfony to resolve # service ids, and a booted kernel for the Doctrine metadata — both # of which the seeding above has already produced. - name: Run PHPStan run: composer stan # Fast and dependency-free, so it runs before the browser suite: # a failure here is far quicker to read than a Playwright trace. - name: Run PHPUnit tests run: composer test - name: Run Playwright tests run: npx playwright test - name: Upload Playwright report if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: name: playwright-report path: playwright-report/ retention-days: 14