package httpserver import ( "encoding/json" "fmt" "net/http" "strings" "time" "github.com/toki/oto/services/core/internal/cicdstate" "github.com/toki/oto/services/core/internal/runnerregistry" otopb "github.com/toki/oto/services/core/oto" ) func jobToJSON(j *cicdstate.Job) map[string]interface{} { out := map[string]interface{}{ "id": j.ID, "name": j.Name, "state": j.State, "created_at": j.CreatedAt.Format(time.RFC3339), "updated_at": j.UpdatedAt.Format(time.RFC3339), "execution_id": j.ExecutionID, } if j.RunInput != nil { out["run_request"] = runInputToJSON(j.RunInput) } return out } // validateRunInput enforces the remote dispatch contract: exactly one of // pipeline_yaml or pipeline_yaml_path must be provided. func validateRunInput(in *cicdstate.RunInput) error { hasYAML := strings.TrimSpace(in.PipelineYAML) != "" hasPath := strings.TrimSpace(in.PipelineYAMLPath) != "" if hasYAML == hasPath { return fmt.Errorf("run request must set exactly one of pipeline_yaml or pipeline_yaml_path") } return nil } // runRequestFromJSON converts an incoming otopb.RunRequest decoded from the job // create body into a store-level RunInput. It returns nil when no run request // was supplied, keeping job creation backward compatible. func runRequestFromJSON(rr *otopb.RunRequest) *cicdstate.RunInput { if rr == nil { return nil } in := &cicdstate.RunInput{ PipelineYAMLPath: rr.GetPipelineYamlPath(), PipelineYAML: rr.GetPipelineYaml(), CommandTypes: append([]string(nil), rr.GetCommandTypes()...), } if len(rr.GetVariables()) > 0 { in.Variables = make(map[string]string, len(rr.GetVariables())) for k, v := range rr.GetVariables() { in.Variables[k] = v } } return in } // runRequestToProto builds the runner-facing RunRequest from the stored input, // filling in the runner/job/execution identifiers resolved at dispatch time. func runRequestToProto(in *cicdstate.RunInput, runnerID, jobID, execID string) *otopb.RunRequest { if in == nil { return nil } rr := &otopb.RunRequest{ RunnerId: runnerID, JobId: jobID, ExecutionId: execID, PipelineYamlPath: in.PipelineYAMLPath, PipelineYaml: in.PipelineYAML, CommandTypes: append([]string(nil), in.CommandTypes...), } if len(in.Variables) > 0 { rr.Variables = make(map[string]string, len(in.Variables)) for k, v := range in.Variables { rr.Variables[k] = v } } return rr } func runInputToJSON(in *cicdstate.RunInput) map[string]interface{} { return map[string]interface{}{ "pipeline_yaml_path": in.PipelineYAMLPath, "pipeline_yaml": in.PipelineYAML, "variables": in.Variables, "command_types": in.CommandTypes, } } func execToJSON(e *cicdstate.Execution) map[string]interface{} { return map[string]interface{}{ "id": e.ID, "job_id": e.JobID, "state": e.State, "created_at": e.CreatedAt.Format(time.RFC3339), "updated_at": e.UpdatedAt.Format(time.RFC3339), "execution_id": e.ID, } } func logsToJSON(logs []cicdstate.LogEntry) map[string]interface{} { return map[string]interface{}{ "logs": logs, } } func artifactsToJSON(artifacts []cicdstate.ArtifactEntry) map[string]interface{} { return map[string]interface{}{ "artifacts": artifacts, } } func errorToJSON(message string) map[string]interface{} { return map[string]interface{}{ "error": message, } } func successToJSON(success bool) map[string]interface{} { return map[string]interface{}{ "success": success, } } func runnerDisconnectToJSON(success bool, err string) map[string]interface{} { out := successToJSON(success) if err != "" { out["error"] = err } return out } func runnerRecordToJSON(record runnerregistry.RunnerRecord) map[string]interface{} { return map[string]interface{}{ "runner_id": record.RunnerID, "alias": record.Alias, "protocol_version": record.ProtocolVersion, "status": record.Status, "accepted_at": record.AcceptedAt.Format(time.RFC3339), "first_heartbeat_at": record.FirstHeartbeatAt.Format(time.RFC3339), "last_heartbeat_at": record.LastHeartbeatAt.Format(time.RFC3339), "failure_reason": record.FailureReason, } } func runnerStatusToJSON(record runnerregistry.RunnerRecord, runnerID string, lastExec *cicdstate.Execution) map[string]interface{} { currentJobID := "" currentExecID := "" if lastExec != nil { currentJobID = lastExec.JobID currentExecID = lastExec.ID } return map[string]interface{}{ "accepted": true, "runner_id": runnerID, "status": record.Status, "current_job_id": currentJobID, "current_execution_id": currentExecID, "message": record.FailureReason, } } func runnerStatusRejectedToJSON(message string) map[string]interface{} { return map[string]interface{}{ "accepted": false, "message": message, } } func selfUpdateToJSON(success bool, accepted bool, deferred bool, targetVersion string, message string) map[string]interface{} { out := map[string]interface{}{ "success": success, "accepted": accepted, "deferred": deferred, "message": message, } if !success && !deferred { out["error_message"] = message } if targetVersion != "" { out["target_version"] = targetVersion } return out } func writeResponse(w http.ResponseWriter, status int, data interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(data) }