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>
110 lines
3.0 KiB
Go
110 lines
3.0 KiB
Go
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"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"
|
|
)
|
|
|
|
// BuildTool creates the build tool definition.
|
|
func BuildTool() mcp.Tool {
|
|
return mcp.NewTool(
|
|
"build",
|
|
mcp.WithDescription("Trigger a Nix build on the build server"),
|
|
mcp.WithString("repo",
|
|
mcp.Required(),
|
|
mcp.Description("Repository name (must match builder config)"),
|
|
),
|
|
mcp.WithString("target",
|
|
mcp.Description("Target hostname, or omit to build all hosts"),
|
|
),
|
|
mcp.WithBoolean("all",
|
|
mcp.Description("Build all hosts in the repository (default if no target specified)"),
|
|
),
|
|
mcp.WithString("branch",
|
|
mcp.Description("Git branch to build (uses repo default if not specified)"),
|
|
),
|
|
)
|
|
}
|
|
|
|
// HandleBuild handles the build tool.
|
|
func (h *ToolHandler) HandleBuild(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
|
repo, err := request.RequireString("repo")
|
|
if err != nil {
|
|
return mcp.NewToolResultError("repo is required"), nil
|
|
}
|
|
|
|
target := request.GetString("target", "")
|
|
all := request.GetBool("all", false)
|
|
branch := request.GetString("branch", "")
|
|
|
|
// Default to "all" if no target specified
|
|
if target == "" {
|
|
if !all {
|
|
all = true
|
|
}
|
|
target = "all"
|
|
}
|
|
if all && target != "all" {
|
|
return mcp.NewToolResultError("cannot specify both target and all"), nil
|
|
}
|
|
|
|
cfg := deploycli.BuildConfig{
|
|
NATSUrl: h.cfg.NATSUrl,
|
|
NKeyFile: h.cfg.NKeyFile,
|
|
Repo: repo,
|
|
Target: target,
|
|
Branch: branch,
|
|
Timeout: h.cfg.Timeout,
|
|
}
|
|
|
|
var output strings.Builder
|
|
branchStr := branch
|
|
if branchStr == "" {
|
|
branchStr = "(default)"
|
|
}
|
|
output.WriteString(fmt.Sprintf("Building %s target=%s branch=%s\n\n", repo, target, branchStr))
|
|
|
|
result, err := deploycli.Build(ctx, cfg, func(resp *messages.BuildResponse) {
|
|
switch resp.Status {
|
|
case messages.BuildStatusStarted:
|
|
output.WriteString(fmt.Sprintf("Started: %s\n", resp.Message))
|
|
case messages.BuildStatusProgress:
|
|
successStr := "..."
|
|
if resp.HostSuccess != nil {
|
|
if *resp.HostSuccess {
|
|
successStr = "success"
|
|
} else {
|
|
successStr = "failed"
|
|
}
|
|
}
|
|
output.WriteString(fmt.Sprintf("[%d/%d] %s: %s\n", resp.HostsCompleted, resp.HostsTotal, resp.Host, successStr))
|
|
case messages.BuildStatusCompleted, messages.BuildStatusFailed:
|
|
output.WriteString(fmt.Sprintf("\n%s\n", resp.Message))
|
|
case messages.BuildStatusRejected:
|
|
output.WriteString(fmt.Sprintf("Rejected: %s\n", resp.Message))
|
|
}
|
|
})
|
|
if err != nil {
|
|
return mcp.NewToolResultError(fmt.Sprintf("build failed: %v", err)), nil
|
|
}
|
|
|
|
if result.FinalResponse != nil {
|
|
output.WriteString(fmt.Sprintf("\nBuild complete: %d succeeded, %d failed (%.1fs)\n",
|
|
result.FinalResponse.Succeeded,
|
|
result.FinalResponse.Failed,
|
|
result.FinalResponse.TotalDurationSeconds))
|
|
}
|
|
|
|
if !result.AllSucceeded() {
|
|
output.WriteString("WARNING: Some builds failed\n")
|
|
}
|
|
|
|
return mcp.NewToolResultText(output.String()), nil
|
|
}
|