diff --git a/appview/db/pipeline.go b/appview/db/pipeline.go
index e845f608..9c76806f 100644
--- a/appview/db/pipeline.go
+++ b/appview/db/pipeline.go
@@ -7,6 +7,7 @@ import (
"time"
"github.com/bluesky-social/indigo/atproto/syntax"
+ "github.com/go-git/go-git/v5/plumbing"
spindle "tangled.sh/tangled.sh/core/spindle/models"
)
@@ -60,6 +61,23 @@ func (p Pipeline) Counts() map[string]int {
return m
}
+func (p Pipeline) TimeTaken() time.Duration {
+ var s time.Duration
+ for _, w := range p.Statuses {
+ s += w.TimeTaken()
+ }
+ return s
+}
+
+func (p Pipeline) Workflows() []string {
+ var ws []string
+ for v := range p.Statuses {
+ ws = append(ws, v)
+ }
+ slices.Sort(ws)
+ return ws
+}
+
type Trigger struct {
Id int
Kind string
@@ -76,6 +94,24 @@ type Trigger struct {
PRAction *string
}
+func (t *Trigger) IsPush() bool {
+ return t != nil && t.Kind == "push"
+}
+
+func (t *Trigger) IsPullRequest() bool {
+ return t != nil && t.Kind == "pull_request"
+}
+
+func (t *Trigger) TargetRef() string {
+ if t.IsPush() {
+ return plumbing.ReferenceName(*t.PushRef).Short()
+ } else if t.IsPullRequest() {
+ return *t.PRTargetBranch
+ }
+
+ return ""
+}
+
type PipelineStatus struct {
ID int
Spindle string
@@ -262,14 +298,14 @@ func GetPipelineStatuses(e Execer, filters ...filter) ([]Pipeline, error) {
query := fmt.Sprintf(`
select
- p.id AS pipeline_id,
+ p.id,
p.knot,
p.rkey,
p.repo_owner,
p.repo_name,
p.sha,
p.created,
- t.id AS trigger_id,
+ t.id,
t.kind,
t.push_ref,
t.push_new_sha,
@@ -283,7 +319,6 @@ func GetPipelineStatuses(e Execer, filters ...filter) ([]Pipeline, error) {
join
triggers t ON p.trigger_id = t.id
%s
- order by p.created desc
`, whereClause)
rows, err := e.Query(query, args...)
@@ -425,5 +460,13 @@ func GetPipelineStatuses(e Execer, filters ...filter) ([]Pipeline, error) {
all = append(all, p)
}
+ // sort pipelines by date
+ slices.SortFunc(all, func(a, b Pipeline) int {
+ if a.Created.After(b.Created) {
+ return -1
+ }
+ return 1
+ })
+
return all, nil
}
diff --git a/appview/pages/funcmap.go b/appview/pages/funcmap.go
index 0005fa85..3a294905 100644
--- a/appview/pages/funcmap.go
+++ b/appview/pages/funcmap.go
@@ -225,6 +225,13 @@ func funcMap() template.FuncMap {
}
return dict, nil
},
+ "deref": func(v any) any {
+ val := reflect.ValueOf(v)
+ if val.Kind() == reflect.Ptr && !val.IsNil() {
+ return val.Elem().Interface()
+ }
+ return nil
+ },
"i": func(name string, classes ...string) template.HTML {
data, err := icon(name, classes)
if err != nil {
diff --git a/appview/pages/pages.go b/appview/pages/pages.go
index 23e3318d..16912af3 100644
--- a/appview/pages/pages.go
+++ b/appview/pages/pages.go
@@ -951,6 +951,18 @@ func (p *Pages) RepoCompareDiff(w io.Writer, params RepoCompareDiffParams) error
return p.executePlain("repo/fragments/diff", w, []any{params.RepoInfo.FullName, ¶ms.Diff})
}
+type PipelinesParams struct {
+ LoggedInUser *oauth.User
+ RepoInfo repoinfo.RepoInfo
+ Pipelines []db.Pipeline
+ Active string
+}
+
+func (p *Pages) Pipelines(w io.Writer, params PipelinesParams) error {
+ params.Active = "pipelines"
+ return p.executeRepo("repo/pipelines/pipelines", w, params)
+}
+
func (p *Pages) Static() http.Handler {
if p.dev {
return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static")))
diff --git a/appview/pages/repoinfo/repoinfo.go b/appview/pages/repoinfo/repoinfo.go
index 8b1ca8c8..0c9dcb31 100644
--- a/appview/pages/repoinfo/repoinfo.go
+++ b/appview/pages/repoinfo/repoinfo.go
@@ -40,6 +40,7 @@ func (r RepoInfo) GetTabs() [][]string {
{"overview", "/", "square-chart-gantt"},
{"issues", "/issues", "circle-dot"},
{"pulls", "/pulls", "git-pull-request"},
+ {"pipelines", "/pipelines", "layers-2"},
}
if r.Roles.SettingsAllowed() {
diff --git a/appview/pages/templates/repo/commit.html b/appview/pages/templates/repo/commit.html
index 702320ed..98b46399 100644
--- a/appview/pages/templates/repo/commit.html
+++ b/appview/pages/templates/repo/commit.html
@@ -71,7 +71,7 @@
{{ if $.Pipeline }}
- {{ template "repo/fragments/pipelineStatusSymbol" $.Pipeline }}
+ {{ template "repo/pipelines/fragments/pipelineSymbolLong" $.Pipeline }}
{{ end }}
diff --git a/appview/pages/templates/repo/fragments/pipelineStatusSymbol.html b/appview/pages/templates/repo/fragments/pipelineStatusSymbol.html
deleted file mode 100644
index 235c6351..00000000
--- a/appview/pages/templates/repo/fragments/pipelineStatusSymbol.html
+++ /dev/null
@@ -1,119 +0,0 @@
-{{ 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 3db6d571..87740333 100644
--- a/appview/pages/templates/repo/index.html
+++ b/appview/pages/templates/repo/index.html
@@ -275,7 +275,7 @@
{{ $pipeline := index $.Pipelines .Hash.String }}
{{ if and $pipeline (gt (len $pipeline.Statuses) 0) }}
- {{ template "repo/fragments/pipelineStatusSymbol" $pipeline }}
+ {{ template "repo/pipelines/fragments/pipelineSymbolLong" $pipeline }}
{{ end }}
diff --git a/appview/pages/templates/repo/log.html b/appview/pages/templates/repo/log.html
index 1f9994d7..dc7c5f1c 100644
--- a/appview/pages/templates/repo/log.html
+++ b/appview/pages/templates/repo/log.html
@@ -84,7 +84,7 @@
{{ $pipeline := index $.Pipelines .Hash.String }}
{{ if and $pipeline (gt (len $pipeline.Statuses) 0) }}
- {{ template "repo/fragments/pipelineStatusSymbol" $pipeline }}
+ {{ template "repo/pipelines/fragments/pipelineSymbolLong" $pipeline }}
{{ end }}
{{ timeFmt $commit.Committer.When }} |
@@ -170,7 +170,7 @@
{{ if and $pipeline (gt (len $pipeline.Statuses) 0) }}
- {{ template "repo/fragments/pipelineStatusSymbol" $pipeline }}
+ {{ template "repo/pipelines/fragments/pipelineSymbolLong" $pipeline }}
{{ end }}
diff --git a/appview/pages/templates/repo/pipelines/fragments/pipelineSymbol.html b/appview/pages/templates/repo/pipelines/fragments/pipelineSymbol.html
new file mode 100644
index 00000000..fab91ddc
--- /dev/null
+++ b/appview/pages/templates/repo/pipelines/fragments/pipelineSymbol.html
@@ -0,0 +1,53 @@
+{{ define "repo/pipelines/fragments/pipelineSymbol" }}
+
+ {{ $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 }}
+
diff --git a/appview/pages/templates/repo/pipelines/fragments/pipelineSymbolLong.html b/appview/pages/templates/repo/pipelines/fragments/pipelineSymbolLong.html
new file mode 100644
index 00000000..3acc4d2e
--- /dev/null
+++ b/appview/pages/templates/repo/pipelines/fragments/pipelineSymbolLong.html
@@ -0,0 +1,8 @@
+{{ define "repo/pipelines/fragments/pipelineSymbolLong" }}
+
+ {{ template "repo/pipelines/fragments/pipelineSymbol" $ }}
+ {{ template "repo/pipelines/fragments/tooltip" $ }}
+
+{{ end }}
+
+
diff --git a/appview/pages/templates/repo/pipelines/fragments/tooltip.html b/appview/pages/templates/repo/pipelines/fragments/tooltip.html
new file mode 100644
index 00000000..c313f87f
--- /dev/null
+++ b/appview/pages/templates/repo/pipelines/fragments/tooltip.html
@@ -0,0 +1,30 @@
+{{ define "repo/pipelines/fragments/tooltip" }}
+
+
+ {{ range $name, $all := .Statuses }}
+
+ {{ $lastStatus := $all.Latest }}
+ {{ $kind := $lastStatus.Status.String }}
+
+ {{ $t := .TimeTaken }}
+ {{ $time := "" }}
+ {{ if $t }}
+ {{ $time = durationFmt $t }}
+ {{ else }}
+ {{ $time = printf "%s ago" (shortTimeFmt $.Created) }}
+ {{ end }}
+
+
+ {{ template "repo/pipelines/fragments/workflowSymbol" $all }}
+ {{ $name }}
+
+
+ {{ $kind }}
+
+
+
+ {{ end }}
+
+
+{{ end }}
+
diff --git a/appview/pages/templates/repo/pipelines/fragments/workflowSymbol.html b/appview/pages/templates/repo/pipelines/fragments/workflowSymbol.html
new file mode 100644
index 00000000..2b2b4d1e
--- /dev/null
+++ b/appview/pages/templates/repo/pipelines/fragments/workflowSymbol.html
@@ -0,0 +1,26 @@
+{{ define "repo/pipelines/fragments/workflowSymbol" }}
+ {{ $lastStatus := .Latest }}
+ {{ $kind := $lastStatus.Status.String }}
+
+ {{ $icon := "dot" }}
+ {{ $color := "text-gray-600 dark:text-gray-500" }}
+
+ {{ if eq $kind "pending" }}
+ {{ $icon = "circle-dashed" }}
+ {{ $color = "text-yellow-600 dark:text-yellow-500" }}
+ {{ else if eq $kind "running" }}
+ {{ $icon = "circle-dashed" }}
+ {{ $color = "text-yellow-600 dark:text-yellow-500" }}
+ {{ else if eq $kind "success" }}
+ {{ $icon = "check" }}
+ {{ $color = "text-green-600 dark:text-green-500" }}
+ {{ else if eq $kind "cancelled" }}
+ {{ $icon = "circle-slash" }}
+ {{ $color = "text-gray-600 dark:text-gray-500" }}
+ {{ else }}
+ {{ $icon = "x" }}
+ {{ $color = "text-red-600 dark:text-red-500" }}
+ {{ end }}
+
+ {{ i $icon "size-4" $color }}
+{{ end }}
diff --git a/appview/pages/templates/repo/pipelines/pipelines.html b/appview/pages/templates/repo/pipelines/pipelines.html
new file mode 100644
index 00000000..69811420
--- /dev/null
+++ b/appview/pages/templates/repo/pipelines/pipelines.html
@@ -0,0 +1,93 @@
+{{ define "title" }}pipelines · {{ .RepoInfo.FullName }}{{ end }}
+
+{{ define "extrameta" }}
+ {{ $title := "pipelines"}}
+ {{ $url := printf "https://tangled.sh/%s/pipelines" .RepoInfo.FullName }}
+ {{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo "Title" $title "Url" $url) }}
+{{ end }}
+
+{{ define "repoContent" }}
+
+
+{{ end }}
+
+{{ define "repoAfter" }}
+
+ {{ range .Pipelines }}
+ {{ block "pipeline" (list $ .) }} {{ end }}
+ {{ else }}
+
+ No pipelines run for this repository.
+
+ {{ end }}
+
+{{ end }}
+
+{{ define "pipeline" }}
+ {{ $root := index . 0 }}
+ {{ $p := index . 1 }}
+
+ {{ block "pipelineHeader" $ }} {{ end }}
+
+{{ end }}
+
+{{ define "pipelineHeader" }}
+ {{ $root := index . 0 }}
+ {{ $p := index . 1 }}
+ {{ with $p }}
+
+
+ {{ $target := .Trigger.TargetRef }}
+ {{ $workflows := .Workflows }}
+ {{ if .Trigger.IsPush }}
+
+ {{ $target }}
+ push
+
+
+ {{ $old := deref .Trigger.PushOldSha }}
+ {{ $new := deref .Trigger.PushNewSha }}
+
+ {{ slice $new 0 8 }}
+ {{ i "arrow-left" "size-4" }}
+ {{ slice $old 0 8 }}
+
+ {{ else if .Trigger.IsPullRequest }}
+
+ pull request
+
+ {{ $target }}
+ {{ i "arrow-left" "size-4" }}
+ {{ .Trigger.PRSourceBranch }}
+
+
+ {{ end }}
+
+
+
+ {{ template "repo/pipelines/fragments/pipelineSymbolLong" . }}
+
+
+
+
+
+
+ {{ $t := .TimeTaken }}
+
+ {{ if $t }}
+
+ {{ else }}
+
+ {{ end }}
+
+
+ {{ end }}
+{{ end }}
diff --git a/appview/pipelines/pipelines.go b/appview/pipelines/pipelines.go
index 4eeff88b..95794509 100644
--- a/appview/pipelines/pipelines.go
+++ b/appview/pipelines/pipelines.go
@@ -56,3 +56,33 @@ func New(
Logger: logger,
}
}
+
+func (p *Pipelines) Index(w http.ResponseWriter, r *http.Request) {
+ user := p.oauth.GetUser(r)
+ l := p.Logger.With("handler", "Index")
+
+ f, err := p.repoResolver.Resolve(r)
+ if err != nil {
+ l.Error("failed to get repo and knot", "err", err)
+ return
+ }
+
+ repoInfo := f.RepoInfo(user)
+
+ ps, err := db.GetPipelineStatuses(
+ p.db,
+ db.FilterEq("repo_owner", repoInfo.OwnerDid),
+ db.FilterEq("repo_name", repoInfo.Name),
+ db.FilterEq("knot", repoInfo.Knot),
+ )
+ if err != nil {
+ l.Error("failed to query db", "err", err)
+ return
+ }
+
+ p.pages.Pipelines(w, pages.PipelinesParams{
+ LoggedInUser: user,
+ RepoInfo: repoInfo,
+ Pipelines: ps,
+ })
+}