Skip to content
151 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.

Fast pages, required checks on the branch, self-hosted runners, honest incidents1//! 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>,
Fast pages, required checks on the branch, self-hosted runners, honest incidents28}
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
116 pub fn poll(&self, running: &[String], wait_ms: u64) -> std::result::Result<Poll, Failure> {
117 let body = json!({ "version": VERSION, "running": running, "wait_ms": wait_ms });
118 let answer = post(&format!("{}/runners/{}/poll", self.base, self.runner), Some(&self.credential), &body)?;
119 serde_json::from_value(answer).map_err(|e| Failure::Unreachable(e.to_string()))
120 }
121
122 pub fn finished(&self, id: &str, exit_code: i32, reason: Option<&str>) -> std::result::Result<(), Failure> {
123 let body = json!({ "id": id, "exit_code": exit_code, "reason": reason });
124 // Tried a few times: a job that crashed should not wait for the
125 // silence check to be noticed.
126 let mut last = None;
127 for attempt in 0..3 {
128 match post(&format!("{}/runners/{}/finished", self.base, self.runner), Some(&self.credential), &body) {
129 Ok(_) => return Ok(()),
130 Err(Failure::Refused(status, message)) => return Err(Failure::Refused(status, message)),
131 Err(failure) => {
132 last = Some(failure);
133 std::thread::sleep(Duration::from_secs(2 * (attempt + 1)));
134 }
135 }
136 }
137 Err(last.unwrap_or(Failure::Unreachable("no answer".into())))
138 }
139
140 pub fn remove(&self) -> std::result::Result<(), Failure> {
141 post(&format!("{}/runners/{}/remove", self.base, self.runner), Some(&self.credential), &json!({})).map(|_| ())
142 }
143}
144
145/// Fetches a file's bytes.
146pub fn download(url: &str) -> Result<Vec<u8>> {
147 let response = agent().get(url).timeout(Duration::from_secs(300)).call().map_err(|e| anyhow!("could not download {url}: {e}"))?;
148 let mut bytes = Vec::new();
149 std::io::Read::read_to_end(&mut response.into_reader(), &mut bytes)?;
150 Ok(bytes)
151}

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