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
107 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.

An issue being worked on no longer offers Bring your own agent beside the pull request doing it1import { 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) : [];
Merge brokered secrets: a secret a job uses only to call an API can be tied to the hosts it is for, and the job gets a stand-in in its place, which g1t fills in on the way out to those hosts and nowhere else, so the value never enters the sandbox; and a request to an allowed host that fails upstream is retried when it can be, or answered with a 502 that says why, rather than reset.74 // A secret is brokered when it names hosts; unticked, it stops being.
75 const brokeredHosts =
76 kind === "secret"
77 ? form.get("brokered") === "on"
78 ? String(form.get("brokeredHosts") ?? "")
79 .split(/[\s,]+/)
80 .map((host) => host.trim())
81 .filter(Boolean)
82 : []
83 : undefined;
84 if (brokeredHosts && form.get("brokered") === "on" && brokeredHosts.length === 0) {
85 return { error: "Name the hosts a brokered secret is for, such as api.example.com." };
86 }
An issue being worked on no longer offers Bring your own agent beside the pull request doing it87 const note = String(form.get("note") ?? "");
88 const key = String(form.get("key") ?? "").trim();
89 const value = String(form.get("value") ?? "");
90 // A pasted .env file adds a row for each line.
91 const pasted = !id && key.includes("=") ? parseDotenv(key) : [];
92 const entries: [string, string | null][] = pasted.length > 0 ? pasted : [[key, value === "" && id ? null : value]];
93 for (const [name, entryValue] of entries) {
94 if (!name) return { error: "Give it a key." };
95 if (entryValue === "" && !id) return { error: `Give ${name} a value.` };
96 const saved = await actions.setSetting(actor, owner, kind, name, entryValue, {
97 id,
98 availableTo,
99 environments,
100 projects: "workspace" in owner ? linked : undefined,
101 note,
Merge brokered secrets: a secret a job uses only to call an API can be tied to the hosts it is for, and the job gets a stand-in in its place, which g1t fills in on the way out to those hosts and nowhere else, so the value never enters the sandbox; and a request to an allowed host that fails upstream is retried when it can be, or answered with a 502 that says why, rather than reset.102 brokeredHosts,
An issue being worked on no longer offers Bring your own agent beside the pull request doing it103 });
104 if (!saved.ok) return { error: pasted.length > 0 ? `${name}: ${saved.error.message}` : saved.error.message };
105 }
106 throw redirect(page);
107}

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