rdev/internal/domain/operation.go
jordan 9a1309a0c5 feat: fix composable monorepo CI builds + health endpoint improvements
Composable monorepo CI fixes:
- Add empty go.sum.tmpl files for pkg, service, worker, and cli components
- Fix Dockerfile.tmpl glob patterns (COPY go.work.sum* is invalid in Kaniko)
- Add deps step to CI that runs go work sync and go mod tidy before builds
- Fix scalar-go dependency version (v0.1.2 doesn't exist, use v0.13.0)

Health endpoint improvements:
- Add registry health check (zot OCI /v2/ endpoint)
- Add health metrics for CI, registry, and Git
- Add /health/ci endpoint for Woodpecker health

Visual verification scaffolding:
- Add Playwright pod and scripts ConfigMap
- Add vision.md and implementation breakdown plan

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 18:46:51 -07:00

215 lines
5.8 KiB
Go

package domain
import (
"strings"
"time"
)
// OperationType represents the type of operation.
type OperationType string
const (
OperationTypeProjectCreate OperationType = "project.create"
OperationTypeComponentAdd OperationType = "component.add"
OperationTypeBuild OperationType = "build"
OperationTypeCIBuild OperationType = "ci.build"
OperationTypeResourceProvision OperationType = "resource.provision"
)
// IsValid returns true if the operation type is known.
func (t OperationType) IsValid() bool {
switch t {
case OperationTypeProjectCreate, OperationTypeComponentAdd,
OperationTypeBuild, OperationTypeCIBuild, OperationTypeResourceProvision:
return true
}
return false
}
// OperationStatus represents the status of an operation.
type OperationStatus string
const (
OperationStatusPending OperationStatus = "pending"
OperationStatusRunning OperationStatus = "running"
OperationStatusCompleted OperationStatus = "completed"
OperationStatusFailed OperationStatus = "failed"
)
// IsValid returns true if the status is known.
func (s OperationStatus) IsValid() bool {
switch s {
case OperationStatusPending, OperationStatusRunning,
OperationStatusCompleted, OperationStatusFailed:
return true
}
return false
}
// IsTerminal returns true if the status is a final state.
func (s OperationStatus) IsTerminal() bool {
return s == OperationStatusCompleted || s == OperationStatusFailed
}
// OperationStep represents a single step within an operation.
type OperationStep struct {
// Name is the step identifier (e.g., "git", "build-api", "deploy-web").
Name string `json:"name"`
// Status is the step status.
Status OperationStatus `json:"status"`
// StartedAt is when the step started.
StartedAt time.Time `json:"started_at"`
// DurationMs is the step duration in milliseconds.
DurationMs int64 `json:"duration_ms,omitempty"`
// Output contains step-specific output data.
Output map[string]any `json:"output,omitempty"`
// Error is a one-line error summary.
Error string `json:"error,omitempty"`
// ErrorDetail is the full error detail.
ErrorDetail string `json:"error_detail,omitempty"`
}
// Operation represents a tracked project operation.
type Operation struct {
// ID is the unique operation identifier.
ID string `json:"id"`
// ProjectID is the project this operation belongs to.
ProjectID string `json:"project_id"`
// Type is the operation type.
Type OperationType `json:"type"`
// Status is the current operation status.
Status OperationStatus `json:"status"`
// RequestID is the HTTP request that initiated this operation.
RequestID string `json:"request_id,omitempty"`
// TriggeredBy is the ID of the parent operation that triggered this one.
TriggeredBy string `json:"triggered_by,omitempty"`
// CommitSHA is the git commit this operation created or was triggered by.
CommitSHA string `json:"commit_sha,omitempty"`
// ExternalRef is an external reference (e.g., "build#42").
ExternalRef string `json:"external_ref,omitempty"`
// StartedAt is when the operation started.
StartedAt time.Time `json:"started_at"`
// CompletedAt is when the operation finished.
CompletedAt *time.Time `json:"completed_at,omitempty"`
// DurationMs is the total operation duration in milliseconds.
DurationMs int64 `json:"duration_ms,omitempty"`
// Input contains the operation input parameters.
Input map[string]any `json:"input,omitempty"`
// Output contains the operation output/result.
Output map[string]any `json:"output,omitempty"`
// Error is a one-line error summary.
Error string `json:"error,omitempty"`
// ErrorDetail is the full error detail (truncated to 10KB).
ErrorDetail string `json:"error_detail,omitempty"`
// Steps contains the operation steps.
Steps []OperationStep `json:"steps,omitempty"`
// CreatedAt is when the record was created.
CreatedAt time.Time `json:"created_at"`
}
// StepsSummary returns a human-readable summary of step statuses.
// Example: "git ✓ → build-web ✓ → build-api ✗"
func (o *Operation) StepsSummary() string {
if len(o.Steps) == 0 {
return ""
}
var parts []string
for _, step := range o.Steps {
symbol := "?"
switch step.Status {
case OperationStatusCompleted:
symbol = "✓"
case OperationStatusFailed:
symbol = "✗"
case OperationStatusRunning:
symbol = "…"
case OperationStatusPending:
symbol = "○"
}
parts = append(parts, step.Name+" "+symbol)
}
return strings.Join(parts, " → ")
}
// FailedStep returns the first failed step, or nil if none failed.
func (o *Operation) FailedStep() *OperationStep {
for i := range o.Steps {
if o.Steps[i].Status == OperationStatusFailed {
return &o.Steps[i]
}
}
return nil
}
// OperationFilters specifies criteria for listing operations.
type OperationFilters struct {
// ProjectID filters by project (required for List).
ProjectID string
// Type filters by operation type.
Type OperationType
// Status filters by operation status.
Status OperationStatus
// CommitSHA filters by commit SHA.
CommitSHA string
// Since filters operations started after this time.
Since time.Time
// Limit is the maximum number of operations to return.
Limit int
}
// DefaultOperationFilters returns filters with default values.
func DefaultOperationFilters() OperationFilters {
return OperationFilters{
Limit: 50,
}
}
// Normalize applies defaults and limits to the filters.
func (f *OperationFilters) Normalize() {
if f.Limit <= 0 {
f.Limit = 50
}
if f.Limit > 200 {
f.Limit = 200
}
}
// MaxErrorDetailSize is the maximum size of error_detail (10KB).
const MaxErrorDetailSize = 10 * 1024
// TruncateErrorDetail truncates error detail to the maximum allowed size.
func TruncateErrorDetail(detail string) string {
if len(detail) <= MaxErrorDetailSize {
return detail
}
return detail[:MaxErrorDetailSize-3] + "..."
}