flagon-io/g1t

public

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/crates/runner/src/actions/report.rs

132 lines4,482 bytesCodeBlame
1//! Telling g1t how a job is going: its steps, its log in batches, its
2//! annotations, and how it ended. Every report carries the job's token.
3
4use std::time::{Duration, Instant};
5
6use anyhow::Result;
7use serde_json::{Value, json};
8
9/// How long log lines wait before they are sent.
10const FLUSH_EVERY: Duration = Duration::from_millis(1500);
11/// How much log is sent at once.
12const FLUSH_BYTES: usize = 64 * 1024;
13
14pub(crate) struct Api {
15 pub(crate) base: String,
16 pub(crate) job: String,
17 pub(crate) token: String,
18}
19
20impl Api {
21 pub(crate) fn spec(&self) -> Result<Value> {
22 let response = ureq::post(&format!("{}/actions/jobs/{}/spec", self.base, self.job))
23 .timeout(Duration::from_secs(60))
24 .send_json(json!({ "token": self.token }))?;
25 Ok(response.into_json()?)
26 }
27
28 pub(crate) fn report(&self, report: Value) {
29 // A report that cannot be sent is tried a few times, then dropped:
30 // the job goes on, and g1t notices a silent job by itself.
31 for attempt in 0..3 {
32 let sent = ureq::post(&format!("{}/actions/jobs/{}", self.base, self.job))
33 .timeout(Duration::from_secs(30))
34 .send_json(json!({ "token": self.token, "report": report }));
35 match sent {
36 Ok(_) => return,
37 // Refused: the job was cancelled or finished; nothing to retry.
38 Err(ureq::Error::Status(code, _)) if (400..500).contains(&code) => return,
39 Err(_) => std::thread::sleep(Duration::from_millis(500 * (attempt + 1))),
40 }
41 }
42 }
43}
44
45/// The job's log, masked, sent in batches.
46pub(crate) struct Log {
47 pub(crate) api: Api,
48 pub(crate) masks: Vec<String>,
49 step: u32,
50 buffer: String,
51 last: Instant,
52}
53
54impl Log {
55 pub(crate) fn new(api: Api, masks: Vec<String>) -> Log {
56 Log {
57 api,
58 masks,
59 step: 0,
60 buffer: String::new(),
61 last: Instant::now(),
62 }
63 }
64
65 /// Starts writing to step `number` (0 for the job's setup).
66 pub(crate) fn step(&mut self, number: u32) {
67 self.flush();
68 self.step = number;
69 }
70
71 pub(crate) fn mask(&self, text: &str) -> String {
72 let mut out = text.to_owned();
73 for mask in self.masks.iter().filter(|mask| !mask.is_empty()) {
74 if out.contains(mask.as_str()) {
75 out = out.replace(mask.as_str(), "***");
76 }
77 }
78 out
79 }
80
81 pub(crate) fn line(&mut self, text: &str) {
82 let masked = self.mask(text);
83 self.buffer.push_str(&masked);
84 self.buffer.push('\n');
85 if self.buffer.len() >= FLUSH_BYTES || self.last.elapsed() >= FLUSH_EVERY {
86 self.flush();
87 }
88 }
89
90 /// Sends what is waiting if it has waited long enough.
91 pub(crate) fn tick(&mut self) {
92 if !self.buffer.is_empty() && self.last.elapsed() >= FLUSH_EVERY {
93 self.flush();
94 }
95 }
96
97 pub(crate) fn flush(&mut self) {
98 self.last = Instant::now();
99 if self.buffer.is_empty() {
100 return;
101 }
102 let text = std::mem::take(&mut self.buffer);
103 self.api.report(json!({ "kind": "log", "step": self.step, "text": text }));
104 }
105
106 pub(crate) fn steps(&self, names: &[String]) {
107 self.api.report(json!({ "kind": "steps", "steps": names }));
108 }
109
110 pub(crate) fn step_state(&mut self, number: u32, name: &str, status: &str, conclusion: Option<&str>) {
111 self.flush();
112 let name = self.mask(name);
113 self.api.report(json!({ "kind": "step", "number": number, "name": name, "status": status, "conclusion": conclusion }));
114 }
115
116 pub(crate) fn annotation(&mut self, level: &str, message: &str, properties: &serde_json::Map<String, Value>) {
117 let message = self.mask(message);
118 self.api.report(json!({
119 "kind": "annotation",
120 "level": level,
121 "message": message,
122 "title": properties.get("title"),
123 "file": properties.get("file"),
124 "line": properties.get("line").and_then(|l| l.as_str()).and_then(|l| l.parse::<u32>().ok()),
125 }));
126 }
127
128 pub(crate) fn done(&mut self, conclusion: &str, outputs: &serde_json::Map<String, Value>, reason: Option<&str>) {
129 self.flush();
130 self.api.report(json!({ "kind": "done", "conclusion": conclusion, "outputs": outputs, "reason": reason }));
131 }
132}