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 '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.25 /// `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.