rdev/internal/handlers/infrastructure_pipelines.go
jordan bc47e426b0 feat: Add CI pipeline proxy, DNS alias management, and worker executor system
- Add ListPipelines/GetPipeline to CIProvider port with Woodpecker adapter
- Add DNS alias endpoints: GET/POST/DELETE /projects/{id}/domains
- Implement worker executor daemon, build executor, and git operations
- Add build service, worker service, and build audit tracking
- Add worker registry with PostgreSQL adapter and migration
- Add multi-provider code agent interface (Claude Code + OpenCode)
- Add create-and-build combo endpoint
- Update landing-page cookbook to reflect all gaps closed
- Fix tech debt: unified validation, auth scopes, error wrapping, slog patterns

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 21:05:28 -07:00

121 lines
3.0 KiB
Go

package handlers
import (
"context"
"fmt"
"net/http"
"strconv"
"time"
"github.com/go-chi/chi/v5"
"github.com/orchard9/rdev/pkg/api"
)
// 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"`
}
// 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(), 10*time.Second)
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),
}
}
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(), 10*time.Second)
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),
})
}
// 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)
}