From ba88455efb743301f362d79cdb70e9b9cfd248c9 Mon Sep 17 00:00:00 2001 From: Snider Date: Sun, 1 Feb 2026 06:32:35 +0000 Subject: [PATCH] feat(php): add --json and --sarif flags to QA commands (#69) * feat(github): add issue templates and auto-labeler - Add bug_report.yml and feature_request.yml templates - Add config.yml for issue creation options - Add auto-label.yml workflow to label issues based on content Co-Authored-By: Claude Opus 4.5 * feat(php): add --json and --sarif flags to QA commands Adds machine-readable output support to PHP quality assurance commands: - test: --json flag for JUnit XML output - fmt: --json flag for JSON formatted output from Pint - stan: --json and --sarif flags for PHPStan output - psalm: --json and --sarif flags for Psalm output - qa: --json flag for JSON summary output SARIF output enables integration with GitHub Security tab for static analysis results. Closes #51 Co-Authored-By: Claude Opus 4.5 * fix(php): address CodeRabbit review feedback - Guard progress messages when JSON/SARIF output is enabled - Guard success messages when JSON/SARIF output is enabled - Guard QA results display when JSON output is enabled - Rename misleading JSON field to JUnit in TestOptions (outputs JUnit XML) - Add mutual exclusion validation for --json and --sarif flags - Remove empty conditional block in auto-label workflow - Add i18n translation for json_sarif_exclusive error Co-Authored-By: Claude Opus 4.5 * fix(php): additional CodeRabbit fixes - Rename test --json flag to --junit (outputs JUnit XML, not JSON) - Add actual JSON marshaling for QA command JSON output - Add JSON tags to QARunResult and QACheckRunResult structs - Add i18n translation for junit flag Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.5 --- .github/ISSUE_TEMPLATE/feature_request.yml | 2 +- .github/workflows/auto-label.yml | 3 - pkg/i18n/locales/en_GB.json | 28 ++- pkg/php/cmd_qa_runner.go | 24 +-- pkg/php/cmd_quality.go | 199 +++++++++++++-------- pkg/php/quality.go | 29 +++ pkg/php/testing.go | 11 ++ 7 files changed, 209 insertions(+), 87 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 2e4aab76..fe73d800 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -55,4 +55,4 @@ body: - "Large - Significant changes, multiple days" - "Unknown - Not sure" validations: - required: false \ No newline at end of file + required: false diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml index 936c3076..552c85ae 100644 --- a/.github/workflows/auto-label.yml +++ b/.github/workflows/auto-label.yml @@ -43,9 +43,6 @@ jobs: if (content.includes('.go') || content.includes('golang') || content.includes('go mod')) { labelsToAdd.push('go'); } - if (content.includes('.php') || content.includes('laravel') || content.includes('composer')) { - // Skip - already handled by project:core-php - } // Priority detection if (content.includes('critical') || content.includes('urgent') || content.includes('breaking')) { diff --git a/pkg/i18n/locales/en_GB.json b/pkg/i18n/locales/en_GB.json index e03cd798..a6796065 100644 --- a/pkg/i18n/locales/en_GB.json +++ b/pkg/i18n/locales/en_GB.json @@ -253,14 +253,38 @@ "dev.short": "Start Laravel development environment", "dev.press_ctrl_c": "Press Ctrl+C to stop all services", "test.short": "Run PHP tests (PHPUnit/Pest)", + "test.flag.parallel": "Run tests in parallel", + "test.flag.coverage": "Generate code coverage report", + "test.flag.filter": "Filter tests by name pattern", + "test.flag.group": "Run only tests in specified group", + "test.flag.junit": "Output results in JUnit XML format", "fmt.short": "Format PHP code with Laravel Pint", + "fmt.flag.fix": "Apply formatting fixes", "analyse.short": "Run PHPStan static analysis", + "analyse.flag.level": "PHPStan analysis level (0-9)", + "analyse.flag.memory": "Memory limit (e.g., 2G)", "audit.short": "Security audit for dependencies", "psalm.short": "Run Psalm static analysis", + "psalm.flag.level": "Psalm error level (1=strictest, 8=lenient)", + "psalm.flag.baseline": "Generate/update baseline file", + "psalm.flag.show_info": "Show info-level issues", "rector.short": "Automated code refactoring", + "rector.flag.fix": "Apply refactoring changes", + "rector.flag.diff": "Show detailed diff of changes", + "rector.flag.clear_cache": "Clear cache before running", "infection.short": "Mutation testing for test quality", + "infection.flag.min_msi": "Minimum mutation score indicator (0-100)", + "infection.flag.min_covered_msi": "Minimum covered mutation score (0-100)", + "infection.flag.threads": "Number of parallel threads", + "infection.flag.filter": "Filter files by pattern", + "infection.flag.only_covered": "Only mutate covered code", "security.short": "Security vulnerability scanning", + "security.flag.severity": "Minimum severity (critical, high, medium, low)", + "security.flag.sarif": "Output as SARIF for GitHub Security tab", + "security.flag.url": "URL to check HTTP security headers", "qa.short": "Run full QA pipeline", + "qa.flag.quick": "Run quick checks only (audit, fmt, stan)", + "qa.flag.full": "Run all stages including slow checks", "build.short": "Build Docker or LinuxKit image", "deploy.short": "Deploy to Coolify", "serve.short": "Run production container", @@ -445,6 +469,7 @@ "fix": "Auto-fix issues where possible", "diff": "Show diff of changes", "json": "Output as JSON", + "sarif": "Output as SARIF for GitHub Security tab", "verbose": "Show detailed output", "registry": "Path to repos.yaml registry file" }, @@ -459,7 +484,8 @@ "completed": "{{.Action}} successfully" }, "error": { - "failed": "Failed to {{.Action}}" + "failed": "Failed to {{.Action}}", + "json_sarif_exclusive": "--json and --sarif flags are mutually exclusive" }, "hint": { "fix_deps": "Update dependencies to fix vulnerabilities" diff --git a/pkg/php/cmd_qa_runner.go b/pkg/php/cmd_qa_runner.go index 9d8c8ce4..c8d20d2b 100644 --- a/pkg/php/cmd_qa_runner.go +++ b/pkg/php/cmd_qa_runner.go @@ -294,22 +294,22 @@ func (r *QARunner) GetCheckOutput(check string) []string { // QARunResult holds the results of running QA checks. type QARunResult struct { - Passed bool - Duration string - Results []QACheckRunResult - PassedCount int - FailedCount int - SkippedCount int + Passed bool `json:"passed"` + Duration string `json:"duration"` + Results []QACheckRunResult `json:"results"` + PassedCount int `json:"passed_count"` + FailedCount int `json:"failed_count"` + SkippedCount int `json:"skipped_count"` } // QACheckRunResult holds the result of a single QA check. type QACheckRunResult struct { - Name string - Passed bool - Skipped bool - ExitCode int - Duration string - Output string + Name string `json:"name"` + Passed bool `json:"passed"` + Skipped bool `json:"skipped"` + ExitCode int `json:"exit_code"` + Duration string `json:"duration"` + Output string `json:"output,omitempty"` } // GetIssueMessage returns an issue message for a check. diff --git a/pkg/php/cmd_quality.go b/pkg/php/cmd_quality.go index 0febf467..3ec74dcd 100644 --- a/pkg/php/cmd_quality.go +++ b/pkg/php/cmd_quality.go @@ -2,11 +2,11 @@ package php import ( "context" + "encoding/json" "errors" "os" "strings" - "github.com/host-uk/core/pkg/cli" "github.com/host-uk/core/pkg/i18n" "github.com/spf13/cobra" @@ -17,6 +17,7 @@ var ( testCoverage bool testFilter string testGroup string + testJSON bool ) func addPHPTestCommand(parent *cobra.Command) { @@ -34,7 +35,9 @@ func addPHPTestCommand(parent *cobra.Command) { return errors.New(i18n.T("cmd.php.error.not_php")) } - cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.ProgressSubject("run", "tests")) + if !testJSON { + cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.ProgressSubject("run", "tests")) + } ctx := context.Background() @@ -43,6 +46,7 @@ func addPHPTestCommand(parent *cobra.Command) { Filter: testFilter, Parallel: testParallel, Coverage: testCoverage, + JUnit: testJSON, Output: os.Stdout, } @@ -62,6 +66,7 @@ func addPHPTestCommand(parent *cobra.Command) { testCmd.Flags().BoolVar(&testCoverage, "coverage", false, i18n.T("cmd.php.test.flag.coverage")) testCmd.Flags().StringVar(&testFilter, "filter", "", i18n.T("cmd.php.test.flag.filter")) testCmd.Flags().StringVar(&testGroup, "group", "", i18n.T("cmd.php.test.flag.group")) + testCmd.Flags().BoolVar(&testJSON, "junit", false, i18n.T("cmd.php.test.flag.junit")) parent.AddCommand(testCmd) } @@ -69,6 +74,7 @@ func addPHPTestCommand(parent *cobra.Command) { var ( fmtFix bool fmtDiff bool + fmtJSON bool ) func addPHPFmtCommand(parent *cobra.Command) { @@ -92,13 +98,15 @@ func addPHPFmtCommand(parent *cobra.Command) { return errors.New(i18n.T("cmd.php.fmt.no_formatter")) } - var msg string - if fmtFix { - msg = i18n.T("cmd.php.fmt.formatting", map[string]interface{}{"Formatter": formatter}) - } else { - msg = i18n.ProgressSubject("check", "code style") + if !fmtJSON { + var msg string + if fmtFix { + msg = i18n.T("cmd.php.fmt.formatting", map[string]interface{}{"Formatter": formatter}) + } else { + msg = i18n.ProgressSubject("check", "code style") + } + cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), msg) } - cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), msg) ctx := context.Background() @@ -106,6 +114,7 @@ func addPHPFmtCommand(parent *cobra.Command) { Dir: cwd, Fix: fmtFix, Diff: fmtDiff, + JSON: fmtJSON, Output: os.Stdout, } @@ -121,10 +130,12 @@ func addPHPFmtCommand(parent *cobra.Command) { return cli.Err("%s: %w", i18n.T("cmd.php.error.fmt_issues"), err) } - if fmtFix { - cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.success.completed", map[string]any{"Action": "Code formatted"})) - } else { - cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.fmt.no_issues")) + if !fmtJSON { + if fmtFix { + cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.success.completed", map[string]any{"Action": "Code formatted"})) + } else { + cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.fmt.no_issues")) + } } return nil @@ -133,6 +144,7 @@ func addPHPFmtCommand(parent *cobra.Command) { fmtCmd.Flags().BoolVar(&fmtFix, "fix", false, i18n.T("cmd.php.fmt.flag.fix")) fmtCmd.Flags().BoolVar(&fmtDiff, "diff", false, i18n.T("common.flag.diff")) + fmtCmd.Flags().BoolVar(&fmtJSON, "json", false, i18n.T("common.flag.json")) parent.AddCommand(fmtCmd) } @@ -140,6 +152,8 @@ func addPHPFmtCommand(parent *cobra.Command) { var ( stanLevel int stanMemory string + stanJSON bool + stanSARIF bool ) func addPHPStanCommand(parent *cobra.Command) { @@ -163,7 +177,13 @@ func addPHPStanCommand(parent *cobra.Command) { return errors.New(i18n.T("cmd.php.analyse.no_analyser")) } - cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.ProgressSubject("run", "static analysis")) + if stanJSON && stanSARIF { + return errors.New(i18n.T("common.error.json_sarif_exclusive")) + } + + if !stanJSON && !stanSARIF { + cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.php")), i18n.ProgressSubject("run", "static analysis")) + } ctx := context.Background() @@ -171,6 +191,8 @@ func addPHPStanCommand(parent *cobra.Command) { Dir: cwd, Level: stanLevel, Memory: stanMemory, + JSON: stanJSON, + SARIF: stanSARIF, Output: os.Stdout, } @@ -183,13 +205,17 @@ func addPHPStanCommand(parent *cobra.Command) { return cli.Err("%s: %w", i18n.T("cmd.php.error.analysis_issues"), err) } - cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.result.no_issues")) + if !stanJSON && !stanSARIF { + cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.result.no_issues")) + } return nil }, } stanCmd.Flags().IntVar(&stanLevel, "level", 0, i18n.T("cmd.php.analyse.flag.level")) stanCmd.Flags().StringVar(&stanMemory, "memory", "", i18n.T("cmd.php.analyse.flag.memory")) + stanCmd.Flags().BoolVar(&stanJSON, "json", false, i18n.T("common.flag.json")) + stanCmd.Flags().BoolVar(&stanSARIF, "sarif", false, i18n.T("common.flag.sarif")) parent.AddCommand(stanCmd) } @@ -203,6 +229,8 @@ var ( psalmFix bool psalmBaseline bool psalmShowInfo bool + psalmJSON bool + psalmSARIF bool ) func addPHPPsalmCommand(parent *cobra.Command) { @@ -229,13 +257,19 @@ func addPHPPsalmCommand(parent *cobra.Command) { return errors.New(i18n.T("cmd.php.error.psalm_not_installed")) } - var msg string - if psalmFix { - msg = i18n.T("cmd.php.psalm.analysing_fixing") - } else { - msg = i18n.T("cmd.php.psalm.analysing") + if psalmJSON && psalmSARIF { + return errors.New(i18n.T("common.error.json_sarif_exclusive")) + } + + if !psalmJSON && !psalmSARIF { + var msg string + if psalmFix { + msg = i18n.T("cmd.php.psalm.analysing_fixing") + } else { + msg = i18n.T("cmd.php.psalm.analysing") + } + cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.psalm")), msg) } - cli.Print("%s %s\n\n", dimStyle.Render(i18n.T("cmd.php.label.psalm")), msg) ctx := context.Background() @@ -245,6 +279,8 @@ func addPHPPsalmCommand(parent *cobra.Command) { Fix: psalmFix, Baseline: psalmBaseline, ShowInfo: psalmShowInfo, + JSON: psalmJSON, + SARIF: psalmSARIF, Output: os.Stdout, } @@ -252,7 +288,9 @@ func addPHPPsalmCommand(parent *cobra.Command) { return cli.Err("%s: %w", i18n.T("cmd.php.error.psalm_issues"), err) } - cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.result.no_issues")) + if !psalmJSON && !psalmSARIF { + cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.result.no_issues")) + } return nil }, } @@ -261,6 +299,8 @@ func addPHPPsalmCommand(parent *cobra.Command) { psalmCmd.Flags().BoolVar(&psalmFix, "fix", false, i18n.T("common.flag.fix")) psalmCmd.Flags().BoolVar(&psalmBaseline, "baseline", false, i18n.T("cmd.php.psalm.flag.baseline")) psalmCmd.Flags().BoolVar(&psalmShowInfo, "show-info", false, i18n.T("cmd.php.psalm.flag.show_info")) + psalmCmd.Flags().BoolVar(&psalmJSON, "json", false, i18n.T("common.flag.json")) + psalmCmd.Flags().BoolVar(&psalmSARIF, "sarif", false, i18n.T("common.flag.sarif")) parent.AddCommand(psalmCmd) } @@ -459,6 +499,7 @@ var ( qaQuick bool qaFull bool qaFix bool + qaJSON bool ) func addPHPQACommand(parent *cobra.Command) { @@ -482,11 +523,14 @@ func addPHPQACommand(parent *cobra.Command) { Quick: qaQuick, Full: qaFull, Fix: qaFix, + JSON: qaJSON, } stages := GetQAStages(opts) // Print header - cli.Print("%s %s\n\n", dimStyle.Render(i18n.Label("qa")), i18n.ProgressSubject("run", "QA pipeline")) + if !qaJSON { + cli.Print("%s %s\n\n", dimStyle.Render(i18n.Label("qa")), i18n.ProgressSubject("run", "QA pipeline")) + } ctx := context.Background() @@ -502,66 +546,81 @@ func addPHPQACommand(parent *cobra.Command) { return cli.Err("%s: %w", i18n.T("i18n.fail.run", "QA checks"), err) } - // Display results by stage - currentStage := "" - for _, checkResult := range result.Results { - // Determine stage for this check - stage := getCheckStage(checkResult.Name, stages, cwd) - if stage != currentStage { - if currentStage != "" { - cli.Blank() + // Display results by stage (skip when JSON output is enabled) + if !qaJSON { + currentStage := "" + for _, checkResult := range result.Results { + // Determine stage for this check + stage := getCheckStage(checkResult.Name, stages, cwd) + if stage != currentStage { + if currentStage != "" { + cli.Blank() + } + currentStage = stage + cli.Print("%s\n", phpQAStageStyle.Render("── "+strings.ToUpper(stage)+" ──")) } - currentStage = stage - cli.Print("%s\n", phpQAStageStyle.Render("── "+strings.ToUpper(stage)+" ──")) + + icon := phpQAPassedStyle.Render("✓") + status := phpQAPassedStyle.Render(i18n.T("i18n.done.pass")) + if checkResult.Skipped { + icon = dimStyle.Render("-") + status = dimStyle.Render(i18n.T("i18n.done.skip")) + } else if !checkResult.Passed { + icon = phpQAFailedStyle.Render("✗") + status = phpQAFailedStyle.Render(i18n.T("i18n.done.fail")) + } + + cli.Print(" %s %s %s %s\n", icon, checkResult.Name, status, dimStyle.Render(checkResult.Duration)) + } + cli.Blank() + + // Print summary + if result.Passed { + cli.Print("%s %s\n", phpQAPassedStyle.Render("QA PASSED:"), i18n.T("i18n.count.check", result.PassedCount)+" "+i18n.T("i18n.done.pass")) + cli.Print("%s %s\n", dimStyle.Render(i18n.T("i18n.label.duration")), result.Duration) + return nil } - icon := phpQAPassedStyle.Render("✓") - status := phpQAPassedStyle.Render(i18n.T("i18n.done.pass")) - if checkResult.Skipped { - icon = dimStyle.Render("-") - status = dimStyle.Render(i18n.T("i18n.done.skip")) - } else if !checkResult.Passed { - icon = phpQAFailedStyle.Render("✗") - status = phpQAFailedStyle.Render(i18n.T("i18n.done.fail")) + cli.Print("%s %s\n\n", phpQAFailedStyle.Render("QA FAILED:"), i18n.T("i18n.count.check", result.PassedCount)+"/"+cli.Sprint(len(result.Results))+" "+i18n.T("i18n.done.pass")) + + // Show what needs fixing + cli.Print("%s\n", dimStyle.Render(i18n.T("i18n.label.fix"))) + for _, checkResult := range result.Results { + if checkResult.Passed || checkResult.Skipped { + continue + } + fixCmd := getQAFixCommand(checkResult.Name, qaFix) + issue := checkResult.GetIssueMessage() + if issue == "" { + issue = "issues found" + } + cli.Print(" %s %s\n", phpQAFailedStyle.Render("*"), checkResult.Name+": "+issue) + if fixCmd != "" { + cli.Print(" %s %s\n", dimStyle.Render("->"), fixCmd) + } } - cli.Print(" %s %s %s %s\n", icon, checkResult.Name, status, dimStyle.Render(checkResult.Duration)) - } - cli.Blank() - - // Print summary - if result.Passed { - cli.Print("%s %s\n", phpQAPassedStyle.Render("QA PASSED:"), i18n.T("i18n.count.check", result.PassedCount)+" "+i18n.T("i18n.done.pass")) - cli.Print("%s %s\n", dimStyle.Render(i18n.T("i18n.label.duration")), result.Duration) - return nil + return cli.Err("%s", i18n.T("i18n.fail.run", "QA pipeline")) } - cli.Print("%s %s\n\n", phpQAFailedStyle.Render("QA FAILED:"), i18n.T("i18n.count.check", result.PassedCount)+"/"+cli.Sprint(len(result.Results))+" "+i18n.T("i18n.done.pass")) - - // Show what needs fixing - cli.Print("%s\n", dimStyle.Render(i18n.T("i18n.label.fix"))) - for _, checkResult := range result.Results { - if checkResult.Passed || checkResult.Skipped { - continue - } - fixCmd := getQAFixCommand(checkResult.Name, qaFix) - issue := checkResult.GetIssueMessage() - if issue == "" { - issue = "issues found" - } - cli.Print(" %s %s\n", phpQAFailedStyle.Render("*"), checkResult.Name+": "+issue) - if fixCmd != "" { - cli.Print(" %s %s\n", dimStyle.Render("->"), fixCmd) - } + // JSON mode: output results as JSON + output, err := json.MarshalIndent(result, "", " ") + if err != nil { + return cli.Wrap(err, "marshal JSON output") } + cli.Text(string(output)) - return cli.Err("%s", i18n.T("i18n.fail.run", "QA pipeline")) + if !result.Passed { + return cli.Err("%s", i18n.T("i18n.fail.run", "QA pipeline")) + } + return nil }, } - qaCmd.Flags().BoolVar(&qaQuick, "quick", false, "Run quick checks only (audit, fmt, stan)") - qaCmd.Flags().BoolVar(&qaFull, "full", false, "Run all stages including slow checks") - qaCmd.Flags().BoolVar(&qaFix, "fix", false, "Auto-fix issues where possible") + qaCmd.Flags().BoolVar(&qaQuick, "quick", false, i18n.T("cmd.php.qa.flag.quick")) + qaCmd.Flags().BoolVar(&qaFull, "full", false, i18n.T("cmd.php.qa.flag.full")) + qaCmd.Flags().BoolVar(&qaFix, "fix", false, i18n.T("common.flag.fix")) + qaCmd.Flags().BoolVar(&qaJSON, "json", false, i18n.T("common.flag.json")) parent.AddCommand(qaCmd) } diff --git a/pkg/php/quality.go b/pkg/php/quality.go index 31c71cde..0f272475 100644 --- a/pkg/php/quality.go +++ b/pkg/php/quality.go @@ -23,6 +23,9 @@ type FormatOptions struct { // Diff shows a diff of changes instead of modifying files. Diff bool + // JSON outputs results in JSON format. + JSON bool + // Paths limits formatting to specific paths. Paths []string @@ -44,6 +47,12 @@ type AnalyseOptions struct { // Memory is the memory limit for analysis (e.g., "2G"). Memory string + // JSON outputs results in JSON format. + JSON bool + + // SARIF outputs results in SARIF format for GitHub Security tab. + SARIF bool + // Output is the writer for output (defaults to os.Stdout). Output io.Writer } @@ -209,6 +218,10 @@ func buildPintCommand(opts FormatOptions) (string, []string) { args = append(args, "--diff") } + if opts.JSON { + args = append(args, "--format=json") + } + // Add specific paths if provided args = append(args, opts.Paths...) @@ -234,6 +247,13 @@ func buildPHPStanCommand(opts AnalyseOptions) (string, []string) { args = append(args, "--memory-limit", opts.Memory) } + // Output format - SARIF takes precedence over JSON + if opts.SARIF { + args = append(args, "--error-format=sarif") + } else if opts.JSON { + args = append(args, "--error-format=json") + } + // Add specific paths if provided args = append(args, opts.Paths...) @@ -251,6 +271,8 @@ type PsalmOptions struct { Fix bool // Auto-fix issues where possible Baseline bool // Generate/update baseline file ShowInfo bool // Show info-level issues + JSON bool // Output in JSON format + SARIF bool // Output in SARIF format for GitHub Security tab Output io.Writer } @@ -327,6 +349,13 @@ func RunPsalm(ctx context.Context, opts PsalmOptions) error { args = append(args, "--show-info=true") } + // Output format - SARIF takes precedence over JSON + if opts.SARIF { + args = append(args, "--output-format=sarif") + } else if opts.JSON { + args = append(args, "--output-format=json") + } + cmd := exec.CommandContext(ctx, cmdName, args...) cmd.Dir = opts.Dir cmd.Stdout = opts.Output diff --git a/pkg/php/testing.go b/pkg/php/testing.go index cb5bd9c5..0c8a9a63 100644 --- a/pkg/php/testing.go +++ b/pkg/php/testing.go @@ -30,6 +30,9 @@ type TestOptions struct { // Groups runs only tests in the specified groups. Groups []string + // JUnit outputs results in JUnit XML format via --log-junit. + JUnit bool + // Output is the writer for test output (defaults to os.Stdout). Output io.Writer } @@ -134,6 +137,10 @@ func buildPestCommand(opts TestOptions) (string, []string) { args = append(args, "--group", group) } + if opts.JUnit { + args = append(args, "--log-junit", "test-results.xml") + } + return cmdName, args } @@ -175,5 +182,9 @@ func buildPHPUnitCommand(opts TestOptions) (string, []string) { args = append(args, "--group", group) } + if opts.JUnit { + args = append(args, "--log-junit", "test-results.xml", "--testdox") + } + return cmdName, args }