Skip to content
102 linesCodeBlameRaw
1//! What the runner's log says, read a line at a time. `g1t-runner` writes
2//! one line per thing it does (crates/runner/src/selfhosted/mod.rs), the
3//! same in every backend: in a sandbox, in a container, or on this
4//! computer. The app reads its state from those lines rather than asking
5//! g1t, so the tray is right even while g1t cannot be reached.
6
7/// What one line of the log means for the app.
8#[derive(Debug, PartialEq, Eq)]
9pub enum Event {
10 /// Registered and polling for work: `g1t-runner 0.2.3: name (labels) is listening for work from …`.
11 Listening { version: String },
12 /// It took work: `Took workflow build (acme/web).`
13 Took { name: String, repo: String },
14 /// The work it had ended: `workflow build ended (0).`
15 Ended,
16 /// g1t does not know this runner any more: it was removed, or its
17 /// credential was. The registration is gone and a new one is needed.
18 Forgotten,
19 /// The registration token was refused: expired, or not one.
20 TokenRefused,
21 /// g1t could not be reached; the runner tries again by itself.
22 Offline,
23 /// Something went wrong that stops the runner: the program's own error line.
24 Failed(String),
25 /// Anything else, kept only in the log file.
26 Other,
27}
28
29/// The line without the time the runner put in front of it (`2026-10-12T08:00:00Z …`).
30pub fn message(line: &str) -> &str {
31 let line = line.trim();
32 match line.split_once(' ') {
33 Some((first, rest)) if first.len() == 20 && first.starts_with("20") && first.ends_with('Z') => rest,
34 _ => line,
35 }
36}
37
38pub fn read(line: &str) -> Event {
39 let text = message(line);
40 if let Some(rest) = text.strip_prefix("g1t-runner ")
41 && let Some((version, after)) = rest.split_once(": ")
42 && after.contains(" is listening for work from ")
43 {
44 return Event::Listening { version: version.to_owned() };
45 }
46 if let Some(rest) = text.strip_prefix("Took ")
47 && let Some(rest) = rest.strip_suffix(").")
48 && let Some((head, repo)) = rest.rsplit_once(" (")
49 {
50 let name = head.split_once(' ').map_or(head, |(_kind, name)| name);
51 return Event::Took { name: name.to_owned(), repo: repo.to_owned() };
52 }
53 if (text.starts_with("workflow ") || text.starts_with("agent ")) && text.contains(" ended (") {
54 return Event::Ended;
55 }
56 if text.contains("g1t no longer knows this runner") || text.contains("This runner was removed from g1t") {
57 return Event::Forgotten;
58 }
59 if text.contains("could not reach g1t") && !text.starts_with("g1t-runner: ") {
60 return Event::Offline;
61 }
62 if let Some(error) = text.strip_prefix("g1t-runner: ") {
63 let lower = error.to_ascii_lowercase();
64 if lower.contains("token") && (lower.contains("not valid") || lower.contains("expired") || lower.contains("invalid")) {
65 return Event::TokenRefused;
66 }
67 return Event::Failed(error.to_owned());
68 }
69 Event::Other
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75
76 #[test]
77 fn the_runner_listening_is_read_with_its_version() {
78 let line = "2026-10-12T08:00:00Z g1t-runner 0.2.3: syntaqx-win (self-hosted, windows, x64, desktop) is listening for work from https://g1t.sh. Isolation: host: jobs run directly on this machine, unsandboxed.";
79 assert_eq!(read(line), Event::Listening { version: "0.2.3".into() });
80 }
81
82 #[test]
83 fn work_taken_and_ended_is_read() {
84 assert_eq!(
85 read("2026-10-12T08:00:01Z Took workflow Build the installer (flagon-io/g1t)."),
86 Event::Took { name: "Build the installer".into(), repo: "flagon-io/g1t".into() }
87 );
88 assert_eq!(read("2026-10-12T08:09:00Z workflow Build the installer ended (0)."), Event::Ended);
89 assert_eq!(read("agent Review ended (1)."), Event::Ended);
90 }
91
92 #[test]
93 fn losing_the_registration_is_read() {
94 assert_eq!(read("g1t-runner: g1t no longer knows this runner (401 unauthorized). Register it again."), Event::Forgotten);
95 assert_eq!(read("2026-10-12T08:00:00Z This runner was removed from g1t; stopping."), Event::Forgotten);
96 assert_eq!(read("g1t-runner: The registration token is not valid, or has expired."), Event::TokenRefused);
97 assert_eq!(read("g1t-runner: could not make /data"), Event::Failed("could not make /data".into()));
98 assert_eq!(read("2026-10-12T08:00:00Z Registered syntaqx-win in acme."), Event::Other);
99 assert_eq!(message("plain"), "plain");
100 assert_eq!(read("2026-10-12T08:00:00Z could not reach g1t: dns error; trying again in 4s"), Event::Offline);
101 }
102}