feat: add lint check to bd preflight --check (bd-lfak.3)

- 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 <noreply@anthropic.com>

Executed-By: beads/crew/dave
Rig: beads
Role: crew
This commit is contained in:
beads/crew/dave
2025-12-31 00:02:59 -08:00
committed by Steve Yegge
parent 64c65974f0
commit 1b432ad9b6
2 changed files with 73 additions and 4 deletions

View File

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

View File

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