Gitea to Forgejo host migration — update Go module path and all import references. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
141 lines
3.3 KiB
Go
141 lines
3.3 KiB
Go
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.<repo>.<target>
|
|
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
|
|
}
|
|
}
|