From f3ece75ee4d1e17550f029e3701e2a1ad2e31583 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 17 Jul 2026 21:43:02 +0100 Subject: [PATCH] ci: namespace Vercel convex preview names per builder (#3161) A second preview deploy-key consumer runs --preview-create on raw branch names; Convex replaces same-name previews by delete-and-create, so it was deleting Vercel's fresh deployments mid-push (get_config_hashes and wait_for_schema 404s on every PR preview today; confirmed via the Convex team audit log create/delete pairs seconds apart). Suffix all Vercel-built preview names with -vercel so no other consumer can collide with them. --- scripts/vercel-build.test.ts | 20 ++++++++++---------- scripts/vercel-build.ts | 11 ++++++++--- specs/ci.md | 4 ++++ 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/scripts/vercel-build.test.ts b/scripts/vercel-build.test.ts index 4664c771..2c7b8b13 100644 --- a/scripts/vercel-build.test.ts +++ b/scripts/vercel-build.test.ts @@ -107,13 +107,13 @@ describe("Vercel build plan", () => { expect(spawn).toHaveBeenCalledTimes(3); expect(spawn.mock.calls.map(([command]) => command)).toEqual(["bunx", "bunx", "bun"]); - expect(spawn.mock.calls[0]?.[1]).toContain("pe/claw-413-pr-previews"); - expect(spawn.mock.calls[1]?.[1]).toContain("pe/claw-413-pr-previews-retry-2"); - expect(spawn.mock.calls[2]?.[1]).toContain("pe/claw-413-pr-previews-retry-2"); + expect(spawn.mock.calls[0]?.[1]).toContain("pe/claw-413-pr-previews-vercel"); + expect(spawn.mock.calls[1]?.[1]).toContain("pe/claw-413-pr-previews-vercel-retry-2"); + expect(spawn.mock.calls[2]?.[1]).toContain("pe/claw-413-pr-previews-vercel-retry-2"); expect(sleep).toHaveBeenCalledOnce(); expect(sleep).toHaveBeenCalledWith(20_000); expect(log).toHaveBeenCalledWith( - "[vercel-build] convex preview pipeline failed (attempt 1/3); retrying in 20s with preview name pe/claw-413-pr-previews-retry-2...", + "[vercel-build] convex preview pipeline failed (attempt 1/3); retrying in 20s with preview name pe/claw-413-pr-previews-vercel-retry-2...", ); }); @@ -130,10 +130,10 @@ describe("Vercel build plan", () => { expect(spawn).toHaveBeenCalledTimes(4); expect(spawn.mock.calls.map(([command]) => command)).toEqual(["bunx", "bun", "bunx", "bun"]); - expect(spawn.mock.calls[0]?.[1]).toContain("pe/claw-413-pr-previews"); - expect(spawn.mock.calls[1]?.[1]).toContain("pe/claw-413-pr-previews"); - expect(spawn.mock.calls[2]?.[1]).toContain("pe/claw-413-pr-previews-retry-2"); - expect(spawn.mock.calls[3]?.[1]).toContain("pe/claw-413-pr-previews-retry-2"); + expect(spawn.mock.calls[0]?.[1]).toContain("pe/claw-413-pr-previews-vercel"); + expect(spawn.mock.calls[1]?.[1]).toContain("pe/claw-413-pr-previews-vercel"); + expect(spawn.mock.calls[2]?.[1]).toContain("pe/claw-413-pr-previews-vercel-retry-2"); + expect(spawn.mock.calls[3]?.[1]).toContain("pe/claw-413-pr-previews-vercel-retry-2"); expect(sleep).toHaveBeenCalledOnce(); expect(sleep).toHaveBeenCalledWith(20_000); }); @@ -153,8 +153,8 @@ describe("Vercel build plan", () => { await expect(main({ env: previewEnv, spawn, sleep })).resolves.toBe(1); expect(spawn).toHaveBeenCalledTimes(6); - expect(spawn.mock.calls[4]?.[1]).toContain("pe/claw-413-pr-previews-retry-3"); - expect(spawn.mock.calls[5]?.[1]).toContain("pe/claw-413-pr-previews-retry-3"); + expect(spawn.mock.calls[4]?.[1]).toContain("pe/claw-413-pr-previews-vercel-retry-3"); + expect(spawn.mock.calls[5]?.[1]).toContain("pe/claw-413-pr-previews-vercel-retry-3"); expect(sleep).toHaveBeenCalledTimes(2); expect(sleep).toHaveBeenNthCalledWith(1, 20_000); expect(sleep).toHaveBeenNthCalledWith(2, 40_000); diff --git a/scripts/vercel-build.ts b/scripts/vercel-build.ts index bb47f518..731573de 100644 --- a/scripts/vercel-build.ts +++ b/scripts/vercel-build.ts @@ -107,10 +107,15 @@ export async function main({ const branchName = env.VERCEL_GIT_COMMIT_REF?.trim(); if (!branchName) throw new Error("Preview builds require VERCEL_GIT_COMMIT_REF"); + // Namespace preview names per builder: a second deploy-key consumer running + // --preview-create on the raw branch name replaces (deletes) our deployment + // mid-push, which surfaced as get_config_hashes/wait_for_schema 404s. + const baseName = `${branchName}-vercel`; + const maxAttempts = 3; for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { - const previewName = attempt === 1 ? branchName : `${branchName}-retry-${attempt}`; - const plan = attempt === 1 ? initialPlan : resolveVercelBuildPlan(env, previewName); + const previewName = attempt === 1 ? baseName : `${baseName}-retry-${attempt}`; + const plan = resolveVercelBuildPlan(env, previewName); const failure = runBuildPlan(plan, spawn); if (!failure) return 0; @@ -120,7 +125,7 @@ export async function main({ } const delayMs = attempt * 20_000; - const nextPreviewName = `${branchName}-retry-${attempt + 1}`; + const nextPreviewName = `${baseName}-retry-${attempt + 1}`; console.error( `[vercel-build] convex preview pipeline failed (attempt ${attempt}/${maxAttempts}); retrying in ${delayMs / 1_000}s with preview name ${nextPreviewName}...`, ); diff --git a/specs/ci.md b/specs/ci.md index 55c8e51f..7d00c01a 100644 --- a/specs/ci.md +++ b/specs/ci.md @@ -40,6 +40,10 @@ chromium Playwright browser has already been installed. Vercel preview builds retry the full Convex deploy-and-seed pipeline up to three times, waiting 20 seconds and then 40 seconds. Retries use fresh suffixed preview names because duplicate names can resolve to an earlier, incomplete deployment. +All Vercel-created preview names carry a `-vercel` suffix: `--preview-create` +replaces (deletes) any same-name deployment, so a second deploy-key consumer +using raw branch names would otherwise delete Vercel's deployment mid-push +(observed 2026-07-17 as get_config_hashes/wait_for_schema 404s on every PR). To reproduce the local-auth browser gate locally, install the chromium Playwright browser once and run: