127 lines
3.7 KiB
Go
127 lines
3.7 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
"xorm.io/xorm"
|
|
|
|
"github.com/forgeo/forgebucket/internal/events"
|
|
"github.com/forgeo/forgebucket/internal/models"
|
|
"github.com/forgeo/forgebucket/internal/observability"
|
|
)
|
|
|
|
// ── /health ───────────────────────────────────────────────────────────────────
|
|
|
|
type HealthHandler struct {
|
|
db *xorm.Engine
|
|
bus events.EventBus
|
|
}
|
|
|
|
func NewHealthHandler(db *xorm.Engine, bus events.EventBus) *HealthHandler {
|
|
return &HealthHandler{db: db, bus: bus}
|
|
}
|
|
|
|
func (h *HealthHandler) Health(w http.ResponseWriter, r *http.Request) {
|
|
status := observability.Check(h.db, h.bus)
|
|
if status.Status != "healthy" {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusServiceUnavailable)
|
|
jsonOK(w, status)
|
|
return
|
|
}
|
|
jsonOK(w, status)
|
|
}
|
|
|
|
// ── /api/v1/repos/{owner}/{repo}/health ──────────────────────────────────────
|
|
|
|
type RepoHealthHandler struct{ db *xorm.Engine }
|
|
|
|
func NewRepoHealthHandler(db *xorm.Engine) *RepoHealthHandler {
|
|
return &RepoHealthHandler{db: db}
|
|
}
|
|
|
|
type latestDeployment struct {
|
|
EnvName string `json:"envName"`
|
|
Status string `json:"status"`
|
|
SHA string `json:"sha"`
|
|
FinishedAt *time.Time `json:"finishedAt"`
|
|
}
|
|
|
|
type repoHealthResponse struct {
|
|
CIPassRate7d float64 `json:"ciPassRate7d"`
|
|
TotalRuns7d int `json:"totalRuns7d"`
|
|
LatestRun *models.PipelineRun `json:"latestRun"`
|
|
LatestDeployments []latestDeployment `json:"latestDeployments"`
|
|
OpenDriftCount int `json:"openDriftCount"`
|
|
OpenPRCount int `json:"openPRCount"`
|
|
}
|
|
|
|
// Get returns an operational health summary for a repository.
|
|
// This feeds the repo page header: CI pass rate, latest deploy per env, drift count.
|
|
func (h *RepoHealthHandler) Get(w http.ResponseWriter, r *http.Request) {
|
|
repoID, ok := resolveRepoID(h.db, w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
since7d := time.Now().UTC().Add(-7 * 24 * time.Hour)
|
|
|
|
// CI pass rate over last 7 days.
|
|
var runs []models.PipelineRun
|
|
h.db.Where("repo_id = ? AND created_at >= ?", repoID, since7d).Find(&runs)
|
|
total := len(runs)
|
|
succeeded := 0
|
|
for _, run := range runs {
|
|
if run.Status == "succeeded" {
|
|
succeeded++
|
|
}
|
|
}
|
|
var passRate float64
|
|
if total > 0 {
|
|
passRate = float64(succeeded) / float64(total)
|
|
}
|
|
|
|
// Latest run overall.
|
|
var latestRun models.PipelineRun
|
|
var hasLatest bool
|
|
hasLatest, _ = h.db.Where("repo_id = ?", repoID).Desc("id").Limit(1).Get(&latestRun)
|
|
|
|
// Latest deployment per environment.
|
|
var envs []models.Environment
|
|
h.db.Where("repo_id = ?", repoID).Find(&envs)
|
|
deploys := make([]latestDeployment, 0, len(envs))
|
|
for _, env := range envs {
|
|
var d models.Deployment
|
|
if found, _ := h.db.Where("env_id = ?", env.ID).Desc("id").Limit(1).Get(&d); found {
|
|
deploys = append(deploys, latestDeployment{
|
|
EnvName: env.Name,
|
|
Status: string(d.Status),
|
|
SHA: d.SHA,
|
|
FinishedAt: d.FinishedAt,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Open drift count (GitOpsConfigs where sync_status = 'drifted').
|
|
driftCount, _ := h.db.Where("repo_id = ? AND sync_status = 'drifted'", repoID).
|
|
Count(&models.GitOpsConfig{})
|
|
|
|
// Open PR count.
|
|
prCount, _ := h.db.Where("repo_id = ? AND status = 'open'", repoID).
|
|
Count(&models.PullRequest{})
|
|
|
|
resp := repoHealthResponse{
|
|
CIPassRate7d: passRate,
|
|
TotalRuns7d: total,
|
|
LatestDeployments: deploys,
|
|
OpenDriftCount: int(driftCount),
|
|
OpenPRCount: int(prCount),
|
|
}
|
|
if hasLatest {
|
|
resp.LatestRun = &latestRun
|
|
}
|
|
|
|
jsonOK(w, resp)
|
|
}
|