diff --git a/cmd/homelab-deploy/main.go b/cmd/homelab-deploy/main.go index 50e0cd2..dd2c3fb 100644 --- a/cmd/homelab-deploy/main.go +++ b/cmd/homelab-deploy/main.go @@ -17,7 +17,7 @@ import ( "github.com/urfave/cli/v3" ) -const version = "0.2.2" +const version = "0.2.3" func main() { app := &cli.Command{ diff --git a/internal/builder/builder.go b/internal/builder/builder.go index 4f5b801..86cee5b 100644 --- a/internal/builder/builder.go +++ b/internal/builder/builder.go @@ -19,6 +19,23 @@ import ( // Allows: alphanumeric, dashes, underscores, dots. var hostnameRegex = regexp.MustCompile(`^[a-zA-Z0-9._-]+$`) +// truncateOutputLines truncates output to the first and last N lines if it exceeds 2*N lines, +// returning the result as a slice of strings. +func truncateOutputLines(output string, keepLines int) []string { + lines := strings.Split(output, "\n") + if len(lines) <= keepLines*2 { + return lines + } + head := lines[:keepLines] + tail := lines[len(lines)-keepLines:] + omitted := len(lines) - keepLines*2 + result := make([]string, 0, keepLines*2+1) + result = append(result, head...) + result = append(result, fmt.Sprintf("... (%d lines omitted) ...", omitted)) + result = append(result, tail...) + return result +} + // truncateOutput truncates output to the first and last N lines if it exceeds 2*N lines. func truncateOutput(output string, keepLines int) string { lines := strings.Split(output, "\n") @@ -256,6 +273,8 @@ func (b *Builder) handleBuildRequest(subject string, data []byte) { hostStart := time.Now() b.logger.Info("building host", "host", host, + "repo", req.Repo, + "rev", branch, "progress", fmt.Sprintf("%d/%d", i+1, len(hosts)), "command", b.executor.BuildCommand(repo.URL, branch, host), ) @@ -280,13 +299,18 @@ func (b *Builder) handleBuildRequest(subject string, data []byte) { if result.Success { succeeded++ - b.logger.Info("host build succeeded", "host", host, "duration", hostDuration) + b.logger.Info("host build succeeded", "host", host, "repo", req.Repo, "rev", branch, "duration", hostDuration) if b.metrics != nil { b.metrics.RecordHostBuildSuccess(req.Repo, host, hostDuration) } } else { failed++ - b.logger.Error("host build failed", "host", host, "error", hostResult.Error, "output", hostResult.Output) + b.logger.Error("host build failed", "host", host, "repo", req.Repo, "rev", branch, "error", hostResult.Error) + if result.Stderr != "" { + for _, line := range truncateOutputLines(result.Stderr, 50) { + b.logger.Warn("build output", "host", host, "repo", req.Repo, "line", line) + } + } if b.metrics != nil { b.metrics.RecordHostBuildFailure(req.Repo, host, hostDuration) } diff --git a/internal/builder/builder_test.go b/internal/builder/builder_test.go index eaca6f2..f3ad3de 100644 --- a/internal/builder/builder_test.go +++ b/internal/builder/builder_test.go @@ -1,6 +1,7 @@ package builder import ( + "fmt" "strings" "testing" ) @@ -83,6 +84,54 @@ func makeLines(n int) []string { return lines } +func TestTruncateOutputLines(t *testing.T) { + t.Run("short output returns all lines", func(t *testing.T) { + input := "line1\nline2\nline3" + got := truncateOutputLines(input, 50) + if len(got) != 3 { + t.Errorf("got %d lines, want 3", len(got)) + } + if got[0] != "line1" || got[1] != "line2" || got[2] != "line3" { + t.Errorf("unexpected lines: %v", got) + } + }) + + t.Run("over threshold returns head + marker + tail", func(t *testing.T) { + lines := makeLines(200) + input := strings.Join(lines, "\n") + got := truncateOutputLines(input, 50) + + // Should be 50 head + 1 marker + 50 tail = 101 + if len(got) != 101 { + t.Errorf("got %d lines, want 101", len(got)) + } + + // Check first and last lines preserved + if got[0] != lines[0] { + t.Errorf("first line = %q, want %q", got[0], lines[0]) + } + if got[len(got)-1] != lines[len(lines)-1] { + t.Errorf("last line = %q, want %q", got[len(got)-1], lines[len(lines)-1]) + } + + // Check omitted marker + marker := got[50] + expected := fmt.Sprintf("... (%d lines omitted) ...", 100) + if marker != expected { + t.Errorf("marker = %q, want %q", marker, expected) + } + }) + + t.Run("exactly at threshold returns all lines", func(t *testing.T) { + lines := makeLines(100) + input := strings.Join(lines, "\n") + got := truncateOutputLines(input, 50) + if len(got) != 100 { + t.Errorf("got %d lines, want 100", len(got)) + } + }) +} + func TestTruncateOutputPreservesContent(t *testing.T) { // Create input with distinct first and last lines lines := make([]string, 200)