From 74cc0e61a4ca659d18f24d9ffca2626338d6041b Mon Sep 17 00:00:00 2001 From: Ally Founder Date: Sat, 8 Aug 2026 20:59:13 -0400 Subject: [PATCH] verify-deploy: retry with backoff and check res.ok The previous deploy (staff-tags 1.0.5) failed CI at this exact step with a raw "Failed to parse JSON" error, even though the deploy itself had succeeded seconds earlier (confirmed live) - Cloudflare's edge needs a moment to propagate a fresh deploy, and the script had no res.ok check to turn a transient non-OK response into a clear message, let alone retry it. --- .github/scripts/verify-deploy.mjs | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/.github/scripts/verify-deploy.mjs b/.github/scripts/verify-deploy.mjs index ac1efaa..5edef63 100644 --- a/.github/scripts/verify-deploy.mjs +++ b/.github/scripts/verify-deploy.mjs @@ -4,7 +4,33 @@ const BASE_URL = process.env.VERIFY_BASE_URL ?? "https://next.jarviscli.dev"; -const idx = await (await fetch(`${BASE_URL}/index.json`)).json(); +// Cloudflare's edge can take a few seconds to propagate a fresh deploy - the very first run of +// this script (2026-08-09, staff-tags 1.0.5) failed with a raw "Failed to parse JSON" error from +// a non-OK response fetched immediately after `wrangler deploy` returned, even though the deploy +// itself had succeeded (confirmed live seconds later). Retrying with a short backoff instead of +// failing on the first transient response avoids that false negative, and checking `res.ok` +// turns any real failure into a clear status-code message instead of an opaque parse error. +async function fetchWithRetry(url, { attempts = 5, delayMs = 3000, binary = false } = {}) { + let lastError; + for (let attempt = 1; attempt <= attempts; attempt++) { + try { + const res = await fetch(url); + if (!res.ok) { + throw new Error(`${url} responded ${res.status} ${res.statusText}`); + } + return binary ? await res.arrayBuffer() : await res.json(); + } catch (e) { + lastError = e; + if (attempt < attempts) { + console.log(`Attempt ${attempt}/${attempts} for ${url} failed (${e.message}), retrying in ${delayMs}ms...`); + await new Promise(r => setTimeout(r, delayMs)); + } + } + } + throw new Error(`${url} failed after ${attempts} attempts: ${lastError.message}`); +} + +const idx = await fetchWithRetry(`${BASE_URL}/index.json`); const ids = Object.keys(idx.plugins ?? {}); if (!ids.length) throw new Error("index.json has no plugins"); @@ -12,7 +38,7 @@ const [id, plugin] = Object.entries(idx.plugins)[0]; const version = plugin.channels.latest; const v = plugin.versions[version]; -const buf = await (await fetch(v.url)).arrayBuffer(); +const buf = await fetchWithRetry(v.url, { binary: true }); const hash = Buffer.from(await crypto.subtle.digest("SHA-256", buf)).toString("hex"); if (hash !== v.sha256) {