generated from mathias/template-go-web
feat(atlas): per-job CI status + build-injected version in the UI
Phase C: the CI stage now shows the latest run's per-JOB status (Lint/Test, Build, Deploy, …), each coloured by outcome, from the Gitea /actions/tasks per-job entries — replacing the static cd.yml job-id list when live. LatestRunJobs/RunNodes + RunSummary.State aggregate, all test-first. Version: injected at build via -ldflags from `git describe --tags` (CI checkout now fetch-depth:0 + --build-arg), served in /api/atlas.json, shown in the header. No more hand-maintained version label drifting from the git tag. Verified: build/vet/lint(0)/test green; version ldflag served correctly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+89
-38
@@ -5,56 +5,107 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Run is one Gitea Actions workflow run — a real execution of the pipeline.
|
||||
type Run struct {
|
||||
Number int `json:"run_number"`
|
||||
Status string `json:"status"`
|
||||
Conclusion string `json:"conclusion"`
|
||||
SHA string `json:"head_sha"`
|
||||
Title string `json:"display_title"`
|
||||
URL string `json:"url"`
|
||||
// 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: the conclusion if set, else the status.
|
||||
func (r Run) State() string {
|
||||
if r.Conclusion != "" {
|
||||
return r.Conclusion
|
||||
// State is the effective outcome: conclusion if set, else status.
|
||||
func (j Job) State() string {
|
||||
if j.Conclusion != "" {
|
||||
return j.Conclusion
|
||||
}
|
||||
return r.Status
|
||||
return j.Status
|
||||
}
|
||||
|
||||
// RunNode renders a run as a stage node, coloured by outcome
|
||||
// (success=green, failure/cancelled=coral, otherwise amber/in-progress).
|
||||
func RunNode(r Run) Node {
|
||||
pill := "var(--amber)"
|
||||
switch r.State() {
|
||||
case "success":
|
||||
pill = "var(--green)"
|
||||
case "failure", "cancelled":
|
||||
pill = "var(--coral)"
|
||||
// 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 {
|
||||
allSucceeded := true
|
||||
for _, j := range s.Jobs {
|
||||
switch j.State() {
|
||||
case "failure", "cancelled", "error":
|
||||
return "failure"
|
||||
case "success":
|
||||
default:
|
||||
allSucceeded = false
|
||||
}
|
||||
}
|
||||
sha := r.SHA
|
||||
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]
|
||||
}
|
||||
return Node{
|
||||
Title: fmt.Sprintf("▶ run #%d · %s", r.Number, r.State()),
|
||||
Desc: r.Title,
|
||||
Pill: pill,
|
||||
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
|
||||
}
|
||||
|
||||
// LatestRun parses a Gitea `/actions/tasks` response and returns the newest run.
|
||||
func LatestRun(tasksJSON []byte) (Run, error) {
|
||||
var resp struct {
|
||||
Runs []Run `json:"workflow_runs"`
|
||||
func statePill(state string) string {
|
||||
switch state {
|
||||
case "success":
|
||||
return "var(--green)"
|
||||
case "failure", "cancelled", "error":
|
||||
return "var(--coral)"
|
||||
}
|
||||
if err := json.Unmarshal(tasksJSON, &resp); err != nil {
|
||||
return Run{}, fmt.Errorf("parse runs: %w", err)
|
||||
}
|
||||
if len(resp.Runs) == 0 {
|
||||
return Run{}, fmt.Errorf("no workflow runs")
|
||||
}
|
||||
return resp.Runs[0], nil
|
||||
return "var(--amber)"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user