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 | //! The runner's calls to g1t: outbound HTTPS only, JSON in `snake_case`. |
| 2 | //! See `g1t_contracts::runners` for the shapes. | |
| 3 | ||
| 4 | use std::time::Duration; | |
| 5 | ||
| 6 | use anyhow::{Result, anyhow}; | |
| 7 | use serde::Deserialize; | |
| 8 | use serde_json::{Map, Value, json}; | |
| 9 | ||
| 10 | use super::VERSION; | |
| 11 | ||
| 12 | #[derive(Clone, Debug, Deserialize)] | |
| 13 | pub 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 incidents | 28 | } |
| 29 | ||
| 30 | #[derive(Debug, Default, Deserialize)] | |
| 31 | pub 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)] | |
| 43 | pub 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)] | |
| 55 | pub 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)] | |
| 63 | pub enum Failure { | |
| 64 | Refused(u16, String), | |
| 65 | Unreachable(String), | |
| 66 | } | |
| 67 | ||
| 68 | impl 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 | ||
| 77 | fn 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 | ||
| 85 | fn 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 | ||
| 104 | pub struct Api { | |
| 105 | pub base: String, | |
| 106 | pub runner: String, | |
| 107 | pub credential: String, | |
| 108 | } | |
| 109 | ||
| 110 | impl 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. | |
| 146 | pub 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.