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.
| A comped workspace's usage chart and breakdowns show what runs used at cost, as the free tier's do | 1 | import { env } from "cloudflare:workers"; |
| 2 | ||
| 3 | import { | |
| 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, |
| A comped workspace's usage chart and breakdowns show what runs used at cost, as the free tier's do | 5 | 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, |
| A comped workspace's usage chart and breakdowns show what runs used at cost, as the free tier's do | 12 | runnersClient, |
| 13 | } from "@g1t/contracts"; | |
| 14 | ||
| 15 | import { 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. | 16 | import { type CloudNow, type CloudPrice, type OwnWaiting, type RunnerCost, cloudNow, cloudPrices, ownWaiting, runnerCost } from "./runners"; |
| 17 | import { agents, billing } from "./services.server"; | |
| A comped workspace's usage chart and breakdowns show what runs used at cost, as the free tier's do | 18 | |
| 19 | /** Self-hosted runners, kept by the actions service. */ | |
| 20 | export const runners = runnersClient(instrumented("actions", env.ACTIONS)); | |
| 21 | ||
| 22 | export 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 | ||
| 32 | export 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. */ |
| 48 | export 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; | |
| Hosted runner time is charged by the job, in minutes: each job's time on g1t's machines (a workflow job, a check, a merge queue run) is rounded up to the whole minute, so 61 seconds is 2 minutes and any job that ran is at least 1, and charged at a fixed rate for the machine it ran on: $0.004 a minute for the standard machine (2 vCPU, 8 GiB, 16 GB; ubuntu-latest, every other hosted label and g1t-2core) and $0.007 for g1t-4core (4 vCPU, 12 GiB, 20 GB). Each rate is the machine's cost a minute at Cloudflare's list prices with half its vCPUs busy (memory, disk and the Durable Object for every second, CPU as used), plus the 20% margin, rounded up to a tenth of a cent, and never under its cost with every vCPU busy ($0.0038 and $0.0068); should a job ever cost g1t more than its minutes at the rate, it is charged that cost. Jobs on self-hosted runners stay free. | 55 | /** Each machine on g1t's cloud and its price a minute, from the machine catalogue. */ |
| 56 | prices: CloudPrice[]; | |
| 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. | 57 | /** People's computers with the desktop app, as each app last said; null when the actions service did not answer. */ |
| 58 | 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. | 59 | }; |
| 60 | ||
| The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404. | 61 | /** Active sandbox runs read: the work service's most in one list. */ |
| 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. | 62 | const RUN_PAGE = 200; |
| 63 | ||
| 64 | const warn = (what: string) => (error: unknown) => { | |
| 65 | console.warn(`runners: ${what} failed`, error); | |
| 66 | return null; | |
| 67 | }; | |
| 68 | ||
| 69 | /** | |
| 70 | * Every read the workspace's Runners page makes, at once. The runners, | |
| 71 | * groups and settings come as on a project's page; what runs on g1t's | |
| 72 | * cloud, what waits, and this month's time each come from their own | |
| 73 | * service, and one that does not answer leaves only its part saying so. | |
| 74 | */ | |
| 75 | export async function loadRunnersPage(workspace: string, actor: User, repositories: string[] = [], now = new Date()): Promise<RunnersPageData> { | |
| 76 | const from = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)).toISOString().slice(0, 10); | |
| 77 | const until = now.toISOString().slice(0, 10); | |
| Hosted runner time is charged by the job, in minutes: each job's time on g1t's machines (a workflow job, a check, a merge queue run) is rounded up to the whole minute, so 61 seconds is 2 minutes and any job that ran is at least 1, and charged at a fixed rate for the machine it ran on: $0.004 a minute for the standard machine (2 vCPU, 8 GiB, 16 GB; ubuntu-latest, every other hosted label and g1t-2core) and $0.007 for g1t-4core (4 vCPU, 12 GiB, 20 GB). Each rate is the machine's cost a minute at Cloudflare's list prices with half its vCPUs busy (memory, disk and the Durable Object for every second, CPU as used), plus the 20% margin, rounded up to a tenth of a cent, and never under its cost with every vCPU busy ($0.0038 and $0.0068); should a job ever cost g1t more than its minutes at the rate, it is charged that cost. Jobs on self-hosted runners stay free. | 78 | const [base, activity, runs, report, 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. | 79 | loadRunners({ workspace }, actor, repositories), |
| 80 | runners | |
| 81 | .activity(actor, workspace) | |
| 82 | .then((result) => (result.ok ? result.value : null)) | |
| 83 | .catch(warn("runner activity")), | |
| 84 | agents | |
| 85 | .listRuns(actor, { workspace, active: true, limit: RUN_PAGE }) | |
| 86 | .then((result) => (result.ok ? result.value : null)) | |
| The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404. | 87 | .catch(warn("sandbox runs")), |
| 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. | 88 | billing |
| 89 | .usageReport(workspace, actor, { from, until }) | |
| 90 | .then((result) => (result.ok ? result.value : null)) | |
| 91 | .catch(warn("usage report")), | |
| 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. | 92 | runners |
| 93 | .desktops(actor, workspace) | |
| 94 | .then((result) => (result.ok ? result.value : null)) | |
| 95 | .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. | 96 | ]); |
| 97 | return { | |
| 98 | ...base, | |
| 99 | cloud: cloudNow(runs, activity, RUN_PAGE, RUNNER_ACTIVITY_LIMIT), | |
| 100 | waiting: ownWaiting(activity), | |
| 101 | cost: runnerCost(report), | |
| Hosted runner time is charged by the job, in minutes: each job's time on g1t's machines (a workflow job, a check, a merge queue run) is rounded up to the whole minute, so 61 seconds is 2 minutes and any job that ran is at least 1, and charged at a fixed rate for the machine it ran on: $0.004 a minute for the standard machine (2 vCPU, 8 GiB, 16 GB; ubuntu-latest, every other hosted label and g1t-2core) and $0.007 for g1t-4core (4 vCPU, 12 GiB, 20 GB). Each rate is the machine's cost a minute at Cloudflare's list prices with half its vCPUs busy (memory, disk and the Durable Object for every second, CPU as used), plus the 20% margin, rounded up to a tenth of a cent, and never under its cost with every vCPU busy ($0.0038 and $0.0068); should a job ever cost g1t more than its minutes at the rate, it is charged that cost. Jobs on self-hosted runners stay free. | 102 | prices: cloudPrices(), |
| 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. | 103 | 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. | 104 | }; |
| 105 | } | |
| 106 | ||
| A comped workspace's usage chart and breakdowns show what runs used at cost, as the free tier's do | 107 | export type RunnersAction = { |
| 108 | error?: string; | |
| 109 | notice?: string; | |
| 110 | /** A registration token just made: shown once, in the install steps. */ | |
| 111 | token?: RegistrationToken; | |
| 112 | }; | |
| 113 | ||
| 114 | /** Every form on the Runners page. */ | |
| 115 | export async function actOnRunners(owner: RunnersOwner, actor: User, form: FormData): Promise<RunnersAction> { | |
| 116 | const intent = String(form.get("intent") ?? ""); | |
| 117 | const workspace = "workspace" in owner ? owner.workspace : owner.repo.namespace.toLowerCase(); | |
| 118 | const list = (name: string) => | |
| 119 | String(form.get(name) ?? "") | |
| 120 | .split(/[\s,]+/) | |
| 121 | .map((item) => item.trim()) | |
| 122 | .filter(Boolean); | |
| 123 | switch (intent) { | |
| 124 | case "token": { | |
| 125 | const made = await runners.createToken(actor, owner, String(form.get("group") ?? "") || undefined); | |
| 126 | return made.ok ? { token: made.value } : { error: made.error.message }; | |
| 127 | } | |
| 128 | case "remove": { | |
| 129 | const removed = await runners.remove(actor, owner, String(form.get("id") ?? "")); | |
| 130 | return removed.ok ? { notice: `Removed ${String(form.get("name") ?? "the runner")}.` } : { error: removed.error.message }; | |
| 131 | } | |
| 132 | case "group": { | |
| 133 | const id = String(form.get("id") ?? "") || undefined; | |
| 134 | const repositories = form.get("reach") === "some" ? form.getAll("repository").map(String) : []; | |
| 135 | if (form.get("reach") === "some" && repositories.length === 0) return { error: "Choose at least one repository, or every repository." }; | |
| 136 | const saved = await runners.setGroup(actor, workspace, { | |
| 137 | id, | |
| 138 | name: String(form.get("name") ?? "").trim() || undefined, | |
| 139 | repositories, | |
| 140 | }); | |
| 141 | return saved.ok ? { notice: `Saved the group ${saved.value.name}.` } : { error: saved.error.message }; | |
| 142 | } | |
| 143 | case "delete-group": { | |
| 144 | const deleted = await runners.deleteGroup(actor, workspace, String(form.get("id") ?? "")); | |
| 145 | return deleted.ok ? { notice: "Deleted the group; its runners joined the default group." } : { error: deleted.error.message }; | |
| 146 | } | |
| 147 | case "settings": { | |
| 148 | const saved = await runners.setSettings(actor, owner, { | |
| 149 | agents_on_self_hosted: form.get("agents") === "on", | |
| 150 | agent_labels: list("agentLabels"), | |
| 151 | fork_pull_requests: form.get("forks") === "on", | |
| 152 | }); | |
| 153 | return saved.ok ? { notice: "Saved." } : { error: saved.error.message }; | |
| 154 | } | |
| 155 | case "inherit": { | |
| 156 | const saved = await runners.setSettings(actor, owner, { inherit: true }); | |
| 157 | return saved.ok ? { notice: "This project follows the workspace's runner settings again." } : { error: saved.error.message }; | |
| 158 | } | |
| 159 | default: | |
| 160 | return { error: "Nothing to do." }; | |
| 161 | } | |
| 162 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.