@@ -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,67 @@ 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+ fn escape_multiline_message ( message : & str ) -> String {
110+ let trailing_newlines = message. len ( ) - message. trim_end_matches ( '\n' ) . len ( ) ;
111+ if trailing_newlines > 0 {
112+ let stripped = & message[ ..message. len ( ) - trailing_newlines] ;
113+ let escaped = stripped. replace ( '\n' , "%0A" ) ;
114+ let newlines = "\n " . repeat ( trailing_newlines) ;
115+ format ! ( "{escaped}{newlines}" )
116+ } else {
117+ message. replace ( '\n' , "%0A" )
118+ }
119+ }
120+
121+ #[ cfg( test) ]
122+ mod tests {
123+ use super :: * ;
124+
125+ #[ test]
126+ fn test_escape_multiline_message_no_newlines ( ) {
127+ assert_eq ! ( escape_multiline_message( "hello world" ) , "hello world" ) ;
128+ }
129+
130+ #[ test]
131+ fn test_escape_multiline_message_single_trailing_newline ( ) {
132+ assert_eq ! ( escape_multiline_message( "hello world\n " ) , "hello world\n " ) ;
133+ }
134+
135+ #[ test]
136+ fn test_escape_multiline_message_internal_newlines ( ) {
137+ assert_eq ! (
138+ escape_multiline_message( "line1\n line2\n line3" ) ,
139+ "line1%0Aline2%0Aline3"
140+ ) ;
141+ }
142+
143+ #[ test]
144+ fn test_escape_multiline_message_internal_and_trailing_newline ( ) {
145+ assert_eq ! (
146+ escape_multiline_message( "line1\n line2\n line3\n " ) ,
147+ "line1%0Aline2%0Aline3\n "
148+ ) ;
149+ }
150+
151+ #[ test]
152+ fn test_escape_multiline_message_empty_string ( ) {
153+ assert_eq ! ( escape_multiline_message( "" ) , "" ) ;
154+ }
155+
156+ #[ test]
157+ fn test_escape_multiline_message_only_newline ( ) {
158+ assert_eq ! ( escape_multiline_message( "\n " ) , "\n " ) ;
159+ }
160+
161+ #[ test]
162+ fn test_escape_multiline_message_multiple_trailing_newlines ( ) {
163+ assert_eq ! ( escape_multiline_message( "hello\n \n " ) , "hello\n \n " ) ;
164+ }
165+ }
0 commit comments