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

Cache hits are counted, not only their bytes1//! 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
5use std::path::{Path, PathBuf};
6
7use anyhow::{Context, Result};
8use serde::{Deserialize, Serialize};
9
10#[derive(Clone, Debug, Serialize, Deserialize)]
11pub 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-7nm8r945 /// 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 bytes67}
68
69fn yes() -> bool {
70 true
71}
72
73/// The runner's folder: `--dir`, `G1T_RUNNER_DIR`, else `.g1t-runner` in
74/// the home folder.
75pub 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
88pub fn path(folder: &Path) -> PathBuf {
89 folder.join("config.json")
90}
91
92pub 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 bytes102pub 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 bytes105}
106
107pub 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`.
112pub 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)]
123mod 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-7nm8r9136 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 bytes145 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-7nm8r9163 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 bytes169 };
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-7nm8r9174 assert_eq!(back.memory.as_deref(), Some("4g"));
Cache hits are counted, not only their bytes175 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.