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.
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 1 | /** |
| 2 | * g1t's own work on a workspace's self-hosted runners. When a workspace (or | |
| The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers. | 3 | * one of its repositories) says so under Settings, Runners, a merge check or |
| 4 | * a state of the merge queue is not started in a sandbox here: the | |
| 5 | * sandbox's Durable Object hands the same environment to the actions | |
| 6 | * service as a task, a runner of the workspace's with the right labels | |
| 7 | * takes it on its next poll, and the actions service tells the Durable | |
| 8 | * Object how it ended (`task_ended`), which then does exactly what it does | |
| Merge branch 'main' into ask-integration | 9 | * when a container stops. The run says where it went. |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 10 | */ |
| 11 | ||
| 12 | import type { RepoPath, ServiceBinding } from "@g1t/contracts"; | |
| 13 | ||
| 14 | /** The actions service's JSON protocol. */ | |
| 15 | async function call<T>(actions: ServiceBinding, method: string, args: object): Promise<T> { | |
| 16 | const response = await actions.fetch(`https://actions/rpc/${method}`, { | |
| 17 | method: "POST", | |
| 18 | headers: { "content-type": "application/json" }, | |
| 19 | body: JSON.stringify(args), | |
| 20 | }); | |
| 21 | if (!response.ok) throw new Error(`${method} failed with status ${response.status}`); | |
| 22 | return (await response.json()) as T; | |
| 23 | } | |
| 24 | ||
| 25 | /** | |
| 26 | * The labels g1t's own work in `repo` runs on, when it runs on the | |
| 27 | * workspace's runners; null for g1t's sandboxes. Never throws: when the | |
| 28 | * actions service cannot say, the work runs in a sandbox as before. | |
| 29 | */ | |
| 30 | export async function selfHostedRoute(actions: ServiceBinding, repo: RepoPath): Promise<string[] | null> { | |
| 31 | try { | |
| 32 | const labels = await call<string[] | null>(actions, "runner_route", { workspace: repo.namespace.toLowerCase(), repo }); | |
| 33 | return Array.isArray(labels) && labels.length > 0 ? labels : null; | |
| 34 | } catch (error) { | |
| 35 | console.log("self-hosted route unknown; using a sandbox", repo.namespace, String(error)); | |
| 36 | return null; | |
| 37 | } | |
| 38 | } | |
| 39 | ||
| 40 | type Outcome<T> = { ok: true; value: T } | { ok: false; error: { message: string } }; | |
| 41 | ||
| 42 | /** Hands a sandbox's work to the workspace's runners. Returns the task's id. */ | |
| 43 | export async function enqueueTask( | |
| 44 | actions: ServiceBinding, | |
| 45 | task: { | |
| 46 | sandbox: string; | |
| 47 | repo: RepoPath; | |
| 48 | kind: string; | |
| 49 | title: string; | |
| 50 | labels: string[]; | |
| 51 | env: Record<string, string>; | |
| 52 | timeoutMinutes: number; | |
| The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers. | 53 | /** The run the work is, when the sandbox opened one: the Runners page links to it. */ |
| 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. | 54 | runId?: string | null; |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 55 | }, |
| 56 | ): Promise<string> { | |
| 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. | 57 | const { runId, ...rest } = task; |
| 58 | const queued = await call<Outcome<string>>(actions, "enqueue_task", { ...rest, run_id: runId ?? null, workspace: task.repo.namespace.toLowerCase() }); | |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 59 | if (!queued.ok) throw new Error(queued.error.message); |
| 60 | return queued.value; | |
| 61 | } | |
| 62 | ||
| 63 | /** Withdraws a sandbox's task: a person stopped it, or its time ran out. Never throws. */ | |
| 64 | export async function cancelTask(actions: ServiceBinding, sandbox: string, reason: string | null): Promise<void> { | |
| 65 | await call(actions, "cancel_task", { sandbox, reason }).catch((error: unknown) => | |
| 66 | console.log("self-hosted task not cancelled", sandbox, String(error)), | |
| 67 | ); | |
| 68 | } | |
| 69 | ||
| 70 | /** | |
| 71 | * The environment a self-hosted runner gets: what the sandbox would have | |
| 72 | * been started with, less what only makes sense in g1t's sandbox (the | |
| 73 | * egress certificate and proxy), with the mining watch off, since the | |
| 74 | * machine and its electricity are the workspace's. | |
| 75 | */ | |
| 76 | export function taskEnv(vars: Record<string, string>): Record<string, string> { | |
| 77 | const env: Record<string, string> = {}; | |
| 78 | for (const [name, value] of Object.entries(vars)) { | |
| 79 | if (/^(HTTPS?_PROXY|NO_PROXY|NODE_EXTRA_CA_CERTS|SSL_CERT_FILE|G1T_EGRESS_CA)$/i.test(name)) continue; | |
| 80 | env[name] = value; | |
| 81 | } | |
| 82 | env.G1T_ABUSE = "off"; | |
| 83 | env.G1T_SELF_HOSTED = "1"; | |
| 84 | return env; | |
| 85 | } | |
| 86 | ||
| 87 | /** What the run's session says when it goes to the workspace's runners. */ | |
| 88 | export function handedOverStep(labels: string[]): string { | |
| Merge branch 'main' into ask-integration | 89 | return `Handed to a self-hosted runner with labels ${labels.join(", ")}.`; |
| Costs: a statement that keeps usage sold, running g1t, subscriptions and what was given away (comped, free use, trial, pool) apart, and says who was paid; free use carries its own cost; the run button says it is running | 90 | } |
| 91 | ||
| 92 | /** Where the work is: the tracked repository, else the meter's `owner/name`. */ | |
| 93 | export function taskRepo(track: { repo: RepoPath } | undefined, meter: { repo: string } | undefined, owner: { repo: string | null } | undefined): RepoPath | null { | |
| 94 | if (track) return track.repo; | |
| 95 | const full = meter?.repo ?? owner?.repo ?? null; | |
| 96 | if (!full) return null; | |
| 97 | const [namespace, name] = full.split("/"); | |
| 98 | return namespace && name ? { namespace, name } : null; | |
| 99 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.