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.
| Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0 | 1 | //! 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 | ||
| 31 | use serde::{Deserialize, Serialize}; | |
| 32 | use serde_json::{Map, Value}; | |
| 33 | ||
| 34 | use crate::User; | |
| 35 | use crate::repos::RepoPath; | |
| 36 | ||
| 37 | /// Every runner has these labels, whatever else it was given. | |
| 38 | pub const SELF_HOSTED: &str = "self-hosted"; | |
| 39 | /// What a registration token starts with. | |
| 40 | pub const REGISTRATION_PREFIX: &str = "g1trt_"; | |
| 41 | /// What a runner's credential starts with. | |
| 42 | pub const CREDENTIAL_PREFIX: &str = "g1tr_"; | |
| 43 | /// How long a registration token lasts, in seconds. | |
| 44 | pub const REGISTRATION_TTL_SECONDS: u64 = 60 * 60; | |
| 45 | /// How long a job may wait for a self-hosted runner before it fails. | |
| 46 | pub const MAX_WAIT_HOURS: u64 = 24; | |
| 47 | /// A runner that has not polled for this long is offline. | |
| 48 | pub const ONLINE_WITHIN_MS: u64 = 90 * 1000; | |
| 49 | /// How long one poll waits for work before answering with none. | |
| 50 | pub const MAX_POLL_WAIT_MS: u64 = 20 * 1000; | |
| 51 | /// How often a runner's credential is replaced. | |
| 52 | pub const ROTATE_AFTER_MS: u64 = 24 * 60 * 60 * 1000; | |
| 53 | /// The most labels a runner has, and the longest one. | |
| 54 | pub const MAX_LABELS: usize = 30; | |
| 55 | pub const MAX_LABEL_LEN: usize = 64; | |
| 56 | /// The most runners a workspace has. | |
| 57 | pub const MAX_RUNNERS: u32 = 500; | |
| 58 | /// The most runners a workspace registers in one minute. | |
| 59 | pub const MAX_REGISTRATIONS_PER_MINUTE: u32 = 30; | |
| 60 | /// The most registration tokens a workspace makes in one hour. | |
| 61 | pub 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")] | |
| 68 | pub 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. | |
| Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0 | 80 | pub run_id: Option<String>, |
| 81 | pub started_at: Option<String>, | |
| 82 | } | |
| 83 | ||
| 84 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 85 | #[serde(rename_all = "camelCase")] | |
| 86 | pub 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>, | |
| Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0 | 114 | } |
| 115 | ||
| 116 | /// Which of a workspace's repositories may use its runners. | |
| 117 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 118 | #[serde(rename_all = "camelCase")] | |
| 119 | pub 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")] | |
| 134 | pub 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")] | |
| 148 | pub 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)] | |
| 167 | pub 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)] | |
| 177 | pub 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)] | |
| 186 | pub 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)] | |
| 199 | pub 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)] | |
| 208 | pub 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)] | |
| 216 | pub 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)] | |
| 231 | pub 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. | |
| 246 | pub const DESKTOP_OPEN_WITHIN_MS: u64 = 10 * 60 * 1000; | |
| 247 | /// The most desktops a workspace keeps reports for. | |
| 248 | pub const MAX_DESKTOPS: u32 = 200; | |
| 249 | ||
| 250 | /// A diagnosis, as the app made it. | |
| 251 | #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] | |
| 252 | pub 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 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. | 266 | } |
| 267 | ||
| 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. | 268 | /// One kind of job a sandboxed desktop runs, as its app says it. |
| 269 | #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] | |
| 270 | pub struct DesktopCapability { | |
| 271 | /// `linux`, `windows` or `macos`. | |
| 272 | pub id: String, | |
| 273 | /// `Linux jobs`. | |
| 274 | pub label: String, | |
| 275 | pub tone: String, | |
| 276 | /// `Waiting for jobs`, `Not available`. | |
| 277 | pub title: String, | |
| 278 | #[serde(default)] | |
| 279 | pub cause: Option<String>, | |
| 280 | #[serde(default)] | |
| 281 | pub fix: Option<String>, | |
| 282 | } | |
| 283 | ||
| 284 | /// The most kinds of job a desktop reports. | |
| 285 | pub const MAX_DESKTOP_CAPABILITIES: usize = 4; | |
| 286 | ||
| 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. | 287 | /// One person's computer, as its desktop app last said. |
| 288 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 289 | #[serde(rename_all = "camelCase")] | |
| 290 | pub struct DesktopRunner { | |
| 291 | pub workspace: String, | |
| 292 | /// The computer's name, which its runner has too. | |
| 293 | pub machine: String, | |
| 294 | /// Whose computer: their username. | |
| 295 | pub person: String, | |
| 296 | /// `sandboxed`, `docker` or `host`. | |
| 297 | pub backend: String, | |
| 298 | pub version: Option<String>, | |
| 299 | pub diagnosis: DesktopDiagnosis, | |
| 300 | /// What to show while g1t is closed on it. | |
| 301 | pub closed: DesktopDiagnosis, | |
| 302 | /// Heard from within `DESKTOP_OPEN_WITHIN_MS`: g1t is open there. | |
| 303 | pub open: bool, | |
| 304 | pub reported_at: String, | |
| 305 | } | |
| 306 | ||
| 307 | /// `report_desktop`: the person's own desktop app, through the site, for a | |
| 308 | /// workspace they own. Returns `Outcome<bool>`. | |
| 309 | #[derive(Debug, Serialize, Deserialize)] | |
| 310 | pub struct ReportDesktopArgs { | |
| 311 | pub actor: User, | |
| 312 | pub workspace: String, | |
| 313 | pub machine: String, | |
| 314 | pub backend: String, | |
| 315 | #[serde(default)] | |
| 316 | pub version: Option<String>, | |
| 317 | pub diagnosis: DesktopDiagnosis, | |
| 318 | pub closed: DesktopDiagnosis, | |
| 319 | } | |
| 320 | ||
| 321 | /// `desktop_runners`: owners of the workspace. Returns `Outcome<Vec<DesktopRunner>>`. | |
| 322 | #[derive(Debug, Serialize, Deserialize)] | |
| 323 | pub struct DesktopRunnersArgs { | |
| 324 | pub actor: User, | |
| 325 | pub workspace: String, | |
| 326 | } | |
| 327 | ||
| Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0 | 328 | /// `runner_settings`: members of the workspace, or admins of the |
| 329 | /// repository. Returns `Outcome<RunnerSettings>`. | |
| 330 | #[derive(Debug, Serialize, Deserialize)] | |
| 331 | pub struct RunnerSettingsArgs { | |
| 332 | pub actor: User, | |
| 333 | #[serde(flatten)] | |
| 334 | pub owner: RunnersOwner, | |
| 335 | } | |
| 336 | ||
| 337 | /// `set_runner_settings`: owners, or admins of the repository. Left out is | |
| 338 | /// unchanged; `inherit` drops a repository's own settings. Returns | |
| 339 | /// `Outcome<RunnerSettings>`. | |
| 340 | #[derive(Debug, Serialize, Deserialize)] | |
| 341 | pub struct SetRunnerSettingsArgs { | |
| 342 | pub actor: User, | |
| 343 | #[serde(flatten)] | |
| 344 | pub owner: RunnersOwner, | |
| 345 | #[serde(default)] | |
| 346 | pub agents_on_self_hosted: Option<bool>, | |
| 347 | #[serde(default)] | |
| 348 | pub agent_labels: Option<Vec<String>>, | |
| 349 | #[serde(default)] | |
| 350 | pub fork_pull_requests: Option<bool>, | |
| 351 | #[serde(default)] | |
| 352 | pub inherit: bool, | |
| 353 | } | |
| 354 | ||
| 355 | /// `stuck_jobs`: Mission control's Needs you. Returns `Vec<StuckJob>`. | |
| 356 | #[derive(Debug, Serialize, Deserialize)] | |
| 357 | pub struct StuckJobsArgs { | |
| 358 | pub viewer: crate::Viewer, | |
| 359 | } | |
| 360 | ||
| 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. | 361 | /// `runner_activity`: what runs for a workspace now, on g1t's cloud and |
| 362 | /// handed to its own runners, for the Runners page. Owners, as the | |
| 363 | /// workspace's runners are. Snake case both ways. Returns | |
| 364 | /// `Outcome<RunnerActivity>`. | |
| 365 | #[derive(Debug, Serialize, Deserialize)] | |
| 366 | pub struct RunnerActivityArgs { | |
| 367 | pub actor: User, | |
| 368 | pub workspace: String, | |
| 369 | } | |
| 370 | ||
| 371 | /// A workflow job running in one of g1t's sandboxes. | |
| 372 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] | |
| 373 | pub struct CloudJob { | |
| 374 | pub id: String, | |
| 375 | pub name: String, | |
| 376 | /// The workflow run it is in. | |
| 377 | pub run_id: String, | |
| 378 | /// `owner/name`. | |
| 379 | pub repo: String, | |
| 380 | pub started_at: Option<String>, | |
| 381 | } | |
| 382 | ||
| 383 | /// Agent work handed to one of the workspace's own runners, waiting for | |
| 384 | /// one or taken. | |
| 385 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] | |
| 386 | pub struct HandedOverTask { | |
| 387 | pub id: String, | |
| 388 | /// The agent run it is, when the sandbox said; older tasks have none. | |
| 389 | pub run_id: Option<String>, | |
| 390 | /// `agent`, `checks`, `review`, `queue`… | |
| 391 | pub kind: String, | |
| 392 | pub title: String, | |
| 393 | /// `owner/name`. | |
| 394 | pub repo: String, | |
| 395 | /// `queued` or `in_progress`. | |
| 396 | pub status: String, | |
| 397 | pub runner_name: Option<String>, | |
| 398 | pub created_at: String, | |
| 399 | pub started_at: Option<String>, | |
| 400 | } | |
| 401 | ||
| 402 | #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] | |
| 403 | pub struct RunnerActivity { | |
| 404 | /// Workflow jobs in g1t's sandboxes now, newest first, at most | |
| 405 | /// `ACTIVITY_LIMIT`. | |
| 406 | pub cloud_jobs: Vec<CloudJob>, | |
| 407 | /// Workflow jobs queued for g1t's sandboxes. | |
| 408 | pub cloud_jobs_queued: u32, | |
| 409 | /// Agent work on, or waiting for, the workspace's own runners. | |
| 410 | pub handed_over: Vec<HandedOverTask>, | |
| 411 | /// Workflow jobs waiting for one of the workspace's own runners. | |
| 412 | pub self_hosted_jobs_queued: u32, | |
| 413 | } | |
| 414 | ||
| 415 | /// The most jobs and tasks `runner_activity` lists of each. | |
| 416 | pub const ACTIVITY_LIMIT: u32 = 100; | |
| 417 | ||
| Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0 | 418 | /// A job that has waited ten minutes or more for a self-hosted runner, |
| 419 | /// with none that matches online. | |
| 420 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 421 | #[serde(rename_all = "camelCase")] | |
| 422 | pub struct StuckJob { | |
| 423 | pub id: String, | |
| 424 | pub name: String, | |
| 425 | pub run_id: String, | |
| 426 | /// `owner/name`. | |
| 427 | pub repo: String, | |
| 428 | /// What it asks for, for people: `self-hosted, linux, gpu`. | |
| 429 | pub labels: String, | |
| 430 | pub queued_at: String, | |
| 431 | } | |
| 432 | ||
| 433 | // --- The runner's side (snake_case on the wire, as the API passes it) ------ | |
| 434 | ||
| 435 | /// `runner_register`: a machine registering with a registration token. | |
| 436 | /// Returns `Outcome<Registered>`. | |
| 437 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] | |
| 438 | pub struct RegisterArgs { | |
| 439 | pub token: String, | |
| 440 | pub name: String, | |
| 441 | #[serde(default)] | |
| 442 | pub labels: Vec<String>, | |
| 443 | pub os: String, | |
| 444 | pub arch: String, | |
| 445 | #[serde(default)] | |
| 446 | pub version: String, | |
| 447 | #[serde(default)] | |
| 448 | pub ephemeral: bool, | |
| 449 | /// A group by name, for a workspace's token; the token's group, else | |
| 450 | /// the default, if left out. | |
| 451 | #[serde(default)] | |
| 452 | pub group: Option<String>, | |
| 453 | /// Replace a runner of the same name instead of refusing. | |
| 454 | #[serde(default)] | |
| 455 | pub replace: bool, | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 456 | /// How the runner keeps work from its machine: `nested-container` (a |
| 457 | /// 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. | 458 | /// `vm` (a throwaway VM per job, native to the machine's OS), |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 459 | /// `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. | 460 | /// its machine) or `host` (directly on the machine). A `host` runner is |
| 461 | /// given no public repository's work ([`isolation_allows`]); older | |
| 462 | /// runners leave it out. | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 463 | #[serde(default)] |
| 464 | pub isolation: Option<String>, | |
| Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0 | 465 | } |
| 466 | ||
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 467 | /// 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. | 468 | pub const ISOLATIONS: &[&str] = &["nested-container", "vm", "container", "host"]; |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 469 | |
| Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0 | 470 | #[derive(Clone, Debug, Serialize, Deserialize)] |
| 471 | pub struct Registered { | |
| 472 | pub runner: Runner, | |
| 473 | /// Shown once; the runner keeps it in its configuration. | |
| 474 | pub credential: String, | |
| 475 | } | |
| 476 | ||
| 477 | /// Every call a registered runner makes carries its id and credential. | |
| 478 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] | |
| 479 | pub struct RunnerAuth { | |
| 480 | pub runner: String, | |
| 481 | pub credential: String, | |
| 482 | } | |
| 483 | ||
| 484 | /// `runner_poll`: waits up to `wait_ms` for work. Doubles as the runner's | |
| 485 | /// heartbeat. Returns `Outcome<Poll>`. | |
| 486 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] | |
| 487 | pub struct PollArgs { | |
| 488 | #[serde(flatten)] | |
| 489 | pub auth: RunnerAuth, | |
| 490 | #[serde(default)] | |
| 491 | pub version: String, | |
| 492 | /// What it is running now; it is given nothing more while busy. | |
| 493 | #[serde(default)] | |
| 494 | pub running: Vec<String>, | |
| 495 | #[serde(default)] | |
| 496 | 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. | 497 | /// How the runner keeps work from its machine now, as at registration |
| 498 | /// (`RegisterArgs::isolation`); kept up to date from each poll. Older | |
| 499 | /// runners leave it out. | |
| 500 | #[serde(default)] | |
| 501 | pub isolation: Option<String>, | |
| Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0 | 502 | } |
| 503 | ||
| 504 | /// Work for a runner. | |
| 505 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 506 | pub struct Assignment { | |
| 507 | /// `workflow` or `agent`. | |
| 508 | pub kind: String, | |
| 509 | pub id: String, | |
| 510 | pub name: String, | |
| 511 | pub repo: String, | |
| 512 | pub timeout_minutes: u32, | |
| 513 | /// The image to run it in: the job's `container:`, when it names one. | |
| 514 | pub image: Option<String>, | |
| 515 | /// A workflow job's own token, for `ACTIONS_JOB`/`ACTIONS_TOKEN`. | |
| 516 | pub token: Option<String>, | |
| 517 | /// An agent task's environment, as g1t's sandbox would have been | |
| 518 | /// started with: `MODE`, the run's short-lived credentials, the model | |
| 519 | /// proxy's address. | |
| 520 | pub env: Option<Map<String, Value>>, | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 521 | /// The repository's visibility, `public` or `private`, when g1t knows |
| 522 | /// it: a runner that runs work directly on its machine refuses a | |
| 523 | /// public repository's. Older g1t leaves it out. | |
| 524 | #[serde(default)] | |
| 525 | pub visibility: Option<String>, | |
| Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0 | 526 | } |
| 527 | ||
| 528 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] | |
| 529 | pub struct Poll { | |
| 530 | pub assignment: Option<Assignment>, | |
| 531 | /// Work it holds that it should stop: cancelled, timed out, removed. | |
| 532 | #[serde(default)] | |
| 533 | pub cancel: Vec<String>, | |
| 534 | /// A new credential: it replaces the old, which stops working once this | |
| 535 | /// one is used. | |
| 536 | pub credential: Option<String>, | |
| 537 | /// The runner was removed: stop and forget the credential. | |
| 538 | #[serde(default)] | |
| 539 | pub removed: bool, | |
| 540 | } | |
| 541 | ||
| 542 | /// `runner_finished`: a runner telling what became of work it was given. | |
| 543 | /// For a workflow job the harness has normally reported already; this | |
| 544 | /// covers one that crashed or was killed. Returns `Outcome<bool>` (whether | |
| 545 | /// it changed anything). | |
| 546 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] | |
| 547 | pub struct FinishedArgs { | |
| 548 | #[serde(flatten)] | |
| 549 | pub auth: RunnerAuth, | |
| 550 | pub id: String, | |
| 551 | pub exit_code: i32, | |
| 552 | #[serde(default)] | |
| 553 | pub reason: Option<String>, | |
| 554 | } | |
| 555 | ||
| 556 | /// `runner_remove_self`: `g1t-runner remove`. Returns `Outcome<bool>`. | |
| 557 | #[derive(Clone, Debug, Default, Serialize, Deserialize)] | |
| 558 | pub struct RemoveSelfArgs { | |
| 559 | #[serde(flatten)] | |
| 560 | pub auth: RunnerAuth, | |
| 561 | } | |
| 562 | ||
| 563 | // --- Between services (camelCase, as every service speaks) ---------------- | |
| 564 | ||
| 565 | /// `runner_route`: whether a kind of g1t's own work in a repository runs on | |
| 566 | /// self-hosted runners, and on which labels. Asked by the runner service | |
| 567 | /// before it starts a sandbox. Returns `Option<Vec<String>>`. | |
| 568 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 569 | #[serde(rename_all = "camelCase")] | |
| 570 | pub struct RouteArgs { | |
| 571 | pub workspace: String, | |
| 572 | pub repo: RepoPath, | |
| 573 | } | |
| 574 | ||
| 575 | /// `enqueue_task`: agent work for a self-hosted runner, from the runner | |
| 576 | /// service's sandbox (`sandbox` is its Durable Object's id), which is told | |
| 577 | /// with `task_ended` how it went. Returns `Outcome<String>` (the task's id). | |
| 578 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 579 | #[serde(rename_all = "camelCase")] | |
| 580 | pub struct EnqueueTaskArgs { | |
| 581 | pub sandbox: String, | |
| 582 | pub workspace: String, | |
| 583 | pub repo: RepoPath, | |
| 584 | /// The run kind: `agent`, `checks`, `review`, `queue`… | |
| 585 | pub kind: String, | |
| 586 | pub title: String, | |
| 587 | pub labels: Vec<String>, | |
| 588 | pub env: Map<String, Value>, | |
| 589 | 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. | 590 | /// The agent run the work belongs to, when the sandbox opened one: what |
| 591 | /// people's pages link a runner's agent work to. | |
| 592 | #[serde(default, alias = "run_id")] | |
| 593 | pub run_id: Option<String>, | |
| Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0 | 594 | } |
| 595 | ||
| 596 | /// `cancel_task`: the sandbox's work stopped from g1t's side (a person, a | |
| 597 | /// time cap). Returns `Outcome<bool>`. | |
| 598 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 599 | #[serde(rename_all = "camelCase")] | |
| 600 | pub struct CancelTaskArgs { | |
| 601 | pub sandbox: String, | |
| 602 | #[serde(default)] | |
| 603 | pub reason: Option<String>, | |
| 604 | } | |
| 605 | ||
| 606 | // --- Labels ---------------------------------------------------------------- | |
| 607 | ||
| 608 | /// A label as it is kept: trimmed and lowercase. `None` when it is not | |
| 609 | /// one: empty, too long, or with characters other than letters, digits, | |
| 610 | /// `-`, `_`, `.`, `:` and `/`. | |
| 611 | pub fn clean_label(label: &str) -> Option<String> { | |
| 612 | let label = label.trim().to_ascii_lowercase(); | |
| 613 | let ok = !label.is_empty() | |
| 614 | && label.len() <= MAX_LABEL_LEN | |
| 615 | && label.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | ':' | '/')); | |
| 616 | ok.then_some(label) | |
| 617 | } | |
| 618 | ||
| 619 | /// A runner's labels: what it was given, cleaned, with `self-hosted`, its | |
| 620 | /// OS and its architecture, each once, in that order first. | |
| 621 | pub fn runner_labels(given: &[String], os: &str, arch: &str) -> Result<Vec<String>, String> { | |
| 622 | let mut labels: Vec<String> = Vec::new(); | |
| 623 | for label in [SELF_HOSTED, os, arch].iter().map(|s| s.to_string()).chain(given.iter().cloned()) { | |
| 624 | let Some(clean) = clean_label(&label) else { | |
| 625 | return Err(format!("`{label}` is not a label: use letters, digits, `-`, `_`, `.`, `:` and `/`, up to {MAX_LABEL_LEN} characters.")); | |
| 626 | }; | |
| 627 | if !labels.contains(&clean) { | |
| 628 | labels.push(clean); | |
| 629 | } | |
| 630 | } | |
| 631 | if labels.len() > MAX_LABELS { | |
| 632 | return Err(format!("A runner has at most {MAX_LABELS} labels.")); | |
| 633 | } | |
| 634 | Ok(labels) | |
| 635 | } | |
| 636 | ||
| 637 | /// The OS a runner reports, as its label: `linux`, `macos` or `windows`. | |
| 638 | pub fn os_label(os: &str) -> Option<&'static str> { | |
| 639 | match os.trim().to_ascii_lowercase().as_str() { | |
| 640 | "linux" => Some("linux"), | |
| 641 | "macos" | "darwin" | "osx" | "mac" => Some("macos"), | |
| 642 | "windows" | "win" => Some("windows"), | |
| 643 | _ => None, | |
| 644 | } | |
| 645 | } | |
| 646 | ||
| 647 | /// The architecture a runner reports, as its label: `x64` or `arm64`. | |
| 648 | pub fn arch_label(arch: &str) -> Option<&'static str> { | |
| 649 | match arch.trim().to_ascii_lowercase().as_str() { | |
| 650 | "x64" | "x86_64" | "amd64" => Some("x64"), | |
| 651 | "arm64" | "aarch64" => Some("arm64"), | |
| 652 | _ => None, | |
| 653 | } | |
| 654 | } | |
| 655 | ||
| 656 | /// What a job's `runs-on` asks for, read for self-hosted runners: its | |
| 657 | /// labels, cleaned, and the group it names. `self_hosted` when it names | |
| 658 | /// `self-hosted` or a group, which only self-hosted runners can satisfy. | |
| 659 | #[derive(Clone, Debug, Default, PartialEq, Eq)] | |
| 660 | pub struct Wanted { | |
| 661 | pub labels: Vec<String>, | |
| 662 | pub group: Option<String>, | |
| 663 | pub self_hosted: bool, | |
| 664 | } | |
| 665 | ||
| 666 | impl Wanted { | |
| 667 | pub fn of(labels: &[String], group: Option<&str>) -> Wanted { | |
| 668 | let mut out: Vec<String> = Vec::new(); | |
| 669 | for label in labels { | |
| 670 | let clean = label.trim().to_ascii_lowercase(); | |
| 671 | if !clean.is_empty() && !out.contains(&clean) { | |
| 672 | out.push(clean); | |
| 673 | } | |
| 674 | } | |
| 675 | let group = group.map(|g| g.trim().to_owned()).filter(|g| !g.is_empty()); | |
| 676 | let self_hosted = group.is_some() || out.iter().any(|l| l == SELF_HOSTED); | |
| 677 | Wanted { labels: out, group, self_hosted } | |
| 678 | } | |
| 679 | ||
| 680 | /// The job's labels as stored on it: its labels, then `group:<name>`. | |
| 681 | pub fn stored(&self) -> Vec<String> { | |
| 682 | let mut out = self.labels.clone(); | |
| 683 | if let Some(group) = &self.group { | |
| 684 | out.push(format!("group:{}", group.to_ascii_lowercase())); | |
| 685 | } | |
| 686 | out | |
| 687 | } | |
| 688 | ||
| 689 | /// Back from what [`Wanted::stored`] kept. | |
| 690 | pub fn from_stored(stored: &[String]) -> Wanted { | |
| 691 | let group = stored.iter().find_map(|l| l.strip_prefix("group:").map(str::to_owned)); | |
| 692 | let labels: Vec<String> = stored.iter().filter(|l| !l.starts_with("group:")).cloned().collect(); | |
| 693 | Wanted::of(&labels, group.as_deref()) | |
| 694 | } | |
| 695 | ||
| 696 | /// Whether a runner with `labels`, in the group called `group`, can | |
| 697 | /// take it: every label it asks for, and its group if it names one. | |
| 698 | pub fn matches(&self, labels: &[String], group: Option<&str>) -> bool { | |
| 699 | let has = |wanted: &String| labels.iter().any(|l| l.eq_ignore_ascii_case(wanted)); | |
| 700 | let group_ok = match (&self.group, group) { | |
| 701 | (None, _) => true, | |
| 702 | (Some(wanted), Some(group)) => wanted.eq_ignore_ascii_case(group), | |
| 703 | (Some(_), None) => false, | |
| 704 | }; | |
| 705 | group_ok && self.labels.iter().all(has) | |
| 706 | } | |
| 707 | ||
| 708 | /// For people: `self-hosted, linux, gpu` (in group `build`). | |
| 709 | pub fn describe(&self) -> String { | |
| 710 | let labels = self.labels.join(", "); | |
| 711 | match &self.group { | |
| 712 | Some(group) if labels.is_empty() => format!("in group {group}"), | |
| 713 | Some(group) => format!("{labels} in group {group}"), | |
| 714 | None => labels, | |
| 715 | } | |
| 716 | } | |
| 717 | } | |
| 718 | ||
| 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. | 719 | /// What a public repository's job says while the only runners that could |
| 720 | /// take it run jobs directly on their machines (`host` isolation), which | |
| 721 | /// are never given a public repository's work. | |
| 722 | pub const WAITING_FOR_SANDBOX: &str = | |
| 723 | "Waiting for a sandboxed runner: this repository is public, and host-mode runners don't run public repositories' jobs."; | |
| 724 | ||
| 725 | /// Whether a runner of `isolation` may be given a repository's work, by | |
| 726 | /// its visibility (`public` or `private`; unknown lets it through, and the | |
| 727 | /// runner decides). A host-mode runner takes no public repository's work. | |
| 728 | pub fn isolation_allows(isolation: Option<&str>, visibility: Option<&str>) -> bool { | |
| 729 | !(isolation == Some("host") && visibility == Some("public")) | |
| 730 | } | |
| 731 | ||
| Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0 | 732 | /// What a job waiting for a runner says. |
| 733 | pub fn waiting_reason(wanted: &Wanted) -> String { | |
| 734 | format!("Waiting for a self-hosted runner with labels {}.", wanted.describe()) | |
| 735 | } | |
| 736 | ||
| 737 | #[cfg(test)] | |
| 738 | mod tests { | |
| 739 | use super::*; | |
| 740 | ||
| 741 | fn strings(items: &[&str]) -> Vec<String> { | |
| 742 | items.iter().map(|s| s.to_string()).collect() | |
| 743 | } | |
| 744 | ||
| 745 | #[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. | 746 | fn a_host_mode_runner_takes_no_public_repositorys_work() { |
| 747 | assert!(!isolation_allows(Some("host"), Some("public"))); | |
| 748 | assert!(isolation_allows(Some("host"), Some("private"))); | |
| 749 | assert!(isolation_allows(Some("host"), None)); | |
| 750 | assert!(isolation_allows(Some("container"), Some("public"))); | |
| 751 | assert!(isolation_allows(None, Some("public")), "an older runner decides itself"); | |
| 752 | let poll: PollArgs = serde_json::from_value(serde_json::json!({ "runner": "rnr_1", "credential": "c" })).unwrap(); | |
| 753 | assert_eq!(poll.isolation, None, "an older runner's poll"); | |
| 754 | } | |
| 755 | ||
| 756 | #[test] | |
| Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0 | 757 | fn a_runner_always_has_self_hosted_its_os_and_its_arch() { |
| 758 | let labels = runner_labels(&strings(&["GPU", "gpu", " cuda-12 "]), "linux", "x64").unwrap(); | |
| 759 | assert_eq!(labels, strings(&["self-hosted", "linux", "x64", "gpu", "cuda-12"])); | |
| 760 | assert!(runner_labels(&strings(&["has space"]), "linux", "x64").is_err()); | |
| 761 | assert!(runner_labels(&strings(&[""]), "linux", "x64").is_err()); | |
| 762 | } | |
| 763 | ||
| 764 | #[test] | |
| 765 | fn os_and_arch_are_read_as_people_write_them() { | |
| 766 | assert_eq!(os_label("Darwin"), Some("macos")); | |
| 767 | assert_eq!(os_label("Windows"), Some("windows")); | |
| 768 | assert_eq!(os_label("plan9"), None); | |
| 769 | assert_eq!(arch_label("x86_64"), Some("x64")); | |
| 770 | assert_eq!(arch_label("aarch64"), Some("arm64")); | |
| 771 | assert_eq!(arch_label("mips"), None); | |
| 772 | } | |
| 773 | ||
| 774 | #[test] | |
| 775 | fn a_job_names_self_hosted_or_a_group_to_need_one() { | |
| 776 | assert!(Wanted::of(&strings(&["Self-Hosted", "linux"]), None).self_hosted); | |
| 777 | assert!(Wanted::of(&strings(&[]), Some("build")).self_hosted); | |
| 778 | assert!(!Wanted::of(&strings(&["ubuntu-latest"]), None).self_hosted); | |
| 779 | } | |
| 780 | ||
| 781 | #[test] | |
| 782 | fn every_label_the_job_names_must_be_the_runners() { | |
| 783 | let runner = strings(&["self-hosted", "linux", "x64", "gpu"]); | |
| 784 | assert!(Wanted::of(&strings(&["self-hosted"]), None).matches(&runner, Some("default"))); | |
| 785 | assert!(Wanted::of(&strings(&["self-hosted", "LINUX", "gpu"]), None).matches(&runner, None)); | |
| 786 | assert!(!Wanted::of(&strings(&["self-hosted", "linux", "arm64"]), None).matches(&runner, None)); | |
| 787 | assert!(!Wanted::of(&strings(&["self-hosted", "windows"]), None).matches(&runner, None)); | |
| 788 | } | |
| 789 | ||
| 790 | #[test] | |
| 791 | fn a_group_is_matched_by_name() { | |
| 792 | let runner = strings(&["self-hosted", "linux", "x64"]); | |
| 793 | let wanted = Wanted::of(&strings(&["linux"]), Some("Build")); | |
| 794 | assert!(wanted.matches(&runner, Some("build"))); | |
| 795 | assert!(!wanted.matches(&runner, Some("default"))); | |
| 796 | assert!(!wanted.matches(&runner, None)); | |
| 797 | assert_eq!(Wanted::from_stored(&wanted.stored()), wanted.clone().tap_lower()); | |
| 798 | } | |
| 799 | ||
| 800 | impl Wanted { | |
| 801 | fn tap_lower(mut self) -> Wanted { | |
| 802 | self.group = self.group.map(|g| g.to_ascii_lowercase()); | |
| 803 | self | |
| 804 | } | |
| 805 | } | |
| 806 | ||
| 807 | #[test] | |
| 808 | fn waiting_says_for_what() { | |
| 809 | let wanted = Wanted::of(&strings(&["self-hosted", "linux", "gpu"]), None); | |
| 810 | assert_eq!(waiting_reason(&wanted), "Waiting for a self-hosted runner with labels self-hosted, linux, gpu."); | |
| 811 | } | |
| 812 | ||
| 813 | #[test] | |
| 814 | fn the_runners_wire_is_snake_case() { | |
| 815 | let poll: PollArgs = serde_json::from_value(serde_json::json!({ | |
| 816 | "runner": "rnr_1", "credential": "g1tr_x", "version": "0.1.0", "running": ["job_1"], "wait_ms": 1000 | |
| 817 | })) | |
| 818 | .unwrap(); | |
| 819 | assert_eq!(poll.auth.runner, "rnr_1"); | |
| 820 | assert_eq!(poll.wait_ms, 1000); | |
| 821 | let assignment = serde_json::to_value(Assignment { | |
| 822 | kind: "workflow".into(), | |
| 823 | id: "job_1".into(), | |
| 824 | name: "build".into(), | |
| 825 | repo: "acme/web".into(), | |
| 826 | timeout_minutes: 60, | |
| 827 | image: None, | |
| 828 | token: Some("t".into()), | |
| 829 | env: None, | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 830 | visibility: Some("private".into()), |
| Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0 | 831 | }) |
| 832 | .unwrap(); | |
| 833 | assert_eq!(assignment["timeout_minutes"], 60); | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 834 | assert_eq!(assignment["visibility"], "private"); |
| 835 | // An older runner's registration, without an isolation level. | |
| 836 | let register: RegisterArgs = serde_json::from_value(serde_json::json!({ "token": "g1trt_x", "name": "a", "os": "linux", "arch": "x64" })).unwrap(); | |
| 837 | assert_eq!(register.isolation, None); | |
| Cleanup: clippy is quiet outside billing, the README says g1t, and http-cache-semantics is 4.3.0 | 838 | } |
| 839 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.