diff --git a/appview/pages/funcmap.go b/appview/pages/funcmap.go
index 67305510..0005fa85 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 694cea9c..fe0e43c2 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 00000000..d087bca1
--- /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 }}
+
+
+ {{$success}}/{{ $total }}
+
+ {{ end }}
+
+{{ end }}
+
+{{ define "tooltip" }}
+
+
+ {{ range $name, $all := .Statuses }}
+
+ {{ $lastStatus := $all.Latest }}
+ {{ $kind := $lastStatus.Status.String }}
+
+ {{ $icon := "dot" }}
+ {{ $color := "text-gray-600 dark:text-gray-500" }}
+ {{ $text := "Failed" }}
+ {{ $time := "" }}
+
+ {{ if eq $kind "pending" }}
+ {{ $icon = "circle-dashed" }}
+ {{ $color = "text-yellow-600 dark:text-yellow-500" }}
+ {{ $text = "Queued" }}
+ {{ $time = timeFmt $lastStatus.Created }}
+ {{ else if eq $kind "running" }}
+ {{ $icon = "circle-dashed" }}
+ {{ $color = "text-yellow-600 dark:text-yellow-500" }}
+ {{ $text = "Running" }}
+ {{ $time = timeFmt $lastStatus.Created }}
+ {{ else if eq $kind "success" }}
+ {{ $icon = "check" }}
+ {{ $color = "text-green-600 dark:text-green-500" }}
+ {{ $text = "Success" }}
+ {{ with $all.TimeTaken }}
+ {{ $time = durationFmt . }}
+ {{ end }}
+ {{ else if eq $kind "cancelled" }}
+ {{ $icon = "circle-slash" }}
+ {{ $color = "text-gray-600 dark:text-gray-500" }}
+ {{ $text = "Cancelled" }}
+ {{ with $all.TimeTaken }}
+ {{ $time = durationFmt . }}
+ {{ end }}
+ {{ else }}
+ {{ $icon = "x" }}
+ {{ $color = "text-red-600 dark:text-red-500" }}
+ {{ $text = "Failed" }}
+ {{ with $all.TimeTaken }}
+ {{ $time = durationFmt . }}
+ {{ end }}
+ {{ end }}
+
+
+ {{ i $icon "size-4" $color }}
+ {{ $name }}
+
+
+ {{ $text }}
+
+
+
+ {{ end }}
+
+
+{{ end }}
diff --git a/appview/pages/templates/repo/index.html b/appview/pages/templates/repo/index.html
index a06e3f6f..e7b06ea6 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 f6f6ca80..48f15eea 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 461245f6..0a3fcf75 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 a998a193..bc4f6b3a 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,
}