Skip to content

Commit 9966026

Browse files
authored
fix(smoke-test): harden release test against Cloudflare propagation race (#1141)
## Summary - The production release smoke test has been flaking on `main` — two consecutive Release workflow failures today ([24390750381](https://github.com/redwoodjs/sdk/actions/runs/24390750381), [24389368095](https://github.com/redwoodjs/sdk/actions/runs/24389368095)) — with the same pair of errors: - `Smoke test status element not found in the page` - `The __rw API or upgradeToRealtime method is not available` - Root cause: a freshly deployed `*.workers.dev` subdomain can return HTTP 200 with Cloudflare's *"There is nothing here yet"* placeholder for a window after `wrangler deploy` completes, before the worker code is globally propagated. The existing `checkServerUp` treats any 2xx as "up", so Puppeteer loads the placeholder and both errors follow — neither the app HTML (no `[data-testid="health-status"]`) nor the client bootstrap (no `window.__rw`) are present. The failing local reproduction's screenshot confirmed it: Cloudflare's "There is nothing here yet. If you expect something to be here, it may take some time. Please check back again later." page. ## Changes 1. **`release.mts`** — after the reachability check, poll the deployed URL until the response body contains `__RWSDK_CONTEXT` (emitted on every rwsdk-rendered page). The placeholder lacks this marker, so the poll only clears once the real worker response is being served. 60s timeout, 2s interval. 2. **`codeUpdates.mts`** — rename the smoke test's `Document.tsx` references to `document.tsx`. The starter was renamed in `20081aad` (2026-01-26) but the smoke test wasn't updated; on case-sensitive Linux CI this was silently swallowed and surfaced as `URL Styles / Client Module Styles: DID NOT RUN` warnings. Picked up from the existing fix on branch `pp-implement-1131-synced-state-reconnect`. ## Test plan - [x] Local smoke test reproduced the original failure (`Smoke test status element not found in the page`). - [x] Same smoke test with this fix passes end-to-end, dev + production (`Smoke tests passed for 'starter' with 'pnpm'`). - [x] SDK build passes (`pnpm --filter rwsdk build`). - [ ] Release workflow rerun on `main` after merge to confirm CI passes consistently. Note: in local testing, propagation cleared on the first poll attempt (`Deployment content ready ... (attempt 1)`), so the retry path itself wasn't exercised end-to-end. The logic is small (fetch → substring check → sleep → retry) and low-risk, but we'll want to watch the next few release runs for any edge cases. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2 parents bc6a058 + 6548f80 commit 9966026

3 files changed

Lines changed: 68 additions & 7 deletions

File tree

sdk/src/lib/e2e/testHarness.mts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,11 +395,16 @@ export function createDeployment() {
395395
resourceUniqueKey,
396396
);
397397

398+
// A fresh *.workers.dev subdomain can return 200 with Cloudflare's
399+
// "There is nothing here yet" placeholder before the worker code
400+
// propagates globally. Wait until the response body contains the
401+
// rwsdk-rendered marker so tests don't run against the placeholder.
398402
await poll(
399403
async () => {
400404
try {
401405
const response = await fetch(deployResult.url);
402-
return response.status > 0;
406+
const body = await response.text();
407+
return body.includes("__RWSDK_CONTEXT");
403408
} catch (e) {
404409
return false;
405410
}

sdk/src/lib/smokeTests/codeUpdates.mts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,9 @@ export async function createSmokeTestStylesheets(targetDir: string) {
9494
log("Creating smokeTestUrlStyles.css at: %s", urlStylesPath);
9595
await fs.writeFile(urlStylesPath, smokeTestUrlStylesCssTemplate);
9696

97-
// Modify Document.tsx to include the URL stylesheet using CSS URL import
98-
const documentPath = join(appDir, "Document.tsx");
99-
log("Modifying Document.tsx to include URL stylesheet at: %s", documentPath);
97+
// Modify document.tsx to include the URL stylesheet using CSS URL import
98+
const documentPath = join(appDir, "document.tsx");
99+
log("Modifying document.tsx to include URL stylesheet at: %s", documentPath);
100100
try {
101101
const documentContent = await fs.readFile(documentPath, "utf-8");
102102
const s = new MagicString(documentContent);
@@ -118,12 +118,12 @@ export async function createSmokeTestStylesheets(targetDir: string) {
118118
' <link rel="stylesheet" href={smokeTestUrlStyles} />\n',
119119
);
120120
await fs.writeFile(documentPath, s.toString(), "utf-8");
121-
log("Successfully modified Document.tsx with CSS URL import pattern");
121+
log("Successfully modified document.tsx with CSS URL import pattern");
122122
} else {
123-
log("Could not find </head> tag in Document.tsx");
123+
log("Could not find </head> tag in document.tsx");
124124
}
125125
} catch (e) {
126-
log("Could not modify Document.tsx: %s", e);
126+
log("Could not modify document.tsx: %s", e);
127127
}
128128

129129
log("Smoke test stylesheets created successfully");

sdk/src/lib/smokeTests/release.mts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,55 @@ export async function runRelease(
3030
return runE2ERelease(cwd, projectDir, resourceUniqueKey);
3131
}
3232

33+
async function waitForDeploymentContent(
34+
baseUrl: string,
35+
{
36+
timeoutMs = 60_000,
37+
intervalMs = 2_000,
38+
}: { timeoutMs?: number; intervalMs?: number } = {},
39+
): Promise<void> {
40+
const marker = "__RWSDK_CONTEXT";
41+
const deadline = Date.now() + timeoutMs;
42+
let attempt = 0;
43+
let lastStatus: number | undefined;
44+
let lastBytes = 0;
45+
while (Date.now() < deadline) {
46+
attempt += 1;
47+
try {
48+
const res = await fetch(baseUrl);
49+
const body = await res.text();
50+
lastStatus = res.status;
51+
lastBytes = body.length;
52+
if (body.includes(marker)) {
53+
log(
54+
"Deployment content verified at %s after %d attempt(s)",
55+
baseUrl,
56+
attempt,
57+
);
58+
console.log(
59+
`✅ Deployment content ready at ${baseUrl} (attempt ${attempt})`,
60+
);
61+
return;
62+
}
63+
log(
64+
"Attempt %d: %s returned %d (%d bytes), no app marker yet",
65+
attempt,
66+
baseUrl,
67+
res.status,
68+
body.length,
69+
);
70+
} catch (err) {
71+
log("Attempt %d: fetch failed for %s: %O", attempt, baseUrl, err);
72+
}
73+
await setTimeout(intervalMs);
74+
}
75+
throw new Error(
76+
`Deployment at ${baseUrl} did not serve app content within ${timeoutMs}ms ` +
77+
`(last status ${lastStatus ?? "n/a"}, ${lastBytes} bytes). ` +
78+
`Likely Cloudflare *.workers.dev propagation still in progress.`,
79+
);
80+
}
81+
3382
/**
3483
* Runs tests against the production deployment
3584
*/
@@ -63,6 +112,13 @@ export async function runReleaseTest(
63112
// DRY: check both root and custom path
64113
await checkServerUp(url, "/");
65114

115+
// A fresh *.workers.dev subdomain can return 200 with Cloudflare's
116+
// "There is nothing here yet" placeholder before the worker code is
117+
// globally propagated. Poll the URL until the response body contains
118+
// an rwsdk-rendered marker so we don't run the browser tests against
119+
// the placeholder.
120+
await waitForDeploymentContent(url);
121+
66122
// Now run the tests with the custom path
67123
const testUrl = new URL("/__smoke_test", url).toString();
68124
await checkUrl(

0 commit comments

Comments
 (0)