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.
| Deploying: a deploy by hand shows on the repository's Deployments page | 1 | // Tells g1t about a deploy run by hand, so the repository's Deployments |
| 2 | // page and its production card say what runs. In g1t Actions the job's | |
| 3 | // `environment:` does this already, so nothing is sent from there. | |
| 4 | // | |
| 5 | // Needs a g1t token with deployments:write: G1T_DEPLOY_TOKEN, or the file | |
| 6 | // .credentials/g1t-deploy-token. Without one, nothing is sent. Reporting | |
| 7 | // never fails a deploy: a problem is one line in the log. | |
| 8 | ||
| 9 | import { execFileSync } from "node:child_process"; | |
| 10 | import { existsSync, readFileSync } from "node:fs"; | |
| 11 | import { join } from "node:path"; | |
| 12 | ||
| 13 | import { ROOT } from "./stack.mjs"; | |
| 14 | ||
| 15 | /** Where production answers, as the Deployments page links it. */ | |
| 16 | const PRODUCTION_URL = process.env.G1T_DEPLOY_URL || "https://g1t.sh"; | |
| 17 | ||
| 18 | /** The token, or null when there is none to use. */ | |
| 19 | export function deployToken(env = process.env, root = ROOT) { | |
| 20 | if (env.G1T_DEPLOY_TOKEN) return env.G1T_DEPLOY_TOKEN.trim(); | |
| 21 | const file = join(root, ".credentials", "g1t-deploy-token"); | |
| 22 | return existsSync(file) ? readFileSync(file, "utf8").trim() || null : null; | |
| 23 | } | |
| 24 | ||
| 25 | /** Whether this runs inside a workflow, which reports for itself. */ | |
| 26 | export function inWorkflow(env = process.env) { | |
| 27 | return env.GITHUB_ACTIONS === "true" || env.G1T_ACTIONS === "true"; | |
| 28 | } | |
| 29 | ||
| 30 | /** `{ api, repo }` from a remote on g1t (e.g. https://g1t.sh/flagon-io/g1t.git), or null. */ | |
| 31 | export function g1tRemote(urls) { | |
| 32 | for (const url of urls) { | |
| 33 | const match = /^https:\/\/(g1t\.[a-z.]+)\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/i.exec(url.trim()); | |
| 34 | if (match) return { api: `https://api.${match[1]}`, repo: `${match[2]}/${match[3]}` }; | |
| 35 | } | |
| 36 | return null; | |
| 37 | } | |
| 38 | ||
| 39 | function remoteUrls() { | |
| 40 | try { | |
| 41 | const names = execFileSync("git", ["remote"], { cwd: ROOT, encoding: "utf8" }).split("\n").filter(Boolean); | |
| 42 | return names.map((name) => execFileSync("git", ["remote", "get-url", name], { cwd: ROOT, encoding: "utf8" }).trim()); | |
| 43 | } catch { | |
| 44 | return []; | |
| 45 | } | |
| 46 | } | |
| 47 | ||
| 48 | /** | |
| 49 | * Starts a production deployment for `head` and returns a function that | |
| 50 | * settles it (true: success), or null when nothing is reported. | |
| 51 | */ | |
| 52 | export async function reportDeployment({ head, subject, units, log, fetchImpl = fetch, env = process.env }) { | |
| 53 | if (inWorkflow(env)) return null; | |
| 54 | const token = deployToken(env); | |
| 55 | const remote = g1tRemote(remoteUrls()); | |
| 56 | if (!token || !remote) return null; | |
| 57 | const headers = { authorization: `Bearer ${token}`, "content-type": "application/json", "user-agent": "g1t-deploy" }; | |
| 58 | const base = `${remote.api}/repos/${remote.repo}/deployments`; | |
| 59 | try { | |
| 60 | const made = await fetchImpl(base, { | |
| 61 | method: "POST", | |
| 62 | headers, | |
| 63 | body: JSON.stringify({ | |
| 64 | ref: head, | |
| 65 | sha: head, | |
| 66 | environment: "production", | |
| 67 | production_environment: true, | |
| 68 | description: `${subject} (${units.join(", ")}), deployed by hand`, | |
| 69 | }), | |
| 70 | }); | |
| 71 | if (!made.ok) { | |
| 72 | log(`(the deployment was not reported to g1t: ${made.status})`); | |
| 73 | return null; | |
| 74 | } | |
| 75 | const { id } = await made.json(); | |
| 76 | const status = async (state) => { | |
| 77 | const sent = await fetchImpl(`${base}/${id}/statuses`, { | |
| 78 | method: "POST", | |
| 79 | headers, | |
| 80 | body: JSON.stringify({ state, environment_url: PRODUCTION_URL, description: state === "in_progress" ? "Deploying" : undefined }), | |
| 81 | }); | |
| 82 | if (!sent.ok) log(`(the deployment's ${state} was not reported to g1t: ${sent.status})`); | |
| 83 | }; | |
| 84 | await status("in_progress"); | |
| 85 | log(`Reported to g1t as deployment ${id} of ${remote.repo}.`); | |
| 86 | return async (ok) => { | |
| 87 | try { | |
| 88 | await status(ok ? "success" : "failure"); | |
| 89 | } catch (error) { | |
| 90 | log(`(the deployment's result was not reported to g1t: ${error.message ?? error})`); | |
| 91 | } | |
| 92 | }; | |
| 93 | } catch (error) { | |
| 94 | log(`(the deployment was not reported to g1t: ${error.message ?? error})`); | |
| 95 | return null; | |
| 96 | } | |
| 97 | } |