package cicdstate import ( "fmt" "sync" "time" ) const ( StateQueued = "queued" StateRunning = "running" StateSucceeded = "succeeded" StateFailed = "failed" StateCanceled = "canceled" ) var validTransitions = map[string][]string{ StateQueued: {StateRunning, StateFailed, StateCanceled}, StateRunning: {StateSucceeded, StateFailed}, StateSucceeded: nil, StateFailed: nil, StateCanceled: nil, } type Job struct { ID string Name string State string CreatedAt time.Time UpdatedAt time.Time ExecutionID string } func (j *Job) TransitionTo(newState string) error { allowed, ok := validTransitions[j.State] if !ok { return fmt.Errorf("unknown state: %s", j.State) } for _, s := range allowed { if s == newState { j.State = newState j.UpdatedAt = time.Now() return nil } } return fmt.Errorf("invalid transition from %s to %s", j.State, newState) } type Execution struct { ID string JobID string State string CreatedAt time.Time UpdatedAt time.Time Logs []LogEntry Artifacts []ArtifactEntry } func (e *Execution) TransitionTo(newState string) error { allowed, ok := validTransitions[e.State] if !ok { return fmt.Errorf("unknown state: %s", e.State) } for _, s := range allowed { if s == newState { e.State = newState e.UpdatedAt = time.Now() return nil } } return fmt.Errorf("invalid transition from %s to %s", e.State, newState) } type LogEntry struct { Timestamp time.Time Line string } type ArtifactEntry struct { Name string Path string } type Store struct { mu sync.RWMutex jobs map[string]*Job executions map[string]*Execution now func() time.Time } func NewStore() *Store { return &Store{ now: time.Now, jobs: make(map[string]*Job), executions: make(map[string]*Execution), } } func (s *Store) CreateJob(id, name string) (*Job, error) { s.mu.Lock() defer s.mu.Unlock() if _, exists := s.jobs[id]; exists { return nil, fmt.Errorf("job %s already exists", id) } now := s.now() job := &Job{ ID: id, Name: name, State: StateQueued, CreatedAt: now, UpdatedAt: now, } s.jobs[id] = job return job, nil } func (s *Store) GetJob(id string) (*Job, error) { s.mu.RLock() defer s.mu.RUnlock() job, ok := s.jobs[id] if !ok { return nil, fmt.Errorf("job not found: %s", id) } return copyJob(job), nil } func (s *Store) CreateExecution(jobID, execID string) (*Execution, error) { s.mu.Lock() defer s.mu.Unlock() job, ok := s.jobs[jobID] if !ok { return nil, fmt.Errorf("job not found: %s", jobID) } if job.ExecutionID != "" { return nil, fmt.Errorf("job %s already has execution %s", jobID, job.ExecutionID) } now := s.now() exec := &Execution{ ID: execID, JobID: jobID, State: StateQueued, CreatedAt: now, UpdatedAt: now, Logs: make([]LogEntry, 0), Artifacts: make([]ArtifactEntry, 0), } s.executions[execID] = exec job.ExecutionID = execID job.UpdatedAt = now return exec, nil } func (s *Store) GetExecution(id string) (*Execution, error) { s.mu.RLock() defer s.mu.RUnlock() exec, ok := s.executions[id] if !ok { return nil, fmt.Errorf("execution not found: %s", id) } return copyExecution(exec), nil } func (s *Store) AppendLog(execID string, line string) error { s.mu.Lock() defer s.mu.Unlock() exec, ok := s.executions[execID] if !ok { return fmt.Errorf("execution not found: %s", execID) } exec.Logs = append(exec.Logs, LogEntry{ Timestamp: s.now(), Line: line, }) exec.UpdatedAt = s.now() return nil } func (s *Store) GetLogs(execID string) ([]LogEntry, error) { s.mu.RLock() defer s.mu.RUnlock() exec, ok := s.executions[execID] if !ok { return nil, fmt.Errorf("execution not found: %s", execID) } result := make([]LogEntry, len(exec.Logs)) copy(result, exec.Logs) return result, nil } func (s *Store) AppendArtifact(execID string, name string, path string) error { s.mu.Lock() defer s.mu.Unlock() exec, ok := s.executions[execID] if !ok { return fmt.Errorf("execution not found: %s", execID) } exec.Artifacts = append(exec.Artifacts, ArtifactEntry{ Name: name, Path: path, }) exec.UpdatedAt = s.now() return nil } func (s *Store) GetArtifacts(execID string) ([]ArtifactEntry, error) { s.mu.RLock() defer s.mu.RUnlock() exec, ok := s.executions[execID] if !ok { return nil, fmt.Errorf("execution not found: %s", execID) } result := make([]ArtifactEntry, len(exec.Artifacts)) copy(result, exec.Artifacts) return result, nil } func (s *Store) TransitionJob(id, newState string) error { s.mu.Lock() defer s.mu.Unlock() job, ok := s.jobs[id] if !ok { return fmt.Errorf("job not found: %s", id) } allowed, ok := validTransitions[job.State] if !ok { return fmt.Errorf("unknown state: %s", job.State) } for _, target := range allowed { if target == newState { job.State = newState job.UpdatedAt = s.now() return nil } } return fmt.Errorf("invalid transition from %s to %s", job.State, newState) } func (s *Store) TransitionExecution(id, newState string) error { s.mu.Lock() defer s.mu.Unlock() exec, ok := s.executions[id] if !ok { return fmt.Errorf("execution not found: %s", id) } allowed, ok := validTransitions[exec.State] if !ok { return fmt.Errorf("unknown state: %s", exec.State) } for _, target := range allowed { if target == newState { exec.State = newState exec.UpdatedAt = s.now() return nil } } return fmt.Errorf("invalid transition from %s to %s", exec.State, newState) } func (s *Store) SnapshotJobs() []Job { s.mu.RLock() defer s.mu.RUnlock() result := make([]Job, 0, len(s.jobs)) for _, j := range s.jobs { result = append(result, *copyJob(j)) } return result } func (s *Store) SnapshotExecutions() []Execution { s.mu.RLock() defer s.mu.RUnlock() result := make([]Execution, 0, len(s.executions)) for _, e := range s.executions { result = append(result, *copyExecution(e)) } return result } func copyJob(j *Job) *Job { if j == nil { return nil } cp := *j return &cp } func copyExecution(e *Execution) *Execution { if e == nil { return nil } cp := *e cp.Logs = make([]LogEntry, len(e.Logs)) copy(cp.Logs, e.Logs) cp.Artifacts = make([]ArtifactEntry, len(e.Artifacts)) copy(cp.Artifacts, e.Artifacts) return &cp }