ci(web): Phase P6 — vite build in PR CI + headless E2E gate on trunk publishes (#1445)
ci/woodpecker/pr/ci Pipeline was successful

- ci.yml: build step (vite build via turbo) runs on every PR pipeline after test
- publish.yml: e2e step boots the gateway from built dist (HOME/cwd-isolated
  throwaway PGlite) and runs the Playwright suite headless inside
  mcr.microsoft.com/playwright:v1.58.2-noble against the SPA bundle served
  exactly as production serves it; both kaniko publishes now gate on e2e
- serve-spa.ts: strip query strings before asset resolution; immutable
  cache-control for hashed assets (onSend hook); e2e spec covers both
- e2e suite hardened: globalSetup seeds admin+member through real
  bootstrap/better-auth APIs (Origin header for CSRF), loginAs waits for the
  post-login redirect (fixes 27-skipped race), stale #152-era assertions
  rewritten to the current command-driven UI, strict-mode violations fixed
  with level-1 heading queries and .or() auto-retrying locators
- verify-release mirrors the new build stage; playwright artifacts ignored
This commit is contained in:
fred
2026-08-27 09:30:38 -05:00
parent b5ee692843
commit 311b4dda59
15 changed files with 451 additions and 77 deletions
+85
View File
@@ -0,0 +1,85 @@
import type { FullConfig } from '@playwright/test';
import { ADMIN_USER, TEST_USER } from './helpers/auth.js';
/**
* Seed the E2E users through the gateway's real APIs (#1445, P6).
*
* On a fresh database (CI boots the gateway on the embedded PGlite path):
* 1. POST /api/bootstrap/setup creates ADMIN_USER as the first admin.
* 2. The admin signs in and creates TEST_USER via the better-auth admin API.
*
* Against an environment that already has users (needsSetup=false), seeding is
* skipped entirely: the specs keep their own skip-when-login-fails guards, so
* a live environment stays usable as a test target without mutation.
*
* On a fresh database, any seeding failure throws and fails the whole run: an
* E2E gate whose authenticated suites silently skip would pass while proving
* nothing.
*/
export default async function globalSetup(config: FullConfig): Promise<void> {
const baseURL = config.projects[0]?.use?.baseURL ?? 'http://localhost:14242';
const statusRes = await fetch(`${baseURL}/api/bootstrap/status`);
if (!statusRes.ok) {
throw new Error(`GET /api/bootstrap/status returned ${statusRes.status} — is the gateway up?`);
}
const status = (await statusRes.json()) as { needsSetup: boolean };
if (!status.needsSetup) {
console.info('[e2e setup] users already exist; skipping seed');
return;
}
const setupRes = await fetch(`${baseURL}/api/bootstrap/setup`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
name: ADMIN_USER.name,
email: ADMIN_USER.email,
password: ADMIN_USER.password,
}),
});
if (!setupRes.ok) {
throw new Error(
`POST /api/bootstrap/setup failed (${setupRes.status}): ${await setupRes.text()}`,
);
}
console.info(`[e2e setup] bootstrap admin created: ${ADMIN_USER.email}`);
// better-auth's CSRF protection rejects requests without an Origin header
// (403 MISSING_OR_NULL_ORIGIN), so the server-side fetches here send the
// gateway's own origin — the same value a browser tab on the SPA would send.
const authHeaders = { 'content-type': 'application/json', origin: baseURL };
const signInRes = await fetch(`${baseURL}/api/auth/sign-in/email`, {
method: 'POST',
headers: authHeaders,
body: JSON.stringify({ email: ADMIN_USER.email, password: ADMIN_USER.password }),
});
if (!signInRes.ok) {
throw new Error(`admin sign-in failed (${signInRes.status}): ${await signInRes.text()}`);
}
const cookies = signInRes.headers
.getSetCookie()
.map((cookie) => cookie.split(';', 1)[0])
.join('; ');
if (!cookies) {
throw new Error('admin sign-in returned no session cookie');
}
const createRes = await fetch(`${baseURL}/api/auth/admin/create-user`, {
method: 'POST',
headers: { ...authHeaders, cookie: cookies },
body: JSON.stringify({
name: TEST_USER.name,
email: TEST_USER.email,
password: TEST_USER.password,
role: 'member',
}),
});
if (!createRes.ok) {
throw new Error(
`POST /api/auth/admin/create-user failed (${createRes.status}): ${await createRes.text()}`,
);
}
console.info(`[e2e setup] test user created: ${TEST_USER.email}`);
}