package atlas import ( "encoding/json" "fmt" ) // Job is one job within a workflow run (a Gitea Actions "task"). type Job struct { Name string Status string Conclusion string } // State is the effective outcome: conclusion if set, else status. func (j Job) State() string { if j.Conclusion != "" { return j.Conclusion } return j.Status } // RunSummary is the newest workflow run and its per-job outcomes. type RunSummary struct { Number int SHA string Title string Jobs []Job } // State aggregates the jobs: failure if any failed, running if any not yet // succeeded, else success. func (s RunSummary) State() string { return aggregateState(s.Jobs) } // RunDot is one run's aggregate outcome for the recent-runs timeline. type RunDot struct { Number int `json:"number"` State string `json:"state"` } // RecentRuns parses a Gitea `/actions/tasks` response (per-job, newest first) // into up to n most-recent runs with their aggregate outcome, newest first. func RecentRuns(tasksJSON []byte, n int) ([]RunDot, error) { var resp struct { Tasks []struct { RunNumber int `json:"run_number"` Status string `json:"status"` Conclusion string `json:"conclusion"` } `json:"workflow_runs"` } if err := json.Unmarshal(tasksJSON, &resp); err != nil { return nil, fmt.Errorf("parse tasks: %w", err) } var order []int jobsByRun := map[int][]Job{} for _, t := range resp.Tasks { if _, seen := jobsByRun[t.RunNumber]; !seen { order = append(order, t.RunNumber) } jobsByRun[t.RunNumber] = append(jobsByRun[t.RunNumber], Job{Status: t.Status, Conclusion: t.Conclusion}) } dots := make([]RunDot, 0, n) for _, rn := range order { if len(dots) >= n { break } dots = append(dots, RunDot{Number: rn, State: aggregateState(jobsByRun[rn])}) } return dots, nil } // aggregateState folds per-job outcomes into a run outcome. func aggregateState(jobs []Job) string { allSucceeded := true for _, j := range jobs { switch j.State() { case "failure", "cancelled", "error": return "failure" case "success": default: allSucceeded = false } } if allSucceeded { return "success" } return "running" } // LatestRunJobs parses a Gitea `/actions/tasks` response (per-job entries, // newest first) and returns the newest run with its jobs in pipeline order. func LatestRunJobs(tasksJSON []byte) (RunSummary, error) { var resp struct { Tasks []struct { RunNumber int `json:"run_number"` Name string `json:"name"` Status string `json:"status"` Conclusion string `json:"conclusion"` SHA string `json:"head_sha"` Title string `json:"display_title"` } `json:"workflow_runs"` } if err := json.Unmarshal(tasksJSON, &resp); err != nil { return RunSummary{}, fmt.Errorf("parse tasks: %w", err) } if len(resp.Tasks) == 0 { return RunSummary{}, fmt.Errorf("no workflow tasks") } latest := resp.Tasks[0] s := RunSummary{Number: latest.RunNumber, SHA: latest.SHA, Title: latest.Title} for _, t := range resp.Tasks { if t.RunNumber == latest.RunNumber { s.Jobs = append(s.Jobs, Job{Name: t.Name, Status: t.Status, Conclusion: t.Conclusion}) } } // Gitea lists newest (last-finished) first; reverse to pipeline order. for i, j := 0, len(s.Jobs)-1; i < j; i, j = i+1, j-1 { s.Jobs[i], s.Jobs[j] = s.Jobs[j], s.Jobs[i] } return s, nil } // RunNodes renders a run as stage nodes: a summary node followed by one node // per job, each coloured by outcome. func RunNodes(s RunSummary) []Node { sha := s.SHA if len(sha) > 7 { sha = sha[:7] } nodes := []Node{{ Title: fmt.Sprintf("▶ run #%d · %s", s.Number, s.State()), Desc: s.Title, Pill: statePill(s.State()), Tags: []string{"live · Gitea Actions", sha}, }} for _, j := range s.Jobs { nodes = append(nodes, Node{Title: j.Name, Pill: statePill(j.State())}) } return nodes } func statePill(state string) string { switch state { case "success": return "var(--green)" case "failure", "cancelled", "error": return "var(--coral)" } return "var(--amber)" }