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.
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 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 | /// The image agent work runs in: git, Node and the agent's CLI. | |
| 39 | #[serde(default)] | |
| 40 | pub agent_image: Option<String>, | |
| 41 | /// A Linux build of `g1t-runner` to run inside containers, instead of | |
| 42 | /// this binary (on Linux) or the release's (elsewhere). | |
| 43 | #[serde(default)] | |
| 44 | pub harness: Option<PathBuf>, | |
| 45 | /// Checks for new versions and updates itself. | |
| 46 | #[serde(default = "yes")] | |
| 47 | pub auto_update: bool, | |
| Merge branch 'claude/cool-newton-7nm8r9' (#36): real GitHub Actions on g1t, managed deploy workflows, g1t/deploy and the Sysbox runner 0.2.0. The merge keeps both sides' webhook events and API operations, says agent images need tar, guards the job engine's stop to Unix so the runner builds on Windows again, and keeps the moves tests' previews from going idle as the calendar passes their fixture's date. | 48 | /// Each job container's memory limit (`--memory`): `8g`. Three |
| 49 | /// quarters of the machine's (or the runner container's) memory if | |
| 50 | /// left out. | |
| 51 | #[serde(default)] | |
| 52 | pub memory: Option<String>, | |
| 53 | /// Each job container's CPUs (`--cpus`): `2`, `1.5`. Every CPU if left out. | |
| 54 | #[serde(default)] | |
| 55 | pub cpus: Option<String>, | |
| 56 | /// The most processes in a job container (`--pids-limit`). 4096 if left out. | |
| 57 | #[serde(default)] | |
| 58 | pub pids_limit: Option<u32>, | |
| 59 | /// With nested-container isolation, a job that uses Docker gets an | |
| 60 | /// engine of its own. Off: jobs have no Docker. | |
| 61 | #[serde(default = "yes")] | |
| 62 | pub job_docker: bool, | |
| 63 | /// The image a job's own engine runs: `docker:28-dind` if left out. | |
| 64 | #[serde(default)] | |
| 65 | pub job_docker_image: Option<String>, | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 66 | } |
| 67 | ||
| 68 | fn yes() -> bool { | |
| 69 | true | |
| 70 | } | |
| 71 | ||
| 72 | /// The runner's folder: `--dir`, `G1T_RUNNER_DIR`, else `.g1t-runner` in | |
| 73 | /// the home folder. | |
| 74 | pub fn folder(given: Option<&str>) -> PathBuf { | |
| 75 | if let Some(dir) = given.filter(|d| !d.is_empty()) { | |
| 76 | return PathBuf::from(dir); | |
| 77 | } | |
| 78 | if let Some(dir) = std::env::var_os("G1T_RUNNER_DIR").filter(|d| !d.is_empty()) { | |
| 79 | return PathBuf::from(dir); | |
| 80 | } | |
| 81 | let home = std::env::var_os(if cfg!(windows) { "USERPROFILE" } else { "HOME" }) | |
| 82 | .map(PathBuf::from) | |
| 83 | .unwrap_or_else(|| PathBuf::from(".")); | |
| 84 | home.join(".g1t-runner") | |
| 85 | } | |
| 86 | ||
| 87 | pub fn path(folder: &Path) -> PathBuf { | |
| 88 | folder.join("config.json") | |
| 89 | } | |
| 90 | ||
| 91 | pub fn load(folder: &Path) -> Result<Config> { | |
| 92 | let file = path(folder); | |
| 93 | let text = std::fs::read_to_string(&file) | |
| 94 | .with_context(|| format!("this runner is not registered ({} not found): run `g1t-runner register` first", file.display()))?; | |
| 95 | serde_json::from_str(&text).with_context(|| format!("{} does not read", file.display())) | |
| 96 | } | |
| 97 | ||
| 98 | /// Writes the configuration so only its owner can read it, through a | |
| 99 | /// temporary file, so a crash never leaves half a credential. | |
| 100 | pub fn save(folder: &Path, config: &Config) -> Result<()> { | |
| 101 | std::fs::create_dir_all(folder).with_context(|| format!("could not make {}", folder.display()))?; | |
| 102 | let file = path(folder); | |
| 103 | let temp = folder.join("config.json.new"); | |
| 104 | std::fs::write(&temp, serde_json::to_string_pretty(config)?)?; | |
| 105 | #[cfg(unix)] | |
| 106 | { | |
| 107 | use std::os::unix::fs::PermissionsExt; | |
| 108 | std::fs::set_permissions(&temp, std::fs::Permissions::from_mode(0o600))?; | |
| 109 | } | |
| 110 | std::fs::rename(&temp, &file).with_context(|| format!("could not write {}", file.display()))?; | |
| 111 | Ok(()) | |
| 112 | } | |
| 113 | ||
| 114 | pub fn forget(folder: &Path) { | |
| 115 | let _ = std::fs::remove_file(path(folder)); | |
| 116 | } | |
| 117 | ||
| 118 | /// The API for a site: `https://g1t.sh` calls `https://api.g1t.sh`. | |
| 119 | pub fn api_for(url: &str) -> String { | |
| 120 | let url = url.trim_end_matches('/'); | |
| 121 | match url.split_once("://") { | |
| 122 | Some((scheme, host)) if !host.starts_with("api.") && !host.starts_with("localhost") && !host.starts_with("127.") => { | |
| 123 | format!("{scheme}://api.{host}") | |
| 124 | } | |
| 125 | _ => url.to_owned(), | |
| 126 | } | |
| 127 | } | |
| 128 | ||
| 129 | #[cfg(test)] | |
| 130 | mod tests { | |
| 131 | use super::*; | |
| 132 | ||
| 133 | #[test] | |
| 134 | fn the_api_is_found_from_the_site() { | |
| 135 | assert_eq!(api_for("https://g1t.sh"), "https://api.g1t.sh"); | |
| 136 | assert_eq!(api_for("https://g1t.sh/"), "https://api.g1t.sh"); | |
| 137 | assert_eq!(api_for("https://api.g1t.sh"), "https://api.g1t.sh"); | |
| 138 | assert_eq!(api_for("https://git.example.com"), "https://api.git.example.com"); | |
| 139 | assert_eq!(api_for("http://localhost:8787"), "http://localhost:8787"); | |
| 140 | } | |
| 141 | ||
| 142 | #[test] | |
| Merge branch 'claude/cool-newton-7nm8r9' (#36): real GitHub Actions on g1t, managed deploy workflows, g1t/deploy and the Sysbox runner 0.2.0. The merge keeps both sides' webhook events and API operations, says agent images need tar, guards the job engine's stop to Unix so the runner builds on Windows again, and keeps the moves tests' previews from going idle as the calendar passes their fixture's date. | 143 | fn an_older_configuration_still_reads() { |
| 144 | let old = r#"{"url":"https://g1t.sh","api":"https://api.g1t.sh","runner":"rnr_1","name":"a","credential":"g1tr_x", | |
| 145 | "workspace":"acme","labels":["self-hosted"],"work_dir":"/tmp/w"}"#; | |
| 146 | let config: Config = serde_json::from_str(old).unwrap(); | |
| 147 | assert!(config.docker && config.job_docker && config.auto_update); | |
| 148 | assert_eq!(config.memory, None); | |
| 149 | } | |
| 150 | ||
| 151 | #[test] | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 152 | fn a_configuration_survives_a_round_trip() { |
| 153 | let dir = std::env::temp_dir().join(format!("g1t-runner-config-{}", std::process::id())); | |
| 154 | let config = Config { | |
| 155 | url: "https://g1t.sh".into(), | |
| 156 | api: "https://api.g1t.sh".into(), | |
| 157 | runner: "rnr_1".into(), | |
| 158 | name: "build-01".into(), | |
| 159 | credential: "g1tr_x".into(), | |
| 160 | workspace: "acme".into(), | |
| 161 | repo: None, | |
| 162 | group: Some("Default".into()), | |
| 163 | labels: vec!["self-hosted".into(), "linux".into()], | |
| 164 | ephemeral: false, | |
| 165 | work_dir: dir.join("work"), | |
| 166 | docker: true, | |
| 167 | image: None, | |
| 168 | agent_image: None, | |
| 169 | harness: None, | |
| 170 | auto_update: true, | |
| Merge branch 'claude/cool-newton-7nm8r9' (#36): real GitHub Actions on g1t, managed deploy workflows, g1t/deploy and the Sysbox runner 0.2.0. The merge keeps both sides' webhook events and API operations, says agent images need tar, guards the job engine's stop to Unix so the runner builds on Windows again, and keeps the moves tests' previews from going idle as the calendar passes their fixture's date. | 171 | memory: Some("4g".into()), |
| 172 | cpus: None, | |
| 173 | pids_limit: None, | |
| 174 | job_docker: true, | |
| 175 | job_docker_image: None, | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 176 | }; |
| 177 | save(&dir, &config).unwrap(); | |
| 178 | let back = load(&dir).unwrap(); | |
| 179 | assert_eq!(back.runner, "rnr_1"); | |
| 180 | assert_eq!(back.labels, config.labels); | |
| Merge branch 'claude/cool-newton-7nm8r9' (#36): real GitHub Actions on g1t, managed deploy workflows, g1t/deploy and the Sysbox runner 0.2.0. The merge keeps both sides' webhook events and API operations, says agent images need tar, guards the job engine's stop to Unix so the runner builds on Windows again, and keeps the moves tests' previews from going idle as the calendar passes their fixture's date. | 181 | assert_eq!(back.memory.as_deref(), Some("4g")); |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 182 | forget(&dir); |
| 183 | assert!(load(&dir).is_err()); | |
| 184 | let _ = std::fs::remove_dir_all(&dir); | |
| 185 | } | |
| 186 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.