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.
This commit is contained in:
Peter Steinberger
2026-07-17 21:43:02 +01:00
committed by GitHub
parent 0c04d6a9d5
commit f3ece75ee4
3 changed files with 22 additions and 13 deletions
+10 -10
View File
@@ -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);
+8 -3
View File
@@ -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}...`,
);
+4
View File
@@ -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: