Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@
coverage.html
*.prof

# Security scan results
*.sarif
gosec-*.json

# Test directories
test-action/

# Dependency directories (remove the comment below to include it)
vendor/

Expand Down
199 changes: 199 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,21 @@ inputs:
required: false
default: 'false'

sarif-output:
description: 'Generate SARIF output for GitHub Code Scanning (requires security-events: write permission)'
required: false
default: 'false'

pr-comment:
description: 'Post validation results as a PR comment (only works on pull_request events)'
required: false
default: 'false'

pr-comment-compact:
description: 'Use compact format for PR comments (limits to 5 errors max)'
required: false
default: 'false'

gosqlx-version:
description: 'GoSQLX version to use (default: latest)'
required: false
Expand Down Expand Up @@ -297,6 +312,190 @@ runs:
exit 1
fi

- name: Generate SARIF output
id: sarif
if: inputs.sarif-output == 'true' && steps.find-files.outputs.file-count != '0'
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
echo "::group::Generate SARIF Report"

# Build validation command with SARIF output
CMD="$HOME/go/bin/gosqlx validate --output-format sarif --output-file gosqlx-results.sarif"

# Add config if provided
if [ -n "${{ inputs.config }}" ]; then
if [ -f "${{ inputs.config }}" ]; then
export GOSQLX_CONFIG="${{ inputs.config }}"
fi
fi

# Add dialect if provided
DIALECT="${{ inputs.dialect }}"
if [ -n "$DIALECT" ] && [[ "$DIALECT" =~ ^(postgresql|mysql|sqlserver|oracle|sqlite)$ ]]; then
CMD="$CMD --dialect $DIALECT"
fi

# Add strict mode if enabled
if [ "${{ inputs.strict }}" = "true" ]; then
CMD="$CMD --strict"
fi

# Read files and run validation to generate SARIF
cat /tmp/gosqlx-files.txt | tr '\n' ' ' | xargs $CMD || true

# Check if SARIF file was created
if [ -f "gosqlx-results.sarif" ]; then
echo "✓ SARIF report generated: gosqlx-results.sarif"
echo "sarif-file=gosqlx-results.sarif" >> $GITHUB_OUTPUT
else
echo "::warning::SARIF report generation failed"
fi

echo "::endgroup::"

- name: Upload SARIF to GitHub Code Scanning
if: inputs.sarif-output == 'true' && steps.sarif.outputs.sarif-file != ''
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: ${{ inputs.working-directory }}/gosqlx-results.sarif
category: gosqlx-sql-validation

- name: Post PR Comment
id: pr-comment
if: inputs.pr-comment == 'true' && github.event_name == 'pull_request' && steps.validate.conclusion != 'skipped'
shell: bash
working-directory: ${{ inputs.working-directory }}
env:
GH_TOKEN: ${{ github.token }}
run: |
echo "::group::Generate PR Comment"

# Create a temporary Go program to format the validation results as a PR comment
cat > /tmp/format_comment.go << 'SCRIPT_EOF'
package main

import (
"encoding/json"
"fmt"
"os"
"strings"
"time"
)

type FileValidationResult struct {
Path string
Valid bool
Size int64
Error *string
}

type ValidationResult struct {
TotalFiles int
ValidFiles int
InvalidFiles int
TotalBytes int64
Duration string
Files []FileValidationResult
}

func formatPRComment(result *ValidationResult, compact bool) string {
var sb strings.Builder

duration, _ := time.ParseDuration(result.Duration)

if compact {
if result.InvalidFiles == 0 {
sb.WriteString("## ✅ GoSQLX: All SQL files valid\n\n")
sb.WriteString(fmt.Sprintf("Validated **%d** file(s) in **%v**\n", result.ValidFiles, duration))
} else {
sb.WriteString(fmt.Sprintf("## ❌ GoSQLX: Found issues in %d/%d files\n\n", result.InvalidFiles, result.TotalFiles))
errorCount := 0
maxErrors := 5
for _, file := range result.Files {
if file.Error != nil && errorCount < maxErrors {
sb.WriteString(fmt.Sprintf("- ❌ `%s`: %s\n", file.Path, *file.Error))
errorCount++
}
}
if result.InvalidFiles > maxErrors {
sb.WriteString(fmt.Sprintf("\n*...and %d more error(s). Run locally for full details.*\n", result.InvalidFiles-maxErrors))
}
}
sb.WriteString("\n---\n")
sb.WriteString(fmt.Sprintf("⏱️ %v", duration))
if result.TotalFiles > 0 && duration.Seconds() > 0 {
throughput := float64(result.TotalFiles) / duration.Seconds()
sb.WriteString(fmt.Sprintf(" | 🚀 %.1f files/sec", throughput))
}
} else {
sb.WriteString("## 🔍 GoSQLX SQL Validation Results\n\n")
if result.InvalidFiles == 0 {
sb.WriteString("### ✅ All SQL files are valid!\n\n")
sb.WriteString(fmt.Sprintf("**%d** file(s) validated successfully in **%v**\n\n", result.ValidFiles, duration))
} else {
sb.WriteString(fmt.Sprintf("### ❌ Found issues in %d file(s)\n\n", result.InvalidFiles))
}
sb.WriteString("| Metric | Value |\n|--------|-------|\n")
sb.WriteString(fmt.Sprintf("| Total Files | %d |\n", result.TotalFiles))
sb.WriteString(fmt.Sprintf("| ✅ Valid | %d |\n", result.ValidFiles))
sb.WriteString(fmt.Sprintf("| ❌ Invalid | %d |\n", result.InvalidFiles))
sb.WriteString(fmt.Sprintf("| ⏱️ Duration | %v |\n", duration))
if result.TotalFiles > 0 && duration.Seconds() > 0 {
throughput := float64(result.TotalFiles) / duration.Seconds()
sb.WriteString(fmt.Sprintf("| 🚀 Throughput | %.1f files/sec |\n", throughput))
}
sb.WriteString("\n")
if result.InvalidFiles > 0 {
sb.WriteString("### 📋 Validation Errors\n\n")
for _, file := range result.Files {
if file.Error != nil {
sb.WriteString(fmt.Sprintf("#### ❌ `%s`\n\n```\n%s\n```\n\n", file.Path, *file.Error))
}
}
}
sb.WriteString("---\n*Powered by [GoSQLX](https://github.com/ajitpratap0/GoSQLX) - Ultra-fast SQL validation (100x faster than SQLFluff)*\n")
}
return sb.String()
}

func main() {
var result ValidationResult
if err := json.NewDecoder(os.Stdin).Decode(&result); err != nil {
fmt.Fprintf(os.Stderr, "Error decoding JSON: %v\n", err)
os.Exit(1)
}
compact := len(os.Args) > 1 && os.Args[1] == "compact"
fmt.Print(formatPRComment(&result, compact))
}
SCRIPT_EOF

# Create JSON from validation results
cat > /tmp/validation_results.json << JSON_EOF
{
"TotalFiles": ${{ steps.validate.outputs.validated-files || 0 }} + ${{ steps.validate.outputs.invalid-files || 0 }},
"ValidFiles": ${{ steps.validate.outputs.validated-files || 0 }},
"InvalidFiles": ${{ steps.validate.outputs.invalid-files || 0 }},
"Duration": "${{ steps.validate.outputs.validation-time || 0 }}ms",
"Files": []
}
JSON_EOF

# Format the compact argument
COMPACT_ARG=""
if [ "${{ inputs.pr-comment-compact }}" = "true" ]; then
COMPACT_ARG="compact"
fi

# Generate comment using Go script
COMMENT_BODY=$(go run /tmp/format_comment.go $COMPACT_ARG < /tmp/validation_results.json)

# Post comment to PR using gh CLI
echo "$COMMENT_BODY" | gh pr comment ${{ github.event.pull_request.number }} --body-file -

echo "✓ Posted validation results to PR #${{ github.event.pull_request.number }}"
echo "::endgroup::"

- name: Check SQL formatting
id: format-check
if: inputs.format-check == 'true' && steps.find-files.outputs.file-count != '0'
Expand Down
2 changes: 1 addition & 1 deletion cmd/gosqlx/cmd/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func formatRun(cmd *cobra.Command, args []string) error {
Check: formatCheck,
MaxLine: formatMaxLine,
Verbose: verbose,
Output: output,
Output: outputFile,
})

// Create formatter with injectable output writers
Expand Down
11 changes: 7 additions & 4 deletions cmd/gosqlx/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import (
"github.com/spf13/cobra"
)

// Version is the current version of gosqlx CLI
var Version = "1.4.0"

var (
// Global flags
verbose bool
output string
format string
verbose bool
outputFile string
format string
)

// rootCmd represents the base command when called without any subcommands
Expand Down Expand Up @@ -42,6 +45,6 @@ func Execute() error {
func init() {
// Global flags
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "enable verbose output")
rootCmd.PersistentFlags().StringVarP(&output, "output", "o", "", "output file (default: stdout)")
rootCmd.PersistentFlags().StringVarP(&outputFile, "output", "o", "", "output file (default: stdout)")
rootCmd.PersistentFlags().StringVarP(&format, "format", "f", "auto", "output format: json, yaml, table, tree, auto")
}
59 changes: 52 additions & 7 deletions cmd/gosqlx/cmd/validate.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
package cmd

import (
"fmt"
"os"

"github.com/spf13/cobra"
"github.com/spf13/pflag"

"github.com/ajitpratap0/GoSQLX/cmd/gosqlx/internal/config"
"github.com/ajitpratap0/GoSQLX/cmd/gosqlx/internal/output"
)

var (
validateRecursive bool
validatePattern string
validateQuiet bool
validateStats bool
validateDialect string
validateStrict bool
validateRecursive bool
validatePattern string
validateQuiet bool
validateStats bool
validateDialect string
validateStrict bool
validateOutputFormat string
validateOutputFile string
)

// validateCmd represents the validate command
Expand All @@ -31,6 +35,12 @@
gosqlx validate -r ./queries/ # Recursively validate directory
gosqlx validate --quiet query.sql # Quiet mode (exit code only)
gosqlx validate --stats ./queries/ # Show performance statistics
gosqlx validate --output-format sarif --output-file results.sarif queries/ # SARIF output for GitHub Code Scanning
Output Formats:
text - Human-readable output (default)
json - JSON format for programmatic consumption
sarif - SARIF 2.1.0 format for GitHub Code Scanning integration
Performance Target: <10ms for typical queries (50-500 characters)
Throughput: 100+ files/second in batch mode`,
Expand All @@ -46,6 +56,11 @@
cfg = config.DefaultConfig()
}

// Validate output format
if validateOutputFormat != "" && validateOutputFormat != "text" && validateOutputFormat != "json" && validateOutputFormat != "sarif" {
return fmt.Errorf("invalid output format: %s (valid options: text, json, sarif)", validateOutputFormat)
}

// Track which flags were explicitly set
flagsChanged := make(map[string]bool)
cmd.Flags().Visit(func(f *pflag.Flag) {
Expand All @@ -58,10 +73,13 @@
}

// Create validator options from config and flags
// When outputting SARIF, automatically enable quiet mode to avoid mixing output
quietMode := validateQuiet || validateOutputFormat == "sarif"

opts := ValidatorOptionsFromConfig(cfg, flagsChanged, ValidatorFlags{
Recursive: validateRecursive,
Pattern: validatePattern,
Quiet: validateQuiet,
Quiet: quietMode,
ShowStats: validateStats,
Dialect: validateDialect,
StrictMode: validateStrict,
Expand All @@ -77,6 +95,31 @@
return err
}

// Handle different output formats
if validateOutputFormat == "sarif" {
// Generate SARIF output
sarifData, err := output.FormatSARIF(result, Version)
if err != nil {
return fmt.Errorf("failed to generate SARIF output: %w", err)
}

// Write SARIF output to file or stdout
if validateOutputFile != "" {
if err := os.WriteFile(validateOutputFile, sarifData, 0644); err != nil {
return fmt.Errorf("failed to write SARIF output: %w", err)
}
if !opts.Quiet {
fmt.Fprintf(cmd.OutOrStdout(), "SARIF output written to %s\n", validateOutputFile)
}
} else {
fmt.Fprint(cmd.OutOrStdout(), string(sarifData))
}
} else if validateOutputFormat == "json" {
// JSON output format will be implemented later
return fmt.Errorf("JSON output format not yet implemented")
}
// Default text output is already handled by the validator

// Exit with error code if there were invalid files
if result.InvalidFiles > 0 {
os.Exit(1)
Expand All @@ -94,4 +137,6 @@
validateCmd.Flags().BoolVarP(&validateStats, "stats", "s", false, "show performance statistics")
validateCmd.Flags().StringVar(&validateDialect, "dialect", "", "SQL dialect: postgresql, mysql, sqlserver, oracle, sqlite (config: validate.dialect)")
validateCmd.Flags().BoolVar(&validateStrict, "strict", false, "enable strict validation mode (config: validate.strict_mode)")
validateCmd.Flags().StringVar(&validateOutputFormat, "output-format", "text", "output format: text, json, sarif")
validateCmd.Flags().StringVar(&validateOutputFile, "output-file", "", "output file path (default: stdout)")
}
Loading
Loading