fix: migration 033 idempotency & smoke test cleanup (#365)
* fix(core): add IF NOT EXISTS to migration 033 index creation The schema registry's createContentTable() creates composite indexes on content tables at creation time. When migration 033 runs on a database where tables were created after the registry added these indexes, it fails with "index already exists". Adding IF NOT EXISTS makes the migration idempotent. * refactor(smoke): trim site matrix to templates + playground, remove typechecks The smoke test matrix included every demo and template, plus typecheck cases. This was slow and redundant — typechecks are already covered by pnpm typecheck, and demos are dev targets not release artifacts. Trim to templates + playground only, and replace the sequential per-site astro build with a single recursive pnpm build. Remove the TypecheckSiteCase type and handling since smoke tests should only verify runtime behavior. * Update env * chore: add changeset for migration 033 fix
This commit is contained in:
@@ -28,19 +28,19 @@ export async function up(db: Kysely<unknown>): Promise<void> {
|
||||
|
||||
// Composite index for listing queries: WHERE deleted_at IS NULL ORDER BY updated_at DESC
|
||||
await sql`
|
||||
CREATE INDEX ${sql.ref(`idx_${table.name}_deleted_updated_id`)}
|
||||
CREATE INDEX IF NOT EXISTS ${sql.ref(`idx_${table.name}_deleted_updated_id`)}
|
||||
ON ${sql.ref(table.name)} (deleted_at, updated_at DESC, id DESC)
|
||||
`.execute(db);
|
||||
|
||||
// Composite index for count-by-status queries: WHERE deleted_at IS NULL AND status = ?
|
||||
await sql`
|
||||
CREATE INDEX ${sql.ref(`idx_${table.name}_deleted_status`)}
|
||||
CREATE INDEX IF NOT EXISTS ${sql.ref(`idx_${table.name}_deleted_status`)}
|
||||
ON ${sql.ref(table.name)} (deleted_at, status)
|
||||
`.execute(db);
|
||||
|
||||
// Composite index for created-at ordering: WHERE deleted_at IS NULL ORDER BY created_at DESC
|
||||
await sql`
|
||||
CREATE INDEX ${sql.ref(`idx_${table.name}_deleted_created_id`)}
|
||||
CREATE INDEX IF NOT EXISTS ${sql.ref(`idx_${table.name}_deleted_created_id`)}
|
||||
ON ${sql.ref(table.name)} (deleted_at, created_at DESC, id DESC)
|
||||
`.execute(db);
|
||||
}
|
||||
@@ -48,25 +48,25 @@ export async function up(db: Kysely<unknown>): Promise<void> {
|
||||
// Add partial indexes for efficient comment status counting
|
||||
// Each index contains only rows for one status, enabling fast COUNT queries
|
||||
await sql`
|
||||
CREATE INDEX idx_comments_pending
|
||||
CREATE INDEX IF NOT EXISTS idx_comments_pending
|
||||
ON _emdash_comments (id)
|
||||
WHERE status = 'pending'
|
||||
`.execute(db);
|
||||
|
||||
await sql`
|
||||
CREATE INDEX idx_comments_approved
|
||||
CREATE INDEX IF NOT EXISTS idx_comments_approved
|
||||
ON _emdash_comments (id)
|
||||
WHERE status = 'approved'
|
||||
`.execute(db);
|
||||
|
||||
await sql`
|
||||
CREATE INDEX idx_comments_spam
|
||||
CREATE INDEX IF NOT EXISTS idx_comments_spam
|
||||
ON _emdash_comments (id)
|
||||
WHERE status = 'spam'
|
||||
`.execute(db);
|
||||
|
||||
await sql`
|
||||
CREATE INDEX idx_comments_trash
|
||||
CREATE INDEX IF NOT EXISTS idx_comments_trash
|
||||
ON _emdash_comments (id)
|
||||
WHERE status = 'trash'
|
||||
`.execute(db);
|
||||
|
||||
@@ -7,11 +7,10 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import { ensureBuilt } from "../server.js";
|
||||
|
||||
interface RuntimeSiteCase {
|
||||
interface SiteCase {
|
||||
name: string;
|
||||
dir: string;
|
||||
port: number;
|
||||
mode: "runtime";
|
||||
startupTimeoutMs: number;
|
||||
waitPath?: string;
|
||||
setupPath?: string | null;
|
||||
@@ -20,121 +19,61 @@ interface RuntimeSiteCase {
|
||||
requireDoctype?: boolean;
|
||||
}
|
||||
|
||||
interface TypecheckSiteCase {
|
||||
name: string;
|
||||
dir: string;
|
||||
mode: "typecheck";
|
||||
}
|
||||
|
||||
type SiteCase = RuntimeSiteCase | TypecheckSiteCase;
|
||||
|
||||
const WORKSPACE_ROOT = resolve(import.meta.dirname, "../../../../..");
|
||||
const execAsync = promisify(execFile);
|
||||
|
||||
const SITE_MATRIX: SiteCase[] = [
|
||||
// Demos
|
||||
{
|
||||
name: "demos/simple",
|
||||
dir: resolve(WORKSPACE_ROOT, "demos/simple"),
|
||||
port: 4601,
|
||||
mode: "runtime",
|
||||
startupTimeoutMs: 60_000,
|
||||
},
|
||||
{
|
||||
name: "demos/cloudflare",
|
||||
dir: resolve(WORKSPACE_ROOT, "demos/cloudflare"),
|
||||
port: 4602,
|
||||
mode: "runtime",
|
||||
startupTimeoutMs: 120_000,
|
||||
},
|
||||
{
|
||||
name: "demos/playground",
|
||||
dir: resolve(WORKSPACE_ROOT, "demos/playground"),
|
||||
port: 4603,
|
||||
mode: "runtime",
|
||||
startupTimeoutMs: 120_000,
|
||||
waitPath: "/playground",
|
||||
frontendPath: "/playground",
|
||||
requireDoctype: false,
|
||||
},
|
||||
{
|
||||
name: "demos/preview",
|
||||
dir: resolve(WORKSPACE_ROOT, "demos/preview"),
|
||||
port: 4604,
|
||||
mode: "runtime",
|
||||
startupTimeoutMs: 120_000,
|
||||
setupPath: null,
|
||||
frontendStatuses: [400],
|
||||
requireDoctype: false,
|
||||
},
|
||||
// Postgres demo requires DATABASE_URL — skip when not available
|
||||
...(process.env.DATABASE_URL
|
||||
? [
|
||||
{
|
||||
name: "demos/postgres",
|
||||
dir: resolve(WORKSPACE_ROOT, "demos/postgres"),
|
||||
port: 4605,
|
||||
mode: "runtime" as const,
|
||||
startupTimeoutMs: 90_000,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: "demos/plugins-demo",
|
||||
dir: resolve(WORKSPACE_ROOT, "demos/plugins-demo"),
|
||||
port: 4606,
|
||||
mode: "runtime",
|
||||
startupTimeoutMs: 90_000,
|
||||
},
|
||||
|
||||
// Templates
|
||||
{
|
||||
name: "templates/blank",
|
||||
dir: resolve(WORKSPACE_ROOT, "templates/blank"),
|
||||
port: 4611,
|
||||
mode: "runtime",
|
||||
startupTimeoutMs: 60_000,
|
||||
},
|
||||
{
|
||||
name: "templates/blog",
|
||||
dir: resolve(WORKSPACE_ROOT, "templates/blog"),
|
||||
port: 4612,
|
||||
mode: "runtime",
|
||||
startupTimeoutMs: 60_000,
|
||||
},
|
||||
{
|
||||
name: "templates/blog-cloudflare",
|
||||
dir: resolve(WORKSPACE_ROOT, "templates/blog-cloudflare"),
|
||||
port: 4613,
|
||||
mode: "runtime",
|
||||
startupTimeoutMs: 120_000,
|
||||
},
|
||||
{
|
||||
name: "templates/marketing",
|
||||
dir: resolve(WORKSPACE_ROOT, "templates/marketing"),
|
||||
port: 4614,
|
||||
mode: "runtime",
|
||||
startupTimeoutMs: 90_000,
|
||||
},
|
||||
{
|
||||
name: "templates/marketing-cloudflare",
|
||||
dir: resolve(WORKSPACE_ROOT, "templates/marketing-cloudflare"),
|
||||
port: 4615,
|
||||
mode: "runtime",
|
||||
startupTimeoutMs: 120_000,
|
||||
},
|
||||
{
|
||||
name: "templates/portfolio",
|
||||
dir: resolve(WORKSPACE_ROOT, "templates/portfolio"),
|
||||
port: 4616,
|
||||
mode: "runtime",
|
||||
startupTimeoutMs: 90_000,
|
||||
},
|
||||
{
|
||||
name: "templates/portfolio-cloudflare",
|
||||
dir: resolve(WORKSPACE_ROOT, "templates/portfolio-cloudflare"),
|
||||
port: 4617,
|
||||
mode: "runtime",
|
||||
startupTimeoutMs: 120_000,
|
||||
},
|
||||
];
|
||||
@@ -181,18 +120,26 @@ async function fetchWithRetry(url: string, retries = 10, delayMs = 1500): Promis
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Build verification — runs a single recursive `pnpm build` across all demos
|
||||
// and templates in parallel, then verifies each site produced output.
|
||||
// Build verification — runs a single recursive `pnpm build` across templates
|
||||
// and the playground demo in parallel.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("Site build verification", () => {
|
||||
it("all demos and templates build successfully", { timeout: 300_000 }, async () => {
|
||||
it("all templates and playground build successfully", { timeout: 300_000 }, async () => {
|
||||
await ensureBuilt();
|
||||
|
||||
try {
|
||||
await execAsync(
|
||||
"pnpm",
|
||||
["run", "--recursive", "--filter", "{./demos/*}", "--filter", "{./templates/*}", "build"],
|
||||
[
|
||||
"run",
|
||||
"--recursive",
|
||||
"--filter",
|
||||
"{./templates/*}",
|
||||
"--filter",
|
||||
"@emdash-cms/playground",
|
||||
"build",
|
||||
],
|
||||
{
|
||||
cwd: WORKSPACE_ROOT,
|
||||
timeout: 240_000,
|
||||
@@ -221,16 +168,6 @@ describe("Site build verification", () => {
|
||||
|
||||
describe.sequential("Site runtime verification", () => {
|
||||
for (const site of SITE_MATRIX) {
|
||||
if (site.mode === "typecheck") {
|
||||
it(`${site.name} typechecks`, { timeout: 120_000 }, async () => {
|
||||
await execAsync("pnpm", ["run", "typecheck"], {
|
||||
cwd: site.dir,
|
||||
timeout: 120_000,
|
||||
});
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const waitPath = site.waitPath ?? "/_emdash/admin/";
|
||||
const setupPath = site.setupPath ?? "/_emdash/api/setup/dev-bypass?redirect=/";
|
||||
const frontendPath = site.frontendPath ?? "/";
|
||||
|
||||
Reference in New Issue
Block a user