@@ -56,9 +56,7 @@ impl Log for GithubActionLogger {
5656 }
5757
5858 if let Some ( announcement) = get_announcement_event ( record) {
59- // properly escape newlines so that GitHub Actions interprets them correctly
60- // https://github.com/actions/toolkit/issues/193#issuecomment-605394935
61- let escaped_announcement = announcement. replace ( '\n' , "%0A" ) ;
59+ let escaped_announcement = escape_multiline_message ( & announcement) ;
6260 // TODO: make the announcement title configurable
6361 println ! ( "::notice title=New CodSpeed Feature::{escaped_announcement}" ) ;
6462 return ;
@@ -79,9 +77,7 @@ impl Log for GithubActionLogger {
7977 Level :: Debug => "::debug::" ,
8078 Level :: Trace => "::debug::[TRACE]" ,
8179 } ;
82- // properly escape newlines so that GitHub Actions interprets them correctly
83- // https://github.com/actions/toolkit/issues/193#issuecomment-605394935
84- let message_string = message. to_string ( ) . replace ( '\n' , "%0A" ) ;
80+ let message_string = escape_multiline_message ( & message. to_string ( ) ) ;
8581 println ! ( "{prefix}{message_string}" ) ;
8682 }
8783
@@ -103,3 +99,68 @@ impl SharedLogger for GithubActionLogger {
10399 Box :: new ( * self )
104100 }
105101}
102+
103+ /// Escapes newlines in a message for GitHub Actions logging.
104+ /// GitHub Actions requires newlines to be replaced with `%0A` to be interpreted correctly.
105+ ///
106+ /// See https://github.com/actions/toolkit/issues/193#issuecomment-605394935
107+ ///
108+ /// One exception: trailing newlines are preserved as actual newlines to maintain formatting.
109+ /// Otherwise, the message gets displayed with extra `%0A` at the end.
110+ fn escape_multiline_message ( message : & str ) -> String {
111+ let trailing_newlines = message. len ( ) - message. trim_end_matches ( '\n' ) . len ( ) ;
112+ if trailing_newlines > 0 {
113+ let stripped = & message[ ..message. len ( ) - trailing_newlines] ;
114+ let escaped = stripped. replace ( '\n' , "%0A" ) ;
115+ let newlines = "\n " . repeat ( trailing_newlines) ;
116+ format ! ( "{escaped}{newlines}" )
117+ } else {
118+ message. replace ( '\n' , "%0A" )
119+ }
120+ }
121+
122+ #[ cfg( test) ]
123+ mod tests {
124+ use super :: * ;
125+
126+ #[ test]
127+ fn test_escape_multiline_message_no_newlines ( ) {
128+ assert_eq ! ( escape_multiline_message( "hello world" ) , "hello world" ) ;
129+ }
130+
131+ #[ test]
132+ fn test_escape_multiline_message_single_trailing_newline ( ) {
133+ assert_eq ! ( escape_multiline_message( "hello world\n " ) , "hello world\n " ) ;
134+ }
135+
136+ #[ test]
137+ fn test_escape_multiline_message_internal_newlines ( ) {
138+ assert_eq ! (
139+ escape_multiline_message( "line1\n line2\n line3" ) ,
140+ "line1%0Aline2%0Aline3"
141+ ) ;
142+ }
143+
144+ #[ test]
145+ fn test_escape_multiline_message_internal_and_trailing_newline ( ) {
146+ assert_eq ! (
147+ escape_multiline_message( "line1\n line2\n line3\n " ) ,
148+ "line1%0Aline2%0Aline3\n "
149+ ) ;
150+ }
151+
152+ #[ test]
153+ fn test_escape_multiline_message_empty_string ( ) {
154+ assert_eq ! ( escape_multiline_message( "" ) , "" ) ;
155+ }
156+
157+ #[ test]
158+ fn test_escape_multiline_message_only_newline ( ) {
159+ assert_eq ! ( escape_multiline_message( "\n " ) , "\n " ) ;
160+ }
161+
162+ #[ test]
163+ fn test_escape_multiline_message_multiple_trailing_newlines ( ) {
164+ assert_eq ! ( escape_multiline_message( "hello\n \n " ) , "hello\n \n " ) ;
165+ }
166+ }
0 commit comments