g1t/apps/web/app/lib/secrets.server.ts
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.
| Secrets and variables: one list, rows per environment, for workflows and deployments | 1 | import { redirect } from "react-router"; |
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 2 | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 3 | import type { Setting, SettingKind, SettingReader, SettingsOwner, User } from "@g1t/contracts"; |
| 4 | ||
| Projects: what a workspace builds and runs, first on every page | 5 | import { actions, projects } from "./services.server"; |
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 6 | |
| 7 | export type SecretsData = { | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 8 | rows: Setting[]; |
| Projects: what a workspace builds and runs, first on every page | 9 | /** For a workspace: its projects' slugs, to link rows to. */ |
| 10 | projects: string[]; | |
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 11 | error: string | null; |
| 12 | }; | |
| 13 | ||
| Secrets and variables: one list, rows per environment, for workflows and deployments | 14 | /** A repository's or a workspace's secrets and variables, as one list. */ |
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 15 | export async function loadSecrets(owner: SettingsOwner, actor: User): Promise<SecretsData> { |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 16 | const [rows, list] = await Promise.all([ |
| 17 | actions.settings(actor, owner, "all"), | |
| Projects: what a workspace builds and runs, first on every page | 18 | "workspace" in owner ? projects.list(owner.workspace, actor) : Promise.resolve(null), |
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 19 | ]); |
| 20 | return { | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 21 | rows: rows.ok ? rows.value : [], |
| Projects: what a workspace builds and runs, first on every page | 22 | projects: list?.ok ? list.value.map((project) => project.slug) : [], |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 23 | error: rows.ok ? null : rows.error.message, |
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 24 | }; |
| 25 | } | |
| 26 | ||
| Secrets and variables: one list, rows per environment, for workflows and deployments | 27 | export type SecretsAction = { error?: string }; |
| 28 | ||
| 29 | /** `KEY=value` lines, as a .env file has them; quotes around a value are dropped. */ | |
| 30 | function parseDotenv(text: string): [string, string][] { | |
| 31 | const pairs: [string, string][] = []; | |
| 32 | for (const raw of text.split(/\r?\n/)) { | |
| 33 | const line = raw.replace(/^\s*export\s+/, "").trim(); | |
| 34 | if (!line || line.startsWith("#")) continue; | |
| 35 | const at = line.indexOf("="); | |
| 36 | if (at <= 0) continue; | |
| 37 | let value = line.slice(at + 1).trim(); | |
| 38 | if (/^(["']).*\1$/.test(value)) value = value.slice(1, -1); | |
| 39 | pairs.push([line.slice(0, at).trim(), value]); | |
| 40 | } | |
| 41 | return pairs; | |
| 42 | } | |
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 43 | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 44 | /** |
| 45 | * Saves the side panel's form (one row, or many pasted as a .env file), or | |
| 46 | * removes a row, then returns to the list. | |
| 47 | */ | |
| 48 | export async function actOnSecrets(owner: SettingsOwner, actor: User, form: FormData, page: string): Promise<SecretsAction> { | |
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 49 | const intent = String(form.get("intent") ?? ""); |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 50 | const id = String(form.get("id") ?? "") || undefined; |
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 51 | if (intent === "delete") { |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 52 | const removed = await actions.deleteSetting(actor, owner, "all", String(form.get("name") ?? ""), id); |
| 53 | if (!removed.ok) return { error: removed.error.message }; | |
| 54 | throw redirect(page); | |
| 55 | } | |
| 56 | const kind: SettingKind = form.get("type") === "config" ? "variable" : "secret"; | |
| 57 | const environments = | |
| 58 | form.get("scope") === "some" | |
| 59 | ? [ | |
| 60 | ...form.getAll("env").map(String), | |
| 61 | ...String(form.get("envCustom") ?? "") | |
| 62 | .split(",") | |
| 63 | .map((name) => name.trim()) | |
| 64 | .filter(Boolean), | |
| 65 | ] | |
| 66 | : []; | |
| 67 | if (form.get("scope") === "some" && environments.length === 0) { | |
| 68 | return { error: "Choose at least one environment, or All environments." }; | |
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 69 | } |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 70 | const availableTo = form.getAll("availableTo").map(String) as SettingReader[]; |
| 71 | if (availableTo.length === 0) return { error: "Choose who reads it: Workflows, Deployments, or both." }; | |
| Projects: what a workspace builds and runs, first on every page | 72 | const linked = |
| 73 | "workspace" in owner && form.get("reach") === "some" ? form.getAll("project").map(String) : []; | |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 74 | const note = String(form.get("note") ?? ""); |
| 75 | const key = String(form.get("key") ?? "").trim(); | |
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 76 | const value = String(form.get("value") ?? ""); |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 77 | // A pasted .env file adds a row for each line. |
| 78 | const pasted = !id && key.includes("=") ? parseDotenv(key) : []; | |
| 79 | const entries: [string, string | null][] = pasted.length > 0 ? pasted : [[key, value === "" && id ? null : value]]; | |
| 80 | for (const [name, entryValue] of entries) { | |
| 81 | if (!name) return { error: "Give it a key." }; | |
| 82 | if (entryValue === "" && !id) return { error: `Give ${name} a value.` }; | |
| 83 | const saved = await actions.setSetting(actor, owner, kind, name, entryValue, { | |
| 84 | id, | |
| 85 | availableTo, | |
| 86 | environments, | |
| Projects: what a workspace builds and runs, first on every page | 87 | projects: "workspace" in owner ? linked : undefined, |
| Secrets and variables: one list, rows per environment, for workflows and deployments | 88 | note, |
| 89 | }); | |
| 90 | if (!saved.ok) return { error: pasted.length > 0 ? `${name}: ${saved.error.message}` : saved.error.message }; | |
| 91 | } | |
| 92 | throw redirect(page); | |
| GitHub Actions on g1t, part three: .g1t/workflows, the pages, the docs | 93 | } |