Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
211 changes: 205 additions & 6 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 @@ -198,8 +213,8 @@ runs:
echo "... and $((FILE_COUNT - 10)) more files"
fi

# Save file list for later steps
echo "$FILES" > /tmp/gosqlx-files.txt
# Save file list for later steps (use RUNNER_TEMP for cross-platform compatibility)
echo "$FILES" > "$RUNNER_TEMP/gosqlx-files.txt"
echo "file-count=$FILE_COUNT" >> $GITHUB_OUTPUT

- name: Validate SQL files
Expand Down Expand Up @@ -266,7 +281,7 @@ runs:
echo "::error file=$SAFE_FILE::SQL validation failed"
INVALID=$((INVALID + 1))
fi
done < /tmp/gosqlx-files.txt
done < "$RUNNER_TEMP/gosqlx-files.txt"

END_TIME=$(date +%s%3N)
DURATION=$((END_TIME - START_TIME))
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 "$RUNNER_TEMP/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 > "$RUNNER_TEMP/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 > "$RUNNER_TEMP/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 "$RUNNER_TEMP/format_comment.go" $COMPACT_ARG < "$RUNNER_TEMP/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 All @@ -317,7 +516,7 @@ runs:
else
echo "✓ Properly formatted: $file"
fi
done < /tmp/gosqlx-files.txt
done < "$RUNNER_TEMP/gosqlx-files.txt"

echo "::endgroup::"

Expand Down Expand Up @@ -357,7 +556,7 @@ runs:
echo "⚠ Issues found: $file"
LINT_ISSUES=$((LINT_ISSUES + 1))
fi
done < /tmp/gosqlx-files.txt
done < "$RUNNER_TEMP/gosqlx-files.txt"

echo "::endgroup::"

Expand All @@ -375,4 +574,4 @@ runs:
if: always()
shell: bash
run: |
rm -f /tmp/gosqlx-files.txt
rm -f "$RUNNER_TEMP/gosqlx-files.txt" "$RUNNER_TEMP/format_comment.go" "$RUNNER_TEMP/validation_results.json"
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")
}
Loading
Loading