This repository has been archived on 2026-03-09. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
homelab-deploy/internal/messages/messages.go
Torjus Håkestad 36a74b8cf9 feat: add heartbeat status updates during deployment
Send periodic "running" status messages while nixos-rebuild executes,
preventing the idle timeout from triggering before deployments complete.
This fixes false "Some deployments failed" warnings in MCP when builds
take longer than 30 seconds.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-07 14:23:33 +01:00

192 lines
5.2 KiB
Go

// Package messages defines the message types used for NATS communication
// between deployment clients and listeners.
package messages
import (
"encoding/json"
"fmt"
"regexp"
)
// Action represents a nixos-rebuild action.
type Action string
const (
ActionSwitch Action = "switch"
ActionBoot Action = "boot"
ActionTest Action = "test"
ActionDryActivate Action = "dry-activate"
)
// Valid returns true if the action is a recognized nixos-rebuild action.
func (a Action) Valid() bool {
switch a {
case ActionSwitch, ActionBoot, ActionTest, ActionDryActivate:
return true
default:
return false
}
}
// Status represents the status of a deployment response.
type Status string
const (
StatusAccepted Status = "accepted"
StatusRejected Status = "rejected"
StatusStarted Status = "started"
StatusRunning Status = "running"
StatusCompleted Status = "completed"
StatusFailed Status = "failed"
)
// IsFinal returns true if this status indicates a terminal state.
func (s Status) IsFinal() bool {
switch s {
case StatusCompleted, StatusFailed, StatusRejected:
return true
default:
return false
}
}
// ErrorCode represents an error condition.
type ErrorCode string
const (
ErrorInvalidRevision ErrorCode = "invalid_revision"
ErrorInvalidAction ErrorCode = "invalid_action"
ErrorAlreadyRunning ErrorCode = "already_running"
ErrorBuildFailed ErrorCode = "build_failed"
ErrorTimeout ErrorCode = "timeout"
)
// DeployRequest is the message sent to request a deployment.
type DeployRequest struct {
Action Action `json:"action"`
Revision string `json:"revision"`
ReplyTo string `json:"reply_to"`
}
// revisionRegex validates git branch names and commit hashes.
// Allows: alphanumeric, dashes, underscores, dots, slashes (for branch names),
// and hex strings (for commit hashes).
var revisionRegex = regexp.MustCompile(`^[a-zA-Z0-9._/-]+$`)
// Validate checks that the request is valid.
func (r *DeployRequest) Validate() error {
if !r.Action.Valid() {
return fmt.Errorf("invalid action: %q", r.Action)
}
if r.Revision == "" {
return fmt.Errorf("revision is required")
}
if !revisionRegex.MatchString(r.Revision) {
return fmt.Errorf("invalid revision format: %q", r.Revision)
}
if r.ReplyTo == "" {
return fmt.Errorf("reply_to is required")
}
return nil
}
// Marshal serializes the request to JSON.
func (r *DeployRequest) Marshal() ([]byte, error) {
return json.Marshal(r)
}
// UnmarshalDeployRequest deserializes a request from JSON.
func UnmarshalDeployRequest(data []byte) (*DeployRequest, error) {
var r DeployRequest
if err := json.Unmarshal(data, &r); err != nil {
return nil, fmt.Errorf("failed to unmarshal deploy request: %w", err)
}
return &r, nil
}
// DeployResponse is the message sent in response to a deployment request.
type DeployResponse struct {
Hostname string `json:"hostname"`
Status Status `json:"status"`
Error *ErrorCode `json:"error"`
Message string `json:"message"`
}
// NewDeployResponse creates a new response with the given hostname and status.
func NewDeployResponse(hostname string, status Status, message string) *DeployResponse {
return &DeployResponse{
Hostname: hostname,
Status: status,
Message: message,
}
}
// WithError adds an error code to the response.
func (r *DeployResponse) WithError(code ErrorCode) *DeployResponse {
r.Error = &code
return r
}
// Marshal serializes the response to JSON.
func (r *DeployResponse) Marshal() ([]byte, error) {
return json.Marshal(r)
}
// UnmarshalDeployResponse deserializes a response from JSON.
func UnmarshalDeployResponse(data []byte) (*DeployResponse, error) {
var r DeployResponse
if err := json.Unmarshal(data, &r); err != nil {
return nil, fmt.Errorf("failed to unmarshal deploy response: %w", err)
}
return &r, nil
}
// DiscoveryRequest is the message sent to discover available hosts.
type DiscoveryRequest struct {
ReplyTo string `json:"reply_to"`
}
// Validate checks that the request is valid.
func (r *DiscoveryRequest) Validate() error {
if r.ReplyTo == "" {
return fmt.Errorf("reply_to is required")
}
return nil
}
// Marshal serializes the request to JSON.
func (r *DiscoveryRequest) Marshal() ([]byte, error) {
return json.Marshal(r)
}
// UnmarshalDiscoveryRequest deserializes a request from JSON.
func UnmarshalDiscoveryRequest(data []byte) (*DiscoveryRequest, error) {
var r DiscoveryRequest
if err := json.Unmarshal(data, &r); err != nil {
return nil, fmt.Errorf("failed to unmarshal discovery request: %w", err)
}
return &r, nil
}
// DiscoveryResponse is the message sent in response to a discovery request.
type DiscoveryResponse struct {
Hostname string `json:"hostname"`
Tier string `json:"tier"`
Role string `json:"role,omitempty"`
DeploySubjects []string `json:"deploy_subjects"`
}
// Marshal serializes the response to JSON.
func (r *DiscoveryResponse) Marshal() ([]byte, error) {
return json.Marshal(r)
}
// UnmarshalDiscoveryResponse deserializes a response from JSON.
func UnmarshalDiscoveryResponse(data []byte) (*DiscoveryResponse, error) {
var r DiscoveryResponse
if err := json.Unmarshal(data, &r); err != nil {
return nil, fmt.Errorf("failed to unmarshal discovery response: %w", err)
}
return &r, nil
}