Skip to main content

pipedash_plugin_gitea/
log_dialect.rs

1use pipedash_plugin_actions::ActionsLogDialect;
2use pipedash_plugin_api::{
3    LineMarker,
4    LogDialect,
5    ParsedLine,
6};
7
8pub struct GiteaLogDialect;
9
10impl LogDialect for GiteaLogDialect {
11    fn parse<'a>(&self, line: &'a [u8]) -> ParsedLine<'a> {
12        let parsed = ActionsLogDialect.parse(line);
13        if parsed.marker != LineMarker::Plain {
14            return parsed;
15        }
16        match workflow_command(parsed.body) {
17            Some((marker, body)) => ParsedLine {
18                marker,
19                body,
20                ..parsed
21            },
22            None => parsed,
23        }
24    }
25}
26
27fn workflow_command(body: &[u8]) -> Option<(LineMarker, &[u8])> {
28    let rest = body.strip_prefix(b"::")?;
29    let end = rest.windows(2).position(|pair| pair == b"::")?;
30    let name = rest[..end].split(|b| *b == b' ').next()?;
31    let marker = match name {
32        b"group" => LineMarker::GroupStart,
33        b"endgroup" => LineMarker::GroupEnd,
34        b"error" => LineMarker::Error,
35        b"warning" => LineMarker::Warning,
36        b"notice" => LineMarker::Notice,
37        b"debug" => LineMarker::Debug,
38        _ => return None,
39    };
40    Some((marker, &rest[end + 2..]))
41}
42
43#[cfg(test)]
44mod tests {
45    use chrono::{
46        DateTime,
47        Utc,
48    };
49
50    use super::*;
51
52    fn parse(line: &[u8]) -> ParsedLine<'_> {
53        GiteaLogDialect.parse(line)
54    }
55
56    #[test]
57    fn recorded_gitea_line_splits_into_timestamp_and_text() {
58        let parsed = parse(b"2026-10-28T10:15:30.1234567Z + go test ./...");
59        assert_eq!(parsed.marker, LineMarker::Plain);
60        assert_eq!(parsed.body, b"+ go test ./...");
61        assert_eq!(
62            parsed.timestamp,
63            Some(
64                "2026-10-28T10:15:30.1234567Z"
65                    .parse::<DateTime<Utc>>()
66                    .unwrap()
67            )
68        );
69    }
70
71    #[test]
72    fn group_and_endgroup_commands_open_and_close_a_section() {
73        let group = parse(b"2026-10-28T10:15:31.0000000Z ::group::Install dependencies");
74        assert_eq!(group.marker, LineMarker::GroupStart);
75        assert_eq!(group.body, b"Install dependencies");
76        assert!(group.timestamp.is_some());
77
78        let end = parse(b"2026-10-28T10:15:32.0000000Z ::endgroup::");
79        assert_eq!(end.marker, LineMarker::GroupEnd);
80        assert_eq!(end.body, b"");
81    }
82
83    #[test]
84    fn annotation_commands_become_severity_markers() {
85        let cases: [(&[u8], LineMarker, &[u8]); 4] = [
86            (b"::error::Build failed", LineMarker::Error, b"Build failed"),
87            (
88                b"::warning::Cache missed",
89                LineMarker::Warning,
90                b"Cache missed",
91            ),
92            (b"::notice::Deployed", LineMarker::Notice, b"Deployed"),
93            (
94                b"::debug::cwd=/workspace",
95                LineMarker::Debug,
96                b"cwd=/workspace",
97            ),
98        ];
99        for (text, marker, body) in cases {
100            let line = [b"2026-10-28T10:15:33.0000000Z ".as_slice(), text].concat();
101            let parsed = parse(&line);
102            assert_eq!(parsed.marker, marker, "{}", String::from_utf8_lossy(text));
103            assert!(parsed.timestamp.is_some());
104            assert_eq!(parsed.body, body);
105        }
106    }
107
108    #[test]
109    fn annotation_parameters_are_dropped_from_the_body() {
110        let parsed = parse(
111            b"2026-10-28T10:15:34.0000000Z ::error file=app.js,line=3,col=7::Missing semicolon",
112        );
113        assert_eq!(parsed.marker, LineMarker::Error);
114        assert_eq!(parsed.body, b"Missing semicolon");
115    }
116
117    #[test]
118    fn github_style_markers_still_work() {
119        let parsed = parse(b"2026-10-28T10:15:35.0000000Z ##[group]Run make");
120        assert_eq!(parsed.marker, LineMarker::GroupStart);
121        assert_eq!(parsed.body, b"Run make");
122
123        let parsed = parse(b"2026-10-28T10:15:36.0000000Z ##[error]exit code 2");
124        assert_eq!(parsed.marker, LineMarker::Error);
125        assert_eq!(parsed.body, b"exit code 2");
126    }
127
128    #[test]
129    fn other_workflow_commands_and_unterminated_commands_stay_plain() {
130        for text in [
131            &b"::set-output name=sha::abc123"[..],
132            b"::add-mask::***",
133            b"::group no closing colons",
134            b"::groupx::name",
135            b"echo ::group::inside a sentence",
136        ] {
137            let line = [b"2026-10-28T10:15:37.0000000Z ".as_slice(), text].concat();
138            let parsed = parse(&line);
139            assert_eq!(
140                parsed.marker,
141                LineMarker::Plain,
142                "{}",
143                String::from_utf8_lossy(text)
144            );
145            assert_eq!(parsed.body, text);
146        }
147    }
148
149    #[test]
150    fn line_without_timestamp_still_reads_its_command() {
151        let parsed = parse(b"::group::Set up job");
152        assert_eq!(parsed.timestamp, None);
153        assert_eq!(parsed.marker, LineMarker::GroupStart);
154        assert_eq!(parsed.body, b"Set up job");
155    }
156
157    #[test]
158    fn no_line_starts_a_step() {
159        for text in [
160            &b"::group::Run actions/checkout@v4"[..],
161            b"Post job cleanup.",
162        ] {
163            let parsed = parse(text);
164            assert!(!GiteaLogDialect.is_step_start(&parsed));
165        }
166    }
167}