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.
| Cache hits are counted, not only their bytes | 1 | //! What `g1t-runner register` keeps: where g1t is, who this runner is, its |
| 2 | //! credential, and how it runs work. One JSON file in the runner's folder, | |
| 3 | //! readable by its owner only. | |
| 4 | ||
| 5 | use std::path::{Path, PathBuf}; | |
| 6 | ||
| 7 | use anyhow::{Context, Result}; | |
| 8 | use serde::{Deserialize, Serialize}; | |
| 9 | ||
| 10 | #[derive(Clone, Debug, Serialize, Deserialize)] | |
| 11 | pub struct Config { | |
| 12 | /// The site, as given to `--url`: `https://g1t.sh`. | |
| 13 | pub url: String, | |
| 14 | /// The API the runner calls: `https://api.g1t.sh`. | |
| 15 | pub api: String, | |
| 16 | /// The runner's id and name. | |
| 17 | pub runner: String, | |
| 18 | pub name: String, | |
| 19 | /// Its credential (`g1tr_…`). Rotated by g1t every day. | |
| 20 | pub credential: String, | |
| 21 | pub workspace: String, | |
| 22 | #[serde(default)] | |
| 23 | pub repo: Option<String>, | |
| 24 | #[serde(default)] | |
| 25 | pub group: Option<String>, | |
| 26 | pub labels: Vec<String>, | |
| 27 | /// Runs one job, then removes itself. | |
| 28 | #[serde(default)] | |
| 29 | pub ephemeral: bool, | |
| 30 | /// Where jobs keep their files when they run on this machine directly. | |
| 31 | pub work_dir: PathBuf, | |
| 32 | /// Runs work in Docker containers (the default), or directly. | |
| 33 | #[serde(default = "yes")] | |
| 34 | pub docker: bool, | |
| 35 | /// The image workflow jobs run in when they name no `container:`. | |
| 36 | #[serde(default)] | |
| 37 | pub image: Option<String>, | |
| 38 | /// A Linux build of `g1t-runner` to run inside containers, instead of | |
| 39 | /// this binary (on Linux) or the release's (elsewhere). | |
| 40 | #[serde(default)] | |
| 41 | pub harness: Option<PathBuf>, | |
| 42 | /// Checks for new versions and updates itself. | |
| 43 | #[serde(default = "yes")] | |
| 44 | pub auto_update: bool, | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 45 | /// Each job container's memory limit (`--memory`): `8g`. Three |
| 46 | /// quarters of the machine's (or the runner container's) memory if | |
| 47 | /// left out. | |
| 48 | #[serde(default)] | |
| 49 | pub memory: Option<String>, | |
| 50 | /// Each job container's CPUs (`--cpus`): `2`, `1.5`. Every CPU if left out. | |
| 51 | #[serde(default)] | |
| 52 | pub cpus: Option<String>, | |
| 53 | /// The most processes in a job container (`--pids-limit`). 4096 if left out. | |
| 54 | #[serde(default)] | |
| 55 | pub pids_limit: Option<u32>, | |
| 56 | /// With nested-container isolation, a job that uses Docker gets an | |
| 57 | /// engine of its own. Off: jobs have no Docker. | |
| 58 | #[serde(default = "yes")] | |
| 59 | pub job_docker: bool, | |
| 60 | /// The image a job's own engine runs: `docker:28-dind` if left out. | |
| 61 | #[serde(default)] | |
| 62 | pub job_docker_image: 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. | 63 | /// Runs jobs native to this OS, each in a throwaway VM of its own |
| 64 | /// (`--native`, native.rs): Windows Sandbox on Windows. | |
| 65 | #[serde(default)] | |
| 66 | pub native: bool, | |
| Cache hits are counted, not only their bytes | 67 | } |
| 68 | ||
| 69 | fn yes() -> bool { | |
| 70 | true | |
| 71 | } | |
| 72 | ||
| 73 | /// The runner's folder: `--dir`, `G1T_RUNNER_DIR`, else `.g1t-runner` in | |
| 74 | /// the home folder. | |
| 75 | pub fn folder(given: Option<&str>) -> PathBuf { | |
| 76 | if let Some(dir) = given.filter(|d| !d.is_empty()) { | |
| 77 | return PathBuf::from(dir); | |
| 78 | } | |
| 79 | if let Some(dir) = std::env::var_os("G1T_RUNNER_DIR").filter(|d| !d.is_empty()) { | |
| 80 | return PathBuf::from(dir); | |
| 81 | } | |
| 82 | let home = std::env::var_os(if cfg!(windows) { "USERPROFILE" } else { "HOME" }) | |
| 83 | .map(PathBuf::from) | |
| 84 | .unwrap_or_else(|| PathBuf::from(".")); | |
| 85 | home.join(".g1t-runner") | |
| 86 | } | |
| 87 | ||
| 88 | pub fn path(folder: &Path) -> PathBuf { | |
| 89 | folder.join("config.json") | |
| 90 | } | |
| 91 | ||
| 92 | pub fn load(folder: &Path) -> Result<Config> { | |
| 93 | let file = path(folder); | |
| 94 | let text = std::fs::read_to_string(&file) | |
| 95 | .with_context(|| format!("this runner is not registered ({} not found): run `g1t-runner register` first", file.display()))?; | |
| 96 | serde_json::from_str(&text).with_context(|| format!("{} does not read", file.display())) | |
| 97 | } | |
| 98 | ||
| 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. | 99 | /// Writes the configuration so only its owner can read it, in a folder |
| 100 | /// only its owner can read (private.rs), through a temporary file, so a | |
| 101 | /// crash never leaves half a credential. | |
| Cache hits are counted, not only their bytes | 102 | pub fn save(folder: &Path, config: &Config) -> Result<()> { |
| 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. | 103 | super::private::folder(folder, "--dir")?; |
| 104 | super::private::write_private(&path(folder), serde_json::to_string_pretty(config)?.as_bytes()) | |
| Cache hits are counted, not only their bytes | 105 | } |
| 106 | ||
| 107 | pub fn forget(folder: &Path) { | |
| 108 | let _ = std::fs::remove_file(path(folder)); | |
| 109 | } | |
| 110 | ||
| 111 | /// The API for a site: `https://g1t.sh` calls `https://api.g1t.sh`. | |
| 112 | pub fn api_for(url: &str) -> String { | |
| 113 | let url = url.trim_end_matches('/'); | |
| 114 | match url.split_once("://") { | |
| 115 | Some((scheme, host)) if !host.starts_with("api.") && !host.starts_with("localhost") && !host.starts_with("127.") => { | |
| 116 | format!("{scheme}://api.{host}") | |
| 117 | } | |
| 118 | _ => url.to_owned(), | |
| 119 | } | |
| 120 | } | |
| 121 | ||
| 122 | #[cfg(test)] | |
| 123 | mod tests { | |
| 124 | use super::*; | |
| 125 | ||
| 126 | #[test] | |
| 127 | fn the_api_is_found_from_the_site() { | |
| 128 | assert_eq!(api_for("https://g1t.sh"), "https://api.g1t.sh"); | |
| 129 | assert_eq!(api_for("https://g1t.sh/"), "https://api.g1t.sh"); | |
| 130 | assert_eq!(api_for("https://api.g1t.sh"), "https://api.g1t.sh"); | |
| 131 | assert_eq!(api_for("https://git.example.com"), "https://api.git.example.com"); | |
| 132 | assert_eq!(api_for("http://localhost:8787"), "http://localhost:8787"); | |
| 133 | } | |
| 134 | ||
| 135 | #[test] | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 136 | fn an_older_configuration_still_reads() { |
| 137 | let old = r#"{"url":"https://g1t.sh","api":"https://api.g1t.sh","runner":"rnr_1","name":"a","credential":"g1tr_x", | |
| 138 | "workspace":"acme","labels":["self-hosted"],"work_dir":"/tmp/w"}"#; | |
| 139 | let config: Config = serde_json::from_str(old).unwrap(); | |
| 140 | assert!(config.docker && config.job_docker && config.auto_update); | |
| 141 | assert_eq!(config.memory, None); | |
| 142 | } | |
| 143 | ||
| 144 | #[test] | |
| Cache hits are counted, not only their bytes | 145 | fn a_configuration_survives_a_round_trip() { |
| 146 | let dir = std::env::temp_dir().join(format!("g1t-runner-config-{}", std::process::id())); | |
| 147 | let config = Config { | |
| 148 | url: "https://g1t.sh".into(), | |
| 149 | api: "https://api.g1t.sh".into(), | |
| 150 | runner: "rnr_1".into(), | |
| 151 | name: "build-01".into(), | |
| 152 | credential: "g1tr_x".into(), | |
| 153 | workspace: "acme".into(), | |
| 154 | repo: None, | |
| 155 | group: Some("Default".into()), | |
| 156 | labels: vec!["self-hosted".into(), "linux".into()], | |
| 157 | ephemeral: false, | |
| 158 | work_dir: dir.join("work"), | |
| 159 | docker: true, | |
| 160 | image: None, | |
| 161 | harness: None, | |
| 162 | auto_update: true, | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 163 | memory: Some("4g".into()), |
| 164 | cpus: None, | |
| 165 | pids_limit: None, | |
| 166 | job_docker: true, | |
| 167 | job_docker_image: None, | |
| 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. | 168 | native: false, |
| Cache hits are counted, not only their bytes | 169 | }; |
| 170 | save(&dir, &config).unwrap(); | |
| 171 | let back = load(&dir).unwrap(); | |
| 172 | assert_eq!(back.runner, "rnr_1"); | |
| 173 | assert_eq!(back.labels, config.labels); | |
| Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r9 | 174 | assert_eq!(back.memory.as_deref(), Some("4g")); |
| Cache hits are counted, not only their bytes | 175 | forget(&dir); |
| 176 | assert!(load(&dir).is_err()); | |
| 177 | let _ = std::fs::remove_dir_all(&dir); | |
| 178 | } | |
| 179 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.