From d1be77b41421f814dbe9823c6e0e8c0d484a2f3a Mon Sep 17 00:00:00 2001 From: oppiliappan Date: Wed, 18 Jun 2025 11:14:07 +0100 Subject: [PATCH] appview/repo: display pipeline status on repo index Change-Id: vmnolsvkwpzomvqpwysxnvvrryqwvwst Signed-off-by: oppiliappan --- appview/pages/funcmap.go | 47 +++++++ appview/pages/pages.go | 1 + .../repo/fragments/pipelineStatusSymbol.html | 118 ++++++++++++++++++ appview/pages/templates/repo/index.html | 40 +++--- appview/repo/index.go | 7 ++ appview/repo/repo_util.go | 41 ++++++ appview/state/spindlestream.go | 14 ++- 7 files changed, 247 insertions(+), 21 deletions(-) create mode 100644 appview/pages/templates/repo/fragments/pipelineStatusSymbol.html diff --git a/appview/pages/funcmap.go b/appview/pages/funcmap.go index 6730551..0005fa8 100644 --- a/appview/pages/funcmap.go +++ b/appview/pages/funcmap.go @@ -49,6 +49,27 @@ func funcMap() template.FuncMap { "sub": func(a, b int) int { return a - b }, + "f64": func(a int) float64 { + return float64(a) + }, + "addf64": func(a, b float64) float64 { + return a + b + }, + "subf64": func(a, b float64) float64 { + return a - b + }, + "mulf64": func(a, b float64) float64 { + return a * b + }, + "divf64": func(a, b float64) float64 { + if b == 0 { + return 0 + } + return a / b + }, + "negf64": func(a float64) float64 { + return -a + }, "cond": func(cond interface{}, a, b string) string { if cond == nil { return b @@ -105,6 +126,32 @@ func funcMap() template.FuncMap { {math.MaxInt64, "a long while %s", 1}, }) }, + "durationFmt": func(duration time.Duration) string { + days := int64(duration.Hours() / 24) + hours := int64(math.Mod(duration.Hours(), 24)) + minutes := int64(math.Mod(duration.Minutes(), 60)) + seconds := int64(math.Mod(duration.Seconds(), 60)) + + chunks := []struct { + name string + amount int64 + }{ + {"d", days}, + {"hr", hours}, + {"min", minutes}, + {"s", seconds}, + } + + parts := []string{} + + for _, chunk := range chunks { + if chunk.amount != 0 { + parts = append(parts, fmt.Sprintf("%d%s", chunk.amount, chunk.name)) + } + } + + return strings.Join(parts, " ") + }, "byteFmt": humanize.Bytes, "length": func(slice any) int { v := reflect.ValueOf(slice) diff --git a/appview/pages/pages.go b/appview/pages/pages.go index 694cea9..fe0e43c 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -436,6 +436,7 @@ type RepoIndexParams struct { EmailToDidOrHandle map[string]string VerifiedCommits commitverify.VerifiedCommits Languages *types.RepoLanguageResponse + Pipelines map[plumbing.Hash]db.Pipeline types.RepoIndexResponse } diff --git a/appview/pages/templates/repo/fragments/pipelineStatusSymbol.html b/appview/pages/templates/repo/fragments/pipelineStatusSymbol.html new file mode 100644 index 0000000..d087bca --- /dev/null +++ b/appview/pages/templates/repo/fragments/pipelineStatusSymbol.html @@ -0,0 +1,118 @@ +{{ define "repo/fragments/pipelineStatusSymbol" }} +
+ {{ block "icon" $ }} {{ end }} + {{ block "tooltip" $ }} {{ end }} +
+{{ end }} + +{{ define "icon" }} +
+ {{ $c := .Counts }} + {{ $statuses := .Statuses }} + {{ $total := len $statuses }} + {{ $success := index $c "success" }} + {{ $allPass := eq $success $total }} + + {{ if $allPass }} +
+ {{ i "check" "size-4 text-green-600 dark:text-green-400 " }} {{ $total }}/{{ $total }} +
+ {{ else }} + {{ $radius := f64 8 }} + {{ $circumference := mulf64 2.0 (mulf64 3.1416 $radius) }} + {{ $offset := 0.0 }} +
+ + + + {{ range $kind, $count := $c }} + {{ $color := "" }} + {{ if or (eq $kind "pending") (eq $kind "running") }} + {{ $color = "#eab308" }} + {{ else if eq $kind "success" }} + {{ $color = "#10b981" }} + {{ else if eq $kind "cancelled" }} + {{ $color = "#6b7280" }} + {{ else }} + {{ $color = "#ef4444" }} + {{ end }} + + {{ $percent := divf64 (f64 $count) (f64 $total) }} + {{ $length := mulf64 $percent $circumference }} + + + {{ $offset = addf64 $offset $length }} + {{ end }} + + {{$success}}/{{ $total }} +
+ {{ end }} +
+{{ end }} + +{{ define "tooltip" }} + +{{ end }} diff --git a/appview/pages/templates/repo/index.html b/appview/pages/templates/repo/index.html index a06e3f6..e7b06ea 100644 --- a/appview/pages/templates/repo/index.html +++ b/appview/pages/templates/repo/index.html @@ -222,6 +222,7 @@ +
{{ $verified := $.VerifiedCommits.IsVerified .Hash.String }} {{ $hashStyle := "text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-900" }} @@ -238,7 +239,7 @@ {{ $didOrHandle := index $.EmailToDidOrHandle .Author.Email }} @@ -256,23 +257,26 @@ {{ end }} -
- {{ timeFmt .Committer.When }} - {{ $tagsForCommit := index $.TagMap .Hash.String }} - {{ if gt (len $tagsForCommit) 0 }} -
- {{ end }} - {{ range $tagsForCommit }} - - {{ . }} - - {{ end }} +
+ {{ timeFmt .Committer.When }} + + + {{ $tagsForCommit := index $.TagMap .Hash.String }} + {{ if gt (len $tagsForCommit) 0 }} +
+ {{ end }} + {{ range $tagsForCommit }} + + {{ . }} + + {{ end }} + + + {{ $pipeline := index $.Pipelines .Hash }} + {{ if and $pipeline (gt (len $pipeline.Statuses) 0) }} +
+ {{ template "repo/fragments/pipelineStatusSymbol" $pipeline }} + {{ end }}
{{ end }} diff --git a/appview/repo/index.go b/appview/repo/index.go index f6f6ca8..48f15ee 100644 --- a/appview/repo/index.go +++ b/appview/repo/index.go @@ -127,6 +127,12 @@ func (rp *Repo) RepoIndex(w http.ResponseWriter, r *http.Request) { // non-fatal } + pipelines, err := rp.getPipelineStatuses(repoInfo, commitsTrunc) + if err != nil { + log.Printf("failed to fetch pipeline statuses: %s", err) + // non-fatal + } + rp.pages.RepoIndexPage(w, pages.RepoIndexParams{ LoggedInUser: user, RepoInfo: repoInfo, @@ -139,6 +145,7 @@ func (rp *Repo) RepoIndex(w http.ResponseWriter, r *http.Request) { EmailToDidOrHandle: emailToDidOrHandle(rp, emailToDidMap), VerifiedCommits: vc, Languages: repoLanguages, + Pipelines: pipelines, }) return } diff --git a/appview/repo/repo_util.go b/appview/repo/repo_util.go index 461245f..0a3fcf7 100644 --- a/appview/repo/repo_util.go +++ b/appview/repo/repo_util.go @@ -6,6 +6,10 @@ import ( "fmt" "math/big" + "tangled.sh/tangled.sh/core/appview/db" + "tangled.sh/tangled.sh/core/appview/pages/repoinfo" + + "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" ) @@ -98,3 +102,40 @@ func randomString(n int) string { return string(result) } + +// grab pipelines from DB and munge that into a hashmap with commit sha as key +// +// golang is so blessed that it requires 35 lines of imperative code for this +func (rp *Repo) getPipelineStatuses( + repoInfo repoinfo.RepoInfo, + commits []*object.Commit, +) (map[plumbing.Hash]db.Pipeline, error) { + m := make(map[plumbing.Hash]db.Pipeline) + + if len(commits) == 0 { + return m, nil + } + + shas := make([]string, len(commits)) + for _, c := range commits { + shas = append(shas, c.Hash.String()) + } + + ps, err := db.GetPipelineStatuses( + rp.db, + db.FilterEq("repo_owner", repoInfo.OwnerDid), + db.FilterEq("repo_name", repoInfo.Name), + db.FilterEq("knot", repoInfo.Knot), + db.FilterIn("sha", shas), + ) + if err != nil { + return nil, err + } + + for _, p := range ps { + hash := plumbing.NewHash(p.Sha) + m[hash] = p + } + + return m, nil +} diff --git a/appview/state/spindlestream.go b/appview/state/spindlestream.go index a998a19..bc4f6b3 100644 --- a/appview/state/spindlestream.go +++ b/appview/state/spindlestream.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "log/slog" + "strings" "time" "github.com/bluesky-social/indigo/atproto/syntax" @@ -15,6 +16,7 @@ import ( "tangled.sh/tangled.sh/core/eventconsumer/cursor" "tangled.sh/tangled.sh/core/log" "tangled.sh/tangled.sh/core/rbac" + spindle "tangled.sh/tangled.sh/core/spindle/models" ) func Spindlestream(ctx context.Context, c *config.Config, d *db.DB, enforcer *rbac.Enforcer) (*ec.Consumer, error) { @@ -77,14 +79,20 @@ func ingestPipelineStatus(ctx context.Context, logger *slog.Logger, d *db.DB, so exitCode = int(*record.ExitCode) } + // pick the record creation time if possible, or use time.Now + created := time.Now() + if t, err := time.Parse(time.RFC3339, record.CreatedAt); err == nil && created.After(t) { + created = t + } + status := db.PipelineStatus{ Spindle: source.Key(), Rkey: msg.Rkey, - PipelineKnot: pipelineUri.Authority().String(), + PipelineKnot: strings.TrimPrefix(pipelineUri.Authority().String(), "did:web:"), PipelineRkey: pipelineUri.RecordKey().String(), - Created: time.Now(), + Created: created, Workflow: record.Workflow, - Status: record.Status, + Status: spindle.StatusKind(record.Status), Error: record.Error, ExitCode: exitCode, } -- 2.43.0