flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/scripts/deploy/status-window.mjs

58 lines2,723 bytesCodeBlame
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
21import { 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 */
28export 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
50if (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}