diff --git a/Taskfile.yml b/Taskfile.yml index d9a9f5ed..12b48729 100644 --- a/Taskfile.yml +++ b/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 diff --git a/pkg/go/cmd_go.go b/pkg/go/cmd_go.go index d3362809..e4b68933 100644 --- a/pkg/go/cmd_go.go +++ b/pkg/go/cmd_go.go @@ -25,6 +25,7 @@ func AddGoCommands(root *cobra.Command) { } root.AddCommand(goCmd) + addGoQACommand(goCmd) addGoTestCommand(goCmd) addGoCovCommand(goCmd) addGoFmtCommand(goCmd) diff --git a/pkg/go/cmd_qa.go b/pkg/go/cmd_qa.go new file mode 100644 index 00000000..61c27eb6 --- /dev/null +++ b/pkg/go/cmd_qa.go @@ -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() +}