Add a new "builder" capability to trigger Nix builds on a dedicated build host via NATS messaging. This allows pre-building NixOS configurations before deployment. New components: - Builder mode: subscribes to build.<repo>.* subjects, executes nix build - Build CLI command: triggers builds with progress tracking - MCP build tool: available with --enable-builds flag - Builder metrics: tracks build success/failure per repo and host - NixOS module: services.homelab-deploy.builder The builder uses a YAML config file to define allowed repositories with their URLs and default branches. Builds can target all hosts or specific hosts, with real-time progress updates. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
117 lines
3.0 KiB
Go
117 lines
3.0 KiB
Go
package builder
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os/exec"
|
|
"time"
|
|
)
|
|
|
|
// Executor handles the execution of nix build commands.
|
|
type Executor struct {
|
|
timeout time.Duration
|
|
}
|
|
|
|
// NewExecutor creates a new build executor.
|
|
func NewExecutor(timeout time.Duration) *Executor {
|
|
return &Executor{
|
|
timeout: timeout,
|
|
}
|
|
}
|
|
|
|
// BuildResult contains the result of a build execution.
|
|
type BuildResult struct {
|
|
Success bool
|
|
ExitCode int
|
|
Stdout string
|
|
Stderr string
|
|
Error error
|
|
}
|
|
|
|
// FlakeShowResult contains the parsed output of nix flake show.
|
|
type FlakeShowResult struct {
|
|
NixosConfigurations map[string]any `json:"nixosConfigurations"`
|
|
}
|
|
|
|
// ListHosts returns the list of hosts (nixosConfigurations) available in a flake.
|
|
func (e *Executor) ListHosts(ctx context.Context, flakeURL, branch string) ([]string, error) {
|
|
ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
|
|
defer cancel()
|
|
|
|
flakeRef := fmt.Sprintf("%s?ref=%s", flakeURL, branch)
|
|
cmd := exec.CommandContext(ctx, "nix", "flake", "show", "--json", flakeRef)
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
if ctx.Err() == context.DeadlineExceeded {
|
|
return nil, fmt.Errorf("timeout listing hosts")
|
|
}
|
|
return nil, fmt.Errorf("failed to list hosts: %w\n%s", err, stderr.String())
|
|
}
|
|
|
|
var result FlakeShowResult
|
|
if err := json.Unmarshal(stdout.Bytes(), &result); err != nil {
|
|
return nil, fmt.Errorf("failed to parse flake show output: %w", err)
|
|
}
|
|
|
|
hosts := make([]string, 0, len(result.NixosConfigurations))
|
|
for host := range result.NixosConfigurations {
|
|
hosts = append(hosts, host)
|
|
}
|
|
|
|
return hosts, nil
|
|
}
|
|
|
|
// Build builds a single host's system configuration.
|
|
func (e *Executor) Build(ctx context.Context, flakeURL, branch, host string) *BuildResult {
|
|
ctx, cancel := context.WithTimeout(ctx, e.timeout)
|
|
defer cancel()
|
|
|
|
// Build the flake reference for the system toplevel
|
|
flakeRef := fmt.Sprintf("%s?ref=%s#nixosConfigurations.%s.config.system.build.toplevel", flakeURL, branch, host)
|
|
|
|
cmd := exec.CommandContext(ctx, "nix", "build", "--no-link", flakeRef)
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
|
|
err := cmd.Run()
|
|
|
|
result := &BuildResult{
|
|
Stdout: stdout.String(),
|
|
Stderr: stderr.String(),
|
|
}
|
|
|
|
if err != nil {
|
|
result.Success = false
|
|
result.Error = err
|
|
|
|
if ctx.Err() == context.DeadlineExceeded {
|
|
result.Error = fmt.Errorf("build timed out after %v", e.timeout)
|
|
}
|
|
|
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
|
result.ExitCode = exitErr.ExitCode()
|
|
} else {
|
|
result.ExitCode = -1
|
|
}
|
|
} else {
|
|
result.Success = true
|
|
result.ExitCode = 0
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// BuildCommand returns the command that would be executed (for logging/debugging).
|
|
func (e *Executor) BuildCommand(flakeURL, branch, host string) string {
|
|
flakeRef := fmt.Sprintf("%s?ref=%s#nixosConfigurations.%s.config.system.build.toplevel", flakeURL, branch, host)
|
|
return fmt.Sprintf("nix build --no-link %s", flakeRef)
|
|
}
|