pipedash_plugin_actions/
log_dialect.rs1use chrono::{
2 DateTime,
3 Utc,
4};
5use pipedash_plugin_api::{
6 LineMarker,
7 LogDialect,
8 ParsedLine,
9};
10
11const MARKERS: [(&[u8], LineMarker); 8] = [
12 (b"##[group]", LineMarker::GroupStart),
13 (b"##[endgroup]", LineMarker::GroupEnd),
14 (b"##[error]", LineMarker::Error),
15 (b"##[warning]", LineMarker::Warning),
16 (b"##[notice]", LineMarker::Notice),
17 (b"##[debug]", LineMarker::Debug),
18 (b"##[command]", LineMarker::Command),
19 (b"[command]", LineMarker::Command),
20];
21
22pub struct ActionsLogDialect;
23
24impl LogDialect for ActionsLogDialect {
25 fn parse<'a>(&self, line: &'a [u8]) -> ParsedLine<'a> {
26 let line = line.strip_prefix(b"\xEF\xBB\xBF").unwrap_or(line);
27 let (timestamp, body) = split_timestamp(line);
28 for (prefix, marker) in MARKERS {
29 if let Some(rest) = body.strip_prefix(prefix) {
30 return ParsedLine {
31 timestamp,
32 marker,
33 body: rest,
34 };
35 }
36 }
37 ParsedLine {
38 timestamp,
39 marker: LineMarker::Plain,
40 body,
41 }
42 }
43
44 fn is_step_start(&self, line: &ParsedLine<'_>) -> bool {
45 match line.marker {
46 LineMarker::GroupStart => line.body.starts_with(b"Run "),
47 LineMarker::Plain => line.body == b"Post job cleanup.",
48 _ => false,
49 }
50 }
51
52 fn infers_errors(&self) -> bool {
53 false
54 }
55}
56
57fn split_timestamp(line: &[u8]) -> (Option<DateTime<Utc>>, &[u8]) {
58 if line.len() <= 20 || line[4] != b'-' || line[10] != b'T' {
59 return (None, line);
60 }
61 let head = &line[..line.len().min(40)];
62 let Some(space) = head.iter().position(|b| *b == b' ') else {
63 return (None, line);
64 };
65 std::str::from_utf8(&line[..space])
66 .ok()
67 .and_then(pipedash_plugin_api::utils::parse_timestamp)
68 .map_or((None, line), |ts| (Some(ts), &line[space + 1..]))
69}
70
71#[cfg(test)]
72mod tests;