package messages import ( "encoding/json" "fmt" "strings" ) // BuildStatus represents the status of a build response. type BuildStatus string const ( BuildStatusStarted BuildStatus = "started" BuildStatusProgress BuildStatus = "progress" BuildStatusCompleted BuildStatus = "completed" BuildStatusFailed BuildStatus = "failed" BuildStatusRejected BuildStatus = "rejected" ) // IsFinal returns true if this status indicates a terminal state. func (s BuildStatus) IsFinal() bool { switch s { case BuildStatusCompleted, BuildStatusFailed, BuildStatusRejected: return true default: return false } } // BuildRequest is the message sent to request a build. type BuildRequest struct { Repo string `json:"repo"` // Must match config Target string `json:"target"` // Hostname or "all" Branch string `json:"branch,omitempty"` // Optional, uses repo default ReplyTo string `json:"reply_to"` } // Validate checks that the request is valid. func (r *BuildRequest) Validate() error { if r.Repo == "" { return fmt.Errorf("repo is required") } if !revisionRegex.MatchString(r.Repo) { return fmt.Errorf("invalid repo name format: %q", r.Repo) } if r.Target == "" { return fmt.Errorf("target is required") } // Target must be "all" or a valid hostname (same format as revision/branch) if r.Target != "all" && !revisionRegex.MatchString(r.Target) { return fmt.Errorf("invalid target format: %q", r.Target) } if r.Branch != "" && !revisionRegex.MatchString(r.Branch) { return fmt.Errorf("invalid branch format: %q", r.Branch) } if r.ReplyTo == "" { return fmt.Errorf("reply_to is required") } // Validate reply_to format to prevent publishing to arbitrary subjects if !strings.HasPrefix(r.ReplyTo, "build.responses.") { return fmt.Errorf("invalid reply_to format: must start with 'build.responses.'") } return nil } // Marshal serializes the request to JSON. func (r *BuildRequest) Marshal() ([]byte, error) { return json.Marshal(r) } // UnmarshalBuildRequest deserializes a request from JSON. func UnmarshalBuildRequest(data []byte) (*BuildRequest, error) { var r BuildRequest if err := json.Unmarshal(data, &r); err != nil { return nil, fmt.Errorf("failed to unmarshal build request: %w", err) } return &r, nil } // BuildHostResult contains the result of building a single host. type BuildHostResult struct { Host string `json:"host"` Success bool `json:"success"` Error string `json:"error,omitempty"` DurationSeconds float64 `json:"duration_seconds"` } // BuildResponse is the message sent in response to a build request. type BuildResponse struct { Status BuildStatus `json:"status"` Message string `json:"message,omitempty"` // Progress updates Host string `json:"host,omitempty"` HostSuccess *bool `json:"host_success,omitempty"` HostsCompleted int `json:"hosts_completed,omitempty"` HostsTotal int `json:"hosts_total,omitempty"` // Final response Results []BuildHostResult `json:"results,omitempty"` TotalDurationSeconds float64 `json:"total_duration_seconds,omitempty"` Succeeded int `json:"succeeded,omitempty"` Failed int `json:"failed,omitempty"` Error string `json:"error,omitempty"` } // NewBuildResponse creates a new response with the given status and message. func NewBuildResponse(status BuildStatus, message string) *BuildResponse { return &BuildResponse{ Status: status, Message: message, } } // WithError adds an error message to the response. func (r *BuildResponse) WithError(err string) *BuildResponse { r.Error = err return r } // Marshal serializes the response to JSON. func (r *BuildResponse) Marshal() ([]byte, error) { return json.Marshal(r) } // UnmarshalBuildResponse deserializes a response from JSON. func UnmarshalBuildResponse(data []byte) (*BuildResponse, error) { var r BuildResponse if err := json.Unmarshal(data, &r); err != nil { return nil, fmt.Errorf("failed to unmarshal build response: %w", err) } return &r, nil }