g1t/scripts/deploy/status-window.mjs
| 1 | // Tells status.g1t.sh a deploy started or finished, so the restarts it |
| 2 | // causes are counted but not drafted as incidents (apps/status detect.ts: |
| 3 | // during the deploy and 3 minutes after; trouble that outlasts that is |
| 4 | // drafted with its true start). |
| 5 | // |
| 6 | // node scripts/deploy/status-window.mjs started [id] |
| 7 | // node scripts/deploy/status-window.mjs finished [id] |
| 8 | // |
| 9 | // Needs STATUS_DEPLOY_TOKEN (the status Worker's secret of the same name); |
| 10 | // STATUS_URL defaults to https://status.g1t.sh. Never fails a deploy: |
| 11 | // without the token it does nothing, and any error is a warning. |
| 12 | // |
| 13 | // In scripts/deploy.mjs `deploy()`, once there is something to ship and |
| 14 | // before the first stage (not in a dry run): |
| 15 | // |
| 16 | // import { announceDeploy } from "./deploy/status-window.mjs"; |
| 17 | // const id = head ?? null; |
| 18 | // if (!dryRun) await announceDeploy("started", { id }); |
| 19 | // try { ...the stages... } finally { if (!dryRun) await announceDeploy("finished", { id }); } |
| 20 | |
| 21 | import { pathToFileURL } from "node:url"; |
| 22 | |
| 23 | /** |
| 24 | * @param {"started" | "finished"} phase |
| 25 | * @param {{ id?: string | null, env?: Record<string, string | undefined>, fetchImpl?: typeof fetch, log?: (line: string) => void }} [options] |
| 26 | * @returns {Promise<boolean>} whether status.g1t.sh took it |
| 27 | */ |
| 28 | export async function announceDeploy(phase, { id = null, env = process.env, fetchImpl = fetch, log = (line) => console.warn(line) } = {}) { |
| 29 | const token = (env.STATUS_DEPLOY_TOKEN ?? "").trim(); |
| 30 | if (!token) return false; |
| 31 | const base = (env.STATUS_URL || "https://status.g1t.sh").replace(/\/+$/, ""); |
| 32 | try { |
| 33 | const response = await fetchImpl(`${base}/deploys`, { |
| 34 | method: "POST", |
| 35 | headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }, |
| 36 | body: JSON.stringify({ phase, ...(id ? { id: String(id).slice(0, 100) } : {}) }), |
| 37 | signal: AbortSignal.timeout(5000), |
| 38 | }); |
| 39 | if (!response.ok) { |
| 40 | log(`status: the deploy ${phase} was not recorded (${response.status}); detection may draft restarts.`); |
| 41 | return false; |
| 42 | } |
| 43 | return true; |
| 44 | } catch (error) { |
| 45 | log(`status: the deploy ${phase} was not recorded (${error instanceof Error ? error.message : String(error)}).`); |
| 46 | return false; |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | if (import.meta.url === `file://${process.argv[1]?.replace(/\\/g, "/").replace(/^(?=[A-Za-z]:)/, "/")}`) { |
| 51 | const [phase, id] = process.argv.slice(2); |
| 52 | if (phase !== "started" && phase !== "finished") { |
| 53 | console.error("usage: node scripts/deploy/status-window.mjs started|finished [id]"); |
| 54 | process.exit(2); |
| 55 | } |
| 56 | const ok = await announceDeploy(phase, { id: id ?? null }); |
| 57 | console.log(ok ? `status: deploy ${phase}` : "status: not recorded (no STATUS_DEPLOY_TOKEN, or status did not answer)"); |
| 58 | } |