Files
Oleg Isonen 4273b7001f refactor: Organize Builder development and test tooling (#6285)
## Outcome

Builder development, backend, E2E, and CI tooling now share one
ownership model: Builder defines named commands and backend modes, while
CI only schedules those commands.

## Technical choices

- Group shared Compose files, PostgreSQL initialization, and backend
helpers under `apps/builder/backend`.
- Resolve `compose.<mode>.yaml` by convention instead of maintaining
Compose filename lists in scripts and workflows.
- Keep persistent development and disposable test Compose projects
separate by default, while allowing the standard `COMPOSE_PROJECT_NAME`
override.
- Make schema snapshots invalidate from discovered migration sources and
backend files.
- Move E2E behavior recipes into named Builder commands; retain concise
root proxies for developer entry points.
- Let Vitest allocate available browser ports instead of maintaining a
cross-package port registry.
- Keep test-suite scheduling explicit in `main.yml`, but move executable
test commands to root package scripts.
- Move fixture-size enforcement from inline GitHub JavaScript into a
locally runnable repository script.

## Todo

- [x] Consolidate Builder backend and Compose ownership.
- [x] Add named development, E2E, schema-cache, Supabase, and CI-shard
commands.
- [x] Remove Compose topology and behavior flags from CI.
- [x] Remove the static Vitest browser-port registry.
- [x] Extract fixture-size checking from the workflow.
- [x] Verify migration bootstrap, snapshot-backed E2E, tests, builds,
types, formatting, lint, generated contracts, and workflow syntax.
- [x] Review documentation impact: updated the E2E developer guide; no
user-facing `docs/` content is affected.

## Verification

- Full package, CLI, Builder core, and Builder app test suites
- Full workspace typecheck
- Builder production build
- Schema-cache generation through the complete migration history
- Supabase migration compatibility
- Snapshot-backed Playwright animation workflow
- Backend regression tests, including demonstrated fail/pass coverage
- All four Compose mode configurations
- `oxfmt`, `oxlint`, package boundaries, generated API, and generated
docs checks
- `actionlint` and Bash syntax checks

The fixture-size checker preserves the existing Linux `du` thresholds.
Its three inputs build locally; macOS allocation sizes are not directly
comparable to the Linux CI thresholds, so CI remains the authoritative
size result.
2026-08-31 21:09:54 +02:00

126 lines
2.9 KiB
TypeScript

import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import {
createClient,
type Database,
} from "@webstudio-is/postgrest/index.server";
import env from "../app/env/env.server";
const execFileAsync = promisify(execFile);
const backendScript = fileURLToPath(
new URL("../backend/run.sh", import.meta.url)
);
const postgrest = createClient(env.POSTGREST_URL, env.POSTGREST_API_KEY);
const throwIfError = <Data>({
error,
data,
}: {
error: { message: string } | null;
data: Data;
}): NonNullable<Data> => {
if (error) {
throw new Error(error.message);
}
if (data === null || data === undefined) {
throw new Error("Expected PostgREST response data");
}
return data;
};
export const resetDatabase = async () => {
const sql = `
DO $$
DECLARE
table_names text;
BEGIN
SELECT string_agg(format('%I.%I', schemaname, tablename), ', ')
INTO table_names
FROM pg_tables
WHERE schemaname = 'public'
AND tablename <> '_prisma_migrations';
IF table_names IS NOT NULL THEN
EXECUTE 'TRUNCATE TABLE ' || table_names || ' RESTART IDENTITY CASCADE';
END IF;
END $$;
`;
await execFileAsync(
"bash",
[
backendScript,
process.env.E2E_BACKEND_MODE ?? "test",
"exec",
"-T",
"db",
"sh",
"-c",
'PGPASSWORD="$POSTGRES_PASSWORD" psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c "$1"',
"sh",
sql,
],
{ maxBuffer: 1024 * 1024 }
);
};
type Tables = Database["public"]["Tables"];
export const updateBuild = async (
id: string,
row: Tables["Build"]["Update"]
) => {
return throwIfError(
await postgrest.from("Build").update(row).eq("id", id).select().single()
);
};
export const updateProject = async (
id: string,
row: Tables["Project"]["Update"]
) => {
return throwIfError(
await postgrest.from("Project").update(row).eq("id", id).select().single()
);
};
export const insertAuthorizationToken = async (
row: Tables["AuthorizationToken"]["Insert"]
) => {
return throwIfError(
await postgrest.from("AuthorizationToken").insert(row).select().single()
);
};
export const insertFile = async (row: Tables["File"]["Insert"]) => {
return throwIfError(
await postgrest.from("File").insert(row).select().single()
);
};
export const insertAsset = async (row: Tables["Asset"]["Insert"]) => {
return throwIfError(
await postgrest.from("Asset").insert(row).select().single()
);
};
export const loadDevBuild = async ({
projectId,
}: {
projectId: string;
}): Promise<Database["public"]["Tables"]["Build"]["Row"]> => {
return throwIfError(
await postgrest
.from("Build")
.select("*")
.eq("projectId", projectId)
.is("deployment", null)
.order("createdAt", { ascending: false })
.limit(1)
.single()
);
};