feat: add nix-hash staleness detection to bd preflight --check (bd-lfak.4)

- Add Warning field to CheckResult for soft failures
- Implement runNixHashCheck() that detects go.sum changes
- Warnings (⚠) shown separately from failures (✗)
- Warnings don't fail the overall preflight result
- Summary shows warning count separately
- Add test for warning 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:05:53 -08:00
committed by Steve Yegge
parent 1b432ad9b6
commit 6298359b60
2 changed files with 75 additions and 0 deletions

View File

@@ -15,6 +15,7 @@ type CheckResult struct {
Name string `json:"name"`
Passed bool `json:"passed"`
Skipped bool `json:"skipped,omitempty"`
Warning bool `json:"warning,omitempty"`
Output string `json:"output,omitempty"`
Command string `json:"command"`
}
@@ -94,13 +95,21 @@ func runChecks(jsonOutput bool) {
lintResult := runLintCheck()
results = append(results, lintResult)
// Run nix hash check
nixResult := runNixHashCheck()
results = append(results, nixResult)
// Calculate overall result
allPassed := true
passCount := 0
skipCount := 0
warnCount := 0
for _, r := range results {
if r.Skipped {
skipCount++
} else if r.Warning {
warnCount++
// Warnings don't fail the overall result but count as "not passed"
} else if r.Passed {
passCount++
} else {
@@ -110,6 +119,9 @@ func runChecks(jsonOutput bool) {
runCount := len(results) - skipCount
summary := fmt.Sprintf("%d/%d checks passed", passCount, runCount)
if warnCount > 0 {
summary += fmt.Sprintf(", %d warning(s)", warnCount)
}
if skipCount > 0 {
summary += fmt.Sprintf(" (%d skipped)", skipCount)
}
@@ -128,6 +140,8 @@ func runChecks(jsonOutput bool) {
for _, r := range results {
if r.Skipped {
fmt.Printf("⚠ %s (skipped)\n", r.Name)
} else if r.Warning {
fmt.Printf("⚠ %s\n", r.Name)
} else if r.Passed {
fmt.Printf("✓ %s\n", r.Name)
} else {
@@ -137,6 +151,9 @@ func runChecks(jsonOutput bool) {
if r.Skipped && r.Output != "" {
// Show skip reason
fmt.Printf(" Reason: %s\n", r.Output)
} else if r.Warning && r.Output != "" {
// Show warning message
fmt.Printf(" Warning: %s\n", r.Output)
} else if !r.Passed && r.Output != "" {
// Truncate output for terminal display
output := truncateOutput(r.Output, 500)
@@ -195,6 +212,38 @@ func runLintCheck() CheckResult {
}
}
// runNixHashCheck checks if go.sum has uncommitted changes that may require vendorHash update.
func runNixHashCheck() CheckResult {
command := "git diff HEAD -- go.sum"
// Check for unstaged changes to go.sum
cmd := exec.Command("git", "diff", "--name-only", "HEAD", "--", "go.sum")
output, _ := cmd.Output()
// Check for staged changes to go.sum
stagedCmd := exec.Command("git", "diff", "--name-only", "--cached", "--", "go.sum")
stagedOutput, _ := stagedCmd.Output()
hasChanges := len(strings.TrimSpace(string(output))) > 0 || len(strings.TrimSpace(string(stagedOutput))) > 0
if hasChanges {
return CheckResult{
Name: "Nix hash current",
Passed: false,
Warning: true,
Output: "go.sum has uncommitted changes - vendorHash in default.nix may need updating",
Command: command,
}
}
return CheckResult{
Name: "Nix hash current",
Passed: true,
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

@@ -126,6 +126,32 @@ func TestPreflightResult_WithSkipped(t *testing.T) {
}
}
func TestPreflightResult_WithWarning(t *testing.T) {
results := PreflightResult{
Checks: []CheckResult{
{Name: "Tests pass", Passed: true, Command: "go test ./..."},
{Name: "Nix hash current", Passed: false, Warning: true, Command: "git diff HEAD -- go.sum", Output: "go.sum changed"},
},
Passed: true, // Warnings don't fail the overall result
Summary: "1/2 checks passed, 1 warning(s)",
}
// Warnings don't count as failures
if !results.Passed {
t.Error("Expected result to pass (warning doesn't count as failure)")
}
warnCount := 0
for _, c := range results.Checks {
if c.Warning {
warnCount++
}
}
if warnCount != 1 {
t.Errorf("Expected 1 warning, got %d", warnCount)
}
}
func TestTruncateOutput(t *testing.T) {
tests := []struct {
name string