completed phase 2b
This commit is contained in:
@@ -2,6 +2,8 @@ package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"xorm.io/xorm"
|
||||
@@ -17,37 +19,209 @@ func NewPipelineHandler(db *xorm.Engine) *PipelineHandler {
|
||||
return &PipelineHandler{db: db}
|
||||
}
|
||||
|
||||
func (h *PipelineHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
ownerName := chi.URLParam(r, "owner")
|
||||
repoName := chi.URLParam(r, "repo")
|
||||
|
||||
repoID, ok := h.repoID(w, ownerName, repoName)
|
||||
// ListPipelines returns all pipeline definitions for a repository.
|
||||
func (h *PipelineHandler) ListPipelines(w http.ResponseWriter, r *http.Request) {
|
||||
repoID, ok := h.repoID(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
_ = repoID
|
||||
// Pipeline records will be added in Phase 3 (CI integration).
|
||||
// Return empty list so the client doesn't break.
|
||||
jsonOK(w, []any{})
|
||||
var pipelines []models.Pipeline
|
||||
if err := h.db.Where("repo_id = ?", repoID).Find(&pipelines); err != nil {
|
||||
jsonError(w, "could not list pipelines", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if pipelines == nil {
|
||||
pipelines = []models.Pipeline{}
|
||||
}
|
||||
jsonOK(w, pipelines)
|
||||
}
|
||||
|
||||
func (h *PipelineHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
jsonError(w, "not implemented", http.StatusNotImplemented)
|
||||
// ListRuns returns pipeline runs for a repository, most recent first.
|
||||
func (h *PipelineHandler) ListRuns(w http.ResponseWriter, r *http.Request) {
|
||||
repoID, ok := h.repoID(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
limit := 30
|
||||
if l, err := strconv.Atoi(r.URL.Query().Get("limit")); err == nil && l > 0 && l <= 100 {
|
||||
limit = l
|
||||
}
|
||||
var runs []models.PipelineRun
|
||||
if err := h.db.Where("repo_id = ?", repoID).Desc("id").Limit(limit).Find(&runs); err != nil {
|
||||
jsonError(w, "could not list runs", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if runs == nil {
|
||||
runs = []models.PipelineRun{}
|
||||
}
|
||||
jsonOK(w, runs)
|
||||
}
|
||||
|
||||
func (h *PipelineHandler) repoID(w http.ResponseWriter, ownerName, repoName string) (int64, bool) {
|
||||
var owner models.User
|
||||
found, err := h.db.Where("username = ?", ownerName).Get(&owner)
|
||||
if err != nil || !found {
|
||||
type runDetailResponse struct {
|
||||
models.PipelineRun
|
||||
Jobs []jobDetailResponse `json:"jobs"`
|
||||
}
|
||||
|
||||
type jobDetailResponse struct {
|
||||
models.PipelineJob
|
||||
Steps []models.PipelineStep `json:"steps"`
|
||||
}
|
||||
|
||||
// GetRun returns a run with its full job + step tree.
|
||||
func (h *PipelineHandler) GetRun(w http.ResponseWriter, r *http.Request) {
|
||||
run, ok := h.lookupRun(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var jobs []models.PipelineJob
|
||||
h.db.Where("run_id = ?", run.ID).Asc("id").Find(&jobs)
|
||||
|
||||
jobDetails := make([]jobDetailResponse, len(jobs))
|
||||
for i, job := range jobs {
|
||||
var steps []models.PipelineStep
|
||||
h.db.Where("job_id = ?", job.ID).Asc("seq").Find(&steps)
|
||||
if steps == nil {
|
||||
steps = []models.PipelineStep{}
|
||||
}
|
||||
jobDetails[i] = jobDetailResponse{PipelineJob: job, Steps: steps}
|
||||
}
|
||||
jsonOK(w, runDetailResponse{PipelineRun: *run, Jobs: jobDetails})
|
||||
}
|
||||
|
||||
// GetJobLogs returns all log chunks for a job, ordered by step seq and chunk index.
|
||||
func (h *PipelineHandler) GetJobLogs(w http.ResponseWriter, r *http.Request) {
|
||||
_, ok := h.lookupRun(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
jobID, err := strconv.ParseInt(chi.URLParam(r, "jobID"), 10, 64)
|
||||
if err != nil {
|
||||
jsonError(w, "invalid job ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify job belongs to this run.
|
||||
var job models.PipelineJob
|
||||
runID, _ := strconv.ParseInt(chi.URLParam(r, "runID"), 10, 64)
|
||||
if found, _ := h.db.Where("id = ? AND run_id = ?", jobID, runID).Get(&job); !found {
|
||||
jsonError(w, "job not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var steps []models.PipelineStep
|
||||
h.db.Where("job_id = ?", jobID).Asc("seq").Find(&steps)
|
||||
|
||||
type stepLogs struct {
|
||||
models.PipelineStep
|
||||
Logs []models.PipelineStepLog `json:"logs"`
|
||||
}
|
||||
result := make([]stepLogs, len(steps))
|
||||
for i, step := range steps {
|
||||
var logs []models.PipelineStepLog
|
||||
h.db.Where("step_id = ?", step.ID).Asc("chunk_index").Find(&logs)
|
||||
if logs == nil {
|
||||
logs = []models.PipelineStepLog{}
|
||||
}
|
||||
result[i] = stepLogs{PipelineStep: step, Logs: logs}
|
||||
}
|
||||
jsonOK(w, result)
|
||||
}
|
||||
|
||||
// CancelRun marks a queued or running run as cancelled.
|
||||
func (h *PipelineHandler) CancelRun(w http.ResponseWriter, r *http.Request) {
|
||||
run, ok := h.lookupRun(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if run.Status != "queued" && run.Status != "running" {
|
||||
jsonError(w, "run is not cancellable in its current state", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
run.Status = "cancelled"
|
||||
run.FinishedAt = &now
|
||||
if _, err := h.db.ID(run.ID).Cols("status", "finished_at").Update(run); err != nil {
|
||||
jsonError(w, "could not cancel run", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Cancel any queued jobs.
|
||||
h.db.Where("run_id = ? AND status = 'queued'", run.ID). //nolint:errcheck
|
||||
Cols("status").Update(&models.PipelineJob{Status: "cancelled"})
|
||||
jsonOK(w, run)
|
||||
}
|
||||
|
||||
// RetryJob re-queues a failed job by resetting its status and re-publishing job.queued.
|
||||
func (h *PipelineHandler) RetryJob(w http.ResponseWriter, r *http.Request) {
|
||||
run, ok := h.lookupRun(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
jobID, err := strconv.ParseInt(chi.URLParam(r, "jobID"), 10, 64)
|
||||
if err != nil {
|
||||
jsonError(w, "invalid job ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var job models.PipelineJob
|
||||
if found, _ := h.db.Where("id = ? AND run_id = ?", jobID, run.ID).Get(&job); !found {
|
||||
jsonError(w, "job not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if job.Status != "failed" && job.Status != "cancelled" {
|
||||
jsonError(w, "only failed or cancelled jobs can be retried", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
job.Status = "queued"
|
||||
job.StartedAt = nil
|
||||
job.FinishedAt = nil
|
||||
h.db.ID(job.ID).Cols("status", "started_at", "finished_at").Update(&job) //nolint:errcheck
|
||||
|
||||
// Reset step statuses.
|
||||
h.db.Where("job_id = ?", job.ID).Cols("status", "exit_code", "started_at", "finished_at"). //nolint:errcheck
|
||||
Update(&models.PipelineStep{Status: "queued"})
|
||||
|
||||
// Also reset the run status if it was failed/cancelled.
|
||||
if run.Status == "failed" || run.Status == "cancelled" {
|
||||
run.Status = "running"
|
||||
run.FinishedAt = nil
|
||||
h.db.ID(run.ID).Cols("status", "finished_at").Update(run) //nolint:errcheck
|
||||
}
|
||||
|
||||
jsonOK(w, job)
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
func (h *PipelineHandler) repoID(w http.ResponseWriter, r *http.Request) (int64, bool) {
|
||||
owner := chi.URLParam(r, "owner")
|
||||
repoName := chi.URLParam(r, "repo")
|
||||
var u models.User
|
||||
if found, _ := h.db.Where("username = ?", owner).Get(&u); !found {
|
||||
jsonError(w, "repository not found", http.StatusNotFound)
|
||||
return 0, false
|
||||
}
|
||||
|
||||
var repo models.Repository
|
||||
found, err = h.db.Where("owner_id = ? AND name = ?", owner.ID, repoName).Get(&repo)
|
||||
if err != nil || !found {
|
||||
if found, _ := h.db.Where("owner_id = ? AND name = ?", u.ID, repoName).Get(&repo); !found {
|
||||
jsonError(w, "repository not found", http.StatusNotFound)
|
||||
return 0, false
|
||||
}
|
||||
return repo.ID, true
|
||||
}
|
||||
|
||||
func (h *PipelineHandler) lookupRun(w http.ResponseWriter, r *http.Request) (*models.PipelineRun, bool) {
|
||||
repoID, ok := h.repoID(w, r)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
runID, err := strconv.ParseInt(chi.URLParam(r, "runID"), 10, 64)
|
||||
if err != nil {
|
||||
jsonError(w, "invalid run ID", http.StatusBadRequest)
|
||||
return nil, false
|
||||
}
|
||||
var run models.PipelineRun
|
||||
if found, _ := h.db.Where("id = ? AND repo_id = ?", runID, repoID).Get(&run); !found {
|
||||
jsonError(w, "run not found", http.StatusNotFound)
|
||||
return nil, false
|
||||
}
|
||||
return &run, true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user