package cli import ( "context" "encoding/json" "fmt" "sync" "time" "github.com/google/uuid" "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. type BuildConfig struct { NATSUrl string NKeyFile string Repo string Target string Branch string Timeout time.Duration } // BuildResult contains the aggregated results from a build. type BuildResult struct { Responses []*messages.BuildResponse FinalResponse *messages.BuildResponse Errors []error } // AllSucceeded returns true if the build completed successfully. func (r *BuildResult) AllSucceeded() bool { if len(r.Errors) > 0 { return false } if r.FinalResponse == nil { return false } return r.FinalResponse.Status == messages.BuildStatusCompleted && r.FinalResponse.Failed == 0 } // MarshalJSON returns the JSON representation of the build result. func (r *BuildResult) MarshalJSON() ([]byte, error) { if r.FinalResponse != nil { return json.Marshal(r.FinalResponse) } return json.Marshal(map[string]any{ "status": "unknown", "responses": r.Responses, "errors": r.Errors, }) } // Build triggers a build and collects responses. func Build(ctx context.Context, cfg BuildConfig, onResponse func(*messages.BuildResponse)) (*BuildResult, error) { // Connect to NATS client, err := nats.Connect(nats.Config{ URL: cfg.NATSUrl, NKeyFile: cfg.NKeyFile, Name: "homelab-deploy-build-cli", }) if err != nil { return nil, fmt.Errorf("failed to connect to NATS: %w", err) } defer client.Close() // Generate unique reply subject requestID := uuid.New().String() replySubject := fmt.Sprintf("build.responses.%s", requestID) var mu sync.Mutex result := &BuildResult{} done := make(chan struct{}) // Subscribe to reply subject sub, err := client.Subscribe(replySubject, func(subject string, data []byte) { resp, err := messages.UnmarshalBuildResponse(data) if err != nil { mu.Lock() result.Errors = append(result.Errors, fmt.Errorf("failed to unmarshal response: %w", err)) mu.Unlock() return } mu.Lock() result.Responses = append(result.Responses, resp) if resp.Status.IsFinal() { result.FinalResponse = resp select { case <-done: default: close(done) } } mu.Unlock() if onResponse != nil { onResponse(resp) } }) if err != nil { return nil, fmt.Errorf("failed to subscribe to reply subject: %w", err) } defer func() { _ = sub.Unsubscribe() }() // Build and send request req := &messages.BuildRequest{ Repo: cfg.Repo, Target: cfg.Target, Branch: cfg.Branch, ReplyTo: replySubject, } data, err := req.Marshal() if err != nil { return nil, fmt.Errorf("failed to marshal request: %w", err) } // Publish to build.. buildSubject := fmt.Sprintf("build.%s.%s", cfg.Repo, cfg.Target) if err := client.Publish(buildSubject, data); err != nil { return nil, fmt.Errorf("failed to publish request: %w", err) } if err := client.Flush(); err != nil { return nil, fmt.Errorf("failed to flush: %w", err) } // Wait for final response or timeout select { case <-ctx.Done(): return result, ctx.Err() case <-done: return result, nil case <-time.After(cfg.Timeout): return result, nil } }