* feat: add FreeBSD release builds Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * chore: allow manual release dispatch Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * fix: stabilize release workflow on fork Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * fix: clean zig download artifact Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * fix: use valid zig target for freebsd arm Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * fix: disable freebsd arm release build Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * fix: switch freebsd build to pure go Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * fix: skip release publishing on forks Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * fix: satisfy golangci-lint for release PR --------- Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
337 lines
8.6 KiB
Go
337 lines
8.6 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// CheckResult represents the result of a single preflight check.
|
|
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"`
|
|
}
|
|
|
|
// PreflightResult represents the overall preflight check results.
|
|
type PreflightResult 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 checks automatically
|
|
bd preflight --check --json # JSON output for programmatic use
|
|
`,
|
|
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)")
|
|
preflightCmd.Flags().Bool("json", false, "Output results as JSON")
|
|
|
|
rootCmd.AddCommand(preflightCmd)
|
|
}
|
|
|
|
func runPreflight(cmd *cobra.Command, args []string) {
|
|
check, _ := cmd.Flags().GetBool("check")
|
|
fix, _ := cmd.Flags().GetBool("fix")
|
|
jsonOutput, _ := cmd.Flags().GetBool("json")
|
|
|
|
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(jsonOutput)
|
|
return
|
|
}
|
|
|
|
// Static checklist mode
|
|
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 all preflight checks and reports results.
|
|
func runChecks(jsonOutput bool) {
|
|
var results []CheckResult
|
|
|
|
// Run test check
|
|
testResult := runTestCheck()
|
|
results = append(results, testResult)
|
|
|
|
// Run lint check
|
|
lintResult := runLintCheck()
|
|
results = append(results, lintResult)
|
|
|
|
// Run nix hash check
|
|
nixResult := runNixHashCheck()
|
|
results = append(results, nixResult)
|
|
|
|
// Run version sync check
|
|
versionResult := runVersionSyncCheck()
|
|
results = append(results, versionResult)
|
|
|
|
// 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 {
|
|
allPassed = false
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
if jsonOutput {
|
|
result := PreflightResult{
|
|
Checks: results,
|
|
Passed: allPassed,
|
|
Summary: summary,
|
|
}
|
|
enc := json.NewEncoder(os.Stdout)
|
|
enc.SetIndent("", " ")
|
|
if err := enc.Encode(result); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error encoding preflight result: %v\n", err)
|
|
}
|
|
} else {
|
|
// Human-readable output
|
|
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 {
|
|
fmt.Printf("✗ %s\n", r.Name)
|
|
}
|
|
fmt.Printf(" Command: %s\n", r.Command)
|
|
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)
|
|
fmt.Printf(" Output:\n")
|
|
for _, line := range strings.Split(output, "\n") {
|
|
fmt.Printf(" %s\n", line)
|
|
}
|
|
}
|
|
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", "./...")
|
|
output, err := cmd.CombinedOutput()
|
|
|
|
return CheckResult{
|
|
Name: "Tests pass",
|
|
Passed: err == nil,
|
|
Output: string(output),
|
|
Command: command,
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
}
|
|
}
|
|
|
|
// runVersionSyncCheck checks that version.go matches default.nix.
|
|
func runVersionSyncCheck() CheckResult {
|
|
command := "Compare cmd/bd/version.go and default.nix"
|
|
|
|
// Read version.go
|
|
versionGoContent, err := os.ReadFile("cmd/bd/version.go")
|
|
if err != nil {
|
|
return CheckResult{
|
|
Name: "Version sync",
|
|
Passed: false,
|
|
Skipped: true,
|
|
Output: fmt.Sprintf("Cannot read cmd/bd/version.go: %v", err),
|
|
Command: command,
|
|
}
|
|
}
|
|
|
|
// Extract version from version.go
|
|
// Pattern: Version = "X.Y.Z"
|
|
versionGoRe := regexp.MustCompile(`Version\s*=\s*"([^"]+)"`)
|
|
versionGoMatch := versionGoRe.FindSubmatch(versionGoContent)
|
|
if versionGoMatch == nil {
|
|
return CheckResult{
|
|
Name: "Version sync",
|
|
Passed: false,
|
|
Skipped: true,
|
|
Output: "Cannot parse version from version.go",
|
|
Command: command,
|
|
}
|
|
}
|
|
goVersion := string(versionGoMatch[1])
|
|
|
|
// Read default.nix
|
|
nixContent, err := os.ReadFile("default.nix")
|
|
if err != nil {
|
|
// No nix file = skip version check (not an error)
|
|
return CheckResult{
|
|
Name: "Version sync",
|
|
Passed: true,
|
|
Skipped: true,
|
|
Output: "default.nix not found (skipping nix version check)",
|
|
Command: command,
|
|
}
|
|
}
|
|
|
|
// Extract version from default.nix
|
|
// Pattern: version = "X.Y.Z";
|
|
nixRe := regexp.MustCompile(`version\s*=\s*"([^"]+)"`)
|
|
nixMatch := nixRe.FindSubmatch(nixContent)
|
|
if nixMatch == nil {
|
|
return CheckResult{
|
|
Name: "Version sync",
|
|
Passed: false,
|
|
Skipped: true,
|
|
Output: "Cannot parse version from default.nix",
|
|
Command: command,
|
|
}
|
|
}
|
|
nixVersion := string(nixMatch[1])
|
|
|
|
if goVersion != nixVersion {
|
|
return CheckResult{
|
|
Name: "Version sync",
|
|
Passed: false,
|
|
Output: fmt.Sprintf("Version mismatch: version.go=%s, default.nix=%s", goVersion, nixVersion),
|
|
Command: command,
|
|
}
|
|
}
|
|
|
|
return CheckResult{
|
|
Name: "Version sync",
|
|
Passed: true,
|
|
Output: fmt.Sprintf("Versions match: %s", goVersion),
|
|
Command: command,
|
|
}
|
|
}
|
|
|
|
// truncateOutput truncates output to maxLen characters, adding ellipsis if truncated.
|
|
func truncateOutput(s string, maxLen int) string {
|
|
if len(s) <= maxLen {
|
|
return strings.TrimSpace(s)
|
|
}
|
|
return strings.TrimSpace(s[:maxLen]) + "\n... (truncated)"
|
|
}
|