From f63daed520ddda7afd087d10736626341e801364 Mon Sep 17 00:00:00 2001 From: "planetrenox@protonmail.com" Date: Mon, 26 Oct 2026 17:30:39 +0000 Subject: [PATCH] Feat: Format the deploy report as markdown in ntfy Co-Authored-By: Claude Opus 5.5 --- scripts/notify.js | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/scripts/notify.js b/scripts/notify.js index 7c0185f..171b3d9 100644 --- a/scripts/notify.js +++ b/scripts/notify.js @@ -7,33 +7,39 @@ const r = JSON.parse(await readFile("promoted/report.json", "utf8").catch(() => const fmt = n => Math.round(n).toLocaleString("en-US"); const gb = b => `${(b / 1e9).toFixed(2)} GB`; const pct = (a, b) => `${Math.round((100 * a) / b)}%`; +// Queries and errors are user input, so markdown in them is escaped +const esc = s => String(s).replace(/[\\`*_[\]#<>|~]/g, "\\$&"); const lines = [], actions = []; if (env.STATUS !== "success") { - actions.push(`${env.FAILED_STEP || "A step"} failed, so the site still runs the previous deploy. Open the run for the error.`); + actions.push(`**${env.FAILED_STEP || "A step"} failed**, so the site still runs the previous deploy. Open the run for the error.`); } if (r.ok) { - lines.push(`Static images: ${fmt(r.promoted)} / ${fmt(r.maxFiles)} slots, ${gb(r.bytes)} / ${gb(r.maxBytes)}`); - lines.push(`+${fmt(r.added)} promoted, −${fmt(r.removed)} dropped (min ${r.minViews} views in ${r.days} days)`); - if (r.views?.total) lines.push(`They got ${pct(r.promotedViews, r.views.total)} of ${fmt(r.views.total)} image views in ${r.days} days`); - // web/1/orange+cat → orange cat, free/2/orange+cat → orange cat i=2 [free] - const label = id => id.replace(/^(\w+)\/(\d+)\/(.*)$/, (_, f, i, q) => `${q.replaceAll("+", " ")}${i > 1 ? ` i=${i}` : ""}${f !== "web" ? ` [${f}]` : ""}`); - if (r.top?.length) lines.push(`Top: ${r.top.map(t => `${label(t.id)} (${fmt(t.views)})`).join(", ")}`); + lines.push(`**Static images:** ${fmt(r.promoted)} / ${fmt(r.maxFiles)} slots, ${gb(r.bytes)} / ${gb(r.maxBytes)}`); + lines.push(`**Changes:** +${fmt(r.added)} promoted, −${fmt(r.removed)} dropped (min ${r.minViews} views in ${r.days} days)`); + if (r.views?.total) lines.push(`**Coverage:** ${pct(r.promotedViews, r.views.total)} of ${fmt(r.views.total)} image views in ${r.days} days`); if (r.notFitted) actions.push(`All ${fmt(r.maxFiles)} static slots are full and ~${fmt(r.notFitted)} more images qualified. Workers Paid ($5/mo) allows 100,000 files.`); - if (r.skipped.budget) actions.push(`Size budget full: ${fmt(r.skipped.budget)} images left out. Raise MAX_BYTES in deploy.yml (runner disk ~14 GB, Actions cache 10 GB).`); - if (r.skipped.failed) actions.push(`${fmt(r.skipped.failed)} downloads from R2 failed, e.g. ${r.firstError}. They're retried next run.`); - if (r.analyticsError) actions.push(`Couldn't read Cloudflare analytics (${r.analyticsError}), so the last run's images were kept. Check the API token's Analytics permission.`); + if (r.skipped.budget) actions.push(`Size budget full: ${fmt(r.skipped.budget)} images left out. Raise \`MAX_BYTES\` in deploy.yml (runner disk ~14 GB, Actions cache 10 GB).`); + if (r.skipped.failed) actions.push(`${fmt(r.skipped.failed)} downloads from R2 failed, e.g. ${esc(r.firstError)}. They're retried next run.`); + if (r.analyticsError) actions.push(`Couldn't read Cloudflare analytics (${esc(r.analyticsError)}), so the last run's images were kept. Check the API token's Analytics permission.`); } if (r.workers) { const { total, byScript } = r.workers; - const top = Object.entries(byScript).sort((a, b) => b[1] - a[1]).slice(0, 3).map(([s, n]) => `${s} ${fmt(n)}`).join(", "); - lines.push(`Worker requests yesterday: ${fmt(total)} / 100,000 (${pct(total, 1e5)})${top && `: ${top}`}`); - if (total >= 8e4) actions.push(`Worker requests hit ${pct(total, 1e5)} of the free 100,000/day; past it, images that aren't static files break until 00:00 UTC. Lower MIN_VIEWS to promote more, or move to Workers Paid ($5/mo).`); + const top = Object.entries(byScript).sort((a, b) => b[1] - a[1]).slice(0, 3).map(([s, n]) => `${esc(s)} ${fmt(n)}`).join(", "); + lines.push(`**Worker requests yesterday:** ${fmt(total)} / 100,000 (${pct(total, 1e5)})${top && ` · ${top}`}`); + if (total >= 8e4) actions.push(`Worker requests hit ${pct(total, 1e5)} of the free 100,000/day; past it, images that aren't static files break until 00:00 UTC. Lower \`MIN_VIEWS\` to promote more, or move to Workers Paid ($5/mo).`); } -if (r.workersError) lines.push(`Couldn't read Worker usage: ${r.workersError}`); +if (r.workersError) lines.push(`**Worker requests:** couldn't read (${esc(r.workersError)})`); +// web/1/orange+cat → orange cat, free/2/orange+cat → orange cat i=2 (free) +const label = id => esc(id.replace(/^(\w+)\/(\d+)\/(.*)$/, (_, f, i, q) => `${q.replaceAll("+", " ")}${i > 1 ? ` i=${i}` : ""}${f !== "web" ? ` (${f})` : ""}`)); const failed = env.STATUS !== "success"; -const body = [...lines, ...actions.map(a => `⚠ ${a}`)].join("\n") || "No report was written."; +const body = [ + lines.join(" \n"), + r.top?.length && `**Top images**\n${r.top.map((t, n) => `${n + 1}. ${label(t.id)} — ${fmt(t.views)}`).join("\n")}`, + actions.length && `**⚠ Needs attention**\n${actions.map(a => `- ${a}`).join("\n")}`, + env.RUN_URL && `[Open the run](${env.RUN_URL})`, +].filter(Boolean).join("\n\n") || "No report was written."; console.log(body); if (!env.NTFY_URL) process.exit(0); const res = await fetch(env.NTFY_URL.startsWith("http") ? env.NTFY_URL : `https://${env.NTFY_URL}`, { @@ -43,6 +49,7 @@ const res = await fetch(env.NTFY_URL.startsWith("http") ? env.NTFY_URL : `https: Title: failed ? "direct-img deploy failed" : actions.length ? "direct-img deploy needs attention" : `direct-img deployed (${env.TRIGGER || "manual"})`, Priority: failed ? "5" : actions.length ? "4" : "2", Tags: failed ? "rotating_light,x" : actions.length ? "warning" : "white_check_mark", + Markdown: "yes", ...(env.RUN_URL && { Click: env.RUN_URL }), ...(env.NTFY_TOKEN && { Authorization: `Bearer ${env.NTFY_TOKEN}` }), },