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:
Matt Kane
2026-04-07 23:00:54 +01:00
committed by GitHub
parent f112ac4819
commit d6cfc437f2
6 changed files with 25 additions and 128 deletions

View File

@@ -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);

View File

@@ -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 ?? "/";