feat(web): OG card gets a call-to-action + bare domain; header mark m⦠#36
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: CI/CD Pipeline | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| # Cancel in-progress runs for the same PR/branch | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| # ββ STAGE 1: Code Quality (parallel with Stage 2) βββββββββ | |
| quality: | |
| name: "Stage 1 Β· Quality" | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: | |
| # Full matrix on main push, single node on PRs | |
| node-version: ${{ github.event_name == 'push' && fromJSON('[20, 22, 24]') || fromJSON('[20]') }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js ${{ matrix.node-version }} | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ matrix.node-version }} | |
| cache: "npm" | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Format check (Prettier) | |
| run: npm run format:check | |
| - name: Lint (ESLint) | |
| run: npm run lint | |
| - name: Type check (engine, CLI, scripts, e2e) | |
| run: npm run typecheck | |
| - name: Type check (web app) | |
| run: npm run typecheck -w apps/web | |
| - name: Unit + property + boundary tests with coverage | |
| run: npm run test:coverage | |
| # replays the 13 recorded fixtures with NANSEN_OFFLINE=1 β no API key, no network, every decision hash must match | |
| - name: Offline fixture replay (13/13 verdicts) | |
| run: npm run verify | |
| - name: Submission readiness (README counts vs reality, no placeholders, no kitchen files, no key) | |
| run: npm run check:submission | |
| - name: Upload coverage report | |
| if: matrix.node-version == 20 | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: coverage | |
| path: coverage/ | |
| retention-days: 7 | |
| # ββ STAGE 2: Security Gate (parallel with Stage 1) ββββββββ | |
| secret-scan: | |
| name: "Stage 2 Β· Secret Scanning" | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # Full history for scanning git commits | |
| - name: TruffleHog Secret Scan | |
| uses: trufflesecurity/trufflehog@main | |
| with: | |
| extra_args: --only-verified | |
| dependency-audit: | |
| name: "Stage 2 Β· Dependency Audit" | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: "npm" | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: npm audit (high + critical) | |
| run: npm audit --audit-level=high | |
| continue-on-error: true # Don't block on transitive dep issues | |
| - name: License compliance check | |
| run: npx license-checker --production --failOn "GPL-3.0;AGPL-3.0" --summary | |
| continue-on-error: true | |
| # ββ STAGE 3: Build Verification βββββββββββββββββββββββββββ | |
| build: | |
| name: "Stage 3 Β· Build Verification" | |
| needs: [quality] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: "npm" | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Next.js production build | |
| run: npm run build | |
| - name: Check bundle size | |
| run: | | |
| echo "π¦ Bundle Size Report" | |
| echo "βββββββββββββββββββββ" | |
| if [ -d "apps/web/.next/static" ]; then | |
| TOTAL_SIZE=$(du -sk apps/web/.next/static | cut -f1) | |
| echo "Total static assets: ${TOTAL_SIZE}KB" | |
| if [ "$TOTAL_SIZE" -gt 2000 ]; then | |
| echo "::error::Bundle size ${TOTAL_SIZE}KB exceeds 2000KB error threshold" | |
| exit 1 | |
| elif [ "$TOTAL_SIZE" -gt 1500 ]; then | |
| echo "::warning::Bundle size ${TOTAL_SIZE}KB exceeds 1500KB warning threshold" | |
| else | |
| echo "β Bundle size within budget" | |
| fi | |
| fi | |
| # ββ STAGE 4: E2E Tests (no key, no network to Nansen) βββββ | |
| e2e: | |
| name: "Stage 4 Β· E2E Tests" | |
| needs: [build] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: "npm" | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Install Playwright browsers | |
| run: npx playwright install --with-deps chromium | |
| - name: Build application | |
| run: npm run build | |
| - name: Run E2E tests (server starts WITHOUT NANSEN_API_KEY) | |
| run: npx playwright test | |
| env: | |
| NODE_ENV: production | |
| - name: Upload E2E artifacts on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: playwright-report | |
| path: | | |
| playwright-report/ | |
| test-results/ | |
| retention-days: 7 | |
| # ββ STAGE 5: Performance (advisory) βββββββββββββββββββββββ | |
| performance: | |
| name: "Stage 5 Β· Performance" | |
| needs: [build] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: "npm" | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Build application | |
| run: npm run build | |
| - name: Lighthouse CI | |
| run: npx lhci autorun | |
| env: | |
| LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }} | |
| continue-on-error: true # Advisory β don't block deploys on perf regressions yet | |
| # ββ STAGE 6: Deploy Gate (main only) ββββββββββββββββββββββ | |
| deploy-gate: | |
| name: "Stage 6 Β· Deploy Gate" | |
| needs: [build, e2e, performance, secret-scan, dependency-audit] | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| steps: | |
| - name: All gates passed | |
| run: | | |
| echo "π All CI/CD gates passed!" | |
| echo "" | |
| echo "Pipeline Summary:" | |
| echo " β Stage 1 β Quality (format, lint, typecheck, 128 tests + coverage, offline replay, readiness)" | |
| echo " β Stage 2 β Secret Scanning (TruffleHog)" | |
| echo " β Stage 2 β Dependency Audit (npm audit + license-checker)" | |
| echo " β Stage 3 β Build Verification" | |
| echo " β Stage 4 β E2E Tests (Playwright, no key)" | |
| echo " β Stage 5 β Performance (Lighthouse CI)" | |
| # ββ STAGE 7: Production Deploy (main only, after every gate) ββ | |
| # Prebuilt flow: `vercel build` runs here on the runner, `vercel deploy --prebuilt` uploads the | |
| # output β Vercel never builds from git for this project. The only secret is VERCEL_TOKEN; the | |
| # org/project ids are plain repository variables. Skipped on PRs and on forks. | |
| deploy: | |
| name: "Stage 7 Β· Production Deploy" | |
| needs: [deploy-gate] | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' && github.repository == 'edycutjong/sentwrong' | |
| concurrency: | |
| group: production-deploy | |
| cancel-in-progress: false | |
| environment: | |
| name: production | |
| url: https://sentwrong.edycu.dev | |
| env: | |
| VERCEL_ORG_ID: ${{ vars.VERCEL_ORG_ID }} | |
| VERCEL_PROJECT_ID: ${{ vars.VERCEL_PROJECT_ID }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: "npm" | |
| - name: Install Vercel CLI | |
| run: npm i -g vercel@latest | |
| - name: Pull Vercel project settings (production) | |
| run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} | |
| - name: Build (vercel build β .vercel/output) | |
| run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }} | |
| - name: Deploy prebuilt output to production | |
| id: deploy | |
| run: | | |
| URL=$(vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}) | |
| echo "url=$URL" >> "$GITHUB_OUTPUT" | |
| { | |
| echo "## π Production deploy" | |
| echo "" | |
| echo "| | |" | |
| echo "|---|---|" | |
| echo "| Deployment | $URL |" | |
| echo "| Production | https://sentwrong.edycu.dev |" | |
| echo "| Commit | \`${{ github.sha }}\` |" | |
| } >> "$GITHUB_STEP_SUMMARY" |