Skip to content

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.sh
93 linesCodeBlameRaw

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.

The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor1import { redirect } from "react-router";
2
3import type { Setting, SettingKind, SettingReader, SettingsOwner, User } from "@g1t/contracts";
4
5import { actions, projects } from "./services.server";
6
7export type SecretsData = {
8 rows: Setting[];
9 /** For a workspace: its projects' slugs, to link rows to. */
10 projects: string[];
11 error: string | null;
12};
13
14/** A repository's or a workspace's secrets and variables, as one list. */
15export async function loadSecrets(owner: SettingsOwner, actor: User): Promise<SecretsData> {
16 const [rows, list] = await Promise.all([
17 actions.settings(actor, owner, "all"),
18 "workspace" in owner ? projects.list(owner.workspace, actor) : Promise.resolve(null),
19 ]);
20 return {
21 rows: rows.ok ? rows.value : [],
22 projects: list?.ok ? list.value.map((project) => project.slug) : [],
23 error: rows.ok ? null : rows.error.message,
24 };
25}
26
27export type SecretsAction = { error?: string };
28
29/** `KEY=value` lines, as a .env file has them; quotes around a value are dropped. */
30function 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}
43
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 */
48export async function actOnSecrets(owner: SettingsOwner, actor: User, form: FormData, page: string): Promise<SecretsAction> {
49 const intent = String(form.get("intent") ?? "");
50 const id = String(form.get("id") ?? "") || undefined;
51 if (intent === "delete") {
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." };
69 }
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." };
72 const linked =
73 "workspace" in owner && form.get("reach") === "some" ? form.getAll("project").map(String) : [];
74 const note = String(form.get("note") ?? "");
75 const key = String(form.get("key") ?? "").trim();
76 const value = String(form.get("value") ?? "");
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,
87 projects: "workspace" in owner ? linked : undefined,
88 note,
89 });
90 if (!saved.ok) return { error: pasted.length > 0 ? `${name}: ${saved.error.message}` : saved.error.message };
91 }
92 throw redirect(page);
93}

This file's history is long; its oldest lines are credited to the oldest commit read.