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:
committed by
Steve Yegge
parent
1b432ad9b6
commit
6298359b60
@@ -15,6 +15,7 @@ type CheckResult struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Passed bool `json:"passed"`
|
Passed bool `json:"passed"`
|
||||||
Skipped bool `json:"skipped,omitempty"`
|
Skipped bool `json:"skipped,omitempty"`
|
||||||
|
Warning bool `json:"warning,omitempty"`
|
||||||
Output string `json:"output,omitempty"`
|
Output string `json:"output,omitempty"`
|
||||||
Command string `json:"command"`
|
Command string `json:"command"`
|
||||||
}
|
}
|
||||||
@@ -94,13 +95,21 @@ func runChecks(jsonOutput bool) {
|
|||||||
lintResult := runLintCheck()
|
lintResult := runLintCheck()
|
||||||
results = append(results, lintResult)
|
results = append(results, lintResult)
|
||||||
|
|
||||||
|
// Run nix hash check
|
||||||
|
nixResult := runNixHashCheck()
|
||||||
|
results = append(results, nixResult)
|
||||||
|
|
||||||
// Calculate overall result
|
// Calculate overall result
|
||||||
allPassed := true
|
allPassed := true
|
||||||
passCount := 0
|
passCount := 0
|
||||||
skipCount := 0
|
skipCount := 0
|
||||||
|
warnCount := 0
|
||||||
for _, r := range results {
|
for _, r := range results {
|
||||||
if r.Skipped {
|
if r.Skipped {
|
||||||
skipCount++
|
skipCount++
|
||||||
|
} else if r.Warning {
|
||||||
|
warnCount++
|
||||||
|
// Warnings don't fail the overall result but count as "not passed"
|
||||||
} else if r.Passed {
|
} else if r.Passed {
|
||||||
passCount++
|
passCount++
|
||||||
} else {
|
} else {
|
||||||
@@ -110,6 +119,9 @@ func runChecks(jsonOutput bool) {
|
|||||||
|
|
||||||
runCount := len(results) - skipCount
|
runCount := len(results) - skipCount
|
||||||
summary := fmt.Sprintf("%d/%d checks passed", passCount, runCount)
|
summary := fmt.Sprintf("%d/%d checks passed", passCount, runCount)
|
||||||
|
if warnCount > 0 {
|
||||||
|
summary += fmt.Sprintf(", %d warning(s)", warnCount)
|
||||||
|
}
|
||||||
if skipCount > 0 {
|
if skipCount > 0 {
|
||||||
summary += fmt.Sprintf(" (%d skipped)", skipCount)
|
summary += fmt.Sprintf(" (%d skipped)", skipCount)
|
||||||
}
|
}
|
||||||
@@ -128,6 +140,8 @@ func runChecks(jsonOutput bool) {
|
|||||||
for _, r := range results {
|
for _, r := range results {
|
||||||
if r.Skipped {
|
if r.Skipped {
|
||||||
fmt.Printf("⚠ %s (skipped)\n", r.Name)
|
fmt.Printf("⚠ %s (skipped)\n", r.Name)
|
||||||
|
} else if r.Warning {
|
||||||
|
fmt.Printf("⚠ %s\n", r.Name)
|
||||||
} else if r.Passed {
|
} else if r.Passed {
|
||||||
fmt.Printf("✓ %s\n", r.Name)
|
fmt.Printf("✓ %s\n", r.Name)
|
||||||
} else {
|
} else {
|
||||||
@@ -137,6 +151,9 @@ func runChecks(jsonOutput bool) {
|
|||||||
if r.Skipped && r.Output != "" {
|
if r.Skipped && r.Output != "" {
|
||||||
// Show skip reason
|
// Show skip reason
|
||||||
fmt.Printf(" Reason: %s\n", r.Output)
|
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 != "" {
|
} else if !r.Passed && r.Output != "" {
|
||||||
// Truncate output for terminal display
|
// Truncate output for terminal display
|
||||||
output := truncateOutput(r.Output, 500)
|
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.
|
// truncateOutput truncates output to maxLen characters, adding ellipsis if truncated.
|
||||||
func truncateOutput(s string, maxLen int) string {
|
func truncateOutput(s string, maxLen int) string {
|
||||||
if len(s) <= maxLen {
|
if len(s) <= maxLen {
|
||||||
|
|||||||
@@ -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) {
|
func TestTruncateOutput(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
Reference in New Issue
Block a user