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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
af9fd33b2a
commit
ba88455efb
7 changed files with 209 additions and 87 deletions
3
.github/workflows/auto-label.yml
vendored
3
.github/workflows/auto-label.yml
vendored
|
|
@ -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')) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue