feat(go): add QA command with subcommands for code quality checks
Add `core go qa` command with subcommands: - fmt: check/fix code formatting (gofmt) - vet: run go vet - lint: run golangci-lint - test: run tests - race: run tests with race detector - vuln: check for vulnerabilities (govulncheck) - sec: run security scanner (gosec) - quick: fmt, vet, lint only - full: all checks Default (no subcommand) runs fmt, vet, lint, test. All commands support --fix flag where applicable. Also update Taskfile.yml to use core CLI commands throughout. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
6b76b4d37f
commit
0fd76d86b4
3 changed files with 345 additions and 35 deletions
140
Taskfile.yml
140
Taskfile.yml
|
|
@ -1,62 +1,132 @@
|
|||
version: '3'
|
||||
|
||||
tasks:
|
||||
# --- CLI Management ---
|
||||
cli:build:
|
||||
desc: "Build the core CLI executable"
|
||||
desc: "Build core CLI to ./bin/core"
|
||||
cmds:
|
||||
- go build -o ./bin/core .
|
||||
|
||||
cli:run:
|
||||
desc: "Build and run the core CLI"
|
||||
cli:install:
|
||||
desc: "Install core CLI to system PATH"
|
||||
cmds:
|
||||
- task: cli:build
|
||||
- ./bin/core {{.CLI_ARGS}}
|
||||
|
||||
- go install .
|
||||
|
||||
# --- Development ---
|
||||
test:
|
||||
desc: "Run all Go tests recursively for the entire project."
|
||||
desc: "Run all tests"
|
||||
cmds:
|
||||
- cmd: clear
|
||||
platforms: [linux, darwin]
|
||||
- cmd: cls
|
||||
platforms: [windows]
|
||||
- cmd: go test ./...
|
||||
- core test
|
||||
|
||||
test:verbose:
|
||||
desc: "Run all tests with verbose output"
|
||||
cmds:
|
||||
- core test --verbose
|
||||
|
||||
test:run:
|
||||
desc: "Run specific test (use: task test:run -- TestName)"
|
||||
cmds:
|
||||
- core test --run {{.CLI_ARGS}}
|
||||
|
||||
cov:
|
||||
desc: "Run tests with coverage report"
|
||||
cmds:
|
||||
- core go cov
|
||||
|
||||
fmt:
|
||||
desc: "Format Go code"
|
||||
cmds:
|
||||
- core go fmt
|
||||
|
||||
lint:
|
||||
desc: "Run linter"
|
||||
cmds:
|
||||
- core go lint
|
||||
|
||||
mod:tidy:
|
||||
desc: "Run go mod tidy"
|
||||
cmds:
|
||||
- core go mod tidy
|
||||
|
||||
# --- Quality Assurance ---
|
||||
qa:
|
||||
desc: "Run QA: fmt, vet, lint, test"
|
||||
cmds:
|
||||
- core go qa
|
||||
|
||||
qa:quick:
|
||||
desc: "Quick QA: fmt, vet, lint only"
|
||||
cmds:
|
||||
- core go qa quick
|
||||
|
||||
qa:full:
|
||||
desc: "Full QA: + race, vuln, security"
|
||||
cmds:
|
||||
- core go qa full
|
||||
|
||||
qa:fix:
|
||||
desc: "QA with auto-fix"
|
||||
cmds:
|
||||
- core go qa --fix
|
||||
|
||||
# --- Build ---
|
||||
build:
|
||||
desc: "Build project with auto-detection"
|
||||
cmds:
|
||||
- core build
|
||||
|
||||
build:ci:
|
||||
desc: "Build for CI (all targets, checksums)"
|
||||
cmds:
|
||||
- core build --ci
|
||||
|
||||
# --- Environment ---
|
||||
doctor:
|
||||
desc: "Check development environment"
|
||||
cmds:
|
||||
- core doctor
|
||||
|
||||
doctor:verbose:
|
||||
desc: "Check environment with details"
|
||||
cmds:
|
||||
- core doctor --verbose
|
||||
|
||||
# --- Code Review ---
|
||||
review:
|
||||
desc: "Run CodeRabbit review to get feedback on the current changes."
|
||||
desc: "Run CodeRabbit review"
|
||||
cmds:
|
||||
- coderabbit review --prompt-only
|
||||
|
||||
check:
|
||||
desc: "Run a CodeRabbit review followed by the full test suite."
|
||||
desc: "Tidy, test, and review"
|
||||
cmds:
|
||||
- task: go:mod:tidy
|
||||
- go test ./... # make sure the code compiles before asking coderabbit to review it
|
||||
- task: mod:tidy
|
||||
- task: test
|
||||
- task: review
|
||||
|
||||
go:mod:tidy:
|
||||
summary: Runs `go mod tidy`
|
||||
internal: true
|
||||
cmds:
|
||||
- go mod tidy
|
||||
|
||||
cov:
|
||||
desc: "Generate coverage profile (coverage.txt)"
|
||||
cmds:
|
||||
- go test -coverprofile=coverage.txt ./...
|
||||
|
||||
cov-view:
|
||||
desc: "Open the coverage report in your browser."
|
||||
cmds:
|
||||
- task: cov
|
||||
- go tool cover -html=coverage.txt
|
||||
|
||||
# --- i18n ---
|
||||
i18n:generate:
|
||||
desc: "Regenerate i18n key constants from locale files"
|
||||
desc: "Regenerate i18n key constants"
|
||||
cmds:
|
||||
- go generate ./pkg/i18n/...
|
||||
|
||||
i18n:validate:
|
||||
desc: "Validate i18n key usage across the codebase"
|
||||
desc: "Validate i18n key usage"
|
||||
cmds:
|
||||
- go run ./internal/tools/i18n-validate ./...
|
||||
|
||||
# --- Multi-repo (when in workspace) ---
|
||||
dev:health:
|
||||
desc: "Check health of all repos"
|
||||
cmds:
|
||||
- core dev health
|
||||
|
||||
dev:work:
|
||||
desc: "Full workflow: status, commit, push"
|
||||
cmds:
|
||||
- core dev work
|
||||
|
||||
dev:status:
|
||||
desc: "Show status of all repos"
|
||||
cmds:
|
||||
- core dev work --status
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ func AddGoCommands(root *cobra.Command) {
|
|||
}
|
||||
|
||||
root.AddCommand(goCmd)
|
||||
addGoQACommand(goCmd)
|
||||
addGoTestCommand(goCmd)
|
||||
addGoCovCommand(goCmd)
|
||||
addGoFmtCommand(goCmd)
|
||||
|
|
|
|||
239
pkg/go/cmd_qa.go
Normal file
239
pkg/go/cmd_qa.go
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
package gocmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/host-uk/core/pkg/cli"
|
||||
"github.com/host-uk/core/pkg/i18n"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var qaFix bool
|
||||
|
||||
func addGoQACommand(parent *cobra.Command) {
|
||||
qaCmd := &cobra.Command{
|
||||
Use: "qa",
|
||||
Short: i18n.T("cmd.go.qa.short"),
|
||||
Long: i18n.T("cmd.go.qa.long"),
|
||||
RunE: runGoQADefault,
|
||||
}
|
||||
|
||||
qaCmd.PersistentFlags().BoolVar(&qaFix, "fix", false, i18n.T("cmd.go.qa.flag.fix"))
|
||||
|
||||
// Subcommands for individual checks
|
||||
qaCmd.AddCommand(&cobra.Command{
|
||||
Use: "fmt",
|
||||
Short: "Check/fix code formatting",
|
||||
RunE: func(cmd *cobra.Command, args []string) error { return runQAChecks([]string{"fmt"}) },
|
||||
})
|
||||
|
||||
qaCmd.AddCommand(&cobra.Command{
|
||||
Use: "vet",
|
||||
Short: "Run go vet",
|
||||
RunE: func(cmd *cobra.Command, args []string) error { return runQAChecks([]string{"vet"}) },
|
||||
})
|
||||
|
||||
qaCmd.AddCommand(&cobra.Command{
|
||||
Use: "lint",
|
||||
Short: "Run golangci-lint",
|
||||
RunE: func(cmd *cobra.Command, args []string) error { return runQAChecks([]string{"lint"}) },
|
||||
})
|
||||
|
||||
qaCmd.AddCommand(&cobra.Command{
|
||||
Use: "test",
|
||||
Short: "Run tests",
|
||||
RunE: func(cmd *cobra.Command, args []string) error { return runQAChecks([]string{"test"}) },
|
||||
})
|
||||
|
||||
qaCmd.AddCommand(&cobra.Command{
|
||||
Use: "race",
|
||||
Short: "Run tests with race detector",
|
||||
RunE: func(cmd *cobra.Command, args []string) error { return runQAChecks([]string{"race"}) },
|
||||
})
|
||||
|
||||
qaCmd.AddCommand(&cobra.Command{
|
||||
Use: "vuln",
|
||||
Short: "Check for vulnerabilities",
|
||||
RunE: func(cmd *cobra.Command, args []string) error { return runQAChecks([]string{"vuln"}) },
|
||||
})
|
||||
|
||||
qaCmd.AddCommand(&cobra.Command{
|
||||
Use: "sec",
|
||||
Short: "Run security scanner",
|
||||
RunE: func(cmd *cobra.Command, args []string) error { return runQAChecks([]string{"sec"}) },
|
||||
})
|
||||
|
||||
qaCmd.AddCommand(&cobra.Command{
|
||||
Use: "quick",
|
||||
Short: "Quick QA: fmt, vet, lint",
|
||||
RunE: func(cmd *cobra.Command, args []string) error { return runQAChecks([]string{"fmt", "vet", "lint"}) },
|
||||
})
|
||||
|
||||
qaCmd.AddCommand(&cobra.Command{
|
||||
Use: "full",
|
||||
Short: "Full QA: all checks including race, vuln, sec",
|
||||
RunE: func(cmd *cobra.Command, args []string) error { return runQAChecks([]string{"fmt", "vet", "lint", "test", "race", "vuln", "sec"}) },
|
||||
})
|
||||
|
||||
parent.AddCommand(qaCmd)
|
||||
}
|
||||
|
||||
// runGoQADefault runs the default QA checks (fmt, vet, lint, test)
|
||||
func runGoQADefault(cmd *cobra.Command, args []string) error {
|
||||
return runQAChecks([]string{"fmt", "vet", "lint", "test"})
|
||||
}
|
||||
|
||||
// QACheck represents a single QA check.
|
||||
type QACheck struct {
|
||||
Name string
|
||||
Command string
|
||||
Args []string
|
||||
}
|
||||
|
||||
func runQAChecks(checkNames []string) error {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get working directory: %w", err)
|
||||
}
|
||||
|
||||
// Detect if this is a Go project
|
||||
if _, err := os.Stat("go.mod"); os.IsNotExist(err) {
|
||||
return fmt.Errorf("not a Go project (no go.mod found)")
|
||||
}
|
||||
|
||||
fmt.Println(cli.TitleStyle.Render("Go QA"))
|
||||
fmt.Println()
|
||||
|
||||
checks := buildChecksForNames(checkNames)
|
||||
|
||||
ctx := context.Background()
|
||||
startTime := time.Now()
|
||||
passed := 0
|
||||
failed := 0
|
||||
|
||||
for _, check := range checks {
|
||||
fmt.Printf("%s %s\n", cli.DimStyle.Render("→"), check.Name)
|
||||
|
||||
if err := runCheck(ctx, cwd, check); err != nil {
|
||||
fmt.Printf(" %s %s\n", cli.ErrorStyle.Render(cli.SymbolCross), err.Error())
|
||||
failed++
|
||||
} else {
|
||||
fmt.Printf(" %s\n", cli.SuccessStyle.Render(cli.SymbolCheck))
|
||||
passed++
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
fmt.Println()
|
||||
duration := time.Since(startTime).Round(time.Millisecond)
|
||||
|
||||
if failed > 0 {
|
||||
fmt.Printf("%s %d passed, %d failed (%s)\n",
|
||||
cli.ErrorStyle.Render(cli.SymbolCross),
|
||||
passed, failed, duration)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("%s %d checks passed (%s)\n",
|
||||
cli.SuccessStyle.Render(cli.SymbolCheck),
|
||||
passed, duration)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildChecksForNames(names []string) []QACheck {
|
||||
allChecks := map[string]QACheck{
|
||||
"fmt": {
|
||||
Name: "fmt",
|
||||
Command: "gofmt",
|
||||
Args: fmtArgs(qaFix),
|
||||
},
|
||||
"vet": {
|
||||
Name: "vet",
|
||||
Command: "go",
|
||||
Args: []string{"vet", "./..."},
|
||||
},
|
||||
"lint": {
|
||||
Name: "lint",
|
||||
Command: "golangci-lint",
|
||||
Args: lintArgs(qaFix),
|
||||
},
|
||||
"test": {
|
||||
Name: "test",
|
||||
Command: "go",
|
||||
Args: []string{"test", "./..."},
|
||||
},
|
||||
"race": {
|
||||
Name: "race",
|
||||
Command: "go",
|
||||
Args: []string{"test", "-race", "./..."},
|
||||
},
|
||||
"vuln": {
|
||||
Name: "vuln",
|
||||
Command: "govulncheck",
|
||||
Args: []string{"./..."},
|
||||
},
|
||||
"sec": {
|
||||
Name: "sec",
|
||||
Command: "gosec",
|
||||
Args: []string{"-quiet", "./..."},
|
||||
},
|
||||
}
|
||||
|
||||
var checks []QACheck
|
||||
for _, name := range names {
|
||||
if check, ok := allChecks[name]; ok {
|
||||
checks = append(checks, check)
|
||||
}
|
||||
}
|
||||
return checks
|
||||
}
|
||||
|
||||
func fmtArgs(fix bool) []string {
|
||||
if fix {
|
||||
return []string{"-w", "."}
|
||||
}
|
||||
return []string{"-l", "."}
|
||||
}
|
||||
|
||||
func lintArgs(fix bool) []string {
|
||||
args := []string{"run"}
|
||||
if fix {
|
||||
args = append(args, "--fix")
|
||||
}
|
||||
args = append(args, "./...")
|
||||
return args
|
||||
}
|
||||
|
||||
func runCheck(ctx context.Context, dir string, check QACheck) error {
|
||||
// Check if command exists
|
||||
if _, err := exec.LookPath(check.Command); err != nil {
|
||||
return fmt.Errorf("%s not installed", check.Command)
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, check.Command, check.Args...)
|
||||
cmd.Dir = dir
|
||||
|
||||
// For gofmt -l, capture output to check if files need formatting
|
||||
if check.Name == "fmt" && len(check.Args) > 0 && check.Args[0] == "-l" {
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(output) > 0 {
|
||||
// Show files that need formatting
|
||||
fmt.Print(string(output))
|
||||
return fmt.Errorf("files need formatting (use --fix)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
return cmd.Run()
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue