revenge-next-plugs/.github/scripts/verify-deploy.mjs

68 lines
3.1 KiB
JavaScript

// Post-deploy smoke test: confirms the live repository index is valid and that
// one published plugin's artifact actually matches the sha256 the index claims for it.
// Run with: bun .github/scripts/verify-deploy.mjs
const BASE_URL = process.env.VERIFY_BASE_URL ?? "https://next.jarviscli.dev";
// 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, html = 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}`);
}
if (binary) return await res.arrayBuffer();
if (html) return await res.text();
return 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");
const [id, plugin] = Object.entries(idx.plugins)[0];
const version = plugin.channels.latest;
const v = plugin.versions[version];
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) {
throw new Error(`sha256 mismatch for ${id}: expected ${v.sha256} got ${hash}`);
}
console.log(`Verified ${id}@${version}: sha256 OK`);
// New checks added for the next.jarviscli.dev site (browse/detail/admin pages).
const detailHtml = await fetchWithRetry(`${BASE_URL}/plugins/${id}`, { html: true });
if (!detailHtml.includes(`property="og:title" content="${plugin.name}"`)) {
throw new Error(`Plugin detail page for ${id} is missing correct og:title for "${plugin.name}"`);
}
console.log(`Verified ${id}: og:title present and correct`);
const unauthedRes = await fetch(`${BASE_URL}/api/admin/overrides/${id}`, {
method: "PUT",
headers: { "content-type": "application/json" },
body: "{}",
});
if (unauthedRes.status !== 401) {
throw new Error(`Unauthenticated admin write should return 401, got ${unauthedRes.status}`);
}
console.log("Verified: unauthenticated admin write correctly rejected (401)");