166 lines
5.6 KiB
Go
166 lines
5.6 KiB
Go
package manifest
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type RunManifestStatus string
|
|
|
|
const (
|
|
RunManifestStatusRunning RunManifestStatus = "running"
|
|
RunManifestStatusSucceeded RunManifestStatus = "succeeded"
|
|
RunManifestStatusFailed RunManifestStatus = "failed"
|
|
)
|
|
|
|
type RunStageAction string
|
|
|
|
const (
|
|
RunStageActionRun RunStageAction = "run"
|
|
RunStageActionSkip RunStageAction = "skip"
|
|
)
|
|
|
|
// RunStageRecord tracks lifecycle and provenance for one stage within a single invocation.
|
|
type RunStageRecord struct {
|
|
Name string `json:"name"`
|
|
Action RunStageAction `json:"action"`
|
|
Status StageStatus `json:"status"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
StartedAt *time.Time `json:"started_at,omitempty"`
|
|
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
|
Outputs []ArtifactRecord `json:"outputs,omitempty"`
|
|
Logs []string `json:"logs,omitempty"`
|
|
GeneratedConfigs []string `json:"generated_configs,omitempty"`
|
|
Error *ErrorRecord `json:"error,omitempty"`
|
|
Metadata map[string]any `json:"metadata,omitempty"`
|
|
}
|
|
|
|
// RunManifest is the invocation-scoped execution record under runs/{run_id}/manifest.json.
|
|
type RunManifest struct {
|
|
SessionID string `json:"session_id"`
|
|
Campaign string `json:"campaign,omitempty"`
|
|
RunID string `json:"run_id"`
|
|
Force bool `json:"force"`
|
|
RequestedStages []string `json:"requested_stages,omitempty"`
|
|
SessionManifestPath string `json:"session_manifest_path,omitempty"`
|
|
LocalWorkDir string `json:"local_workdir,omitempty"`
|
|
LocalSpoolDir string `json:"local_spool_dir,omitempty"`
|
|
S3Bucket string `json:"s3_bucket,omitempty"`
|
|
S3SessionPrefix string `json:"s3_session_prefix,omitempty"`
|
|
S3RunPrefix string `json:"s3_run_prefix,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
StartedAt *time.Time `json:"started_at,omitempty"`
|
|
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
|
Status RunManifestStatus `json:"status"`
|
|
LastError *ErrorRecord `json:"last_error,omitempty"`
|
|
Stages map[string]*RunStageRecord `json:"stages"`
|
|
Metadata map[string]any `json:"metadata,omitempty"`
|
|
}
|
|
|
|
// NewRun constructs a new run manifest with deterministic timestamps.
|
|
func NewRun(sessionID, campaign, runID string, force bool, requestedStages []string, now time.Time) *RunManifest {
|
|
return &RunManifest{
|
|
SessionID: strings.TrimSpace(sessionID),
|
|
Campaign: strings.TrimSpace(campaign),
|
|
RunID: strings.TrimSpace(runID),
|
|
Force: force,
|
|
RequestedStages: append([]string(nil), requestedStages...),
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
StartedAt: timePtr(now),
|
|
Status: RunManifestStatusRunning,
|
|
Stages: map[string]*RunStageRecord{},
|
|
}
|
|
}
|
|
|
|
func (m *RunManifest) SetStageAction(name string, action RunStageAction, at time.Time) {
|
|
s := m.ensureStage(name, at)
|
|
s.Action = action
|
|
s.UpdatedAt = at
|
|
m.UpdatedAt = at
|
|
}
|
|
|
|
func (m *RunManifest) MarkStageRunning(name string, at time.Time) {
|
|
s := m.ensureStage(name, at)
|
|
s.Status = StatusRunning
|
|
s.StartedAt = timePtr(at)
|
|
s.CompletedAt = nil
|
|
s.Error = nil
|
|
s.UpdatedAt = at
|
|
m.UpdatedAt = at
|
|
}
|
|
|
|
func (m *RunManifest) MarkStageSucceeded(name string, at time.Time, outputs []ArtifactRecord) {
|
|
s := m.ensureStage(name, at)
|
|
s.Status = StatusSucceeded
|
|
s.CompletedAt = timePtr(at)
|
|
s.Error = nil
|
|
s.Outputs = append([]ArtifactRecord(nil), outputs...)
|
|
s.UpdatedAt = at
|
|
m.UpdatedAt = at
|
|
}
|
|
|
|
func (m *RunManifest) MarkStageFailed(name string, at time.Time, message string) {
|
|
s := m.ensureStage(name, at)
|
|
s.Status = StatusFailed
|
|
s.CompletedAt = timePtr(at)
|
|
s.Error = &ErrorRecord{Message: strings.TrimSpace(message), At: timePtr(at)}
|
|
s.UpdatedAt = at
|
|
m.LastError = &ErrorRecord{Message: strings.TrimSpace(message), At: timePtr(at)}
|
|
m.UpdatedAt = at
|
|
m.Status = RunManifestStatusFailed
|
|
m.CompletedAt = timePtr(at)
|
|
}
|
|
|
|
func (m *RunManifest) MarkStageSkipped(name string, at time.Time, reason string) {
|
|
s := m.ensureStage(name, at)
|
|
s.Status = StatusSkipped
|
|
s.CompletedAt = timePtr(at)
|
|
s.Error = &ErrorRecord{Message: strings.TrimSpace(reason), Code: "skipped", At: timePtr(at)}
|
|
s.Outputs = nil
|
|
s.UpdatedAt = at
|
|
m.UpdatedAt = at
|
|
}
|
|
|
|
func (m *RunManifest) MarkSucceeded(at time.Time) {
|
|
m.Status = RunManifestStatusSucceeded
|
|
m.CompletedAt = timePtr(at)
|
|
m.UpdatedAt = at
|
|
}
|
|
|
|
func (m *RunManifest) MarkFailed(at time.Time, message string) {
|
|
m.Status = RunManifestStatusFailed
|
|
m.CompletedAt = timePtr(at)
|
|
m.LastError = &ErrorRecord{Message: strings.TrimSpace(message), At: timePtr(at)}
|
|
m.UpdatedAt = at
|
|
}
|
|
|
|
func (m *RunManifest) ensureStage(name string, at time.Time) *RunStageRecord {
|
|
if m.Stages == nil {
|
|
m.Stages = map[string]*RunStageRecord{}
|
|
}
|
|
|
|
stageName := strings.TrimSpace(name)
|
|
s, ok := m.Stages[stageName]
|
|
if !ok || s == nil {
|
|
s = &RunStageRecord{
|
|
Name: stageName,
|
|
Action: RunStageActionRun,
|
|
Status: StatusPending,
|
|
CreatedAt: at,
|
|
UpdatedAt: at,
|
|
}
|
|
m.Stages[stageName] = s
|
|
}
|
|
if s.Name == "" {
|
|
s.Name = stageName
|
|
}
|
|
if s.CreatedAt.IsZero() {
|
|
s.CreatedAt = at
|
|
}
|
|
|
|
return s
|
|
}
|