g1t/apps/web/app/lib/runners.server.ts

98 lines3,846 bytesCodeBlame
1import { env } from "cloudflare:workers";
2
3import {
4 type RegistrationToken,
5 type Runner,
6 type RunnerGroup,
7 type RunnerSettings,
8 type RunnersOwner,
9 type User,
10 runnersClient,
11} from "@g1t/contracts";
12
13import { instrumented } from "./perf.server";
14
15/** Self-hosted runners, kept by the actions service. */
16export const runners = runnersClient(instrumented("actions", env.ACTIONS));
17
18export type RunnersData = {
19 runners: Runner[];
20 /** A workspace's groups; empty for a repository. */
21 groups: RunnerGroup[];
22 settings: RunnerSettings | null;
23 /** The workspace's repositories, for choosing a group's. */
24 repositories: string[];
25 error: string | null;
26};
27
28export async function loadRunners(owner: RunnersOwner, actor: User, repositories: string[] = []): Promise<RunnersData> {
29 const [list, groups, settings] = await Promise.all([
30 runners.list(actor, owner),
31 "workspace" in owner ? runners.groups(actor, owner.workspace) : Promise.resolve(null),
32 runners.settings(actor, owner),
33 ]);
34 return {
35 runners: list.ok ? list.value : [],
36 groups: groups?.ok ? groups.value : [],
37 settings: settings.ok ? settings.value : null,
38 repositories,
39 error: list.ok ? null : list.error.message,
40 };
41}
42
43export type RunnersAction = {
44 error?: string;
45 notice?: string;
46 /** A registration token just made: shown once, in the install steps. */
47 token?: RegistrationToken;
48};
49
50/** Every form on the Runners page. */
51export async function actOnRunners(owner: RunnersOwner, actor: User, form: FormData): Promise<RunnersAction> {
52 const intent = String(form.get("intent") ?? "");
53 const workspace = "workspace" in owner ? owner.workspace : owner.repo.namespace.toLowerCase();
54 const list = (name: string) =>
55 String(form.get(name) ?? "")
56 .split(/[\s,]+/)
57 .map((item) => item.trim())
58 .filter(Boolean);
59 switch (intent) {
60 case "token": {
61 const made = await runners.createToken(actor, owner, String(form.get("group") ?? "") || undefined);
62 return made.ok ? { token: made.value } : { error: made.error.message };
63 }
64 case "remove": {
65 const removed = await runners.remove(actor, owner, String(form.get("id") ?? ""));
66 return removed.ok ? { notice: `Removed ${String(form.get("name") ?? "the runner")}.` } : { error: removed.error.message };
67 }
68 case "group": {
69 const id = String(form.get("id") ?? "") || undefined;
70 const repositories = form.get("reach") === "some" ? form.getAll("repository").map(String) : [];
71 if (form.get("reach") === "some" && repositories.length === 0) return { error: "Choose at least one repository, or every repository." };
72 const saved = await runners.setGroup(actor, workspace, {
73 id,
74 name: String(form.get("name") ?? "").trim() || undefined,
75 repositories,
76 });
77 return saved.ok ? { notice: `Saved the group ${saved.value.name}.` } : { error: saved.error.message };
78 }
79 case "delete-group": {
80 const deleted = await runners.deleteGroup(actor, workspace, String(form.get("id") ?? ""));
81 return deleted.ok ? { notice: "Deleted the group; its runners joined the default group." } : { error: deleted.error.message };
82 }
83 case "settings": {
84 const saved = await runners.setSettings(actor, owner, {
85 agents_on_self_hosted: form.get("agents") === "on",
86 agent_labels: list("agentLabels"),
87 fork_pull_requests: form.get("forks") === "on",
88 });
89 return saved.ok ? { notice: "Saved." } : { error: saved.error.message };
90 }
91 case "inherit": {
92 const saved = await runners.setSettings(actor, owner, { inherit: true });
93 return saved.ok ? { notice: "This project follows the workspace's runner settings again." } : { error: saved.error.message };
94 }
95 default:
96 return { error: "Nothing to do." };
97 }
98}