Skip to content

feat(meta): add OG image, Twitter card, and PWA manifest #174

feat(meta): add OG image, Twitter card, and PWA manifest

feat(meta): add OG image, Twitter card, and PWA manifest #174

Workflow file for this run

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 }})