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
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
|
|
@ -55,4 +55,4 @@ body:
|
||||||
- "Large - Significant changes, multiple days"
|
- "Large - Significant changes, multiple days"
|
||||||
- "Unknown - Not sure"
|
- "Unknown - Not sure"
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
|
|
||||||
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')) {
|
if (content.includes('.go') || content.includes('golang') || content.includes('go mod')) {
|
||||||
labelsToAdd.push('go');
|
labelsToAdd.push('go');
|
||||||
}
|
}
|
||||||
if (content.includes('.php') || content.includes('laravel') || content.includes('composer')) {
|
|
||||||
// Skip - already handled by project:core-php
|
|
||||||
}
|
|
||||||
|
|
||||||
// Priority detection
|
// Priority detection
|
||||||
if (content.includes('critical') || content.includes('urgent') || content.includes('breaking')) {
|
if (content.includes('critical') || content.includes('urgent') || content.includes('breaking')) {
|
||||||
|
|
|
||||||
|
|
@ -253,14 +253,38 @@
|
||||||
"dev.short": "Start Laravel development environment",
|
"dev.short": "Start Laravel development environment",
|
||||||
"dev.press_ctrl_c": "Press Ctrl+C to stop all services",
|
"dev.press_ctrl_c": "Press Ctrl+C to stop all services",
|
||||||
"test.short": "Run PHP tests (PHPUnit/Pest)",
|
"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.short": "Format PHP code with Laravel Pint",
|
||||||
|
"fmt.flag.fix": "Apply formatting fixes",
|
||||||
"analyse.short": "Run PHPStan static analysis",
|
"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",
|
"audit.short": "Security audit for dependencies",
|
||||||
"psalm.short": "Run Psalm static analysis",
|
"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.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.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.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.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",
|
"build.short": "Build Docker or LinuxKit image",
|
||||||
"deploy.short": "Deploy to Coolify",
|
"deploy.short": "Deploy to Coolify",
|
||||||
"serve.short": "Run production container",
|
"serve.short": "Run production container",
|
||||||
|
|
@ -445,6 +469,7 @@
|
||||||
"fix": "Auto-fix issues where possible",
|
"fix": "Auto-fix issues where possible",
|
||||||
"diff": "Show diff of changes",
|
"diff": "Show diff of changes",
|
||||||
"json": "Output as JSON",
|
"json": "Output as JSON",
|
||||||
|
"sarif": "Output as SARIF for GitHub Security tab",
|
||||||
"verbose": "Show detailed output",
|
"verbose": "Show detailed output",
|
||||||
"registry": "Path to repos.yaml registry file"
|
"registry": "Path to repos.yaml registry file"
|
||||||
},
|
},
|
||||||
|
|
@ -459,7 +484,8 @@
|
||||||
"completed": "{{.Action}} successfully"
|
"completed": "{{.Action}} successfully"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"failed": "Failed to {{.Action}}"
|
"failed": "Failed to {{.Action}}",
|
||||||
|
"json_sarif_exclusive": "--json and --sarif flags are mutually exclusive"
|
||||||
},
|
},
|
||||||
"hint": {
|
"hint": {
|
||||||
"fix_deps": "Update dependencies to fix vulnerabilities"
|
"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.
|
// QARunResult holds the results of running QA checks.
|
||||||
type QARunResult struct {
|
type QARunResult struct {
|
||||||
Passed bool
|
Passed bool `json:"passed"`
|
||||||
Duration string
|
Duration string `json:"duration"`
|
||||||
Results []QACheckRunResult
|
Results []QACheckRunResult `json:"results"`
|
||||||
PassedCount int
|
PassedCount int `json:"passed_count"`
|
||||||
FailedCount int
|
FailedCount int `json:"failed_count"`
|
||||||
SkippedCount int
|
SkippedCount int `json:"skipped_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// QACheckRunResult holds the result of a single QA check.
|
// QACheckRunResult holds the result of a single QA check.
|
||||||
type QACheckRunResult struct {
|
type QACheckRunResult struct {
|
||||||
Name string
|
Name string `json:"name"`
|
||||||
Passed bool
|
Passed bool `json:"passed"`
|
||||||
Skipped bool
|
Skipped bool `json:"skipped"`
|
||||||
ExitCode int
|
ExitCode int `json:"exit_code"`
|
||||||
Duration string
|
Duration string `json:"duration"`
|
||||||
Output string
|
Output string `json:"output,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetIssueMessage returns an issue message for a check.
|
// GetIssueMessage returns an issue message for a check.
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@ package php
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
|
||||||
"github.com/host-uk/core/pkg/cli"
|
"github.com/host-uk/core/pkg/cli"
|
||||||
"github.com/host-uk/core/pkg/i18n"
|
"github.com/host-uk/core/pkg/i18n"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
@ -17,6 +17,7 @@ var (
|
||||||
testCoverage bool
|
testCoverage bool
|
||||||
testFilter string
|
testFilter string
|
||||||
testGroup string
|
testGroup string
|
||||||
|
testJSON bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func addPHPTestCommand(parent *cobra.Command) {
|
func addPHPTestCommand(parent *cobra.Command) {
|
||||||
|
|
@ -34,7 +35,9 @@ func addPHPTestCommand(parent *cobra.Command) {
|
||||||
return errors.New(i18n.T("cmd.php.error.not_php"))
|
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()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|
@ -43,6 +46,7 @@ func addPHPTestCommand(parent *cobra.Command) {
|
||||||
Filter: testFilter,
|
Filter: testFilter,
|
||||||
Parallel: testParallel,
|
Parallel: testParallel,
|
||||||
Coverage: testCoverage,
|
Coverage: testCoverage,
|
||||||
|
JUnit: testJSON,
|
||||||
Output: os.Stdout,
|
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().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(&testFilter, "filter", "", i18n.T("cmd.php.test.flag.filter"))
|
||||||
testCmd.Flags().StringVar(&testGroup, "group", "", i18n.T("cmd.php.test.flag.group"))
|
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)
|
parent.AddCommand(testCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -69,6 +74,7 @@ func addPHPTestCommand(parent *cobra.Command) {
|
||||||
var (
|
var (
|
||||||
fmtFix bool
|
fmtFix bool
|
||||||
fmtDiff bool
|
fmtDiff bool
|
||||||
|
fmtJSON bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func addPHPFmtCommand(parent *cobra.Command) {
|
func addPHPFmtCommand(parent *cobra.Command) {
|
||||||
|
|
@ -92,13 +98,15 @@ func addPHPFmtCommand(parent *cobra.Command) {
|
||||||
return errors.New(i18n.T("cmd.php.fmt.no_formatter"))
|
return errors.New(i18n.T("cmd.php.fmt.no_formatter"))
|
||||||
}
|
}
|
||||||
|
|
||||||
var msg string
|
if !fmtJSON {
|
||||||
if fmtFix {
|
var msg string
|
||||||
msg = i18n.T("cmd.php.fmt.formatting", map[string]interface{}{"Formatter": formatter})
|
if fmtFix {
|
||||||
} else {
|
msg = i18n.T("cmd.php.fmt.formatting", map[string]interface{}{"Formatter": formatter})
|
||||||
msg = i18n.ProgressSubject("check", "code style")
|
} 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()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|
@ -106,6 +114,7 @@ func addPHPFmtCommand(parent *cobra.Command) {
|
||||||
Dir: cwd,
|
Dir: cwd,
|
||||||
Fix: fmtFix,
|
Fix: fmtFix,
|
||||||
Diff: fmtDiff,
|
Diff: fmtDiff,
|
||||||
|
JSON: fmtJSON,
|
||||||
Output: os.Stdout,
|
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)
|
return cli.Err("%s: %w", i18n.T("cmd.php.error.fmt_issues"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if fmtFix {
|
if !fmtJSON {
|
||||||
cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.success.completed", map[string]any{"Action": "Code formatted"}))
|
if fmtFix {
|
||||||
} else {
|
cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("common.success.completed", map[string]any{"Action": "Code formatted"}))
|
||||||
cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.fmt.no_issues"))
|
} else {
|
||||||
|
cli.Print("\n%s %s\n", successStyle.Render(i18n.Label("done")), i18n.T("cmd.php.fmt.no_issues"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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(&fmtFix, "fix", false, i18n.T("cmd.php.fmt.flag.fix"))
|
||||||
fmtCmd.Flags().BoolVar(&fmtDiff, "diff", false, i18n.T("common.flag.diff"))
|
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)
|
parent.AddCommand(fmtCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -140,6 +152,8 @@ func addPHPFmtCommand(parent *cobra.Command) {
|
||||||
var (
|
var (
|
||||||
stanLevel int
|
stanLevel int
|
||||||
stanMemory string
|
stanMemory string
|
||||||
|
stanJSON bool
|
||||||
|
stanSARIF bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func addPHPStanCommand(parent *cobra.Command) {
|
func addPHPStanCommand(parent *cobra.Command) {
|
||||||
|
|
@ -163,7 +177,13 @@ func addPHPStanCommand(parent *cobra.Command) {
|
||||||
return errors.New(i18n.T("cmd.php.analyse.no_analyser"))
|
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()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|
@ -171,6 +191,8 @@ func addPHPStanCommand(parent *cobra.Command) {
|
||||||
Dir: cwd,
|
Dir: cwd,
|
||||||
Level: stanLevel,
|
Level: stanLevel,
|
||||||
Memory: stanMemory,
|
Memory: stanMemory,
|
||||||
|
JSON: stanJSON,
|
||||||
|
SARIF: stanSARIF,
|
||||||
Output: os.Stdout,
|
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)
|
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
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
stanCmd.Flags().IntVar(&stanLevel, "level", 0, i18n.T("cmd.php.analyse.flag.level"))
|
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().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)
|
parent.AddCommand(stanCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -203,6 +229,8 @@ var (
|
||||||
psalmFix bool
|
psalmFix bool
|
||||||
psalmBaseline bool
|
psalmBaseline bool
|
||||||
psalmShowInfo bool
|
psalmShowInfo bool
|
||||||
|
psalmJSON bool
|
||||||
|
psalmSARIF bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func addPHPPsalmCommand(parent *cobra.Command) {
|
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"))
|
return errors.New(i18n.T("cmd.php.error.psalm_not_installed"))
|
||||||
}
|
}
|
||||||
|
|
||||||
var msg string
|
if psalmJSON && psalmSARIF {
|
||||||
if psalmFix {
|
return errors.New(i18n.T("common.error.json_sarif_exclusive"))
|
||||||
msg = i18n.T("cmd.php.psalm.analysing_fixing")
|
}
|
||||||
} else {
|
|
||||||
msg = i18n.T("cmd.php.psalm.analysing")
|
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()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|
@ -245,6 +279,8 @@ func addPHPPsalmCommand(parent *cobra.Command) {
|
||||||
Fix: psalmFix,
|
Fix: psalmFix,
|
||||||
Baseline: psalmBaseline,
|
Baseline: psalmBaseline,
|
||||||
ShowInfo: psalmShowInfo,
|
ShowInfo: psalmShowInfo,
|
||||||
|
JSON: psalmJSON,
|
||||||
|
SARIF: psalmSARIF,
|
||||||
Output: os.Stdout,
|
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)
|
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
|
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(&psalmFix, "fix", false, i18n.T("common.flag.fix"))
|
||||||
psalmCmd.Flags().BoolVar(&psalmBaseline, "baseline", false, i18n.T("cmd.php.psalm.flag.baseline"))
|
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(&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)
|
parent.AddCommand(psalmCmd)
|
||||||
}
|
}
|
||||||
|
|
@ -459,6 +499,7 @@ var (
|
||||||
qaQuick bool
|
qaQuick bool
|
||||||
qaFull bool
|
qaFull bool
|
||||||
qaFix bool
|
qaFix bool
|
||||||
|
qaJSON bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func addPHPQACommand(parent *cobra.Command) {
|
func addPHPQACommand(parent *cobra.Command) {
|
||||||
|
|
@ -482,11 +523,14 @@ func addPHPQACommand(parent *cobra.Command) {
|
||||||
Quick: qaQuick,
|
Quick: qaQuick,
|
||||||
Full: qaFull,
|
Full: qaFull,
|
||||||
Fix: qaFix,
|
Fix: qaFix,
|
||||||
|
JSON: qaJSON,
|
||||||
}
|
}
|
||||||
stages := GetQAStages(opts)
|
stages := GetQAStages(opts)
|
||||||
|
|
||||||
// Print header
|
// 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()
|
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)
|
return cli.Err("%s: %w", i18n.T("i18n.fail.run", "QA checks"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display results by stage
|
// Display results by stage (skip when JSON output is enabled)
|
||||||
currentStage := ""
|
if !qaJSON {
|
||||||
for _, checkResult := range result.Results {
|
currentStage := ""
|
||||||
// Determine stage for this check
|
for _, checkResult := range result.Results {
|
||||||
stage := getCheckStage(checkResult.Name, stages, cwd)
|
// Determine stage for this check
|
||||||
if stage != currentStage {
|
stage := getCheckStage(checkResult.Name, stages, cwd)
|
||||||
if currentStage != "" {
|
if stage != currentStage {
|
||||||
cli.Blank()
|
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("✓")
|
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"))
|
||||||
status := phpQAPassedStyle.Render(i18n.T("i18n.done.pass"))
|
|
||||||
if checkResult.Skipped {
|
// Show what needs fixing
|
||||||
icon = dimStyle.Render("-")
|
cli.Print("%s\n", dimStyle.Render(i18n.T("i18n.label.fix")))
|
||||||
status = dimStyle.Render(i18n.T("i18n.done.skip"))
|
for _, checkResult := range result.Results {
|
||||||
} else if !checkResult.Passed {
|
if checkResult.Passed || checkResult.Skipped {
|
||||||
icon = phpQAFailedStyle.Render("✗")
|
continue
|
||||||
status = phpQAFailedStyle.Render(i18n.T("i18n.done.fail"))
|
}
|
||||||
|
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))
|
return cli.Err("%s", i18n.T("i18n.fail.run", "QA pipeline"))
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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"))
|
// JSON mode: output results as JSON
|
||||||
|
output, err := json.MarshalIndent(result, "", " ")
|
||||||
// Show what needs fixing
|
if err != nil {
|
||||||
cli.Print("%s\n", dimStyle.Render(i18n.T("i18n.label.fix")))
|
return cli.Wrap(err, "marshal JSON output")
|
||||||
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.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(&qaQuick, "quick", false, i18n.T("cmd.php.qa.flag.quick"))
|
||||||
qaCmd.Flags().BoolVar(&qaFull, "full", false, "Run all stages including slow checks")
|
qaCmd.Flags().BoolVar(&qaFull, "full", false, i18n.T("cmd.php.qa.flag.full"))
|
||||||
qaCmd.Flags().BoolVar(&qaFix, "fix", false, "Auto-fix issues where possible")
|
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)
|
parent.AddCommand(qaCmd)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@ type FormatOptions struct {
|
||||||
// Diff shows a diff of changes instead of modifying files.
|
// Diff shows a diff of changes instead of modifying files.
|
||||||
Diff bool
|
Diff bool
|
||||||
|
|
||||||
|
// JSON outputs results in JSON format.
|
||||||
|
JSON bool
|
||||||
|
|
||||||
// Paths limits formatting to specific paths.
|
// Paths limits formatting to specific paths.
|
||||||
Paths []string
|
Paths []string
|
||||||
|
|
||||||
|
|
@ -44,6 +47,12 @@ type AnalyseOptions struct {
|
||||||
// Memory is the memory limit for analysis (e.g., "2G").
|
// Memory is the memory limit for analysis (e.g., "2G").
|
||||||
Memory string
|
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 is the writer for output (defaults to os.Stdout).
|
||||||
Output io.Writer
|
Output io.Writer
|
||||||
}
|
}
|
||||||
|
|
@ -209,6 +218,10 @@ func buildPintCommand(opts FormatOptions) (string, []string) {
|
||||||
args = append(args, "--diff")
|
args = append(args, "--diff")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.JSON {
|
||||||
|
args = append(args, "--format=json")
|
||||||
|
}
|
||||||
|
|
||||||
// Add specific paths if provided
|
// Add specific paths if provided
|
||||||
args = append(args, opts.Paths...)
|
args = append(args, opts.Paths...)
|
||||||
|
|
||||||
|
|
@ -234,6 +247,13 @@ func buildPHPStanCommand(opts AnalyseOptions) (string, []string) {
|
||||||
args = append(args, "--memory-limit", opts.Memory)
|
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
|
// Add specific paths if provided
|
||||||
args = append(args, opts.Paths...)
|
args = append(args, opts.Paths...)
|
||||||
|
|
||||||
|
|
@ -251,6 +271,8 @@ type PsalmOptions struct {
|
||||||
Fix bool // Auto-fix issues where possible
|
Fix bool // Auto-fix issues where possible
|
||||||
Baseline bool // Generate/update baseline file
|
Baseline bool // Generate/update baseline file
|
||||||
ShowInfo bool // Show info-level issues
|
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
|
Output io.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -327,6 +349,13 @@ func RunPsalm(ctx context.Context, opts PsalmOptions) error {
|
||||||
args = append(args, "--show-info=true")
|
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 := exec.CommandContext(ctx, cmdName, args...)
|
||||||
cmd.Dir = opts.Dir
|
cmd.Dir = opts.Dir
|
||||||
cmd.Stdout = opts.Output
|
cmd.Stdout = opts.Output
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,9 @@ type TestOptions struct {
|
||||||
// Groups runs only tests in the specified groups.
|
// Groups runs only tests in the specified groups.
|
||||||
Groups []string
|
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 is the writer for test output (defaults to os.Stdout).
|
||||||
Output io.Writer
|
Output io.Writer
|
||||||
}
|
}
|
||||||
|
|
@ -134,6 +137,10 @@ func buildPestCommand(opts TestOptions) (string, []string) {
|
||||||
args = append(args, "--group", group)
|
args = append(args, "--group", group)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.JUnit {
|
||||||
|
args = append(args, "--log-junit", "test-results.xml")
|
||||||
|
}
|
||||||
|
|
||||||
return cmdName, args
|
return cmdName, args
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -175,5 +182,9 @@ func buildPHPUnitCommand(opts TestOptions) (string, []string) {
|
||||||
args = append(args, "--group", group)
|
args = append(args, "--group", group)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.JUnit {
|
||||||
|
args = append(args, "--log-junit", "test-results.xml", "--testdox")
|
||||||
|
}
|
||||||
|
|
||||||
return cmdName, args
|
return cmdName, args
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue