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

An issue being worked on no longer offers Bring your own agent beside the pull request doing it1//! The runner's calls to g1t: outbound HTTPS only, JSON in `snake_case`.
2//! See `g1t_contracts::runners` for the shapes.
3
4use std::time::Duration;
5
6use anyhow::{Result, anyhow};
7use serde::Deserialize;
8use serde_json::{Map, Value, json};
9
10use super::VERSION;
11
12#[derive(Clone, Debug, Deserialize)]
13pub struct Assignment {
14 pub kind: String,
15 pub id: String,
16 pub name: String,
17 pub repo: String,
18 pub timeout_minutes: u32,
19 #[serde(default)]
20 pub image: Option<String>,
21 #[serde(default)]
22 pub token: Option<String>,
23 #[serde(default)]
24 pub env: Option<Map<String, Value>>,
Merge branch 'worktree-agent-ac79a505eca749d4d' into claude/cool-newton-7nm8r925 /// `public` or `private`; none from an older g1t.
26 #[serde(default)]
27 pub visibility: Option<String>,
An issue being worked on no longer offers Bring your own agent beside the pull request doing it28}
29
30#[derive(Debug, Default, Deserialize)]
31pub struct Poll {
32 #[serde(default)]
33 pub assignment: Option<Assignment>,
34 #[serde(default)]
35 pub cancel: Vec<String>,
36 #[serde(default)]
37 pub credential: Option<String>,
38 #[serde(default)]
39 pub removed: bool,
40}
41
42#[derive(Debug, Deserialize)]
43pub struct RegisteredRunner {
44 pub id: String,
45 pub name: String,
46 pub workspace: String,
47 #[serde(default)]
48 pub repo: Option<String>,
49 #[serde(default)]
50 pub group: Option<String>,
51 pub labels: Vec<String>,
52}
53
54#[derive(Debug, Deserialize)]
55pub struct Registered {
56 pub runner: RegisteredRunner,
57 pub credential: String,
58}
59
60/// How a call failed: g1t refused it (with its status and message), or it
61/// did not get through.
62#[derive(Debug)]
63pub enum Failure {
64 Refused(u16, String),
65 Unreachable(String),
66}
67
68impl std::fmt::Display for Failure {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 match self {
71 Failure::Refused(status, message) => write!(f, "{message} ({status})"),
72 Failure::Unreachable(why) => write!(f, "could not reach g1t: {why}"),
73 }
74 }
75}
76
77fn agent() -> ureq::Agent {
78 ureq::AgentBuilder::new()
79 .timeout_connect(Duration::from_secs(15))
80 .timeout_read(Duration::from_secs(60))
81 .user_agent(&format!("g1t-runner/{VERSION} ({}; {})", std::env::consts::OS, std::env::consts::ARCH))
82 .build()
83}
84
85fn post(url: &str, credential: Option<&str>, body: &Value) -> std::result::Result<Value, Failure> {
86 let mut request = agent().post(url);
87 if let Some(credential) = credential {
88 request = request.set("authorization", &format!("Bearer {credential}"));
89 }
90 match request.send_json(body.clone()) {
91 Ok(response) => response.into_json::<Value>().map_err(|e| Failure::Unreachable(e.to_string())),
92 Err(ureq::Error::Status(status, response)) => {
93 let text = response.into_string().unwrap_or_default();
94 let message = serde_json::from_str::<Value>(&text)
95 .ok()
96 .and_then(|v| v["error"]["message"].as_str().map(str::to_owned))
97 .unwrap_or(text);
98 Err(Failure::Refused(status, message))
99 }
100 Err(error) => Err(Failure::Unreachable(error.to_string())),
101 }
102}
103
104pub struct Api {
105 pub base: String,
106 pub runner: String,
107 pub credential: String,
108}
109
110impl Api {
111 pub fn register(base: &str, body: &Value) -> Result<Registered> {
112 let answer = post(&format!("{base}/runners/register"), None, body).map_err(|failure| anyhow!("{failure}"))?;
113 Ok(serde_json::from_value(answer)?)
114 }
115
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.116 /// Asks for work. `isolation` is how this runner keeps work from its
117 /// machine now, which g1t keeps up to date from each poll (it gives a
118 /// host-mode runner no public repository's work); an older g1t ignores it.
119 pub fn poll(&self, running: &[String], wait_ms: u64, isolation: &str) -> std::result::Result<Poll, Failure> {
120 let body = poll_body(running, wait_ms, isolation);
An issue being worked on no longer offers Bring your own agent beside the pull request doing it121 let answer = post(&format!("{}/runners/{}/poll", self.base, self.runner), Some(&self.credential), &body)?;
122 serde_json::from_value(answer).map_err(|e| Failure::Unreachable(e.to_string()))
123 }
124
125 pub fn finished(&self, id: &str, exit_code: i32, reason: Option<&str>) -> std::result::Result<(), Failure> {
126 let body = json!({ "id": id, "exit_code": exit_code, "reason": reason });
127 // Tried a few times: a job that crashed should not wait for the
128 // silence check to be noticed.
129 let mut last = None;
130 for attempt in 0..3 {
131 match post(&format!("{}/runners/{}/finished", self.base, self.runner), Some(&self.credential), &body) {
132 Ok(_) => return Ok(()),
133 Err(Failure::Refused(status, message)) => return Err(Failure::Refused(status, message)),
134 Err(failure) => {
135 last = Some(failure);
136 std::thread::sleep(Duration::from_secs(2 * (attempt + 1)));
137 }
138 }
139 }
140 Err(last.unwrap_or(Failure::Unreachable("no answer".into())))
141 }
142
143 pub fn remove(&self) -> std::result::Result<(), Failure> {
144 post(&format!("{}/runners/{}/remove", self.base, self.runner), Some(&self.credential), &json!({})).map(|_| ())
145 }
146}
147
148/// Fetches a file's bytes.
149pub fn download(url: &str) -> Result<Vec<u8>> {
150 let response = agent().get(url).timeout(Duration::from_secs(300)).call().map_err(|e| anyhow!("could not download {url}: {e}"))?;
151 let mut bytes = Vec::new();
152 std::io::Read::read_to_end(&mut response.into_reader(), &mut bytes)?;
153 Ok(bytes)
154}
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.155
156fn poll_body(running: &[String], wait_ms: u64, isolation: &str) -> Value {
157 json!({ "version": VERSION, "running": running, "wait_ms": wait_ms, "isolation": isolation })
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn a_poll_says_how_the_runner_is_isolated() {
166 let body = poll_body(&["job_1".to_owned()], 0, "host");
167 assert_eq!(body["isolation"], "host");
168 assert_eq!(body["running"], json!(["job_1"]));
169 assert_eq!(body["version"], VERSION);
170 }
171}

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