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.
| Merge the desktop app's runner: it starts with the app for the signed-in person, sandboxed by default and running nothing where no sandbox is available yet, registers through the site, keeps itself up to date, and says everywhere what it is doing, why it is not, and how to fix it. | 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)] | |
| 9 | pub 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 …`). | |
| 30 | pub 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 | ||
| 38 | pub 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)] | |
| 73 | mod 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 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.