- 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>
194 lines
4.5 KiB
Go
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:]
|
|
}
|