From 1b432ad9b6c4000b3af9b9972010ca99e10dd12f Mon Sep 17 00:00:00 2001 From: beads/crew/dave Date: Wed, 31 Dec 2025 00:02:59 -0800 Subject: [PATCH] feat: add lint check to bd preflight --check (bd-lfak.3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Skipped field to CheckResult for graceful handling of missing tools - Implement runLintCheck() that runs golangci-lint run ./... - Skip lint check gracefully if golangci-lint not in PATH - Update summary to show skipped count separately - Add test for skipped state 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 Executed-By: beads/crew/dave Rig: beads Role: crew --- cmd/bd/preflight.go | 51 ++++++++++++++++++++++++++++++++++++---- cmd/bd/preflight_test.go | 26 ++++++++++++++++++++ 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/cmd/bd/preflight.go b/cmd/bd/preflight.go index 2082cbf0..dfa3a561 100644 --- a/cmd/bd/preflight.go +++ b/cmd/bd/preflight.go @@ -14,6 +14,7 @@ import ( type CheckResult struct { Name string `json:"name"` Passed bool `json:"passed"` + Skipped bool `json:"skipped,omitempty"` Output string `json:"output,omitempty"` Command string `json:"command"` } @@ -89,18 +90,29 @@ func runChecks(jsonOutput bool) { testResult := runTestCheck() results = append(results, testResult) + // Run lint check + lintResult := runLintCheck() + results = append(results, lintResult) + // Calculate overall result allPassed := true passCount := 0 + skipCount := 0 for _, r := range results { - if r.Passed { + if r.Skipped { + skipCount++ + } else if r.Passed { passCount++ } else { allPassed = false } } - summary := fmt.Sprintf("%d/%d checks passed", passCount, len(results)) + runCount := len(results) - skipCount + summary := fmt.Sprintf("%d/%d checks passed", passCount, runCount) + if skipCount > 0 { + summary += fmt.Sprintf(" (%d skipped)", skipCount) + } if jsonOutput { result := PreflightResult{ @@ -114,13 +126,18 @@ func runChecks(jsonOutput bool) { } else { // Human-readable output for _, r := range results { - if r.Passed { + if r.Skipped { + fmt.Printf("⚠ %s (skipped)\n", r.Name) + } else if r.Passed { fmt.Printf("✓ %s\n", r.Name) } else { fmt.Printf("✗ %s\n", r.Name) } fmt.Printf(" Command: %s\n", r.Command) - if !r.Passed && r.Output != "" { + if r.Skipped && r.Output != "" { + // Show skip reason + fmt.Printf(" Reason: %s\n", r.Output) + } else if !r.Passed && r.Output != "" { // Truncate output for terminal display output := truncateOutput(r.Output, 500) fmt.Printf(" Output:\n") @@ -152,6 +169,32 @@ func runTestCheck() CheckResult { } } +// runLintCheck runs golangci-lint and returns the result. +func runLintCheck() CheckResult { + command := "golangci-lint run ./..." + + // Check if golangci-lint is available + if _, err := exec.LookPath("golangci-lint"); err != nil { + return CheckResult{ + Name: "Lint passes", + Passed: false, + Skipped: true, + Output: "golangci-lint not found in PATH", + Command: command, + } + } + + cmd := exec.Command("golangci-lint", "run", "./...") + output, err := cmd.CombinedOutput() + + return CheckResult{ + Name: "Lint passes", + Passed: err == nil, + Output: string(output), + Command: command, + } +} + // truncateOutput truncates output to maxLen characters, adding ellipsis if truncated. func truncateOutput(s string, maxLen int) string { if len(s) <= maxLen { diff --git a/cmd/bd/preflight_test.go b/cmd/bd/preflight_test.go index a9c14f53..f749b9e0 100644 --- a/cmd/bd/preflight_test.go +++ b/cmd/bd/preflight_test.go @@ -100,6 +100,32 @@ func TestPreflightResult_SomeFailed(t *testing.T) { } } +func TestPreflightResult_WithSkipped(t *testing.T) { + results := PreflightResult{ + Checks: []CheckResult{ + {Name: "Tests pass", Passed: true, Command: "go test ./..."}, + {Name: "Lint passes", Passed: false, Skipped: true, Command: "golangci-lint run", Output: "not installed"}, + }, + Passed: true, + Summary: "1/1 checks passed (1 skipped)", + } + + // Skipped checks don't count as failures + if !results.Passed { + t.Error("Expected result to pass (skipped doesn't count as failure)") + } + + skipCount := 0 + for _, c := range results.Checks { + if c.Skipped { + skipCount++ + } + } + if skipCount != 1 { + t.Errorf("Expected 1 skipped, got %d", skipCount) + } +} + func TestTruncateOutput(t *testing.T) { tests := []struct { name string