Monorepo for Tangled
0

Configure Feed

Select the types of activity you want to include in your feed.

appview: add types.Pipeline for templates to work

also fix a few things in templates themselves

Signed-off-by: dawn <dawn@tangled.org>

authored by

dawn and committed by
Tangled
(Jul 4, 2026, 8:12 AM +0300) cb4ed19f b092670e

+381 -41
+58 -1
api/tangled/cbor_gen.go
··· 1203 1203 } 1204 1204 1205 1205 cw := cbg.NewCborWriter(w) 1206 - fieldCount := 5 1206 + fieldCount := 6 1207 + 1208 + if t.Error == nil { 1209 + fieldCount-- 1210 + } 1207 1211 1208 1212 if t.FinishedAt == nil { 1209 1213 fieldCount-- ··· 1261 1265 } 1262 1266 if _, err := cw.WriteString(string(t.Name)); err != nil { 1263 1267 return err 1268 + } 1269 + 1270 + // t.Error (string) (string) 1271 + if t.Error != nil { 1272 + 1273 + if len("error") > 1000000 { 1274 + return xerrors.Errorf("Value in field \"error\" was too long") 1275 + } 1276 + 1277 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("error"))); err != nil { 1278 + return err 1279 + } 1280 + if _, err := cw.WriteString(string("error")); err != nil { 1281 + return err 1282 + } 1283 + 1284 + if t.Error == nil { 1285 + if _, err := cw.Write(cbg.CborNull); err != nil { 1286 + return err 1287 + } 1288 + } else { 1289 + if len(*t.Error) > 1000000 { 1290 + return xerrors.Errorf("Value in field t.Error was too long") 1291 + } 1292 + 1293 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Error))); err != nil { 1294 + return err 1295 + } 1296 + if _, err := cw.WriteString(string(*t.Error)); err != nil { 1297 + return err 1298 + } 1299 + } 1264 1300 } 1265 1301 1266 1302 // t.Status (string) (string) ··· 1414 1450 } 1415 1451 1416 1452 t.Name = string(sval) 1453 + } 1454 + // t.Error (string) (string) 1455 + case "error": 1456 + 1457 + { 1458 + b, err := cr.ReadByte() 1459 + if err != nil { 1460 + return err 1461 + } 1462 + if b != cbg.CborNull[0] { 1463 + if err := cr.UnreadByte(); err != nil { 1464 + return err 1465 + } 1466 + 1467 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1468 + if err != nil { 1469 + return err 1470 + } 1471 + 1472 + t.Error = (*string)(&sval) 1473 + } 1417 1474 } 1418 1475 // t.Status (string) (string) 1419 1476 case "status":
+1
api/tangled/cidefs.go
··· 74 74 75 75 // CiDefs_Workflow is a "workflow" in the sh.tangled.ci.defs schema. 76 76 type CiDefs_Workflow struct { 77 + Error *string `json:"error,omitempty" cborgen:"error,omitempty"` 77 78 FinishedAt *string `json:"finishedAt,omitempty" cborgen:"finishedAt,omitempty"` 78 79 // id: Spindle-local workflow id. Unique per pipeline, usually same as name. 79 80 Id string `json:"id" cborgen:"id"`
+32 -12
appview/pages/pages.go
··· 242 242 return err 243 243 } 244 244 245 - return tpl.Execute(w, params) 245 + err = tpl.Execute(w, params) 246 + if err != nil { 247 + p.logger.Error("failed to execute template", "template", name, "err", err) 248 + } 249 + return err 246 250 } 247 251 248 252 func (p *Pages) executeLogin(name string, w io.Writer, params any) error { ··· 251 255 return err 252 256 } 253 257 254 - return tpl.ExecuteTemplate(w, "layouts/base", params) 258 + err = tpl.ExecuteTemplate(w, "layouts/base", params) 259 + if err != nil { 260 + p.logger.Error("failed to execute login template", "template", name, "err", err) 261 + } 262 + return err 255 263 } 256 264 257 265 func (p *Pages) execute(name string, w io.Writer, params any) error { ··· 260 268 return err 261 269 } 262 270 263 - return tpl.ExecuteTemplate(w, "layouts/base", params) 271 + err = tpl.ExecuteTemplate(w, "layouts/base", params) 272 + if err != nil { 273 + p.logger.Error("failed to execute template", "template", name, "err", err) 274 + } 275 + return err 264 276 } 265 277 266 278 func (p *Pages) executeRepo(name string, w io.Writer, params any) error { ··· 269 281 return err 270 282 } 271 283 272 - return tpl.ExecuteTemplate(w, "layouts/base", params) 284 + err = tpl.ExecuteTemplate(w, "layouts/base", params) 285 + if err != nil { 286 + p.logger.Error("failed to execute repo template", "template", name, "err", err) 287 + } 288 + return err 273 289 } 274 290 275 291 func (p *Pages) executeProfile(name string, w io.Writer, params any) error { ··· 278 294 return err 279 295 } 280 296 281 - return tpl.ExecuteTemplate(w, "layouts/base", params) 297 + err = tpl.ExecuteTemplate(w, "layouts/base", params) 298 + if err != nil { 299 + p.logger.Error("failed to execute profile template", "template", name, "err", err) 300 + } 301 + return err 282 302 } 283 303 284 304 type DollyParams struct { ··· 891 911 EmailToDid map[string]string 892 912 VerifiedCommits commitverify.VerifiedCommits 893 913 Languages []types.RepoLanguageDetails 894 - Pipelines map[string]*tangled.CiDefs_Pipeline 914 + Pipelines map[string]types.Pipeline 895 915 NeedsKnotUpgrade bool 896 916 KnotUnreachable bool 897 917 types.RepoIndexResponse ··· 960 980 Active string 961 981 EmailToDid map[string]string 962 982 VerifiedCommits commitverify.VerifiedCommits 963 - Pipelines map[string]*tangled.CiDefs_Pipeline 983 + Pipelines map[string]types.Pipeline 964 984 965 985 types.RepoLogResponse 966 986 } ··· 975 995 RepoInfo repoinfo.RepoInfo 976 996 Active string 977 997 EmailToDid map[string]string 978 - Pipeline *tangled.CiDefs_Pipeline 998 + Pipeline *types.Pipeline 979 999 DiffOpts types.DiffOpts 980 1000 981 1001 // singular because it's always going to be just one ··· 1376 1396 FilterQuery string 1377 1397 BaseFilterQuery string 1378 1398 Stacks []models.Stack 1379 - Pipelines map[string]tangled.CiDefs_Pipeline 1399 + Pipelines map[string]types.Pipeline 1380 1400 LabelDefs map[string]*models.LabelDefinition 1381 1401 Page pagination.Page 1382 1402 PullCount int ··· 1416 1436 BranchDeleteStatus *models.BranchDeleteStatus 1417 1437 MergeCheck types.MergeCheckResponse 1418 1438 ResubmitCheck ResubmitResult 1419 - Pipelines map[string]tangled.CiDefs_Pipeline 1439 + Pipelines map[string]types.Pipeline 1420 1440 Diff types.DiffRenderer 1421 1441 DiffOpts types.DiffOpts 1422 1442 ActiveRound int ··· 1588 1608 type PipelinesParams struct { 1589 1609 BaseParams 1590 1610 RepoInfo repoinfo.RepoInfo 1591 - Pipelines []*tangled.CiDefs_Pipeline 1611 + Pipelines []types.Pipeline 1592 1612 Active string 1593 1613 FilterKind string 1594 1614 Total int64 ··· 1642 1662 type WorkflowParams struct { 1643 1663 BaseParams 1644 1664 RepoInfo repoinfo.RepoInfo 1645 - Pipeline *tangled.CiDefs_Pipeline 1665 + Pipeline types.Pipeline 1646 1666 Workflow string 1647 1667 LogUrl string 1648 1668 Active string
+5 -3
appview/pages/templates/repo/index.html
··· 260 260 261 261 <!-- ci status --> 262 262 {{ $pipeline := index $.Pipelines .Hash.String }} 263 - {{ if and $pipeline (gt (len $pipeline.Statuses) 0) }} 264 - <div class="inline-block px-1 select-none after:content-['·']"></div> 265 - {{ template "repo/pipelines/fragments/pipelineSymbolLong" (dict "RepoInfo" $.RepoInfo "Pipeline" $pipeline) }} 263 + {{ if $pipeline }} 264 + {{ if gt (len $pipeline.Statuses) 0 }} 265 + <div class="inline-block px-1 select-none after:content-['·']"></div> 266 + {{ template "repo/pipelines/fragments/pipelineSymbolLong" (dict "RepoInfo" $.RepoInfo "Pipeline" $pipeline) }} 267 + {{ end }} 266 268 {{ end }} 267 269 </div> 268 270 </div>
+11 -7
appview/pages/templates/repo/log.html
··· 73 73 <!-- ci status --> 74 74 <span class="text-xs"> 75 75 {{ $pipeline := index $.Pipelines .Hash.String }} 76 - {{ if and $pipeline (gt (len $pipeline.Statuses) 0) }} 77 - {{ template "repo/pipelines/fragments/pipelineSymbolLong" (dict "Pipeline" $pipeline "RepoInfo" $.RepoInfo) }} 76 + {{ if $pipeline }} 77 + {{ if gt (len $pipeline.Statuses) 0 }} 78 + {{ template "repo/pipelines/fragments/pipelineSymbolLong" (dict "Pipeline" $pipeline "RepoInfo" $.RepoInfo) }} 79 + {{ end }} 78 80 {{ end }} 79 81 </span> 80 82 </div> ··· 157 159 158 160 <!-- ci status --> 159 161 {{ $pipeline := index $.Pipelines .Hash.String }} 160 - {{ if and $pipeline (gt (len $pipeline.Statuses) 0) }} 161 - <div class="inline-block px-1 select-none after:content-['·']"></div> 162 - <span class="text-sm"> 163 - {{ template "repo/pipelines/fragments/pipelineSymbolLong" (dict "Pipeline" $pipeline "RepoInfo" $.RepoInfo) }} 164 - </span> 162 + {{ if $pipeline }} 163 + {{ if gt (len $pipeline.Statuses) 0 }} 164 + <div class="inline-block px-1 select-none after:content-['·']"></div> 165 + <span class="text-sm"> 166 + {{ template "repo/pipelines/fragments/pipelineSymbolLong" (dict "Pipeline" $pipeline "RepoInfo" $.RepoInfo) }} 167 + </span> 168 + {{ end }} 165 169 {{ end }} 166 170 </div> 167 171 </div>
+1 -1
appview/pages/templates/repo/pipelines/fragments/pipelineSymbolLong.html
··· 1 1 {{ define "repo/pipelines/fragments/pipelineSymbolLong" }} 2 2 {{ $pipeline := .Pipeline }} 3 3 {{ $repoinfo := .RepoInfo }} 4 - {{ $popoverId := printf "pipeline-status-%d" $pipeline.Id }} 4 + {{ $popoverId := printf "pipeline-status-%s" $pipeline.Id }} 5 5 6 6 <div class="relative inline-block"> 7 7 <button
+11 -3
appview/pipelines/pipelines.go
··· 20 20 "tangled.org/core/lexutil" 21 21 "tangled.org/core/orm" 22 22 "tangled.org/core/rbac" 23 + "tangled.org/core/types" 23 24 24 25 "github.com/bluesky-social/indigo/atproto/syntax" 25 26 indigoxrpc "github.com/bluesky-social/indigo/xrpc" ··· 122 123 123 124 // sh.tangled.ci.queryPipelines(repo, kind, limit=30) 124 125 xrpcc := indigoxrpc.Client{Host: spindleUrl} 125 - out, err := tangled.CiQueryPipelines(r.Context(), &xrpcc, nil, "", 1, f.RepoDid) 126 + out, err := tangled.CiQueryPipelines(r.Context(), &xrpcc, nil, "", 30, f.RepoDid) 126 127 if err != nil { 127 128 l.Error("failed to fetch pipelines", "err", err) 128 129 p.pages.Pipelines(w, pages.PipelinesParams{ ··· 135 136 return 136 137 } 137 138 139 + var pipelines []types.Pipeline 140 + for _, pipeline := range out.Pipelines { 141 + pipelines = append(pipelines, types.Pipeline{CiDefs_Pipeline: pipeline}) 142 + } 143 + 138 144 p.pages.Pipelines(w, pages.PipelinesParams{ 139 145 BaseParams: pages.BaseParamsFromContext(r.Context()), 140 146 RepoInfo: p.repoResolver.GetRepoInfo(r, user), 141 - Pipelines: out.Pipelines, 147 + Pipelines: pipelines, 142 148 FilterKind: filterKind, 143 149 Total: out.Total, 144 150 }) ··· 212 218 p.pages.Workflow(w, pages.WorkflowParams{ 213 219 BaseParams: pages.BaseParamsFromContext(r.Context()), 214 220 RepoInfo: p.repoResolver.GetRepoInfo(r, user), 215 - Pipeline: out, 221 + Pipeline: types.Pipeline{CiDefs_Pipeline: out}, 216 222 Workflow: workflowName, 217 223 }) 218 224 } ··· 357 363 if err := <-done; !isExpectedClose(err) { 358 364 l.Error("spindle stream error", "err", err) 359 365 } 366 + msg := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "finished") 367 + _ = clientConn.WriteMessage(websocket.CloseMessage, msg) 360 368 return 361 369 } 362 370
+4 -3
appview/pulls/list.go
··· 16 16 "github.com/bluesky-social/indigo/atproto/syntax" 17 17 indigoxrpc "github.com/bluesky-social/indigo/xrpc" 18 18 "tangled.org/core/hostutil" 19 + "tangled.org/core/types" 19 20 ) 20 21 21 22 func (s *Pulls) RepoPulls(w http.ResponseWriter, r *http.Request) { ··· 264 265 } 265 266 266 267 // commitId -> latest pipeline 267 - pipelines := func(ctx context.Context, shas []string) map[string]tangled.CiDefs_Pipeline { 268 - m := make(map[string]tangled.CiDefs_Pipeline) 268 + pipelines := func(ctx context.Context, shas []string) map[string]types.Pipeline { 269 + m := make(map[string]types.Pipeline) 269 270 if f.Spindle == "" { 270 271 return m 271 272 } ··· 285 286 if pipeline == nil { 286 287 continue 287 288 } 288 - m[pipeline.Commit] = *pipeline 289 + m[pipeline.Commit] = types.Pipeline{CiDefs_Pipeline: pipeline} 289 290 } 290 291 return m 291 292 }(r.Context(), shas)
+3 -3
appview/pulls/single.go
··· 161 161 } 162 162 163 163 // commitId -> latest pipeline 164 - pipelines := func(ctx context.Context) map[string]tangled.CiDefs_Pipeline { 165 - m := make(map[string]tangled.CiDefs_Pipeline) 164 + pipelines := func(ctx context.Context) map[string]types.Pipeline { 165 + m := make(map[string]types.Pipeline) 166 166 if f.Spindle == "" { 167 167 return m 168 168 } ··· 182 182 if pipeline == nil { 183 183 continue 184 184 } 185 - m[pipeline.Commit] = *pipeline 185 + m[pipeline.Commit] = types.Pipeline{CiDefs_Pipeline: pipeline} 186 186 } 187 187 return m 188 188 }(r.Context())
+2 -2
appview/repo/log.go
··· 254 254 l.Error("failed to getPipelineStatuses", "err", err) 255 255 // non-fatal 256 256 } 257 - var pipeline *tangled.CiDefs_Pipeline 257 + var pipeline *types.Pipeline 258 258 if p, ok := pipelines[result.Diff.Commit.This]; ok { 259 - pipeline = p 259 + pipeline = &p 260 260 } 261 261 262 262 rp.pages.RepoCommit(w, pages.RepoCommitParams{
+5 -5
appview/repo/repo_util.go
··· 88 88 return 89 89 } 90 90 91 - // fetch pipelines from DB and map by commit sha 91 + // fetch pipelines from spindle and map by commit sha 92 92 func getPipelineStatuses( 93 93 ctx context.Context, 94 94 repo *models.Repo, 95 95 shas []string, 96 - ) (map[string]*tangled.CiDefs_Pipeline, error) { 97 - m := make(map[string]*tangled.CiDefs_Pipeline) 96 + ) (map[string]types.Pipeline, error) { 97 + m := make(map[string]types.Pipeline) 98 98 99 99 if len(shas) == 0 { 100 100 return m, nil ··· 106 106 107 107 spindleUrl, err := hostutil.EnsureHttpScheme(repo.Spindle) 108 108 if err != nil { 109 - return m, nil // Don't block repo rendering on bad spindle configuration 109 + return m, nil 110 110 } 111 111 112 112 xrpcc := &indigoxrpc.Client{Host: spindleUrl} ··· 116 116 } 117 117 118 118 for _, p := range out.Pipelines { 119 - m[p.Commit] = p 119 + m[p.Commit] = types.Pipeline{CiDefs_Pipeline: p} 120 120 } 121 121 122 122 return m, nil
+3
lexicons/ci/defs.json
··· 77 77 "finishedAt": { 78 78 "type": "string", 79 79 "format": "datetime" 80 + }, 81 + "error": { 82 + "type": "string" 80 83 } 81 84 } 82 85 }
+3 -1
spindle/db/pipelines.go
··· 162 162 var workflows []*tangled.CiDefs_Workflow 163 163 for _, wf := range raw.Workflows { 164 164 status := "pending" 165 - var startedAt, finishedAt *string 165 + var startedAt, finishedAt, wfError *string 166 166 167 167 if raw.TriggerMetadata != nil && raw.TriggerMetadata.Repo != nil { 168 168 wfId := models.WorkflowId{ ··· 177 177 if err == nil && wfStatus != nil { 178 178 status = wfStatus.Status 179 179 startedAt, finishedAt = d.GetWorkflowTimes(wfId) 180 + wfError = wfStatus.Error 180 181 } 181 182 } 182 183 ··· 186 187 Status: status, 187 188 StartedAt: startedAt, 188 189 FinishedAt: finishedAt, 190 + Error: wfError, 189 191 }) 190 192 } 191 193
+242
types/pipeline.go
··· 1 + package types 2 + 3 + import ( 4 + "fmt" 5 + "strings" 6 + "time" 7 + 8 + "tangled.org/core/api/tangled" 9 + ) 10 + 11 + type StatusKind string 12 + 13 + func (s StatusKind) String() string { 14 + return string(s) 15 + } 16 + 17 + func (s StatusKind) IsFinish() bool { 18 + switch string(s) { 19 + case "failed", "timeout", "cancelled", "success": 20 + return true 21 + } 22 + return false 23 + } 24 + 25 + func (s StatusKind) IsStart() bool { 26 + switch string(s) { 27 + case "pending", "running": 28 + return true 29 + } 30 + return false 31 + } 32 + 33 + type WorkflowStatus struct { 34 + *tangled.CiDefs_Workflow 35 + PipelineCreatedAt *string 36 + } 37 + 38 + func (w WorkflowStatus) Latest() WorkflowStatus { 39 + return w 40 + } 41 + 42 + func (w WorkflowStatus) Error() string { 43 + if w.CiDefs_Workflow == nil || w.CiDefs_Workflow.Error == nil { 44 + return "" 45 + } 46 + return *w.CiDefs_Workflow.Error 47 + } 48 + 49 + func (w WorkflowStatus) Status() StatusKind { 50 + if w.CiDefs_Workflow == nil { 51 + return "" 52 + } 53 + return StatusKind(w.CiDefs_Workflow.Status) 54 + } 55 + 56 + func (w WorkflowStatus) TimeTaken() time.Duration { 57 + if w.CiDefs_Workflow == nil || w.StartedAt == nil || w.FinishedAt == nil || *w.StartedAt == "" || *w.FinishedAt == "" { 58 + return 0 59 + } 60 + t1, err1 := time.Parse(time.RFC3339, *w.StartedAt) 61 + t2, err2 := time.Parse(time.RFC3339, *w.FinishedAt) 62 + if err1 == nil && err2 == nil && t2.After(t1) { 63 + return t2.Sub(t1) 64 + } 65 + return 0 66 + } 67 + 68 + func (w WorkflowStatus) Created() time.Time { 69 + var timeStr string 70 + if w.CiDefs_Workflow != nil && w.StartedAt != nil && *w.StartedAt != "" { 71 + timeStr = *w.StartedAt 72 + } else if w.CiDefs_Workflow != nil && w.FinishedAt != nil && *w.FinishedAt != "" { 73 + timeStr = *w.FinishedAt 74 + } else if w.PipelineCreatedAt != nil && *w.PipelineCreatedAt != "" { 75 + timeStr = *w.PipelineCreatedAt 76 + } else { 77 + return time.Time{} 78 + } 79 + 80 + t, err := time.Parse(time.RFC3339, timeStr) 81 + if err != nil { 82 + return time.Time{} 83 + } 84 + return t 85 + } 86 + 87 + type Trigger struct { 88 + *tangled.CiDefs_Pipeline_Trigger 89 + } 90 + 91 + func (t Trigger) IsPush() bool { 92 + return t.CiDefs_Pipeline_Trigger != nil && t.CiDefs_Pipeline_Trigger.CiTrigger_Push != nil 93 + } 94 + 95 + func (t Trigger) IsPullRequest() bool { 96 + return t.CiDefs_Pipeline_Trigger != nil && t.CiDefs_Pipeline_Trigger.CiTrigger_PullRequest != nil 97 + } 98 + 99 + func (t Trigger) TargetRef() string { 100 + if t.CiDefs_Pipeline_Trigger == nil { 101 + return "" 102 + } 103 + if t.CiDefs_Pipeline_Trigger.CiTrigger_Push != nil { 104 + ref := t.CiDefs_Pipeline_Trigger.CiTrigger_Push.Ref 105 + if strings.HasPrefix(ref, "refs/heads/") { 106 + return strings.TrimPrefix(ref, "refs/heads/") 107 + } 108 + if strings.HasPrefix(ref, "refs/tags/") { 109 + return strings.TrimPrefix(ref, "refs/tags/") 110 + } 111 + return ref 112 + } 113 + if t.CiDefs_Pipeline_Trigger.CiTrigger_PullRequest != nil { 114 + return t.CiDefs_Pipeline_Trigger.CiTrigger_PullRequest.TargetBranch 115 + } 116 + return "" 117 + } 118 + 119 + func (t Trigger) PRSourceBranch() string { 120 + if t.CiDefs_Pipeline_Trigger == nil || t.CiDefs_Pipeline_Trigger.CiTrigger_PullRequest == nil { 121 + return "" 122 + } 123 + sb := t.CiDefs_Pipeline_Trigger.CiTrigger_PullRequest.SourceBranch 124 + if sb == nil { 125 + return "" 126 + } 127 + return *sb 128 + } 129 + 130 + type Pipeline struct { 131 + *tangled.CiDefs_Pipeline 132 + } 133 + 134 + func (p Pipeline) Valid() bool { 135 + return p.CiDefs_Pipeline != nil 136 + } 137 + 138 + func (p Pipeline) Id() string { 139 + if p.CiDefs_Pipeline == nil { 140 + return "" 141 + } 142 + return p.CiDefs_Pipeline.Id 143 + } 144 + 145 + func (p Pipeline) Statuses() map[string]WorkflowStatus { 146 + m := make(map[string]WorkflowStatus) 147 + if p.CiDefs_Pipeline != nil { 148 + for _, w := range p.CiDefs_Pipeline.Workflows { 149 + m[w.Name] = WorkflowStatus{ 150 + CiDefs_Workflow: w, 151 + PipelineCreatedAt: p.CreatedAt, 152 + } 153 + } 154 + } 155 + return m 156 + } 157 + 158 + func (p Pipeline) Counts() map[string]int { 159 + m := make(map[string]int) 160 + if p.CiDefs_Pipeline != nil { 161 + for _, w := range p.CiDefs_Pipeline.Workflows { 162 + m[w.Status]++ 163 + } 164 + } 165 + return m 166 + } 167 + 168 + func (p Pipeline) ShortStatusSummary() string { 169 + if p.CiDefs_Pipeline == nil { 170 + return "" 171 + } 172 + counts := p.Counts() 173 + total := len(p.CiDefs_Pipeline.Workflows) 174 + successes := counts["success"] 175 + return fmt.Sprintf("%d/%d", successes, total) 176 + } 177 + 178 + func (p Pipeline) LongStatusSummary() string { 179 + if p.CiDefs_Pipeline == nil { 180 + return "" 181 + } 182 + counts := p.Counts() 183 + total := len(p.CiDefs_Pipeline.Workflows) 184 + var parts []string 185 + states := []string{"success", "failed", "timeout", "cancelled", "running", "pending"} 186 + for _, state := range states { 187 + if c, ok := counts[state]; ok { 188 + parts = append(parts, fmt.Sprintf("%d/%d %s", c, total, state)) 189 + } 190 + } 191 + return strings.Join(parts, ", ") 192 + } 193 + 194 + func (p Pipeline) TimeTaken() time.Duration { 195 + if p.CiDefs_Pipeline == nil { 196 + return 0 197 + } 198 + var s time.Duration 199 + for _, w := range p.CiDefs_Pipeline.Workflows { 200 + s += WorkflowStatus{CiDefs_Workflow: w}.TimeTaken() 201 + } 202 + return s 203 + } 204 + 205 + func (p Pipeline) Created() time.Time { 206 + if p.CiDefs_Pipeline == nil || p.CreatedAt == nil || *p.CreatedAt == "" { 207 + return time.Time{} 208 + } 209 + t, err := time.Parse(time.RFC3339, *p.CreatedAt) 210 + if err != nil { 211 + return time.Time{} 212 + } 213 + return t 214 + } 215 + 216 + func (p Pipeline) Trigger() Trigger { 217 + if p.CiDefs_Pipeline == nil { 218 + return Trigger{nil} 219 + } 220 + return Trigger{p.CiDefs_Pipeline.Trigger} 221 + } 222 + 223 + func (p Pipeline) IsResponding() bool { 224 + return p.CiDefs_Pipeline != nil && len(p.CiDefs_Pipeline.Workflows) > 0 225 + } 226 + 227 + func (p Pipeline) Sha() string { 228 + if p.CiDefs_Pipeline == nil { 229 + return "" 230 + } 231 + return p.CiDefs_Pipeline.Commit 232 + } 233 + 234 + func (p Pipeline) Workflows() []string { 235 + var ws []string 236 + if p.CiDefs_Pipeline != nil { 237 + for _, w := range p.CiDefs_Pipeline.Workflows { 238 + ws = append(ws, w.Name) 239 + } 240 + } 241 + return ws 242 + }