Compare commits

4 Commits

Author SHA1 Message Date
713d1e7584 chore: migrate module path from git.t-juice.club to code.t-juice.club
Gitea to Forgejo host migration — update Go module path and all
import references.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 19:37:47 +01:00
2d26de5055 fix(metrics): adjust build duration histogram buckets for better resolution
Add lower buckets (5s, 10s) for cached builds and higher buckets (7200s, 14400s)
for cold builds up to the 4h timeout.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:03:15 +01:00
e5e8be86ec Merge pull request 'feat(builder): log build failure output as separate lines' (#3) from feat/builder-logging into master
Reviewed-on: #3
2026-02-13 17:35:23 +00:00
3ac5d9777f feat(builder): log build failure output as separate lines
Log each line of build failure output as a separate structured log entry
at WARN level, making output readable and queryable in Loki/Grafana.
Add repo and rev fields to all build-related log entries. Add
truncateOutputLines helper that returns a []string for per-line logging.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 18:34:21 +01:00
15 changed files with 103 additions and 30 deletions

View File

@@ -9,15 +9,15 @@ import (
"syscall"
"time"
"git.t-juice.club/torjus/homelab-deploy/internal/builder"
deploycli "git.t-juice.club/torjus/homelab-deploy/internal/cli"
"git.t-juice.club/torjus/homelab-deploy/internal/listener"
"git.t-juice.club/torjus/homelab-deploy/internal/mcp"
"git.t-juice.club/torjus/homelab-deploy/internal/messages"
"code.t-juice.club/torjus/homelab-deploy/internal/builder"
deploycli "code.t-juice.club/torjus/homelab-deploy/internal/cli"
"code.t-juice.club/torjus/homelab-deploy/internal/listener"
"code.t-juice.club/torjus/homelab-deploy/internal/mcp"
"code.t-juice.club/torjus/homelab-deploy/internal/messages"
"github.com/urfave/cli/v3"
)
const version = "0.2.2"
const version = "0.2.5"
func main() {
app := &cli.Command{

2
go.mod
View File

@@ -1,4 +1,4 @@
module git.t-juice.club/torjus/homelab-deploy
module code.t-juice.club/torjus/homelab-deploy
go 1.25.5

View File

@@ -10,15 +10,32 @@ import (
"sync"
"time"
"git.t-juice.club/torjus/homelab-deploy/internal/messages"
"git.t-juice.club/torjus/homelab-deploy/internal/metrics"
"git.t-juice.club/torjus/homelab-deploy/internal/nats"
"code.t-juice.club/torjus/homelab-deploy/internal/messages"
"code.t-juice.club/torjus/homelab-deploy/internal/metrics"
"code.t-juice.club/torjus/homelab-deploy/internal/nats"
)
// hostnameRegex validates hostnames from flake output.
// 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)
}

View File

@@ -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)

View File

@@ -9,8 +9,8 @@ import (
"github.com/google/uuid"
"git.t-juice.club/torjus/homelab-deploy/internal/messages"
"git.t-juice.club/torjus/homelab-deploy/internal/nats"
"code.t-juice.club/torjus/homelab-deploy/internal/messages"
"code.t-juice.club/torjus/homelab-deploy/internal/nats"
)
// BuildConfig holds configuration for a build operation.

View File

@@ -8,8 +8,8 @@ import (
"github.com/google/uuid"
"git.t-juice.club/torjus/homelab-deploy/internal/messages"
"git.t-juice.club/torjus/homelab-deploy/internal/nats"
"code.t-juice.club/torjus/homelab-deploy/internal/messages"
"code.t-juice.club/torjus/homelab-deploy/internal/nats"
)
// DeployConfig holds configuration for a deploy operation.

View File

@@ -3,7 +3,7 @@ package cli
import (
"testing"
"git.t-juice.club/torjus/homelab-deploy/internal/messages"
"code.t-juice.club/torjus/homelab-deploy/internal/messages"
)
func TestDeployResult_AllSucceeded(t *testing.T) {

View File

@@ -7,7 +7,7 @@ import (
"os/exec"
"time"
"git.t-juice.club/torjus/homelab-deploy/internal/messages"
"code.t-juice.club/torjus/homelab-deploy/internal/messages"
)
// Executor handles the execution of nixos-rebuild commands.

View File

@@ -4,7 +4,7 @@ import (
"testing"
"time"
"git.t-juice.club/torjus/homelab-deploy/internal/messages"
"code.t-juice.club/torjus/homelab-deploy/internal/messages"
)
func TestExecutor_BuildCommand(t *testing.T) {

View File

@@ -6,10 +6,10 @@ import (
"log/slog"
"time"
"git.t-juice.club/torjus/homelab-deploy/internal/deploy"
"git.t-juice.club/torjus/homelab-deploy/internal/messages"
"git.t-juice.club/torjus/homelab-deploy/internal/metrics"
"git.t-juice.club/torjus/homelab-deploy/internal/nats"
"code.t-juice.club/torjus/homelab-deploy/internal/deploy"
"code.t-juice.club/torjus/homelab-deploy/internal/messages"
"code.t-juice.club/torjus/homelab-deploy/internal/metrics"
"code.t-juice.club/torjus/homelab-deploy/internal/nats"
)
// Config holds the configuration for the listener.

View File

@@ -7,8 +7,8 @@ import (
"github.com/mark3labs/mcp-go/mcp"
deploycli "git.t-juice.club/torjus/homelab-deploy/internal/cli"
"git.t-juice.club/torjus/homelab-deploy/internal/messages"
deploycli "code.t-juice.club/torjus/homelab-deploy/internal/cli"
"code.t-juice.club/torjus/homelab-deploy/internal/messages"
)
// BuildTool creates the build tool definition.

View File

@@ -9,8 +9,8 @@ import (
"github.com/mark3labs/mcp-go/mcp"
deploycli "git.t-juice.club/torjus/homelab-deploy/internal/cli"
"git.t-juice.club/torjus/homelab-deploy/internal/messages"
deploycli "code.t-juice.club/torjus/homelab-deploy/internal/cli"
"code.t-juice.club/torjus/homelab-deploy/internal/messages"
)
// ToolConfig holds configuration for the MCP tools.

View File

@@ -35,7 +35,7 @@ func NewBuildCollector(reg prometheus.Registerer) *BuildCollector {
prometheus.HistogramOpts{
Name: "homelab_deploy_build_duration_seconds",
Help: "Build execution time per host",
Buckets: []float64{30, 60, 120, 300, 600, 900, 1200, 1800, 3600},
Buckets: []float64{5, 10, 30, 60, 120, 300, 600, 1800, 3600, 7200, 14400},
},
[]string{"repo", "host"},
),

View File

@@ -2,7 +2,7 @@
package metrics
import (
"git.t-juice.club/torjus/homelab-deploy/internal/messages"
"code.t-juice.club/torjus/homelab-deploy/internal/messages"
"github.com/prometheus/client_golang/prometheus"
)

View File

@@ -8,7 +8,7 @@ import (
"testing"
"time"
"git.t-juice.club/torjus/homelab-deploy/internal/messages"
"code.t-juice.club/torjus/homelab-deploy/internal/messages"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/testutil"
)