generated from mathias/template-go-web
feat(atlas): Phase C — recent-runs timeline
A strip of the last 12 runs (aggregate pass/fail/running per run) below the substrate ribbon, coloured, newest-first, run # on hover. Parsed from the same Gitea /actions/tasks fetch (RecentRuns, test-first; State refactored to share the aggregate). No new RBAC. Hidden when no live data. Verified: build/vet/lint(0)/test green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,10 +35,11 @@ type Stage struct {
|
||||
|
||||
// Atlas is the full data model the frontend renders.
|
||||
type Atlas struct {
|
||||
Version string `json:"version,omitempty"`
|
||||
Substrate []Host `json:"substrate"`
|
||||
NS string `json:"ns,omitempty"`
|
||||
Stages []Stage `json:"stages"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Substrate []Host `json:"substrate"`
|
||||
NS string `json:"ns,omitempty"`
|
||||
Timeline []RunDot `json:"timeline,omitempty"`
|
||||
Stages []Stage `json:"stages"`
|
||||
}
|
||||
|
||||
// Build unmarshals the authored atlas JSON and overlays generated facts from
|
||||
|
||||
+43
-1
@@ -31,8 +31,50 @@ type RunSummary struct {
|
||||
// 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 s.Jobs {
|
||||
for _, j := range jobs {
|
||||
switch j.State() {
|
||||
case "failure", "cancelled", "error":
|
||||
return "failure"
|
||||
|
||||
@@ -65,6 +65,30 @@ func TestRunNodes_SummaryThenPerJobColoured(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecentRuns_GroupsRunsNewestFirstWithAggregateState(t *testing.T) {
|
||||
tasks := []byte(`{"workflow_runs":[
|
||||
{"run_number":28,"name":"Deploy","status":"success"},
|
||||
{"run_number":28,"name":"Build","status":"success"},
|
||||
{"run_number":27,"name":"Deploy","status":"completed","conclusion":"failure"},
|
||||
{"run_number":26,"name":"Build","status":"running"},
|
||||
{"run_number":25,"name":"Deploy","status":"success"}
|
||||
]}`)
|
||||
|
||||
dots, err := atlas.RecentRuns(tasks, 3)
|
||||
if err != nil {
|
||||
t.Fatalf("RecentRuns: %v", err)
|
||||
}
|
||||
if len(dots) != 3 {
|
||||
t.Fatalf("want 3 dots (capped), got %d: %+v", len(dots), dots)
|
||||
}
|
||||
want := []atlas.RunDot{{28, "success"}, {27, "failure"}, {26, "running"}}
|
||||
for i := range want {
|
||||
if dots[i] != want[i] {
|
||||
t.Fatalf("dot[%d] = %+v, want %+v", i, dots[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLatestRunJobs_ErrorsWhenEmpty(t *testing.T) {
|
||||
if _, err := atlas.LatestRunJobs([]byte(`{"workflow_runs":[]}`)); err == nil {
|
||||
t.Fatal("expected error on empty, got nil")
|
||||
|
||||
Reference in New Issue
Block a user