@@ -11,6 +11,11 @@ use std::io;
11
11
12
12
const VERSION : & str = env ! ( "CARGO_PKG_VERSION" ) ;
13
13
14
+ // GitHub API limit for issue comment body is 65,536 characters
15
+ // We use a slightly smaller limit to leave room for the comment structure
16
+ const GITHUB_COMMENT_MAX_SIZE : usize = 65_000 ;
17
+ const MAX_SQL_PREVIEW_LINES : usize = 50 ;
18
+
14
19
fn get_github_private_key (
15
20
github_private_key : Option < String > ,
16
21
github_private_key_base64 : Option < String > ,
@@ -158,7 +163,13 @@ fn get_comment_body(files: &[CheckReport], version: &str) -> String {
158
163
159
164
let violations_emoji = get_violations_emoji ( violations_count) ;
160
165
161
- format ! (
166
+ // First, try to generate the full comment
167
+ let sql_file_contents: Vec < String > = files
168
+ . iter ( )
169
+ . filter_map ( |x| get_sql_file_content ( x) . ok ( ) )
170
+ . collect ( ) ;
171
+
172
+ let full_comment = format ! (
162
173
r"
163
174
{COMMENT_HEADER}
164
175
@@ -174,11 +185,79 @@ fn get_comment_body(files: &[CheckReport], version: &str) -> String {
174
185
violations_emoji = violations_emoji,
175
186
violation_count = violations_count,
176
187
file_count = files. len( ) ,
177
- sql_file_content = files
178
- . iter( )
179
- . filter_map( |x| get_sql_file_content( x) . ok( ) )
180
- . collect:: <Vec <String >>( )
181
- . join( "\n " ) ,
188
+ sql_file_content = sql_file_contents. join( "\n " ) ,
189
+ version = version
190
+ ) ;
191
+
192
+ // Check if the comment exceeds GitHub's size limit
193
+ if full_comment. len ( ) <= GITHUB_COMMENT_MAX_SIZE {
194
+ return full_comment. trim_matches ( '\n' ) . into ( ) ;
195
+ }
196
+
197
+ // If the comment is too large, create a summary instead
198
+ get_summary_comment_body ( files, violations_count, violations_emoji, version)
199
+ }
200
+
201
+ fn get_summary_comment_body (
202
+ files : & [ CheckReport ] ,
203
+ violations_count : usize ,
204
+ violations_emoji : & str ,
205
+ version : & str ,
206
+ ) -> String {
207
+ let mut file_summaries = Vec :: new ( ) ;
208
+
209
+ for file in files {
210
+ let violation_count = file. violations . len ( ) ;
211
+ let violations_emoji = get_violations_emoji ( violation_count) ;
212
+ let line_count = file. sql . lines ( ) . count ( ) ;
213
+
214
+ let summary = format ! (
215
+ r"
216
+ <h3><code>{filename}</code></h3>
217
+
218
+ 📄 **{line_count} lines** | {violations_emoji} **{violation_count} violations**
219
+
220
+ {violations_detail}
221
+
222
+ ---
223
+ " ,
224
+ filename = file. filename,
225
+ line_count = line_count,
226
+ violations_emoji = violations_emoji,
227
+ violation_count = violation_count,
228
+ violations_detail = if violation_count > 0 {
229
+ let violation_rules: Vec <String > = file
230
+ . violations
231
+ . iter( )
232
+ . map( |v| format!( "• `{}` (line {})" , v. rule_name, v. line + 1 ) )
233
+ . collect( ) ;
234
+ format!( "**Violations found:**\n {}" , violation_rules. join( "\n " ) )
235
+ } else {
236
+ "✅ No violations found." . to_string( )
237
+ }
238
+ ) ;
239
+ file_summaries. push ( summary) ;
240
+ }
241
+
242
+ format ! (
243
+ r"
244
+ {COMMENT_HEADER}
245
+
246
+ ### **{violations_emoji} {violation_count}** violations across **{file_count}** file(s)
247
+
248
+ > ⚠️ **Large Report**: This report was summarized due to size constraints. SQL content has been omitted but all violations were analyzed.
249
+
250
+ ---
251
+ {file_summaries}
252
+
253
+ [📚 More info on rules](https://github.com/sbdchd/squawk#rules)
254
+
255
+ ⚡️ Powered by [`Squawk`](https://github.com/sbdchd/squawk) ({version}), a linter for PostgreSQL, focused on migrations
256
+ " ,
257
+ violations_emoji = violations_emoji,
258
+ violation_count = violations_count,
259
+ file_count = files. len( ) ,
260
+ file_summaries = file_summaries. join( "\n " ) ,
182
261
version = version
183
262
)
184
263
. trim_matches ( '\n' )
@@ -189,6 +268,22 @@ const fn get_violations_emoji(count: usize) -> &'static str {
189
268
if count > 0 { "🚒" } else { "✅" }
190
269
}
191
270
271
+ fn truncate_sql_if_needed ( sql : & str , max_lines : usize ) -> ( String , bool ) {
272
+ let lines: Vec < & str > = sql. lines ( ) . collect ( ) ;
273
+ if lines. len ( ) <= max_lines {
274
+ ( sql. to_string ( ) , false )
275
+ } else {
276
+ let truncated_lines = lines[ ..max_lines] . join ( "\n " ) ;
277
+ let remaining_lines = lines. len ( ) - max_lines;
278
+ (
279
+ format ! (
280
+ "{truncated_lines}\n \n -- ... ({remaining_lines} more lines truncated for brevity)"
281
+ ) ,
282
+ true ,
283
+ )
284
+ }
285
+ }
286
+
192
287
fn get_sql_file_content ( violation : & CheckReport ) -> Result < String > {
193
288
let sql = & violation. sql ;
194
289
let mut buff = Vec :: new ( ) ;
@@ -212,14 +307,21 @@ fn get_sql_file_content(violation: &CheckReport) -> Result<String> {
212
307
} ;
213
308
214
309
let violations_emoji = get_violations_emoji ( violation_count) ;
310
+ let ( display_sql, was_truncated) = truncate_sql_if_needed ( sql, MAX_SQL_PREVIEW_LINES ) ;
311
+
312
+ let truncation_notice = if was_truncated {
313
+ "\n \n > ⚠️ **Note**: SQL content has been truncated for display purposes. The full analysis was performed on the complete file."
314
+ } else {
315
+ ""
316
+ } ;
215
317
216
318
Ok ( format ! (
217
319
r"
218
320
<h3><code>{filename}</code></h3>
219
321
220
322
```sql
221
323
{sql}
222
- ```
324
+ ```{truncation_notice}
223
325
224
326
<h4>{violations_emoji} Rule Violations ({violation_count})</h4>
225
327
@@ -229,7 +331,8 @@ fn get_sql_file_content(violation: &CheckReport) -> Result<String> {
229
331
" ,
230
332
violations_emoji = violations_emoji,
231
333
filename = violation. filename,
232
- sql = sql,
334
+ sql = display_sql,
335
+ truncation_notice = truncation_notice,
233
336
violation_count = violation_count,
234
337
violation_content = violation_content
235
338
) )
@@ -320,4 +423,100 @@ ALTER TABLE "core_recipe" ADD COLUMN "foo" integer DEFAULT 10;
320
423
321
424
assert_snapshot ! ( body) ;
322
425
}
426
+
427
+ #[ test]
428
+ fn test_sql_truncation ( ) {
429
+ let short_sql = "SELECT 1;" ;
430
+ let ( result, truncated) = crate :: github:: truncate_sql_if_needed ( short_sql, 5 ) ;
431
+ assert ! ( !truncated) ;
432
+ assert_eq ! ( result, short_sql) ;
433
+
434
+ let long_sql = ( 0 ..100 )
435
+ . map ( |i| format ! ( "-- Line {}" , i) )
436
+ . collect :: < Vec < _ > > ( )
437
+ . join ( "\n " ) ;
438
+ let ( result, truncated) = crate :: github:: truncate_sql_if_needed ( & long_sql, 50 ) ;
439
+ assert ! ( truncated) ;
440
+ assert ! ( result. contains( "-- ... (50 more lines truncated for brevity)" ) ) ;
441
+ }
442
+
443
+ #[ test]
444
+ fn generating_comment_with_large_content ( ) {
445
+ // Create a very large SQL content
446
+ let large_sql = ( 0 ..1000 )
447
+ . map ( |i| format ! ( "SELECT {} as col{};" , i, i) )
448
+ . collect :: < Vec < _ > > ( )
449
+ . join ( "\n " ) ;
450
+
451
+ let violations = vec ! [ CheckReport {
452
+ filename: "large.sql" . into( ) ,
453
+ sql: large_sql,
454
+ violations: vec![ ReportViolation {
455
+ file: "large.sql" . into( ) ,
456
+ line: 1 ,
457
+ column: 0 ,
458
+ level: ViolationLevel :: Warning ,
459
+ rule_name: "prefer-bigint-over-int" . to_string( ) ,
460
+ range: TextRange :: new( TextSize :: new( 0 ) , TextSize :: new( 0 ) ) ,
461
+ message: "Prefer bigint over int." . to_string( ) ,
462
+ help: Some ( "Use bigint instead." . to_string( ) ) ,
463
+ column_end: 0 ,
464
+ line_end: 1 ,
465
+ } ] ,
466
+ } ] ;
467
+
468
+ let body = get_comment_body ( & violations, "0.2.3" ) ;
469
+
470
+ // The comment should be within GitHub's size limits
471
+ assert ! ( body. len( ) <= super :: GITHUB_COMMENT_MAX_SIZE ) ;
472
+
473
+ // Should contain summary information even if the full content was too large
474
+ assert ! ( body. contains( "violations" ) ) ;
475
+ }
476
+
477
+ #[ test]
478
+ fn generating_comment_forced_summary ( ) {
479
+ // Create content that will definitely trigger summary mode
480
+ let massive_sql = ( 0 ..10000 )
481
+ . map ( |i| format ! ( "SELECT {} as col{};" , i, i) )
482
+ . collect :: < Vec < _ > > ( )
483
+ . join ( "\n " ) ;
484
+
485
+ let violations = vec ! [ CheckReport {
486
+ filename: "massive.sql" . into( ) ,
487
+ sql: massive_sql,
488
+ violations: vec![ ReportViolation {
489
+ file: "massive.sql" . into( ) ,
490
+ line: 1 ,
491
+ column: 0 ,
492
+ level: ViolationLevel :: Warning ,
493
+ rule_name: "prefer-bigint-over-int" . to_string( ) ,
494
+ range: TextRange :: new( TextSize :: new( 0 ) , TextSize :: new( 0 ) ) ,
495
+ message: "Prefer bigint over int." . to_string( ) ,
496
+ help: Some ( "Use bigint instead." . to_string( ) ) ,
497
+ column_end: 0 ,
498
+ line_end: 1 ,
499
+ } ] ,
500
+ } ] ;
501
+
502
+ let body = get_comment_body ( & violations, "0.2.3" ) ;
503
+
504
+ // Debug: Print the actual size to see what's happening
505
+ println ! (
506
+ "Comment body size: {}, limit: {}" ,
507
+ body. len( ) ,
508
+ super :: GITHUB_COMMENT_MAX_SIZE
509
+ ) ;
510
+
511
+ // The comment should be within GitHub's size limits
512
+ assert ! ( body. len( ) <= super :: GITHUB_COMMENT_MAX_SIZE ) ;
513
+
514
+ // Should contain the summary notice
515
+ if body. contains ( "Large Report" ) {
516
+ assert ! ( body. contains( "summarized due to size constraints" ) ) ;
517
+ } else {
518
+ // If it didn't trigger summary mode, at least verify it contains violations info
519
+ assert ! ( body. contains( "violations" ) ) ;
520
+ }
521
+ }
323
522
}
0 commit comments