Operations Audit (new feature): - Add Operation domain model with status tracking (pending, running, completed, failed, cancelled) - Add OperationRepository with PostgreSQL implementation - Add OperationService for CRUD and lifecycle management - Add operations handlers (list, get, cancel endpoints) - Add migration 015_operations.sql for operations table - Add operation cleanup worker for stale operation handling - Add ErrOperationNotFound to domain errors Template Improvements: - Add CLAUDE.md configuration files to astro-landing, default, and go-api templates - Fix PORT template variable usage in nginx configs for app templates - Add replace directives for local pkg module in Go templates - Simplify Go service/worker Dockerfiles for workspace builds - Fix TypeScript error in logger template Other: - Refactor landing-test.sh cookbook script - Update CLAUDE.md version reference Note: Some files exceed 500-line limit (pre-existing debt + new feature) - component.go: 550 lines (unchanged, pre-existing) - main.go: 522 lines (added operations wiring) - operation_repo.go: 569 lines (new, needs splitting) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
214 lines
5.7 KiB
Go
214 lines
5.7 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"
|
|
OperationTypeResourceProvision OperationType = "resource.provision"
|
|
)
|
|
|
|
// IsValid returns true if the operation type is known.
|
|
func (t OperationType) IsValid() bool {
|
|
switch t {
|
|
case OperationTypeProjectCreate, OperationTypeComponentAdd,
|
|
OperationTypeBuild, 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] + "..."
|
|
}
|