feat(meta): add OG image, Twitter card, and PWA manifest #174
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: E2E | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| jobs: | |
| # Sharded E2E test execution | |
| e2e-tests: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] | |
| shardTotal: [12] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| cache: 'pnpm' | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Install Playwright browsers | |
| run: pnpm exec playwright install --with-deps chromium | |
| - name: Build application | |
| run: pnpm build | |
| env: | |
| NEXT_PUBLIC_SUPABASE_URL: https://mock.supabase.co | |
| NEXT_PUBLIC_SUPABASE_ANON_KEY: mock-anon-key | |
| NEXT_PUBLIC_ENABLE_MSW_MOCK: 'true' | |
| APP_ENV: test | |
| SUPABASE_SERVICE_ROLE_KEY: mock-service-role-key | |
| - name: Run E2E tests (shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}) | |
| run: pnpm exec playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} | |
| env: | |
| NEXT_PUBLIC_SUPABASE_URL: https://mock.supabase.co | |
| NEXT_PUBLIC_SUPABASE_ANON_KEY: mock-anon-key | |
| NEXT_PUBLIC_ENABLE_MSW_MOCK: 'true' | |
| APP_ENV: test | |
| SHARD_INDEX: ${{ matrix.shardIndex }} | |
| SUPABASE_SERVICE_ROLE_KEY: mock-service-role-key | |
| - name: Upload blob report | |
| if: ${{ !cancelled() }} | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: blob-report-${{ matrix.shardIndex }} | |
| path: blob-report | |
| retention-days: 1 | |
| - name: Upload coverage report | |
| if: ${{ !cancelled() }} | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: coverage-report-${{ matrix.shardIndex }} | |
| path: coverage-e2e-shard-${{ matrix.shardIndex }}/ | |
| retention-days: 1 | |
| # Merge reports from all shards | |
| merge-reports: | |
| if: ${{ !cancelled() }} | |
| needs: [e2e-tests] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: pnpm/action-setup@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| cache: 'pnpm' | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Download blob reports | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: all-blob-reports | |
| pattern: blob-report-* | |
| merge-multiple: true | |
| - name: Download coverage reports | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: all-coverage-reports | |
| pattern: coverage-report-* | |
| # NOTE: Do NOT use merge-multiple: true here. | |
| # Each shard produces coverage/coverage-summary.json at the same relative path. | |
| # merge-multiple would overwrite all files, leaving only the last shard's data. | |
| # Without it, each artifact is downloaded to its own subdirectory: | |
| # all-coverage-reports/coverage-report-{N}/coverage/coverage-summary.json | |
| - name: Merge Playwright reports | |
| run: | | |
| pnpm exec playwright merge-reports --reporter html ./all-blob-reports | |
| - name: Merge coverage reports | |
| run: | | |
| # Create directory for merged coverage | |
| mkdir -p coverage-e2e/coverage | |
| # Find and merge all coverage.json files | |
| find all-coverage-reports -name 'coverage.json' -exec cp {} coverage-e2e/coverage/ \; 2>/dev/null || true | |
| # If we have coverage files, merge them | |
| if ls all-coverage-reports/*/coverage/v8/*.json 1> /dev/null 2>&1; then | |
| # Copy V8 coverage files | |
| mkdir -p coverage-e2e/coverage/v8 | |
| for dir in all-coverage-reports/*/coverage/v8/; do | |
| if [ -d "$dir" ]; then | |
| cp "$dir"*.json coverage-e2e/coverage/v8/ 2>/dev/null || true | |
| fi | |
| done | |
| fi | |
| # Merge coverage summaries | |
| node -e " | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const summaryFiles = []; | |
| const dirs = fs.readdirSync('all-coverage-reports').filter(f => | |
| fs.statSync(path.join('all-coverage-reports', f)).isDirectory() | |
| ); | |
| for (const dir of dirs) { | |
| const summaryPath = path.join('all-coverage-reports', dir, 'coverage', 'coverage-summary.json'); | |
| if (fs.existsSync(summaryPath)) { | |
| summaryFiles.push(JSON.parse(fs.readFileSync(summaryPath, 'utf8'))); | |
| } | |
| } | |
| if (summaryFiles.length === 0) { | |
| console.log('No coverage summaries found'); | |
| process.exit(0); | |
| } | |
| // Merge totals | |
| const merged = { total: { lines: { total: 0, covered: 0, pct: 0 }, statements: { total: 0, covered: 0, pct: 0 }, functions: { total: 0, covered: 0, pct: 0 }, branches: { total: 0, covered: 0, pct: 0 } } }; | |
| for (const summary of summaryFiles) { | |
| if (summary.total) { | |
| for (const metric of ['lines', 'statements', 'functions', 'branches']) { | |
| if (summary.total[metric]) { | |
| merged.total[metric].total += summary.total[metric].total || 0; | |
| merged.total[metric].covered += summary.total[metric].covered || 0; | |
| } | |
| } | |
| } | |
| } | |
| // Calculate percentages | |
| for (const metric of ['lines', 'statements', 'functions', 'branches']) { | |
| if (merged.total[metric].total > 0) { | |
| merged.total[metric].pct = Math.round((merged.total[metric].covered / merged.total[metric].total) * 10000) / 100; | |
| } | |
| } | |
| fs.writeFileSync('coverage-e2e/coverage/coverage-summary.json', JSON.stringify(merged, null, 2)); | |
| console.log('Merged coverage:', JSON.stringify(merged.total, null, 2)); | |
| " | |
| - name: Upload merged Playwright report | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: playwright-report | |
| path: playwright-report/ | |
| retention-days: 30 | |
| - name: Upload merged coverage report | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: e2e-coverage-report | |
| path: coverage-e2e/ | |
| retention-days: 30 | |
| # Check if any shard failed | |
| - name: Check test results | |
| run: | | |
| if [ "${{ needs.e2e-tests.result }}" == "failure" ]; then | |
| echo "Some E2E tests failed" | |
| exit 1 | |
| fi | |
| # Extract coverage percentage | |
| - name: Extract coverage percentage | |
| id: coverage | |
| if: always() | |
| run: | | |
| if [ -f "coverage-e2e/coverage/coverage-summary.json" ]; then | |
| LINES_PCT=$(jq '.total.lines.pct // 0' coverage-e2e/coverage/coverage-summary.json) | |
| FUNCTIONS_PCT=$(jq '.total.functions.pct // 0' coverage-e2e/coverage/coverage-summary.json) | |
| BRANCHES_PCT=$(jq '.total.branches.pct // 0' coverage-e2e/coverage/coverage-summary.json) | |
| STATEMENTS_PCT=$(jq '.total.statements.pct // 0' coverage-e2e/coverage/coverage-summary.json) | |
| echo "lines_pct=${LINES_PCT}" >> $GITHUB_OUTPUT | |
| echo "functions_pct=${FUNCTIONS_PCT}" >> $GITHUB_OUTPUT | |
| echo "branches_pct=${BRANCHES_PCT}" >> $GITHUB_OUTPUT | |
| echo "statements_pct=${STATEMENTS_PCT}" >> $GITHUB_OUTPUT | |
| if (( $(echo "$LINES_PCT >= 80" | bc -l) )); then | |
| COLOR="brightgreen" | |
| elif (( $(echo "$LINES_PCT >= 60" | bc -l) )); then | |
| COLOR="yellow" | |
| elif (( $(echo "$LINES_PCT >= 40" | bc -l) )); then | |
| COLOR="orange" | |
| else | |
| COLOR="red" | |
| fi | |
| echo "color=${COLOR}" >> $GITHUB_OUTPUT | |
| echo "coverage_available=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "coverage_available=false" >> $GITHUB_OUTPUT | |
| echo "lines_pct=0" >> $GITHUB_OUTPUT | |
| fi | |
| # Badge update (main branch only) | |
| - name: Update coverage badge | |
| if: always() && github.ref == 'refs/heads/main' && steps.coverage.outputs.coverage_available == 'true' | |
| uses: Schneegans/dynamic-badges-action@v1.7.0 | |
| with: | |
| auth: ${{ secrets.PERSONAL_ACCESS_TOKEN }} | |
| gistID: 7782ae901e4ba955b064eadeeac72c45 | |
| filename: gitbox-e2e-coverage.json | |
| label: E2E Coverage | |
| message: ${{ steps.coverage.outputs.lines_pct }}% | |
| color: ${{ steps.coverage.outputs.color }} | |
| namedLogo: playwright | |
| # PR Comment | |
| - name: Comment coverage on PR | |
| if: always() && github.event_name == 'pull_request' && steps.coverage.outputs.coverage_available == 'true' | |
| uses: marocchino/sticky-pull-request-comment@v2 | |
| with: | |
| header: e2e-coverage | |
| message: | | |
| ## 🧪 E2E Coverage Report (Sharded: 12 parallel jobs) | |
| | Metric | Coverage | | |
| |--------|----------| | |
| | **Lines** | ${{ steps.coverage.outputs.lines_pct }}% | | |
| | **Functions** | ${{ steps.coverage.outputs.functions_pct }}% | | |
| | **Branches** | ${{ steps.coverage.outputs.branches_pct }}% | | |
| | **Statements** | ${{ steps.coverage.outputs.statements_pct }}% | | |
| 📊 Full report available in [workflow artifacts](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) |