name: CI on: push: branches: [main, develop] pull_request: branches: [main, develop] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: NODE_VERSION: '20' PNPM_VERSION: '9' jobs: # =========================================================================== # Lint Job - ESLint & Prettier # =========================================================================== lint: name: Lint runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup pnpm uses: pnpm/action-setup@v3 with: version: ${{ env.PNPM_VERSION }} - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: 'pnpm' - name: Install dependencies run: pnpm install --frozen-lockfile - name: Run ESLint run: pnpm lint # Warnings are allowed, only errors fail the build - name: Check Prettier formatting run: pnpm format:check continue-on-error: true # =========================================================================== # TypeScript Type Check (optional - Next.js build has own type check) # =========================================================================== typecheck: name: TypeScript runs-on: ubuntu-latest continue-on-error: true # Don't block CI - some legacy code has type issues steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup pnpm uses: pnpm/action-setup@v3 with: version: ${{ env.PNPM_VERSION }} - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: 'pnpm' - name: Install dependencies run: pnpm install --frozen-lockfile - name: Run TypeScript compiler run: pnpm typecheck # =========================================================================== # Unit & Integration Tests # =========================================================================== test: name: Tests runs-on: ubuntu-latest needs: [lint, typecheck] timeout-minutes: 30 # Prevent 6-hour hangs services: postgres: image: postgres:17 env: POSTGRES_USER: payload POSTGRES_PASSWORD: payload_test_password POSTGRES_DB: payload_test ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup pnpm uses: pnpm/action-setup@v3 with: version: ${{ env.PNPM_VERSION }} - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: 'pnpm' - name: Install dependencies run: pnpm install --frozen-lockfile - name: Run Unit Tests run: pnpm test:unit timeout-minutes: 10 env: CSRF_SECRET: test-csrf-secret PAYLOAD_SECRET: test-payload-secret PAYLOAD_PUBLIC_SERVER_URL: https://test.example.com NEXT_PUBLIC_SERVER_URL: https://test.example.com EMAIL_DELIVERY_DISABLED: 'true' DATABASE_URI: postgresql://placeholder:placeholder@localhost:5432/placeholder CONSENT_LOGGING_API_KEY: ci-consent-api-key-placeholder IP_ANONYMIZATION_PEPPER: ci-anonymization-pepper-placeholder - name: Setup Database Schema run: | echo "Testing database connection..." PGPASSWORD=payload_test_password psql -h localhost -U payload -d payload_test -c "SELECT 1;" echo "Dropping existing tables..." PGPASSWORD=payload_test_password psql -h localhost -U payload -d payload_test -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;" echo "Creating schema from generated file..." pnpm exec drizzle-kit push --config=./drizzle.ci.config.ts --force echo "Schema created." timeout-minutes: 5 env: CI: true NODE_OPTIONS: --no-deprecation PAYLOAD_SECRET: test-payload-secret DATABASE_URI: postgresql://payload:payload_test_password@localhost:5432/payload_test NEXT_PUBLIC_SERVER_URL: https://test.example.com PAYLOAD_PUBLIC_SERVER_URL: https://test.example.com CONSENT_LOGGING_API_KEY: ci-consent-api-key-placeholder IP_ANONYMIZATION_PEPPER: ci-anonymization-pepper-placeholder - name: Run Integration Tests run: pnpm test:int timeout-minutes: 15 env: CSRF_SECRET: test-csrf-secret PAYLOAD_SECRET: test-payload-secret PAYLOAD_PUBLIC_SERVER_URL: https://test.example.com NEXT_PUBLIC_SERVER_URL: https://test.example.com EMAIL_DELIVERY_DISABLED: 'true' DATABASE_URI: postgresql://payload:payload_test_password@localhost:5432/payload_test CONSENT_LOGGING_API_KEY: ci-consent-api-key-placeholder IP_ANONYMIZATION_PEPPER: ci-anonymization-pepper-placeholder - name: Upload coverage report if: always() uses: actions/upload-artifact@v4 with: name: test-coverage path: coverage/ retention-days: 7 # =========================================================================== # Build Job # =========================================================================== build: name: Build runs-on: ubuntu-latest needs: [lint, typecheck] steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup pnpm uses: pnpm/action-setup@v3 with: version: ${{ env.PNPM_VERSION }} - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: 'pnpm' - name: Install dependencies run: pnpm install --frozen-lockfile - name: Build application run: pnpm build env: # Minimal env vars for build PAYLOAD_SECRET: build-secret-placeholder DATABASE_URI: postgresql://placeholder:placeholder@localhost:5432/placeholder NEXT_PUBLIC_SERVER_URL: https://build.example.com PAYLOAD_PUBLIC_SERVER_URL: https://build.example.com CONSENT_LOGGING_API_KEY: ci-consent-api-key-placeholder IP_ANONYMIZATION_PEPPER: ci-anonymization-pepper-placeholder - name: Verify build output run: | if [ ! -f .next/BUILD_ID ]; then echo "Build failed - no BUILD_ID found" exit 1 fi echo "Build successful - BUILD_ID: $(cat .next/BUILD_ID)" - name: Upload build artifacts uses: actions/upload-artifact@v4 with: name: build-output path: | .next/ !.next/cache/ retention-days: 1 include-hidden-files: true # =========================================================================== # E2E Tests (after build) # =========================================================================== e2e: name: E2E Tests runs-on: ubuntu-latest needs: [build] timeout-minutes: 30 # Prevent 6-hour hangs services: postgres: image: postgres:17 env: POSTGRES_USER: payload POSTGRES_PASSWORD: payload_test_password POSTGRES_DB: payload_test ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup pnpm uses: pnpm/action-setup@v3 with: version: ${{ env.PNPM_VERSION }} - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} cache: 'pnpm' - name: Install dependencies run: pnpm install --frozen-lockfile - name: Download build artifacts uses: actions/download-artifact@v4 with: name: build-output path: .next/ - name: Install Playwright browsers run: pnpm exec playwright install chromium --with-deps - name: Setup Database Schema run: | echo "Testing database connection..." PGPASSWORD=payload_test_password psql -h localhost -U payload -d payload_test -c "SELECT 1;" echo "Dropping existing tables..." PGPASSWORD=payload_test_password psql -h localhost -U payload -d payload_test -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;" echo "Creating schema from generated file..." pnpm exec drizzle-kit push --config=./drizzle.ci.config.ts --force echo "Schema created." timeout-minutes: 5 env: CI: true NODE_OPTIONS: --no-deprecation PAYLOAD_SECRET: e2e-secret-placeholder DATABASE_URI: postgresql://payload:payload_test_password@localhost:5432/payload_test NEXT_PUBLIC_SERVER_URL: http://localhost:3001 PAYLOAD_PUBLIC_SERVER_URL: http://localhost:3001 CONSENT_LOGGING_API_KEY: ci-consent-api-key-placeholder IP_ANONYMIZATION_PEPPER: ci-anonymization-pepper-placeholder - name: Run E2E tests run: pnpm test:e2e timeout-minutes: 15 env: CI: true CSRF_SECRET: e2e-csrf-secret-placeholder PAYLOAD_SECRET: e2e-secret-placeholder DATABASE_URI: postgresql://payload:payload_test_password@localhost:5432/payload_test NEXT_PUBLIC_SERVER_URL: http://localhost:3001 PAYLOAD_PUBLIC_SERVER_URL: http://localhost:3001 EMAIL_DELIVERY_DISABLED: 'true' CONSENT_LOGGING_API_KEY: ci-consent-api-key-placeholder IP_ANONYMIZATION_PEPPER: ci-anonymization-pepper-placeholder - name: Upload Playwright report if: always() uses: actions/upload-artifact@v4 with: name: playwright-report path: playwright-report/ retention-days: 7 # =========================================================================== # Summary Job # =========================================================================== ci-success: name: CI Success runs-on: ubuntu-latest needs: [lint, typecheck, test, build, e2e] if: always() steps: - name: Check required jobs run: | # Required jobs (must succeed) if [ "${{ needs.lint.result }}" != "success" ] || \ [ "${{ needs.test.result }}" != "success" ] || \ [ "${{ needs.build.result }}" != "success" ] || \ [ "${{ needs.e2e.result }}" != "success" ]; then echo "One or more required jobs failed" exit 1 fi # Optional jobs (typecheck) - just report status if [ "${{ needs.typecheck.result }}" != "success" ]; then echo "⚠️ TypeScript check failed (optional)" fi echo "All required CI checks passed!" - name: Create summary run: | echo "## CI Summary" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "| Job | Status | Required |" >> $GITHUB_STEP_SUMMARY echo "|-----|--------|----------|" >> $GITHUB_STEP_SUMMARY echo "| Lint | ${{ needs.lint.result }} | ✅ |" >> $GITHUB_STEP_SUMMARY echo "| TypeScript | ${{ needs.typecheck.result }} | ⚠️ optional |" >> $GITHUB_STEP_SUMMARY echo "| Tests | ${{ needs.test.result }} | ✅ |" >> $GITHUB_STEP_SUMMARY echo "| Build | ${{ needs.build.result }} | ✅ |" >> $GITHUB_STEP_SUMMARY echo "| E2E | ${{ needs.e2e.result }} | ✅ |" >> $GITHUB_STEP_SUMMARY