diff --git a/.gitignore b/.gitignore index 1ba0f592ee..dd44157c20 100644 --- a/.gitignore +++ b/.gitignore @@ -134,3 +134,7 @@ tmpclaude-* # Turborepo local task cache .turbo/ + +# Lighthouse CI — runtime config (cookie-injected) + report output, never committed +lighthouserc.runtime.json +.lighthouseci/ diff --git a/lighthouserc.json b/lighthouserc.json new file mode 100644 index 0000000000..947cc66bfc --- /dev/null +++ b/lighthouserc.json @@ -0,0 +1,30 @@ +{ + "//": "Lighthouse CI config for the PR-preview pipeline (datapacket-talos pr-preview-pipeline.yaml `lighthouse` task). REPORT-ONLY: no assertions are set to `error`, so a regression never fails the build. The Tekton task mints a gold-tester NextAuth session cookie (the preview is gated by preview-auth.middleware -> unauthenticated requests 302 to /login) and injects it into collect.settings.extraHeaders at runtime, producing lighthouserc.runtime.json from this base. See tests/lighthouse-mint-cookie.cjs.", + "ci": { + "collect": { + "//": "5 runs + LHCI median aggregation is the single biggest stabilizer on a capacity-constrained build pool — lab noise is real, which is exactly why this is report-only and why timing metrics (LCP/TBT/INP) must NOT be promoted to a gating assertion until a soak proves the threshold sits above the noise band. URLs are filled in per-PR by the task (BASE_URL + these paths). All routes are hit AUTHENTICATED as ci-smoke-gold (a gate-passing paid member) — an anonymous GET would 302 to /login and measure the login page, not the app.", + "numberOfRuns": 5, + "settings": { + "preset": "desktop", + "//preset": "Desktop preset (not mobile). Desktop is the lower-variance profile on a noisy shared runner and matches the primary civitai surface; revisit mobile once the desktop signal is trusted.", + "chromeFlags": "--no-sandbox --disable-gpu --disable-dev-shm-usage --headless=new", + "//chromeFlags": "--no-sandbox is required: the Tekton step runs Chrome without the kernel sandbox privileges. --disable-dev-shm-usage avoids /dev/shm exhaustion in the small container.", + "maxWaitForLoad": 60000 + } + }, + "assert": { + "//": "REPORT-ONLY. Every assertion is `warn` — LHCI surfaces the diff but exits 0, so the build never fails. Promotion path (mirrors the bundle-budget soak->gate philosophy): promote the DETERMINISTIC, low-variance metrics to `error` FIRST once a soak confirms a stable baseline — cumulative-layout-shift (geometry, near-zero variance) and total byte/resource budgets. Promote the NOISY timing metrics (largest-contentful-paint, total-blocking-time, interactive, interaction-to-next-paint) LAST, and only with a threshold set comfortably above the observed 5-run noise band. To gate: change the relevant `warn` to `error`.", + "assertions": { + "categories:performance": ["warn", { "minScore": 0.5 }], + "categories:accessibility": ["warn", { "minScore": 0.9 }], + "cumulative-layout-shift": ["warn", { "maxNumericValue": 0.1 }], + "largest-contentful-paint": ["warn", { "maxNumericValue": 4000 }], + "total-blocking-time": ["warn", { "maxNumericValue": 600 }] + } + }, + "upload": { + "//": "temporary-public-storage gives a public, 7-day report URL with zero infra (no @lhci/server stood up). The task ALSO reads .lighthouseci/manifest.json (median-run summary scores) and the representative lhr-*.json (LCP/CLS/TBT/INP audits) for the PR comment, and .lighthouseci/links.json for the public URLs.", + "target": "temporary-public-storage" + } + } +} diff --git a/tests/lighthouse-mint-cookie.cjs b/tests/lighthouse-mint-cookie.cjs new file mode 100644 index 0000000000..2e2e82bf37 --- /dev/null +++ b/tests/lighthouse-mint-cookie.cjs @@ -0,0 +1,85 @@ +#!/usr/bin/env node +/** + * Mint a gate-passing NextAuth session cookie for a deployed PR preview and + * produce the runtime Lighthouse CI config that injects it. + * + * Why this exists: a deployed PR preview runs IS_PREVIEW=true and is gated by + * preview-auth.middleware — an UNAUTHENTICATED request 302s to /login, so a + * naive Lighthouse run would measure the login page, not the app. We mint the + * same `__Secure-civitai-token` JWE the app signs with (next-auth/jwt encode + + * the preview's shared NEXTAUTH_SECRET), as ci-smoke-gold (a paid member in the + * flipt `preview-site-access` testers allowlist — id mirrors preview-fixtures / + * the datapacket-talos seed-smoke-test-users CronJob), and put it in + * collect.settings.extraHeaders.Cookie so headless Chrome clears the gate. + * + * This is the SAME mechanism tests/preview-auth.setup.ts uses for the smoke + * suite; kept as a standalone .cjs so the Tekton lighthouse task can run it + * with plain `node` (no ts/playwright) using the next-auth/jwt + uuid already + * installed into the shared workspace node_modules by the typecheck task. + * + * Usage: + * NEXTAUTH_SECRET=... BASE_URL=https://pr-123.civitaic.com \ + * node tests/lighthouse-mint-cookie.cjs + * Writes lighthouserc.runtime.json (cwd) with extraHeaders + per-PR URLs. + * Prints nothing sensitive (never the cookie value) to stdout. + */ +const fs = require('fs'); +const path = require('path'); +const { encode } = require('next-auth/jwt'); +const { v4: uuid } = require('uuid'); + +const SECRET = process.env.NEXTAUTH_SECRET; +const BASE_URL = process.env.BASE_URL; +const COOKIE_NAME = '__Secure-civitai-token'; // libs/auth.ts — https => __Secure- prefix +const MAX_AGE_S = 30 * 24 * 60 * 60; + +// Representative authed routes. Kept small (3) so 5 runs x 3 routes stays inside +// the build pool's time budget. All are reachable by ci-smoke-gold and are +// present regardless of the preview's DB content (they don't depend on a +// specific seeded entity). `/` = SSR home, `/models` = heavy list/feed, +// `/generate` = client-heavy generator. Edit here to change what's measured. +const ROUTES = ['/', '/models', '/generate']; + +// ci-smoke-gold — must mirror tests/preview-fixtures.ts PREVIEW_USERS.gold. +const GOLD = { + id: 2000000004, + username: 'ci-smoke-gold', + email: 'ci-smoke-gold@civitai.test', + isModerator: false, + tier: 'gold', + showNsfw: true, + blurNsfw: false, + browsingLevel: 1, + onboarding: 15, + muted: false, +}; + +async function main() { + if (!SECRET) throw new Error('NEXTAUTH_SECRET is required to mint the preview cookie'); + if (!BASE_URL) throw new Error('BASE_URL is required (e.g. https://pr-123.civitaic.com)'); + + const token = { user: GOLD, sub: String(GOLD.id), id: uuid(), signedAt: Date.now() }; + const value = await encode({ token, secret: SECRET, maxAge: MAX_AGE_S }); + + const base = JSON.parse( + fs.readFileSync(path.join(process.cwd(), 'lighthouserc.json'), 'utf8') + ); + + base.ci.collect.url = ROUTES.map((r) => `${BASE_URL.replace(/\/$/, '')}${r}`); + base.ci.collect.settings = base.ci.collect.settings || {}; + // extraHeaders must be a JSON STRING per the LHCI schema. + base.ci.collect.settings.extraHeaders = JSON.stringify({ + Cookie: `${COOKIE_NAME}=${value}`, + }); + + fs.writeFileSync('lighthouserc.runtime.json', JSON.stringify(base, null, 2)); + console.log( + `Wrote lighthouserc.runtime.json — ${ROUTES.length} routes x ${base.ci.collect.numberOfRuns} runs, authed as ${GOLD.username}` + ); + for (const u of base.ci.collect.url) console.log(` - ${u}`); +} + +main().catch((e) => { + console.error(`mint-cookie failed: ${e.message}`); + process.exit(1); +});