Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 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 | // | |
| Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders | 13 | // scripts/deploy.mjs `deploy()` wraps its stages in `withDeployWindow` |
| 14 | // once there is something to ship (never in a dry run), so every deploy, | |
| 15 | // by hand or in g1t Actions (.g1t/workflows/deploy.yml passes the | |
| 16 | // STATUS_DEPLOY_TOKEN secret), says so. Jobs that run at once (a stage's | |
| 17 | // matrix) each say started and finished; the status Worker counts them and | |
| 18 | // the window closes when the last one finishes. | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 19 | |
| 20 | import { pathToFileURL } from "node:url"; | |
| 21 | ||
| 22 | /** | |
| Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders | 23 | * Runs `work` inside a deploy window: "started" before, "finished" after, |
| 24 | * whether it succeeded or threw. Neither announcement can fail the deploy. | |
| 25 | * | |
| 26 | * @template T | |
| 27 | * @param {() => Promise<T>} work | |
| 28 | * @param {{ id?: string | null, dryRun?: boolean, announce?: typeof announceDeploy, env?: Record<string, string | undefined>, log?: (line: string) => void }} [options] | |
| 29 | * @returns {Promise<T>} | |
| 30 | */ | |
| 31 | export async function withDeployWindow(work, { id = null, dryRun = false, announce = announceDeploy, env = process.env, log } = {}) { | |
| 32 | if (dryRun) return work(); | |
| 33 | const options = { id, env, ...(log ? { log } : {}) }; | |
| 34 | await announce("started", options); | |
| 35 | try { | |
| 36 | return await work(); | |
| 37 | } finally { | |
| 38 | await announce("finished", options); | |
| 39 | } | |
| 40 | } | |
| 41 | ||
| 42 | /** | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 43 | * @param {"started" | "finished"} phase |
| 44 | * @param {{ id?: string | null, env?: Record<string, string | undefined>, fetchImpl?: typeof fetch, log?: (line: string) => void }} [options] | |
| 45 | * @returns {Promise<boolean>} whether status.g1t.sh took it | |
| 46 | */ | |
| 47 | export async function announceDeploy(phase, { id = null, env = process.env, fetchImpl = fetch, log = (line) => console.warn(line) } = {}) { | |
| 48 | const token = (env.STATUS_DEPLOY_TOKEN ?? "").trim(); | |
| 49 | if (!token) return false; | |
| 50 | const base = (env.STATUS_URL || "https://status.g1t.sh").replace(/\/+$/, ""); | |
| 51 | try { | |
| 52 | const response = await fetchImpl(`${base}/deploys`, { | |
| 53 | method: "POST", | |
| 54 | headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }, | |
| 55 | body: JSON.stringify({ phase, ...(id ? { id: String(id).slice(0, 100) } : {}) }), | |
| 56 | signal: AbortSignal.timeout(5000), | |
| 57 | }); | |
| 58 | if (!response.ok) { | |
| 59 | log(`status: the deploy ${phase} was not recorded (${response.status}); detection may draft restarts.`); | |
| 60 | return false; | |
| 61 | } | |
| 62 | return true; | |
| 63 | } catch (error) { | |
| 64 | log(`status: the deploy ${phase} was not recorded (${error instanceof Error ? error.message : String(error)}).`); | |
| 65 | return false; | |
| 66 | } | |
| 67 | } | |
| 68 | ||
| 69 | if (import.meta.url === `file://${process.argv[1]?.replace(/\\/g, "/").replace(/^(?=[A-Za-z]:)/, "/")}`) { | |
| 70 | const [phase, id] = process.argv.slice(2); | |
| 71 | if (phase !== "started" && phase !== "finished") { | |
| 72 | console.error("usage: node scripts/deploy/status-window.mjs started|finished [id]"); | |
| 73 | process.exit(2); | |
| 74 | } | |
| 75 | const ok = await announceDeploy(phase, { id: id ?? null }); | |
| 76 | console.log(ok ? `status: deploy ${phase}` : "status: not recorded (no STATUS_DEPLOY_TOKEN, or status did not answer)"); | |
| 77 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.