Files
beads/cmd/bd/preflight.go
beads/polecats/basalt 8dcfdda186 feat: implement bd preflight --check flag with test runner (bd-lfak.2)
- Add CheckResult and PreflightResults structs for check outcomes
- Implement runTestCheck() to execute go test -short ./...
- Wire up --check flag to actually run tests instead of placeholder
- Add ✓/✗ output formatting with command and truncated output
- Support --json flag for programmatic consumption
- Exit with non-zero code when tests fail
- Add tests for new preflight functionality

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 22:22:15 -08:00

194 lines
4.5 KiB
Go

package main
import (
"bytes"
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
"github.com/spf13/cobra"
)
// CheckResult represents the outcome of a single preflight check
type CheckResult struct {
Name string `json:"name"`
Passed bool `json:"passed"`
Command string `json:"command"`
Output string `json:"output,omitempty"`
}
// PreflightResults holds all check results for JSON output
type PreflightResults struct {
Checks []CheckResult `json:"checks"`
Passed bool `json:"passed"`
Summary string `json:"summary"`
}
var preflightCmd = &cobra.Command{
Use: "preflight",
GroupID: "maint",
Short: "Show PR readiness checklist",
Long: `Display a checklist of common pre-PR checks for contributors.
This command helps catch common issues before pushing to CI:
- Tests not run locally
- Lint errors
- Stale nix vendorHash
- Version mismatches
Examples:
bd preflight # Show checklist
bd preflight --check # Run tests automatically
bd preflight --check --json # Run tests with JSON output
bd preflight --fix # (future) Auto-fix where possible
`,
Run: runPreflight,
}
func init() {
preflightCmd.Flags().Bool("check", false, "Run checks automatically")
preflightCmd.Flags().Bool("fix", false, "Auto-fix issues where possible (not yet implemented)")
rootCmd.AddCommand(preflightCmd)
}
func runPreflight(cmd *cobra.Command, args []string) {
check, _ := cmd.Flags().GetBool("check")
fix, _ := cmd.Flags().GetBool("fix")
if fix {
fmt.Println("Note: --fix is not yet implemented.")
fmt.Println("See bd-lfak.3 through bd-lfak.5 for implementation roadmap.")
fmt.Println()
}
if check {
runChecks(cmd)
return
}
fmt.Println("PR Readiness Checklist:")
fmt.Println()
fmt.Println("[ ] Tests pass: go test -short ./...")
fmt.Println("[ ] Lint passes: golangci-lint run ./...")
fmt.Println("[ ] No beads pollution: check .beads/issues.jsonl diff")
fmt.Println("[ ] Nix hash current: go.sum unchanged or vendorHash updated")
fmt.Println("[ ] Version sync: version.go matches default.nix")
fmt.Println()
fmt.Println("Run 'bd preflight --check' to validate automatically.")
}
// runChecks executes the preflight checks and reports results
func runChecks(cmd *cobra.Command) {
results := []CheckResult{
runTestCheck(),
}
allPassed := true
for _, r := range results {
if !r.Passed {
allPassed = false
break
}
}
// Build summary
passCount := 0
failCount := 0
for _, r := range results {
if r.Passed {
passCount++
} else {
failCount++
}
}
summary := fmt.Sprintf("%d passed, %d failed", passCount, failCount)
preflightResults := PreflightResults{
Checks: results,
Passed: allPassed,
Summary: summary,
}
if jsonOutput {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
enc.Encode(preflightResults)
} else {
for _, r := range results {
printCheckResult(r)
}
fmt.Println()
fmt.Println(summary)
}
if !allPassed {
os.Exit(1)
}
}
// runTestCheck runs go test -short ./... and returns the result
func runTestCheck() CheckResult {
command := "go test -short ./..."
cmd := exec.Command("go", "test", "-short", "./...")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
output := stdout.String()
if stderr.Len() > 0 {
if output != "" {
output += "\n"
}
output += stderr.String()
}
// Truncate output if too long
// On failure, keep beginning (failure context) and end (summary)
if len(output) > 3000 {
lines := strings.Split(output, "\n")
// Keep first 30 lines and last 20 lines
if len(lines) > 50 {
firstPart := strings.Join(lines[:30], "\n")
lastPart := strings.Join(lines[len(lines)-20:], "\n")
output = firstPart + "\n\n...(truncated " + fmt.Sprintf("%d", len(lines)-50) + " lines)...\n\n" + lastPart
}
}
return CheckResult{
Name: "tests",
Passed: err == nil,
Command: command,
Output: strings.TrimSpace(output),
}
}
// printCheckResult prints a single check result with formatting
func printCheckResult(r CheckResult) {
if r.Passed {
fmt.Printf("✓ %s\n", capitalizeFirst(r.Name))
fmt.Printf(" Command: %s\n", r.Command)
} else {
fmt.Printf("✗ %s\n", capitalizeFirst(r.Name))
fmt.Printf(" Command: %s\n", r.Command)
if r.Output != "" {
fmt.Println(" Output:")
for _, line := range strings.Split(r.Output, "\n") {
fmt.Printf(" %s\n", line)
}
}
}
}
// capitalizeFirst capitalizes the first letter of a string
func capitalizeFirst(s string) string {
if s == "" {
return s
}
return strings.ToUpper(s[:1]) + s[1:]
}