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:
Snider 2026-01-30 22:29:20 +00:00
parent 6b76b4d37f
commit 0fd76d86b4
3 changed files with 345 additions and 35 deletions

View file

@ -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

View file

@ -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
View 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()
}