Skip to content

Commit 4365b2f

Browse files
Ajit Pratap Singhclaude
authored andcommitted
feat: add PR comment integration for SQL validation results
Implements automatic posting of validation results as GitHub PR comments. Changes: - Add FormatPRComment() for detailed validation results formatting - Add FormatPRCommentCompact() for compact results (max 5 errors) - Comprehensive test suite with 3 test functions covering: - Full format with valid/invalid files - Compact format with error truncation - Markdown structure validation - Update action.yml with new inputs: - pr-comment: Enable/disable PR comment posting - pr-comment-compact: Use compact format - Integrate gh CLI for automated PR comment posting - Add inline Go script in action to format comments Features: - GitHub-flavored markdown with tables, emoji, and code blocks - Statistics table with throughput and duration metrics - Detailed error sections with file paths and error messages - Compact mode limits errors to prevent overly long comments - Automatic branding footer with performance claims Testing: - All tests passing with comprehensive edge case coverage - Validates markdown structure (headers, tables, bold text) - Tests both success and error scenarios 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent a4b36f6 commit 4365b2f

3 files changed

Lines changed: 523 additions & 0 deletions

File tree

action.yml

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,16 @@ inputs:
5757
required: false
5858
default: 'false'
5959

60+
pr-comment:
61+
description: 'Post validation results as a PR comment (only works on pull_request events)'
62+
required: false
63+
default: 'false'
64+
65+
pr-comment-compact:
66+
description: 'Use compact format for PR comments (limits to 5 errors max)'
67+
required: false
68+
default: 'false'
69+
6070
gosqlx-version:
6171
description: 'GoSQLX version to use (default: latest)'
6272
required: false
@@ -351,6 +361,141 @@ runs:
351361
sarif_file: ${{ inputs.working-directory }}/gosqlx-results.sarif
352362
category: gosqlx-sql-validation
353363

364+
- name: Post PR Comment
365+
id: pr-comment
366+
if: inputs.pr-comment == 'true' && github.event_name == 'pull_request' && steps.validate.conclusion != 'skipped'
367+
shell: bash
368+
working-directory: ${{ inputs.working-directory }}
369+
env:
370+
GH_TOKEN: ${{ github.token }}
371+
run: |
372+
echo "::group::Generate PR Comment"
373+
374+
# Create a temporary Go program to format the validation results as a PR comment
375+
cat > /tmp/format_comment.go << 'SCRIPT_EOF'
376+
package main
377+
378+
import (
379+
"encoding/json"
380+
"fmt"
381+
"os"
382+
"strings"
383+
"time"
384+
)
385+
386+
type FileValidationResult struct {
387+
Path string
388+
Valid bool
389+
Size int64
390+
Error *string
391+
}
392+
393+
type ValidationResult struct {
394+
TotalFiles int
395+
ValidFiles int
396+
InvalidFiles int
397+
TotalBytes int64
398+
Duration string
399+
Files []FileValidationResult
400+
}
401+
402+
func formatPRComment(result *ValidationResult, compact bool) string {
403+
var sb strings.Builder
404+
405+
duration, _ := time.ParseDuration(result.Duration)
406+
407+
if compact {
408+
if result.InvalidFiles == 0 {
409+
sb.WriteString("## ✅ GoSQLX: All SQL files valid\n\n")
410+
sb.WriteString(fmt.Sprintf("Validated **%d** file(s) in **%v**\n", result.ValidFiles, duration))
411+
} else {
412+
sb.WriteString(fmt.Sprintf("## ❌ GoSQLX: Found issues in %d/%d files\n\n", result.InvalidFiles, result.TotalFiles))
413+
errorCount := 0
414+
maxErrors := 5
415+
for _, file := range result.Files {
416+
if file.Error != nil && errorCount < maxErrors {
417+
sb.WriteString(fmt.Sprintf("- ❌ `%s`: %s\n", file.Path, *file.Error))
418+
errorCount++
419+
}
420+
}
421+
if result.InvalidFiles > maxErrors {
422+
sb.WriteString(fmt.Sprintf("\n*...and %d more error(s). Run locally for full details.*\n", result.InvalidFiles-maxErrors))
423+
}
424+
}
425+
sb.WriteString("\n---\n")
426+
sb.WriteString(fmt.Sprintf("⏱️ %v", duration))
427+
if result.TotalFiles > 0 && duration.Seconds() > 0 {
428+
throughput := float64(result.TotalFiles) / duration.Seconds()
429+
sb.WriteString(fmt.Sprintf(" | 🚀 %.1f files/sec", throughput))
430+
}
431+
} else {
432+
sb.WriteString("## 🔍 GoSQLX SQL Validation Results\n\n")
433+
if result.InvalidFiles == 0 {
434+
sb.WriteString("### ✅ All SQL files are valid!\n\n")
435+
sb.WriteString(fmt.Sprintf("**%d** file(s) validated successfully in **%v**\n\n", result.ValidFiles, duration))
436+
} else {
437+
sb.WriteString(fmt.Sprintf("### ❌ Found issues in %d file(s)\n\n", result.InvalidFiles))
438+
}
439+
sb.WriteString("| Metric | Value |\n|--------|-------|\n")
440+
sb.WriteString(fmt.Sprintf("| Total Files | %d |\n", result.TotalFiles))
441+
sb.WriteString(fmt.Sprintf("| ✅ Valid | %d |\n", result.ValidFiles))
442+
sb.WriteString(fmt.Sprintf("| ❌ Invalid | %d |\n", result.InvalidFiles))
443+
sb.WriteString(fmt.Sprintf("| ⏱️ Duration | %v |\n", duration))
444+
if result.TotalFiles > 0 && duration.Seconds() > 0 {
445+
throughput := float64(result.TotalFiles) / duration.Seconds()
446+
sb.WriteString(fmt.Sprintf("| 🚀 Throughput | %.1f files/sec |\n", throughput))
447+
}
448+
sb.WriteString("\n")
449+
if result.InvalidFiles > 0 {
450+
sb.WriteString("### 📋 Validation Errors\n\n")
451+
for _, file := range result.Files {
452+
if file.Error != nil {
453+
sb.WriteString(fmt.Sprintf("#### ❌ `%s`\n\n```\n%s\n```\n\n", file.Path, *file.Error))
454+
}
455+
}
456+
}
457+
sb.WriteString("---\n*Powered by [GoSQLX](https://github.com/ajitpratap0/GoSQLX) - Ultra-fast SQL validation (100x faster than SQLFluff)*\n")
458+
}
459+
return sb.String()
460+
}
461+
462+
func main() {
463+
var result ValidationResult
464+
if err := json.NewDecoder(os.Stdin).Decode(&result); err != nil {
465+
fmt.Fprintf(os.Stderr, "Error decoding JSON: %v\n", err)
466+
os.Exit(1)
467+
}
468+
compact := len(os.Args) > 1 && os.Args[1] == "compact"
469+
fmt.Print(formatPRComment(&result, compact))
470+
}
471+
SCRIPT_EOF
472+
473+
# Create JSON from validation results
474+
cat > /tmp/validation_results.json << JSON_EOF
475+
{
476+
"TotalFiles": ${{ steps.validate.outputs.validated-files || 0 }} + ${{ steps.validate.outputs.invalid-files || 0 }},
477+
"ValidFiles": ${{ steps.validate.outputs.validated-files || 0 }},
478+
"InvalidFiles": ${{ steps.validate.outputs.invalid-files || 0 }},
479+
"Duration": "${{ steps.validate.outputs.validation-time || 0 }}ms",
480+
"Files": []
481+
}
482+
JSON_EOF
483+
484+
# Format the compact argument
485+
COMPACT_ARG=""
486+
if [ "${{ inputs.pr-comment-compact }}" = "true" ]; then
487+
COMPACT_ARG="compact"
488+
fi
489+
490+
# Generate comment using Go script
491+
COMMENT_BODY=$(go run /tmp/format_comment.go $COMPACT_ARG < /tmp/validation_results.json)
492+
493+
# Post comment to PR using gh CLI
494+
echo "$COMMENT_BODY" | gh pr comment ${{ github.event.pull_request.number }} --body-file -
495+
496+
echo "✓ Posted validation results to PR #${{ github.event.pull_request.number }}"
497+
echo "::endgroup::"
498+
354499
- name: Check SQL formatting
355500
id: format-check
356501
if: inputs.format-check == 'true' && steps.find-files.outputs.file-count != '0'
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package output
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
// FormatPRComment formats validation results as a GitHub PR comment with markdown
9+
func FormatPRComment(result *ValidationResult) string {
10+
var sb strings.Builder
11+
12+
// Header
13+
sb.WriteString("## 🔍 GoSQLX SQL Validation Results\n\n")
14+
15+
// Summary section
16+
if result.InvalidFiles == 0 {
17+
sb.WriteString("### ✅ All SQL files are valid!\n\n")
18+
sb.WriteString(fmt.Sprintf("**%d** file(s) validated successfully in **%v**\n\n",
19+
result.ValidFiles, result.Duration))
20+
} else {
21+
sb.WriteString(fmt.Sprintf("### ❌ Found issues in **%d** file(s)\n\n", result.InvalidFiles))
22+
}
23+
24+
// Statistics table
25+
sb.WriteString("| Metric | Value |\n")
26+
sb.WriteString("|--------|-------|\n")
27+
sb.WriteString(fmt.Sprintf("| Total Files | %d |\n", result.TotalFiles))
28+
sb.WriteString(fmt.Sprintf("| ✅ Valid | %d |\n", result.ValidFiles))
29+
sb.WriteString(fmt.Sprintf("| ❌ Invalid | %d |\n", result.InvalidFiles))
30+
sb.WriteString(fmt.Sprintf("| ⏱️ Duration | %v |\n", result.Duration))
31+
32+
if result.TotalFiles > 0 && result.Duration.Seconds() > 0 {
33+
throughput := float64(result.TotalFiles) / result.Duration.Seconds()
34+
sb.WriteString(fmt.Sprintf("| 🚀 Throughput | %.1f files/sec |\n", throughput))
35+
}
36+
37+
sb.WriteString("\n")
38+
39+
// Detailed errors section
40+
if result.InvalidFiles > 0 {
41+
sb.WriteString("### 📋 Validation Errors\n\n")
42+
43+
for _, file := range result.Files {
44+
if file.Error != nil {
45+
// File header with error icon
46+
sb.WriteString(fmt.Sprintf("#### ❌ `%s`\n\n", file.Path))
47+
48+
// Error details in a code block
49+
sb.WriteString("```\n")
50+
sb.WriteString(file.Error.Error())
51+
sb.WriteString("\n```\n\n")
52+
}
53+
}
54+
}
55+
56+
// Footer
57+
sb.WriteString("---\n")
58+
sb.WriteString("*Powered by [GoSQLX](https://github.com/ajitpratap0/GoSQLX) - ")
59+
sb.WriteString("Ultra-fast SQL validation (100x faster than SQLFluff)*\n")
60+
61+
return sb.String()
62+
}
63+
64+
// FormatPRCommentCompact formats validation results as a compact PR comment
65+
// Useful for large validation runs to avoid overly long comments
66+
func FormatPRCommentCompact(result *ValidationResult, maxErrors int) string {
67+
var sb strings.Builder
68+
69+
// Header with summary
70+
if result.InvalidFiles == 0 {
71+
sb.WriteString("## ✅ GoSQLX: All SQL files valid\n\n")
72+
sb.WriteString(fmt.Sprintf("Validated **%d** file(s) in **%v**\n",
73+
result.ValidFiles, result.Duration))
74+
} else {
75+
sb.WriteString(fmt.Sprintf("## ❌ GoSQLX: Found issues in %d/%d files\n\n",
76+
result.InvalidFiles, result.TotalFiles))
77+
78+
// Show limited errors
79+
errorCount := 0
80+
for _, file := range result.Files {
81+
if file.Error != nil && errorCount < maxErrors {
82+
sb.WriteString(fmt.Sprintf("- ❌ `%s`: %s\n", file.Path, file.Error.Error()))
83+
errorCount++
84+
}
85+
}
86+
87+
// Show truncation message if needed
88+
if result.InvalidFiles > maxErrors {
89+
remaining := result.InvalidFiles - maxErrors
90+
sb.WriteString(fmt.Sprintf("\n*...and %d more error(s). Run locally for full details.*\n", remaining))
91+
}
92+
}
93+
94+
sb.WriteString("\n---\n")
95+
sb.WriteString(fmt.Sprintf("⏱️ %v | ", result.Duration))
96+
if result.TotalFiles > 0 && result.Duration.Seconds() > 0 {
97+
throughput := float64(result.TotalFiles) / result.Duration.Seconds()
98+
sb.WriteString(fmt.Sprintf("🚀 %.1f files/sec", throughput))
99+
}
100+
101+
return sb.String()
102+
}

0 commit comments

Comments
 (0)