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

The Packages page names every registry it has1//! Self-hosted runners: a workspace's or a repository's own machines, which
2//! run its workflow jobs (and, when it says so, its agents' work) instead
3//! of g1t's sandboxes. Time on them costs nothing.
4//!
5//! The actions service keeps them, beside the jobs they run. A machine runs
6//! `g1t-runner` (`crates/runner`), which registers once with a short-lived
7//! registration token an admin made, gets a credential of its own, and then
8//! only ever calls out: it polls `api.g1t.sh` for work, runs what it is
9//! given with the same harness g1t's sandboxes use, and reports back. No
10//! port is opened on the machine.
11//!
12//! - A **registration token** (`g1trt_…`) is made by an owner of the
13//! workspace (or an admin of the repository) and lasts an hour. It can
14//! register any number of runners until then, and nothing else.
15//! - A **runner credential** (`g1tr_…`) belongs to one runner. It is stored
16//! only as a hash, rotates every day (the poll that rotates it hands the
17//! new one over), and is revoked when the runner is removed. It can poll
18//! for work, report on what that runner was given, and remove the runner;
19//! nothing else. Any other endpoint refuses it.
20//! - A job is given to a runner when every label in its `runs-on` is one
21//! of the runner's labels (ignoring case), and the runner's group lets
22//! the job's repository use it. A job whose `runs-on` names
23//! `self-hosted` (or a `group`) only ever runs on self-hosted runners; it
24//! waits, saying for which labels, until one picks it up, for a day at
25//! most.
26//!
27//! The management methods take an `actor` and an owner (`repo` or
28//! `workspace`), as secrets do. The runner's own methods take its id and
29//! credential. Mirrors `packages/contracts/src/runners.ts`.
30
31use serde::{Deserialize, Serialize};
32use serde_json::{Map, Value};
33
34use crate::User;
35use crate::repos::RepoPath;
36
37/// Every runner has these labels, whatever else it was given.
38pub const SELF_HOSTED: &str = "self-hosted";
39/// What a registration token starts with.
40pub const REGISTRATION_PREFIX: &str = "g1trt_";
41/// What a runner's credential starts with.
42pub const CREDENTIAL_PREFIX: &str = "g1tr_";
43/// How long a registration token lasts, in seconds.
44pub const REGISTRATION_TTL_SECONDS: u64 = 60 * 60;
45/// How long a job may wait for a self-hosted runner before it fails.
46pub const MAX_WAIT_HOURS: u64 = 24;
47/// A runner that has not polled for this long is offline.
48pub const ONLINE_WITHIN_MS: u64 = 90 * 1000;
49/// How long one poll waits for work before answering with none.
50pub const MAX_POLL_WAIT_MS: u64 = 20 * 1000;
51/// How often a runner's credential is replaced.
52pub const ROTATE_AFTER_MS: u64 = 24 * 60 * 60 * 1000;
53/// The most labels a runner has, and the longest one.
54pub const MAX_LABELS: usize = 30;
55pub const MAX_LABEL_LEN: usize = 64;
56/// The most runners a workspace has.
57pub const MAX_RUNNERS: u32 = 500;
58/// The most runners a workspace registers in one minute.
59pub const MAX_REGISTRATIONS_PER_MINUTE: u32 = 30;
60/// The most registration tokens a workspace makes in one hour.
61pub const MAX_TOKENS_PER_HOUR: u32 = 60;
62
63// --- What people see -------------------------------------------------------
64
65/// What a runner is doing now.
66#[derive(Clone, Debug, Serialize, Deserialize)]
67#[serde(rename_all = "camelCase")]
68pub struct RunnerWork {
69 /// `workflow` (a workflow job) or `agent` (an agent's run, checks, a
70 /// review, the merge queue).
71 pub kind: String,
72 pub id: String,
73 /// The job's name, or what the agent is doing.
74 pub name: String,
75 /// `owner/name`.
76 pub repo: Option<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.77 /// For a link: the workflow run the job is in, or for `agent` work
78 /// the agent run it is (`/<owner>/<name>/agents/runs/<id>`), when
79 /// the sandbox that handed it over said which.
The Packages page names every registry it has80 pub run_id: Option<String>,
81 pub started_at: Option<String>,
82}
83
84#[derive(Clone, Debug, Serialize, Deserialize)]
85#[serde(rename_all = "camelCase")]
86pub struct Runner {
87 pub id: String,
88 pub name: String,
89 pub workspace: String,
90 /// `owner/name` for a repository's own runner; null for the workspace's.
91 pub repo: Option<String>,
92 /// Its group's name; null for a repository's runner.
93 pub group: Option<String>,
94 /// Every label, `self-hosted`, its OS and architecture among them.
95 pub labels: Vec<String>,
96 /// `linux`, `macos` or `windows`.
97 pub os: String,
98 /// `x64` or `arm64`.
99 pub arch: String,
100 /// The `g1t-runner` version it last reported.
101 pub version: String,
102 /// Runs one job, then removes itself.
103 pub ephemeral: bool,
104 /// `online`, `busy` or `offline`.
105 pub status: String,
106 pub work: Option<RunnerWork>,
107 pub last_seen_at: Option<String>,
108 pub created_at: String,
109 pub created_by: Option<String>,
Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it.110 /// How it keeps work from its machine, as it last said
111 /// ([`ISOLATIONS`]); null for a runner from before runners said.
112 #[serde(default)]
113 pub isolation: Option<String>,
The Packages page names every registry it has114}
115
116/// Which of a workspace's repositories may use its runners.
117#[derive(Clone, Debug, Serialize, Deserialize)]
118#[serde(rename_all = "camelCase")]
119pub struct RunnerGroup {
120 pub id: String,
121 pub name: String,
122 /// The group runners join when none is named: every repository.
123 pub default: bool,
124 /// Repository names (without the workspace) that may use it; empty is
125 /// every repository in the workspace.
126 pub repositories: Vec<String>,
127 pub runners: u32,
128 pub updated_at: String,
129}
130
131/// A registration token, shown once.
132#[derive(Clone, Debug, Serialize, Deserialize)]
133#[serde(rename_all = "camelCase")]
134pub struct RegistrationToken {
135 pub token: String,
136 pub expires_at: String,
137 pub workspace: String,
138 pub repo: Option<String>,
139 pub group: Option<String>,
140 /// What to pass as `--url`.
141 pub url: String,
142}
143
144/// Where a workspace's (or a repository's) agents work, and whether pull
145/// requests from forks may use its runners.
146#[derive(Clone, Debug, Default, Serialize, Deserialize)]
147#[serde(rename_all = "camelCase")]
148pub struct RunnerSettings {
149 /// Agent runs, checks, reviews and the merge queue run on self-hosted
150 /// runners with `agent_labels`, instead of g1t's sandboxes.
151 pub agents_on_self_hosted: bool,
152 /// The labels a runner needs to take agent work; `self-hosted` is
153 /// always one.
154 pub agent_labels: Vec<String>,
155 /// Jobs of pull requests from forks may run on self-hosted runners.
156 /// Off by default: anyone who can open a pull request could run code
157 /// on the machine.
158 pub fork_pull_requests: bool,
159 /// For a repository: these are its workspace's, not its own.
160 pub inherited: bool,
161}
162
163// --- Management ------------------------------------------------------------
164
165/// A workspace's runners (`workspace`), or a repository's own (`repo`).
166#[derive(Clone, Debug, Default, Serialize, Deserialize)]
167pub struct RunnersOwner {
168 #[serde(default)]
169 pub repo: Option<RepoPath>,
170 #[serde(default)]
171 pub workspace: Option<String>,
172}
173
174/// `runners`: a workspace's runners (members), or a repository's own
175/// (its admins). Returns `Outcome<Vec<Runner>>`.
176#[derive(Debug, Serialize, Deserialize)]
177pub struct ListRunnersArgs {
178 pub actor: User,
179 #[serde(flatten)]
180 pub owner: RunnersOwner,
181}
182
183/// `create_registration_token`: owners of the workspace, or admins of the
184/// repository. Returns `Outcome<RegistrationToken>`.
185#[derive(Debug, Serialize, Deserialize)]
186pub struct CreateRegistrationTokenArgs {
187 pub actor: User,
188 #[serde(flatten)]
189 pub owner: RunnersOwner,
190 /// The group its runners join, by name or id; the default group if
191 /// left out. Not for a repository's runners.
192 #[serde(default)]
193 pub group: Option<String>,
194}
195
196/// `remove_runner`: as `create_registration_token`. A job it is running is
197/// cancelled. Returns `Outcome<bool>`.
198#[derive(Debug, Serialize, Deserialize)]
199pub struct RemoveRunnerArgs {
200 pub actor: User,
201 #[serde(flatten)]
202 pub owner: RunnersOwner,
203 pub id: String,
204}
205
206/// `runner_groups`: members. Returns `Outcome<Vec<RunnerGroup>>`.
207#[derive(Debug, Serialize, Deserialize)]
208pub struct RunnerGroupsArgs {
209 pub actor: User,
210 pub workspace: String,
211}
212
213/// `set_runner_group`: create one (no `id`) or change one. Owners.
214/// Returns `Outcome<RunnerGroup>`.
215#[derive(Debug, Serialize, Deserialize)]
216pub struct SetRunnerGroupArgs {
217 pub actor: User,
218 pub workspace: String,
219 #[serde(default)]
220 pub id: Option<String>,
221 #[serde(default)]
222 pub name: Option<String>,
223 /// Repository names; empty for every repository.
224 #[serde(default)]
225 pub repositories: Option<Vec<String>>,
226}
227
228/// `delete_runner_group`: owners. Its runners move to the default group,
229/// which cannot be deleted. Returns `Outcome<bool>`.
230#[derive(Debug, Serialize, Deserialize)]
231pub struct DeleteRunnerGroupArgs {
232 pub actor: User,
233 pub workspace: String,
234 pub id: String,
235}
236
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.237// --- The desktop app's runners ----------------------------------------------
238
239/// The desktop app keeps a runner on a person's computer while it is open
240/// (apps/desktop/src-tauri/src/runner). What it says of that runner, as it
241/// says it in its tray and settings (its diagnosis.rs, the one catalogue
242/// of those words), is kept here per computer, so the workspace's Runners
243/// page lists every desktop with the same state, why, and the fix. A
244/// desktop not heard from for `DESKTOP_OPEN_WITHIN_MS` has g1t closed, and
245/// shows `closed`, which the app sent too.
246pub const DESKTOP_OPEN_WITHIN_MS: u64 = 10 * 60 * 1000;
247/// The most desktops a workspace keeps reports for.
248pub const MAX_DESKTOPS: u32 = 200;
249
250/// A diagnosis, as the app made it.
251#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
252pub struct DesktopDiagnosis {
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.253 /// `idle`, `busy`, `no_docker`, `no_wsl`, `sandbox_failed`, `not_allowed`, …
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.254 pub code: String,
255 /// `ok`, `working`, `off`, `warning` or `problem`.
256 pub tone: String,
257 pub title: String,
258 #[serde(default)]
259 pub cause: Option<String>,
260 #[serde(default)]
261 pub fix: Option<String>,
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.262 /// A sandboxed desktop's kinds of job, each with how it is: Linux jobs
263 /// can run while Windows jobs cannot. Empty for the other backends.
264 #[serde(default, skip_serializing_if = "Vec::is_empty")]
265 pub capabilities: Vec<DesktopCapability>,
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.266 /// The kinds of job say all of this, each for itself: a page showing
267 /// them shows no line of the whole runner's as well.
268 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
269 pub in_capabilities: bool,
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.270 /// How its runner's updates stand. From newer apps; older ones leave it out.
271 #[serde(default, skip_serializing_if = "Option::is_none")]
272 pub update: Option<DesktopUpdate>,
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.273}
274
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.275/// How a desktop's runner updates stand, as its app says it. The app
276/// looks every hour, and installs a newer runner kind of job by kind of
277/// job, as each is idle.
278#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
279pub struct DesktopUpdate {
280 /// When the app last heard from the runner's releases, RFC 3339.
281 #[serde(default)]
282 pub checked_at: Option<String>,
283 /// A newer version, downloaded and checked, waiting to be installed.
284 #[serde(default)]
285 pub ready: Option<String>,
286 /// What becomes of it, for a desktop with no kinds of job of its own:
287 /// `0.3.2 ready, installs after this job`.
288 #[serde(default)]
289 pub note: Option<String>,
290}
291
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.292/// One kind of job a sandboxed desktop runs, as its app says it. Each
293/// starts and stops on its own: `enabled` is false for one the person
294/// stopped, which is not failing. The fields after `fix` come from newer
295/// apps; older ones leave them out.
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.296#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
297pub struct DesktopCapability {
298 /// `linux`, `windows` or `macos`.
299 pub id: String,
300 /// `Linux jobs`.
301 pub label: String,
302 pub tone: String,
303 /// `Waiting for jobs`, `Not available`.
304 pub title: String,
305 #[serde(default)]
306 pub cause: Option<String>,
307 #[serde(default)]
308 pub fix: Option<String>,
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.309 /// `busy`, `idle`, `starting`, `downloading`, `updating`, `waiting`,
310 /// `stopped`, `unavailable` or `problem`.
311 #[serde(default, skip_serializing_if = "Option::is_none")]
312 pub state: Option<String>,
313 /// What is wrong, in a few words: `WSL isn't installed`.
314 #[serde(default, skip_serializing_if = "Option::is_none")]
315 pub headline: Option<String>,
316 /// What it is, in a line.
317 #[serde(default, skip_serializing_if = "Option::is_none")]
318 pub about: Option<String>,
319 /// On in the person's settings; false when they stopped it.
320 #[serde(default, skip_serializing_if = "Option::is_none")]
321 pub enabled: Option<bool>,
322 #[serde(default, skip_serializing_if = "Option::is_none")]
323 pub job: Option<DesktopJob>,
324 /// Its runner's name on g1t: `build-box-linux`.
325 #[serde(default, skip_serializing_if = "Option::is_none")]
326 pub runner: Option<String>,
327 /// `nested-container` or `vm`.
328 #[serde(default, skip_serializing_if = "Option::is_none")]
329 pub isolation: Option<String>,
330 #[serde(default, skip_serializing_if = "Option::is_none")]
331 pub version: Option<String>,
The desktop app is 0.3.2, and its runner updates are fast, seen and only ever g1t's. The app now trusts a g1t-runner release only when its manifest's Ed25519 signature, latest.json.sig, checks out against g1t's runner release key, which is built into the app (the public half of RUNNER_RELEASE_PUBLIC_KEY, kept in runner/native.rs, where development builds have it too; G1T_RUNNER_RELEASE_KEY overrides it for a g1t with its own key), as the runner already did with its own updates. A manifest with no signature, or one that doesn't check out, is refused and nothing in it is used: the runner keeps the version it has and says "Update refused: not signed by g1t", or, with no runner installed yet, "Not running: the runner's release isn't signed by g1t", each with why and what to do. It looks every hour rather than every six, on a thread of its own so the runner is looked after meanwhile, and what it finds waits, downloaded and checked, until each kind of job is idle, then goes in kind by kind, never cutting a job short: Linux jobs take the release's image (sandbox update) as soon as they have no job, while Windows jobs, whose runner is the program itself, are stopped after their job (sandbox down --only windows --drain), the program is replaced and they start again, the other kind running on throughout; a runner in Docker or on the computer stops between jobs and starts on the new one. The program there is moved aside rather than overwritten, so a status the app is asking at that moment can't stop it. Each kind of job's card in Settings shows its runner version, when the app last looked ("Checked 12 min ago") and a newer one waiting ("0.3.2 ready, installs after this job", "Installing 0.3.2"), and "Check for runner updates" beside the computer's name looks at once and installs what it finds, even with "Download updates by themselves" off, which otherwise stops the hourly look. The desktop report carries the same (snake_case: the diagnosis's update, with checked_at, ready and note, and each kind's update), so the Runners page's Desktops cards show it, "Checked" with how long ago, and this computer's row has the button too. All the words are diagnosis.rs's. The desktop guide says how updates work, what each card shows, the new troubleshooting rows and where the release key lives. Tests cover the signature with a key pair of their own and with release 0.3.0's manifest and signature as g1t.sh published them, kept byte for byte (.gitattributes), the wording, the cards and the report.332 /// A newer version waiting for it: `0.3.2 ready, installs after this job`.
333 #[serde(default, skip_serializing_if = "Option::is_none")]
334 pub update: Option<String>,
Merge the desktop runner's settings: a card for each kind of job, Linux and Windows, each with its state, job, cause and fix, and its own Start and Stop; an honest wait for a release's sandbox images, checked again by itself; and the same on the Runners page.335}
336
337/// What a desktop's kind of job is running.
338#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
339pub struct DesktopJob {
340 /// `workflow` or `agent`.
341 pub kind: String,
342 pub name: String,
343 /// `acme/web`.
344 pub repo: String,
Merge the desktop app's sandbox: its default runner now starts g1t-runner's sandbox on Windows, macOS and Linux, and says per kind of job (Linux, Windows) what it is doing or why it cannot and how to fix it.345}
346
347/// The most kinds of job a desktop reports.
348pub const MAX_DESKTOP_CAPABILITIES: usize = 4;
349
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.350/// One person's computer, as its desktop app last said.
351#[derive(Clone, Debug, Serialize, Deserialize)]
352#[serde(rename_all = "camelCase")]
353pub struct DesktopRunner {
354 pub workspace: String,
355 /// The computer's name, which its runner has too.
356 pub machine: String,
357 /// Whose computer: their username.
358 pub person: String,
359 /// `sandboxed`, `docker` or `host`.
360 pub backend: String,
361 pub version: Option<String>,
362 pub diagnosis: DesktopDiagnosis,
363 /// What to show while g1t is closed on it.
364 pub closed: DesktopDiagnosis,
365 /// Heard from within `DESKTOP_OPEN_WITHIN_MS`: g1t is open there.
366 pub open: bool,
367 pub reported_at: String,
368}
369
370/// `report_desktop`: the person's own desktop app, through the site, for a
371/// workspace they own. Returns `Outcome<bool>`.
372#[derive(Debug, Serialize, Deserialize)]
373pub struct ReportDesktopArgs {
374 pub actor: User,
375 pub workspace: String,
376 pub machine: String,
377 pub backend: String,
378 #[serde(default)]
379 pub version: Option<String>,
380 pub diagnosis: DesktopDiagnosis,
381 pub closed: DesktopDiagnosis,
382}
383
384/// `desktop_runners`: owners of the workspace. Returns `Outcome<Vec<DesktopRunner>>`.
385#[derive(Debug, Serialize, Deserialize)]
386pub struct DesktopRunnersArgs {
387 pub actor: User,
388 pub workspace: String,
389}
390
The Packages page names every registry it has391/// `runner_settings`: members of the workspace, or admins of the
392/// repository. Returns `Outcome<RunnerSettings>`.
393#[derive(Debug, Serialize, Deserialize)]
394pub struct RunnerSettingsArgs {
395 pub actor: User,
396 #[serde(flatten)]
397 pub owner: RunnersOwner,
398}
399
400/// `set_runner_settings`: owners, or admins of the repository. Left out is
401/// unchanged; `inherit` drops a repository's own settings. Returns
402/// `Outcome<RunnerSettings>`.
403#[derive(Debug, Serialize, Deserialize)]
404pub struct SetRunnerSettingsArgs {
405 pub actor: User,
406 #[serde(flatten)]
407 pub owner: RunnersOwner,
408 #[serde(default)]
409 pub agents_on_self_hosted: Option<bool>,
410 #[serde(default)]
411 pub agent_labels: Option<Vec<String>>,
412 #[serde(default)]
413 pub fork_pull_requests: Option<bool>,
414 #[serde(default)]
415 pub inherit: bool,
416}
417
418/// `stuck_jobs`: Mission control's Needs you. Returns `Vec<StuckJob>`.
419#[derive(Debug, Serialize, Deserialize)]
420pub struct StuckJobsArgs {
421 pub viewer: crate::Viewer,
422}
423
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.424/// `runner_activity`: what runs for a workspace now, on g1t's cloud and
425/// handed to its own runners, for the Runners page. Owners, as the
426/// workspace's runners are. Snake case both ways. Returns
427/// `Outcome<RunnerActivity>`.
428#[derive(Debug, Serialize, Deserialize)]
429pub struct RunnerActivityArgs {
430 pub actor: User,
431 pub workspace: String,
432}
433
434/// A workflow job running in one of g1t's sandboxes.
435#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
436pub struct CloudJob {
437 pub id: String,
438 pub name: String,
439 /// The workflow run it is in.
440 pub run_id: String,
441 /// `owner/name`.
442 pub repo: String,
443 pub started_at: Option<String>,
444}
445
446/// Agent work handed to one of the workspace's own runners, waiting for
447/// one or taken.
448#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
449pub struct HandedOverTask {
450 pub id: String,
451 /// The agent run it is, when the sandbox said; older tasks have none.
452 pub run_id: Option<String>,
453 /// `agent`, `checks`, `review`, `queue`…
454 pub kind: String,
455 pub title: String,
456 /// `owner/name`.
457 pub repo: String,
458 /// `queued` or `in_progress`.
459 pub status: String,
460 pub runner_name: Option<String>,
461 pub created_at: String,
462 pub started_at: Option<String>,
463}
464
465#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
466pub struct RunnerActivity {
467 /// Workflow jobs in g1t's sandboxes now, newest first, at most
468 /// `ACTIVITY_LIMIT`.
469 pub cloud_jobs: Vec<CloudJob>,
470 /// Workflow jobs queued for g1t's sandboxes.
471 pub cloud_jobs_queued: u32,
472 /// Agent work on, or waiting for, the workspace's own runners.
473 pub handed_over: Vec<HandedOverTask>,
474 /// Workflow jobs waiting for one of the workspace's own runners.
475 pub self_hosted_jobs_queued: u32,
476}
477
478/// The most jobs and tasks `runner_activity` lists of each.
479pub const ACTIVITY_LIMIT: u32 = 100;
480
The Packages page names every registry it has481/// A job that has waited ten minutes or more for a self-hosted runner,
482/// with none that matches online.
483#[derive(Clone, Debug, Serialize, Deserialize)]
484#[serde(rename_all = "camelCase")]
485pub struct StuckJob {
486 pub id: String,
487 pub name: String,
488 pub run_id: String,
489 /// `owner/name`.
490 pub repo: String,
491 /// What it asks for, for people: `self-hosted, linux, gpu`.
492 pub labels: String,
493 pub queued_at: String,
494}
495
496// --- The runner's side (snake_case on the wire, as the API passes it) ------
497
498/// `runner_register`: a machine registering with a registration token.
499/// Returns `Outcome<Registered>`.
500#[derive(Clone, Debug, Default, Serialize, Deserialize)]
501pub struct RegisterArgs {
502 pub token: String,
503 pub name: String,
504 #[serde(default)]
505 pub labels: Vec<String>,
506 pub os: String,
507 pub arch: String,
508 #[serde(default)]
509 pub version: String,
510 #[serde(default)]
511 pub ephemeral: bool,
512 /// A group by name, for a workspace's token; the token's group, else
513 /// the default, if left out.
514 #[serde(default)]
515 pub group: Option<String>,
516 /// Replace a runner of the same name instead of refusing.
517 #[serde(default)]
518 pub replace: bool,
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9519 /// How the runner keeps work from its machine: `nested-container` (a
520 /// container per job, inside the runner's own sandboxed container),
Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it.521 /// `vm` (a throwaway VM per job, native to the machine's OS),
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9522 /// `container` (a container per job, on a Docker Engine it shares with
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.523 /// its machine) or `host` (directly on the machine). A `host` runner is
524 /// given no public repository's work ([`isolation_allows`]); older
525 /// runners leave it out.
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9526 #[serde(default)]
527 pub isolation: Option<String>,
A computer whose runner is registered again takes its own runner's name back instead of being refused. When the desktop app's sandbox was made again (a new VM, its registration gone), its runners registered under the names the old sandbox's still held on g1t, were refused with a 409 asking for --replace, and tried again forever. A runner now says which computer it is when it registers: a fingerprint g1t-runner works out from the operating system's machine id (Windows' MachineGuid, macOS's platform UUID, Linux's /etc/machine-id), hashed so the id never leaves the computer, or from --machine / G1T_RUNNER_MACHINE; a container sends none, since its machine id is its image's, and g1t-runner sandbox passes the computer's to the runner in the VM. The actions service keeps it hashed again with the workspace (migration 0022 adds runners.machine_hash), and a registration of a name already taken replaces that runner when it was registered from the same computer, or, for a runner from before fingerprints, when the same person made both tokens and the old runner has been offline for 90 seconds; a runner of the same name from another computer is still refused without --replace, and the refusal says so. The self-hosted runners guide covers registering again and --machine.528 /// Which computer it is: a fingerprint the runner derives from its
529 /// operating system's machine id, never the id itself. A runner of the
530 /// same name registered from the same computer is taken over without
531 /// `replace`, so a computer whose sandbox was made again registers
532 /// again by itself. Older runners, and those in a container that can't
533 /// tell, leave it out.
534 #[serde(default)]
535 pub machine: Option<String>,
The Packages page names every registry it has536}
537
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9538/// The isolation levels a runner may say it has (`RegisterArgs::isolation`).
Merge the runner sandbox: g1t-runner sandbox up makes a person's own computer a self-hosted runner that nothing a job runs can get out of. Linux jobs run in a Linux VM of the runner's own (a WSL 2 distribution with no host mounts, interop or Docker socket on Windows; vfkit on macOS; Sysbox, or QEMU and KVM, on Linux), each in a fresh container under Sysbox inside it; on Windows 11 Pro and up, Windows jobs each run in a Windows Sandbox discarded after the job. Runners report their isolation, and the Runners page shows it.539pub const ISOLATIONS: &[&str] = &["nested-container", "vm", "container", "host"];
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9540
The Packages page names every registry it has541#[derive(Clone, Debug, Serialize, Deserialize)]
542pub struct Registered {
543 pub runner: Runner,
544 /// Shown once; the runner keeps it in its configuration.
545 pub credential: String,
546}
547
548/// Every call a registered runner makes carries its id and credential.
549#[derive(Clone, Debug, Default, Serialize, Deserialize)]
550pub struct RunnerAuth {
551 pub runner: String,
552 pub credential: String,
553}
554
555/// `runner_poll`: waits up to `wait_ms` for work. Doubles as the runner's
556/// heartbeat. Returns `Outcome<Poll>`.
557#[derive(Clone, Debug, Default, Serialize, Deserialize)]
558pub struct PollArgs {
559 #[serde(flatten)]
560 pub auth: RunnerAuth,
561 #[serde(default)]
562 pub version: String,
563 /// What it is running now; it is given nothing more while busy.
564 #[serde(default)]
565 pub running: Vec<String>,
566 #[serde(default)]
567 pub wait_ms: u64,
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.568 /// How the runner keeps work from its machine now, as at registration
569 /// (`RegisterArgs::isolation`); kept up to date from each poll. Older
570 /// runners leave it out.
571 #[serde(default)]
572 pub isolation: Option<String>,
The Packages page names every registry it has573}
574
575/// Work for a runner.
576#[derive(Clone, Debug, Serialize, Deserialize)]
577pub struct Assignment {
578 /// `workflow` or `agent`.
579 pub kind: String,
580 pub id: String,
581 pub name: String,
582 pub repo: String,
583 pub timeout_minutes: u32,
584 /// The image to run it in: the job's `container:`, when it names one.
585 pub image: Option<String>,
586 /// A workflow job's own token, for `ACTIONS_JOB`/`ACTIONS_TOKEN`.
587 pub token: Option<String>,
588 /// An agent task's environment, as g1t's sandbox would have been
589 /// started with: `MODE`, the run's short-lived credentials, the model
590 /// proxy's address.
591 pub env: Option<Map<String, Value>>,
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9592 /// The repository's visibility, `public` or `private`, when g1t knows
593 /// it: a runner that runs work directly on its machine refuses a
594 /// public repository's. Older g1t leaves it out.
595 #[serde(default)]
596 pub visibility: Option<String>,
The Packages page names every registry it has597}
598
599#[derive(Clone, Debug, Default, Serialize, Deserialize)]
600pub struct Poll {
601 pub assignment: Option<Assignment>,
602 /// Work it holds that it should stop: cancelled, timed out, removed.
603 #[serde(default)]
604 pub cancel: Vec<String>,
605 /// A new credential: it replaces the old, which stops working once this
606 /// one is used.
607 pub credential: Option<String>,
608 /// The runner was removed: stop and forget the credential.
609 #[serde(default)]
610 pub removed: bool,
611}
612
613/// `runner_finished`: a runner telling what became of work it was given.
614/// For a workflow job the harness has normally reported already; this
615/// covers one that crashed or was killed. Returns `Outcome<bool>` (whether
616/// it changed anything).
617#[derive(Clone, Debug, Default, Serialize, Deserialize)]
618pub struct FinishedArgs {
619 #[serde(flatten)]
620 pub auth: RunnerAuth,
621 pub id: String,
622 pub exit_code: i32,
623 #[serde(default)]
624 pub reason: Option<String>,
625}
626
627/// `runner_remove_self`: `g1t-runner remove`. Returns `Outcome<bool>`.
628#[derive(Clone, Debug, Default, Serialize, Deserialize)]
629pub struct RemoveSelfArgs {
630 #[serde(flatten)]
631 pub auth: RunnerAuth,
632}
633
634// --- Between services (camelCase, as every service speaks) ----------------
635
636/// `runner_route`: whether a kind of g1t's own work in a repository runs on
637/// self-hosted runners, and on which labels. Asked by the runner service
638/// before it starts a sandbox. Returns `Option<Vec<String>>`.
639#[derive(Clone, Debug, Serialize, Deserialize)]
640#[serde(rename_all = "camelCase")]
641pub struct RouteArgs {
642 pub workspace: String,
643 pub repo: RepoPath,
644}
645
646/// `enqueue_task`: agent work for a self-hosted runner, from the runner
647/// service's sandbox (`sandbox` is its Durable Object's id), which is told
648/// with `task_ended` how it went. Returns `Outcome<String>` (the task's id).
649#[derive(Clone, Debug, Serialize, Deserialize)]
650#[serde(rename_all = "camelCase")]
651pub struct EnqueueTaskArgs {
652 pub sandbox: String,
653 pub workspace: String,
654 pub repo: RepoPath,
655 /// The run kind: `agent`, `checks`, `review`, `queue`…
656 pub kind: String,
657 pub title: String,
658 pub labels: Vec<String>,
659 pub env: Map<String, Value>,
660 pub timeout_minutes: u32,
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.661 /// The agent run the work belongs to, when the sandbox opened one: what
662 /// people's pages link a runner's agent work to.
663 #[serde(default, alias = "run_id")]
664 pub run_id: Option<String>,
The Packages page names every registry it has665}
666
667/// `cancel_task`: the sandbox's work stopped from g1t's side (a person, a
668/// time cap). Returns `Outcome<bool>`.
669#[derive(Clone, Debug, Serialize, Deserialize)]
670#[serde(rename_all = "camelCase")]
671pub struct CancelTaskArgs {
672 pub sandbox: String,
673 #[serde(default)]
674 pub reason: Option<String>,
675}
676
677// --- Labels ----------------------------------------------------------------
678
679/// A label as it is kept: trimmed and lowercase. `None` when it is not
680/// one: empty, too long, or with characters other than letters, digits,
681/// `-`, `_`, `.`, `:` and `/`.
682pub fn clean_label(label: &str) -> Option<String> {
683 let label = label.trim().to_ascii_lowercase();
684 let ok = !label.is_empty()
685 && label.len() <= MAX_LABEL_LEN
686 && label.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | ':' | '/'));
687 ok.then_some(label)
688}
689
690/// A runner's labels: what it was given, cleaned, with `self-hosted`, its
691/// OS and its architecture, each once, in that order first.
692pub fn runner_labels(given: &[String], os: &str, arch: &str) -> Result<Vec<String>, String> {
693 let mut labels: Vec<String> = Vec::new();
694 for label in [SELF_HOSTED, os, arch].iter().map(|s| s.to_string()).chain(given.iter().cloned()) {
695 let Some(clean) = clean_label(&label) else {
696 return Err(format!("`{label}` is not a label: use letters, digits, `-`, `_`, `.`, `:` and `/`, up to {MAX_LABEL_LEN} characters."));
697 };
698 if !labels.contains(&clean) {
699 labels.push(clean);
700 }
701 }
702 if labels.len() > MAX_LABELS {
703 return Err(format!("A runner has at most {MAX_LABELS} labels."));
704 }
705 Ok(labels)
706}
707
708/// The OS a runner reports, as its label: `linux`, `macos` or `windows`.
709pub fn os_label(os: &str) -> Option<&'static str> {
710 match os.trim().to_ascii_lowercase().as_str() {
711 "linux" => Some("linux"),
712 "macos" | "darwin" | "osx" | "mac" => Some("macos"),
713 "windows" | "win" => Some("windows"),
714 _ => None,
715 }
716}
717
718/// The architecture a runner reports, as its label: `x64` or `arm64`.
719pub fn arch_label(arch: &str) -> Option<&'static str> {
720 match arch.trim().to_ascii_lowercase().as_str() {
721 "x64" | "x86_64" | "amd64" => Some("x64"),
722 "arm64" | "aarch64" => Some("arm64"),
723 _ => None,
724 }
725}
726
727/// What a job's `runs-on` asks for, read for self-hosted runners: its
728/// labels, cleaned, and the group it names. `self_hosted` when it names
729/// `self-hosted` or a group, which only self-hosted runners can satisfy.
730#[derive(Clone, Debug, Default, PartialEq, Eq)]
731pub struct Wanted {
732 pub labels: Vec<String>,
733 pub group: Option<String>,
734 pub self_hosted: bool,
735}
736
737impl Wanted {
738 pub fn of(labels: &[String], group: Option<&str>) -> Wanted {
739 let mut out: Vec<String> = Vec::new();
740 for label in labels {
741 let clean = label.trim().to_ascii_lowercase();
742 if !clean.is_empty() && !out.contains(&clean) {
743 out.push(clean);
744 }
745 }
746 let group = group.map(|g| g.trim().to_owned()).filter(|g| !g.is_empty());
747 let self_hosted = group.is_some() || out.iter().any(|l| l == SELF_HOSTED);
748 Wanted { labels: out, group, self_hosted }
749 }
750
751 /// The job's labels as stored on it: its labels, then `group:<name>`.
752 pub fn stored(&self) -> Vec<String> {
753 let mut out = self.labels.clone();
754 if let Some(group) = &self.group {
755 out.push(format!("group:{}", group.to_ascii_lowercase()));
756 }
757 out
758 }
759
760 /// Back from what [`Wanted::stored`] kept.
761 pub fn from_stored(stored: &[String]) -> Wanted {
762 let group = stored.iter().find_map(|l| l.strip_prefix("group:").map(str::to_owned));
763 let labels: Vec<String> = stored.iter().filter(|l| !l.starts_with("group:")).cloned().collect();
764 Wanted::of(&labels, group.as_deref())
765 }
766
767 /// Whether a runner with `labels`, in the group called `group`, can
768 /// take it: every label it asks for, and its group if it names one.
769 pub fn matches(&self, labels: &[String], group: Option<&str>) -> bool {
770 let has = |wanted: &String| labels.iter().any(|l| l.eq_ignore_ascii_case(wanted));
771 let group_ok = match (&self.group, group) {
772 (None, _) => true,
773 (Some(wanted), Some(group)) => wanted.eq_ignore_ascii_case(group),
774 (Some(_), None) => false,
775 };
776 group_ok && self.labels.iter().all(has)
777 }
778
779 /// For people: `self-hosted, linux, gpu` (in group `build`).
780 pub fn describe(&self) -> String {
781 let labels = self.labels.join(", ");
782 match &self.group {
783 Some(group) if labels.is_empty() => format!("in group {group}"),
784 Some(group) => format!("{labels} in group {group}"),
785 None => labels,
786 }
787 }
788}
789
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.790/// What a public repository's job says while the only runners that could
791/// take it run jobs directly on their machines (`host` isolation), which
792/// are never given a public repository's work.
793pub const WAITING_FOR_SANDBOX: &str =
794 "Waiting for a sandboxed runner: this repository is public, and host-mode runners don't run public repositories' jobs.";
795
796/// Whether a runner of `isolation` may be given a repository's work, by
797/// its visibility (`public` or `private`; unknown lets it through, and the
798/// runner decides). A host-mode runner takes no public repository's work.
799pub fn isolation_allows(isolation: Option<&str>, visibility: Option<&str>) -> bool {
800 !(isolation == Some("host") && visibility == Some("public"))
801}
802
The Packages page names every registry it has803/// What a job waiting for a runner says.
804pub fn waiting_reason(wanted: &Wanted) -> String {
805 format!("Waiting for a self-hosted runner with labels {}.", wanted.describe())
806}
807
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.808// --- Staff only (sudo.g1t.sh) ---
809
810/// `admin_runners`: a workspace's self-hosted runners, its own and its
811/// repositories', for staff. Returns `Vec<AdminRunner>`.
812#[derive(Debug, Serialize, Deserialize)]
813pub struct AdminRunnersArgs {
814 pub workspace: String,
815}
816
817/// A self-hosted runner as staff see it.
818#[derive(Clone, Debug, Serialize, Deserialize)]
819#[serde(rename_all = "camelCase")]
820pub struct AdminRunner {
821 pub id: String,
822 pub name: String,
823 /// `workspace/repo` for a repository's own runner.
824 pub repo: Option<String>,
825 pub os: String,
826 pub version: String,
827 /// Running something now.
828 pub busy: bool,
829 /// RFC 3339.
830 pub last_seen_at: Option<String>,
831 /// RFC 3339.
832 pub created_at: String,
833 pub created_by: Option<String>,
834}
835
836/// `admin_remove_runners`: staff remove one of a workspace's runners
837/// (`id`), or every one (`id` null), and end the registration tokens it
838/// has out. What they were running fails; their credentials stop at once.
839/// Returns `Outcome<u32>`, how many were removed.
840#[derive(Debug, Serialize, Deserialize)]
841pub struct AdminRemoveRunnersArgs {
842 pub workspace: String,
843 #[serde(default)]
844 pub id: Option<String>,
845 pub staff: String,
846}
847
The Packages page names every registry it has848#[cfg(test)]
849mod tests {
850 use super::*;
851
852 fn strings(items: &[&str]) -> Vec<String> {
853 items.iter().map(|s| s.to_string()).collect()
854 }
855
856 #[test]
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.857 fn a_host_mode_runner_takes_no_public_repositorys_work() {
858 assert!(!isolation_allows(Some("host"), Some("public")));
859 assert!(isolation_allows(Some("host"), Some("private")));
860 assert!(isolation_allows(Some("host"), None));
861 assert!(isolation_allows(Some("container"), Some("public")));
862 assert!(isolation_allows(None, Some("public")), "an older runner decides itself");
863 let poll: PollArgs = serde_json::from_value(serde_json::json!({ "runner": "rnr_1", "credential": "c" })).unwrap();
864 assert_eq!(poll.isolation, None, "an older runner's poll");
865 }
866
867 #[test]
The Packages page names every registry it has868 fn a_runner_always_has_self_hosted_its_os_and_its_arch() {
869 let labels = runner_labels(&strings(&["GPU", "gpu", " cuda-12 "]), "linux", "x64").unwrap();
870 assert_eq!(labels, strings(&["self-hosted", "linux", "x64", "gpu", "cuda-12"]));
871 assert!(runner_labels(&strings(&["has space"]), "linux", "x64").is_err());
872 assert!(runner_labels(&strings(&[""]), "linux", "x64").is_err());
873 }
874
875 #[test]
876 fn os_and_arch_are_read_as_people_write_them() {
877 assert_eq!(os_label("Darwin"), Some("macos"));
878 assert_eq!(os_label("Windows"), Some("windows"));
879 assert_eq!(os_label("plan9"), None);
880 assert_eq!(arch_label("x86_64"), Some("x64"));
881 assert_eq!(arch_label("aarch64"), Some("arm64"));
882 assert_eq!(arch_label("mips"), None);
883 }
884
885 #[test]
886 fn a_job_names_self_hosted_or_a_group_to_need_one() {
887 assert!(Wanted::of(&strings(&["Self-Hosted", "linux"]), None).self_hosted);
888 assert!(Wanted::of(&strings(&[]), Some("build")).self_hosted);
889 assert!(!Wanted::of(&strings(&["ubuntu-latest"]), None).self_hosted);
890 }
891
892 #[test]
893 fn every_label_the_job_names_must_be_the_runners() {
894 let runner = strings(&["self-hosted", "linux", "x64", "gpu"]);
895 assert!(Wanted::of(&strings(&["self-hosted"]), None).matches(&runner, Some("default")));
896 assert!(Wanted::of(&strings(&["self-hosted", "LINUX", "gpu"]), None).matches(&runner, None));
897 assert!(!Wanted::of(&strings(&["self-hosted", "linux", "arm64"]), None).matches(&runner, None));
898 assert!(!Wanted::of(&strings(&["self-hosted", "windows"]), None).matches(&runner, None));
899 }
900
901 #[test]
902 fn a_group_is_matched_by_name() {
903 let runner = strings(&["self-hosted", "linux", "x64"]);
904 let wanted = Wanted::of(&strings(&["linux"]), Some("Build"));
905 assert!(wanted.matches(&runner, Some("build")));
906 assert!(!wanted.matches(&runner, Some("default")));
907 assert!(!wanted.matches(&runner, None));
908 assert_eq!(Wanted::from_stored(&wanted.stored()), wanted.clone().tap_lower());
909 }
910
911 impl Wanted {
912 fn tap_lower(mut self) -> Wanted {
913 self.group = self.group.map(|g| g.to_ascii_lowercase());
914 self
915 }
916 }
917
918 #[test]
919 fn waiting_says_for_what() {
920 let wanted = Wanted::of(&strings(&["self-hosted", "linux", "gpu"]), None);
921 assert_eq!(waiting_reason(&wanted), "Waiting for a self-hosted runner with labels self-hosted, linux, gpu.");
922 }
923
924 #[test]
925 fn the_runners_wire_is_snake_case() {
926 let poll: PollArgs = serde_json::from_value(serde_json::json!({
927 "runner": "rnr_1", "credential": "g1tr_x", "version": "0.1.0", "running": ["job_1"], "wait_ms": 1000
928 }))
929 .unwrap();
930 assert_eq!(poll.auth.runner, "rnr_1");
931 assert_eq!(poll.wait_ms, 1000);
932 let assignment = serde_json::to_value(Assignment {
933 kind: "workflow".into(),
934 id: "job_1".into(),
935 name: "build".into(),
936 repo: "acme/web".into(),
937 timeout_minutes: 60,
938 image: None,
939 token: Some("t".into()),
940 env: None,
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9941 visibility: Some("private".into()),
The Packages page names every registry it has942 })
943 .unwrap();
944 assert_eq!(assignment["timeout_minutes"], 60);
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9945 assert_eq!(assignment["visibility"], "private");
946 // An older runner's registration, without an isolation level.
947 let register: RegisterArgs = serde_json::from_value(serde_json::json!({ "token": "g1trt_x", "name": "a", "os": "linux", "arch": "x64" })).unwrap();
948 assert_eq!(register.isolation, None);
The Packages page names every registry it has949 }
950}

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