When a build timed out, the timeout error was silently replaced by truncated stderr output. Split into separate Error and Output fields on BuildHostResult so the cause (e.g. "build timed out after 30m0s") is always visible in logs and CLI output. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
136 lines
4.1 KiB
Go
136 lines
4.1 KiB
Go
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"`
|
|
Output string `json:"output,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
|
|
}
|