Skip to content
167 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.

Fast pages, required checks on the branch, self-hosted runners, honest incidents1import { env } from "cloudflare:workers";
2
3import {
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.4 type DesktopRunner,
Fast pages, required checks on the branch, self-hosted runners, honest incidents5 type RegistrationToken,
6 type Runner,
7 type RunnerGroup,
8 type RunnerSettings,
9 type RunnersOwner,
10 type User,
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.11 RUNNER_ACTIVITY_LIMIT,
Fast pages, required checks on the branch, self-hosted runners, honest incidents12 runnersClient,
13} from "@g1t/contracts";
14
15import { instrumented } from "./perf.server";
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.16import { type CloudNow, type CloudPrice, type OwnWaiting, type RunnerCost, cloudNow, cloudPrices, ownWaiting, runnerCost } from "./runners";
17import { agents, billing } from "./services.server";
Fast pages, required checks on the branch, self-hosted runners, honest incidents18
19/** Self-hosted runners, kept by the actions service. */
20export const runners = runnersClient(instrumented("actions", env.ACTIONS));
21
22export type RunnersData = {
23 runners: Runner[];
24 /** A workspace's groups; empty for a repository. */
25 groups: RunnerGroup[];
26 settings: RunnerSettings | null;
27 /** The workspace's repositories, for choosing a group's. */
28 repositories: string[];
29 error: string | null;
30};
31
32export async function loadRunners(owner: RunnersOwner, actor: User, repositories: string[] = []): Promise<RunnersData> {
33 const [list, groups, settings] = await Promise.all([
34 runners.list(actor, owner),
35 "workspace" in owner ? runners.groups(actor, owner.workspace) : Promise.resolve(null),
36 runners.settings(actor, owner),
37 ]);
38 return {
39 runners: list.ok ? list.value : [],
40 groups: groups?.ok ? groups.value : [],
41 settings: settings.ok ? settings.value : null,
42 repositories,
43 error: list.ok ? null : list.error.message,
44 };
45}
46
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.47/** The workspace's Runners page: its runners, and g1t's cloud beside them. */
48export type RunnersPageData = RunnersData & {
49 /** What runs on g1t's cloud now; null when neither the work nor the actions service answered. */
50 cloud: CloudNow | null;
51 /** What waits for the workspace's own runners; null when the actions service did not answer. */
52 waiting: OwnWaiting | null;
53 /** This month's machine time; null when billing did not answer. */
54 cost: RunnerCost | null;
55 /** A minute on g1t's cloud, from the price book; null when it has none. */
56 prices: CloudPrice[] | null;
57 /** Agent runs at once on g1t's cloud the plan allows; null for no cap, or unknown. */
58 agentCap: number | null;
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.59 /** People's computers with the desktop app, as each app last said; null when the actions service did not answer. */
60 desktops: DesktopRunner[] | null;
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.61};
62
63/** Active agent runs read: the work service's most in one list. */
64const RUN_PAGE = 200;
65
66const warn = (what: string) => (error: unknown) => {
67 console.warn(`runners: ${what} failed`, error);
68 return null;
69};
70
71/**
72 * Every read the workspace's Runners page makes, at once. The runners,
73 * groups and settings come as on a project's page; what runs on g1t's
74 * cloud, what waits, and this month's time each come from their own
75 * service, and one that does not answer leaves only its part saying so.
76 */
77export async function loadRunnersPage(workspace: string, actor: User, repositories: string[] = [], now = new Date()): Promise<RunnersPageData> {
78 const from = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)).toISOString().slice(0, 10);
79 const until = now.toISOString().slice(0, 10);
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.80 const [base, activity, runs, report, book, entitlements, desktops] = await Promise.all([
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.81 loadRunners({ workspace }, actor, repositories),
82 runners
83 .activity(actor, workspace)
84 .then((result) => (result.ok ? result.value : null))
85 .catch(warn("runner activity")),
86 agents
87 .listRuns(actor, { workspace, active: true, limit: RUN_PAGE })
88 .then((result) => (result.ok ? result.value : null))
89 .catch(warn("agent runs")),
90 billing
91 .usageReport(workspace, actor, { from, until })
92 .then((result) => (result.ok ? result.value : null))
93 .catch(warn("usage report")),
94 billing.prices().catch(warn("prices")),
95 billing.entitlements(workspace).catch(warn("entitlements")),
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.96 runners
97 .desktops(actor, workspace)
98 .then((result) => (result.ok ? result.value : null))
99 .catch(warn("desktops")),
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.100 ]);
101 return {
102 ...base,
103 cloud: cloudNow(runs, activity, RUN_PAGE, RUNNER_ACTIVITY_LIMIT),
104 waiting: ownWaiting(activity),
105 cost: runnerCost(report),
106 prices: cloudPrices(book?.prices ?? null),
107 agentCap: entitlements && entitlements.maxConcurrentAgents > 0 ? entitlements.maxConcurrentAgents : null,
Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it.108 desktops,
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.109 };
110}
111
Fast pages, required checks on the branch, self-hosted runners, honest incidents112export type RunnersAction = {
113 error?: string;
114 notice?: string;
115 /** A registration token just made: shown once, in the install steps. */
116 token?: RegistrationToken;
117};
118
119/** Every form on the Runners page. */
120export async function actOnRunners(owner: RunnersOwner, actor: User, form: FormData): Promise<RunnersAction> {
121 const intent = String(form.get("intent") ?? "");
122 const workspace = "workspace" in owner ? owner.workspace : owner.repo.namespace.toLowerCase();
123 const list = (name: string) =>
124 String(form.get(name) ?? "")
125 .split(/[\s,]+/)
126 .map((item) => item.trim())
127 .filter(Boolean);
128 switch (intent) {
129 case "token": {
130 const made = await runners.createToken(actor, owner, String(form.get("group") ?? "") || undefined);
131 return made.ok ? { token: made.value } : { error: made.error.message };
132 }
133 case "remove": {
134 const removed = await runners.remove(actor, owner, String(form.get("id") ?? ""));
135 return removed.ok ? { notice: `Removed ${String(form.get("name") ?? "the runner")}.` } : { error: removed.error.message };
136 }
137 case "group": {
138 const id = String(form.get("id") ?? "") || undefined;
139 const repositories = form.get("reach") === "some" ? form.getAll("repository").map(String) : [];
140 if (form.get("reach") === "some" && repositories.length === 0) return { error: "Choose at least one repository, or every repository." };
141 const saved = await runners.setGroup(actor, workspace, {
142 id,
143 name: String(form.get("name") ?? "").trim() || undefined,
144 repositories,
145 });
146 return saved.ok ? { notice: `Saved the group ${saved.value.name}.` } : { error: saved.error.message };
147 }
148 case "delete-group": {
149 const deleted = await runners.deleteGroup(actor, workspace, String(form.get("id") ?? ""));
150 return deleted.ok ? { notice: "Deleted the group; its runners joined the default group." } : { error: deleted.error.message };
151 }
152 case "settings": {
153 const saved = await runners.setSettings(actor, owner, {
154 agents_on_self_hosted: form.get("agents") === "on",
155 agent_labels: list("agentLabels"),
156 fork_pull_requests: form.get("forks") === "on",
157 });
158 return saved.ok ? { notice: "Saved." } : { error: saved.error.message };
159 }
160 case "inherit": {
161 const saved = await runners.setSettings(actor, owner, { inherit: true });
162 return saved.ok ? { notice: "This project follows the workspace's runner settings again." } : { error: saved.error.message };
163 }
164 default:
165 return { error: "Nothing to do." };
166 }
167}

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