pipedash_plugin_gitea/
log_dialect.rs1use 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}