package handlers import ( "context" "fmt" "net/http" "strconv" "time" "github.com/go-chi/chi/v5" "github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/pkg/api" ) // PipelineErrorResponse is the JSON representation of a pipeline error. type PipelineErrorResponse struct { Type string `json:"type"` Message string `json:"message"` IsWarning bool `json:"is_warning"` } // PipelineResponse is the JSON representation of a CI pipeline. type PipelineResponse struct { ID int64 `json:"id"` Number int64 `json:"number"` Status string `json:"status"` Event string `json:"event"` Branch string `json:"branch"` Commit string `json:"commit"` Message string `json:"message"` Author string `json:"author"` Started string `json:"started,omitempty"` Finished string `json:"finished,omitempty"` Errors []PipelineErrorResponse `json:"errors,omitempty"` } // ListPipelines returns recent CI pipeline executions for a project. // GET /projects/{id}/pipelines func (h *InfrastructureHandler) ListPipelines(w http.ResponseWriter, r *http.Request) { projectID := chi.URLParam(r, "id") ctx, cancel := context.WithTimeout(r.Context(), TimeoutLookup) defer cancel() if err := validateProjectID(projectID); err != nil { api.WriteBadRequest(w, r, err.Error()) return } if h.ciProvider == nil { api.WriteInternalError(w, r, "CI provider not configured") return } pipelines, err := h.ciProvider.ListPipelines(ctx, h.defaultGitOwner, projectID) if err != nil { api.WriteNotFound(w, r, fmt.Sprintf("pipelines not found: %v", err)) return } resp := make([]PipelineResponse, len(pipelines)) for i, p := range pipelines { resp[i] = PipelineResponse{ ID: p.ID, Number: p.Number, Status: p.Status, Event: p.Event, Branch: p.Branch, Commit: p.Commit, Message: p.Message, Author: p.Author, Started: formatTime(p.Started), Finished: formatTime(p.Finished), Errors: mapPipelineErrors(p.Errors), } } api.WriteSuccess(w, r, resp) } // GetPipeline returns a specific CI pipeline execution for a project. // GET /projects/{id}/pipelines/{number} func (h *InfrastructureHandler) GetPipeline(w http.ResponseWriter, r *http.Request) { projectID := chi.URLParam(r, "id") numberStr := chi.URLParam(r, "number") ctx, cancel := context.WithTimeout(r.Context(), TimeoutLookup) defer cancel() if err := validateProjectID(projectID); err != nil { api.WriteBadRequest(w, r, err.Error()) return } number, err := strconv.ParseInt(numberStr, 10, 64) if err != nil { api.WriteBadRequest(w, r, "invalid pipeline number") return } if h.ciProvider == nil { api.WriteInternalError(w, r, "CI provider not configured") return } p, err := h.ciProvider.GetPipeline(ctx, h.defaultGitOwner, projectID, number) if err != nil { api.WriteNotFound(w, r, fmt.Sprintf("pipeline not found: %v", err)) return } api.WriteSuccess(w, r, PipelineResponse{ ID: p.ID, Number: p.Number, Status: p.Status, Event: p.Event, Branch: p.Branch, Commit: p.Commit, Message: p.Message, Author: p.Author, Started: formatTime(p.Started), Finished: formatTime(p.Finished), Errors: mapPipelineErrors(p.Errors), }) } // formatTime formats a time.Time as RFC3339, returning empty string for zero time. func formatTime(t time.Time) string { if t.IsZero() { return "" } return t.Format(time.RFC3339) } // mapPipelineErrors converts domain pipeline errors to response format. func mapPipelineErrors(errors []domain.CIPipelineError) []PipelineErrorResponse { if len(errors) == 0 { return nil } resp := make([]PipelineErrorResponse, len(errors)) for i, e := range errors { resp[i] = PipelineErrorResponse{ Type: e.Type, Message: e.Message, IsWarning: e.IsWarning, } } return resp } // PipelineStepResponse is the JSON representation of a pipeline step. type PipelineStepResponse struct { ID int64 `json:"id"` Name string `json:"name"` Status string `json:"status"` ExitCode *int `json:"exit_code,omitempty"` Duration int `json:"duration_seconds"` Error string `json:"error,omitempty"` Log string `json:"log,omitempty"` } // PipelineStepsResponse is the JSON representation of pipeline steps. type PipelineStepsResponse struct { PipelineNumber int64 `json:"pipeline_number"` URL string `json:"url"` Steps []PipelineStepResponse `json:"steps"` } // GetPipelineSteps returns detailed step information for a pipeline. // GET /projects/{id}/pipelines/{number}/steps func (h *InfrastructureHandler) GetPipelineSteps(w http.ResponseWriter, r *http.Request) { projectID := chi.URLParam(r, "id") numberStr := chi.URLParam(r, "number") ctx, cancel := context.WithTimeout(r.Context(), TimeoutLookup) defer cancel() if err := validateProjectID(projectID); err != nil { api.WriteBadRequest(w, r, err.Error()) return } number, err := strconv.ParseInt(numberStr, 10, 64) if err != nil { api.WriteBadRequest(w, r, "invalid pipeline number") return } if h.ciProvider == nil { api.WriteInternalError(w, r, "CI provider not configured") return } steps, err := h.ciProvider.GetPipelineSteps(ctx, h.defaultGitOwner, projectID, number) if err != nil { api.WriteNotFound(w, r, fmt.Sprintf("pipeline steps not found: %v", err)) return } // Map to response respSteps := make([]PipelineStepResponse, len(steps.Steps)) for i, s := range steps.Steps { respSteps[i] = PipelineStepResponse{ ID: s.ID, Name: s.Name, Status: s.Status, ExitCode: s.ExitCode, Duration: s.Duration, Error: s.Error, Log: s.Log, } } api.WriteSuccess(w, r, PipelineStepsResponse{ PipelineNumber: steps.PipelineNumber, URL: steps.URL, Steps: respSteps, }) }