This repository has no description
0

Configure Feed

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

appview/pages: load pipeline statuses using htmx in pulls pages

Signed-off-by: oppiliappan <me@oppi.li>

author
oppiliappan
committer
Tangled
date (Jul 24, 2026, 11:00 AM +0300) commit 35883cbb parent 50574018 change-id rznslptn
+100 -115
+1 -1
appview/pages/pages.go
··· 1758 1758 } 1759 1759 1760 1760 func (p *Pages) PipelineStatusesFragment(w io.Writer, params PipelineStatusesParams) error { 1761 - return p.executePlain("repo/fragments/commitPipelineStatuses", w, params) 1761 + return p.executePlain("repo/fragments/pipelineStatuses", w, params) 1762 1762 } 1763 1763 1764 1764 type WorkflowParams struct {
-9
appview/pages/templates/repo/fragments/commitPipelineStatuses.html
··· 1 - {{ define "repo/fragments/commitPipelineStatuses" }} 2 - {{ range $sha, $pipeline := .Pipelines }} 3 - {{ if gt (len $pipeline.Statuses) 0 }} 4 - <span id="commit-pipeline-{{ $sha }}" hx-swap-oob="outerHTML:#commit-pipeline-{{ $sha }}"> 5 - {{ template "repo/pipelines/fragments/pipelineSymbolLong" (dict "RepoInfo" $.RepoInfo "Pipeline" $pipeline) }} 6 - </span> 7 - {{ end }} 8 - {{ end }} 9 - {{ end }}
+9
appview/pages/templates/repo/fragments/pipelineStatuses.html
··· 1 + {{ define "repo/fragments/pipelineStatuses" }} 2 + {{ $root := . }} 3 + {{ range $sha, $pipeline := .Pipelines }} 4 + {{ if gt (len $pipeline.Statuses) 0 }} 5 + {{ $id := printf "pipeline-status-%s" $sha }} 6 + <span id="{{ $id }}" hx-swap-oob="outerHTML:#{{ $id }}">{{ template "repo/pipelines/fragments/pipelineSymbolLong" (dict "RepoInfo" $root.RepoInfo "Pipeline" $pipeline) }}</span> 7 + {{ end }} 8 + {{ end }} 9 + {{ end }}
+1 -1
appview/pages/templates/repo/index.html
··· 254 254 {{ end }} 255 255 </span> 256 256 {{ end }} 257 - <span id="commit-pipeline-{{ .Hash.String }}"></span> 257 + <span id="pipeline-status-{{ .Hash.String }}"></span> 258 258 </div> 259 259 </div> 260 260 {{ end }}
+2 -2
appview/pages/templates/repo/log.html
··· 72 72 73 73 <!-- ci status --> 74 74 <span class="text-xs"> 75 - <span id="commit-pipeline-{{ $commit.Hash.String }}"></span> 75 + <span id="pipeline-status-{{ $commit.Hash.String }}"></span> 76 76 </span> 77 77 </div> 78 78 ··· 151 151 <span>{{ template "repo/fragments/shortTime" $commit.Committer.When }}</span> 152 152 153 153 <!-- ci status --> 154 - <span id="commit-pipeline-{{ $commit.Hash.String }}"></span> 154 + <span id="pipeline-status-{{ $commit.Hash.String }}"></span> 155 155 </div> 156 156 </div> 157 157 {{ end }}
+1 -1
appview/pages/templates/repo/pulls/fragments/pullStack.html
··· 34 34 </div> 35 35 {{ end }} 36 36 <div class="{{ if not $isCurrent }} pl-6 {{ end }} flex-grow min-w-0 w-full py-2"> 37 - {{ template "repo/pulls/fragments/summarizedPullHeader" (list $pull $pipeline) }} 37 + {{ template "repo/pulls/fragments/summarizedPullHeader" (list $pull $pipeline $root.RepoInfo) }} 38 38 </div> 39 39 </div> 40 40 </a>
+2 -4
appview/pages/templates/repo/pulls/fragments/summarizedPullHeader.html
··· 1 1 {{ define "repo/pulls/fragments/summarizedPullHeader" }} 2 2 {{ $pull := index . 0 }} 3 3 {{ $pipeline := index . 1 }} 4 + {{ $repoInfo := index . 2 }} 4 5 {{ with $pull }} 5 6 <div class="flex text-sm items-center justify-between w-full"> 6 7 <div class="flex items-center gap-2 min-w-0 flex-1 pr-2"> ··· 16 17 <div class="flex-shrink-0 flex items-center gap-2"> 17 18 {{ $latestRound := .LastRoundNumber }} 18 19 {{ $commentCount := .TotalComments }} 19 - {{ if and $pipeline $pipeline.Id }} 20 - {{ template "repo/pipelines/fragments/pipelineSymbol" (dict "Pipeline" $pipeline "ShortSummary" true) }} 21 - <span class="before:content-['·'] before:select-none text-gray-500 dark:text-gray-400"></span> 22 - {{ end }} 20 + <span id="pipeline-status-{{ .LatestSha }}" class="contents">{{ if and $pipeline $pipeline.Id }}{{ template "repo/pipelines/fragments/pipelineSymbolLong" (dict "RepoInfo" $repoInfo "Pipeline" $pipeline) }}{{ end }}</span> 23 21 <span> 24 22 <div class="inline-flex items-center gap-1"> 25 23 {{ i "message-square" "w-3 h-3 md:hidden" }}
+10 -6
appview/pages/templates/repo/pulls/pulls.html
··· 102 102 </span> 103 103 </span> 104 104 105 - {{ $pipeline := index $.Pipelines $topPR.LatestSha }} 106 - {{ if and $pipeline $pipeline.Id }} 107 - <span class="before:content-['·']"></span> 108 - {{ template "repo/pipelines/fragments/pipelineSymbol" (dict "Pipeline" $pipeline "ShortSummary" true) }} 109 - {{ end }} 105 + <span id="pipeline-status-{{ $topPR.LatestSha }}" class="contents"></span> 110 106 111 107 {{ $state := $topPR.Labels }} 112 108 {{ range $k, $d := $.LabelDefs }} ··· 144 140 "QueryParams" (queryParams "q" .FilterQuery) 145 141 ) }} 146 142 {{ end }} 143 + {{ if and .Stacks .RepoInfo.Spindle }} 144 + <div 145 + class="hidden" 146 + hx-get="/{{ .RepoInfo.FullName }}/pulls/pipeline-statuses?{{ range .Stacks }}{{ range . }}sha={{ .LatestSha }}&{{ end }}{{ end }}" 147 + hx-trigger="load" 148 + hx-swap="none" 149 + ></div> 150 + {{ end }} 147 151 {{ end }} 148 152 149 153 {{ define "stackedPullList" }} ··· 155 159 <a href="/{{ $root.RepoInfo.FullName }}/pulls/{{ $pull.PullId }}" class="no-underline hover:no-underline hover:bg-gray-100/25 hover:dark:bg-gray-700/25"> 156 160 <div class="flex gap-2 items-center px-6"> 157 161 <div class="flex-grow min-w-0 w-full py-2"> 158 - {{ template "repo/pulls/fragments/summarizedPullHeader" (list $pull $pipeline) }} 162 + {{ template "repo/pulls/fragments/summarizedPullHeader" (list $pull $pipeline $root.RepoInfo) }} 159 163 </div> 160 164 </div> 161 165 </a>
+64
appview/pipelines/pipelines.go
··· 14 14 "tangled.org/core/appview/config" 15 15 "tangled.org/core/appview/db" 16 16 "tangled.org/core/appview/middleware" 17 + "tangled.org/core/appview/models" 17 18 "tangled.org/core/appview/oauth" 18 19 "tangled.org/core/appview/pages" 19 20 "tangled.org/core/appview/reporesolver" ··· 75 76 db: db, 76 77 enforcer: enforcer, 77 78 logger: logger, 79 + } 80 + } 81 + 82 + // FetchStatuses fetches pipelines from the repo's spindle, keyed by commit sha. 83 + func FetchStatuses( 84 + ctx context.Context, 85 + repo *models.Repo, 86 + shas []string, 87 + ) (map[string]types.Pipeline, error) { 88 + m := make(map[string]types.Pipeline) 89 + 90 + if len(shas) == 0 || repo.Spindle == "" { 91 + return m, nil 92 + } 93 + 94 + spindleUrl, err := hostutil.EnsureHttpScheme(repo.Spindle) 95 + if err != nil { 96 + return m, nil 97 + } 98 + 99 + xrpcc := &indigoxrpc.Client{Host: spindleUrl} 100 + out, err := tangled.CiQueryPipelines(ctx, xrpcc, shas, "", nil, 0, repo.RepoDid) 101 + if err != nil { 102 + return nil, err 103 + } 104 + 105 + return types.PipelinesByCommit(out.Pipelines), nil 106 + } 107 + 108 + // StatusesHandler renders the pipeline-statuses fragment for a set of commit 109 + // shas (given as repeated "sha" query params). It backs both the repo's 110 + // commit-statuses endpoint and the pull request's pipeline-statuses endpoint. 111 + func StatusesHandler( 112 + oauth *oauth.OAuth, 113 + repoResolver *reporesolver.RepoResolver, 114 + pgs *pages.Pages, 115 + logger *slog.Logger, 116 + ) http.HandlerFunc { 117 + return func(w http.ResponseWriter, r *http.Request) { 118 + l := logger.With("handler", "StatusesHandler") 119 + 120 + f, err := repoResolver.Resolve(r) 121 + if err != nil { 122 + l.Error("failed to resolve repo", "err", err) 123 + return 124 + } 125 + 126 + user := oauth.GetMultiAccountUser(r) 127 + shas := r.URL.Query()["sha"] 128 + 129 + statuses, err := FetchStatuses(r.Context(), f, shas) 130 + if err != nil { 131 + l.Error("failed to fetch pipeline statuses", "err", err) 132 + return 133 + } 134 + 135 + err = pgs.PipelineStatusesFragment(w, pages.PipelineStatusesParams{ 136 + RepoInfo: repoResolver.GetRepoInfo(r, user), 137 + Pipelines: statuses, 138 + }) 139 + if err != nil { 140 + l.Error("failed to render pipeline statuses", "err", err) 141 + } 78 142 } 79 143 } 80 144
-27
appview/pulls/list.go
··· 14 14 "tangled.org/core/orm" 15 15 16 16 "github.com/bluesky-social/indigo/atproto/syntax" 17 - indigoxrpc "github.com/bluesky-social/indigo/xrpc" 18 - "tangled.org/core/hostutil" 19 - "tangled.org/core/types" 20 17 ) 21 18 22 19 func (s *Pulls) RepoPulls(w http.ResponseWriter, r *http.Request) { ··· 211 208 } 212 209 213 210 var stacks []models.Stack 214 - var shas []string 215 211 216 212 pullMap := make(map[string]*models.Pull) 217 213 for _, p := range pulls { 218 - shas = append(shas, p.LatestSha()) 219 214 pullMap[p.AtUri().String()] = p 220 215 } 221 216 ··· 264 259 stacks = append(stacks, stack) 265 260 } 266 261 267 - // commitId -> latest pipeline 268 - pipelines := func(ctx context.Context, shas []string) map[string]types.Pipeline { 269 - m := make(map[string]types.Pipeline) 270 - if f.Spindle == "" { 271 - return m 272 - } 273 - spindleUrl, err := hostutil.EnsureHttpScheme(f.Spindle) 274 - if err != nil { 275 - l.Error("invalid spindle host", "host", f.Spindle, "err", err) 276 - return m 277 - } 278 - xrpcc := &indigoxrpc.Client{Host: spindleUrl} 279 - out, err := tangled.CiQueryPipelines(ctx, xrpcc, shas, "", nil, 0, f.RepoDid) 280 - if err != nil { 281 - l.Error("failed to fetch pipelines", "err", err) 282 - return m 283 - } 284 - 285 - return types.PipelinesByCommit(out.Pipelines) 286 - }(r.Context(), shas) 287 - 288 262 labelDefs, err := db.GetLabelDefinitions( 289 263 s.db, 290 264 orm.FilterIn("at_uri", f.Labels), ··· 326 300 FilterState: filterState, 327 301 FilterQuery: query.String(), 328 302 Stacks: stacks, 329 - Pipelines: pipelines, 330 303 Page: page, 331 304 PullCount: totalPulls, 332 305 VouchRelationships: vouchRelationships,
+2
appview/pulls/router.go
··· 5 5 6 6 "github.com/go-chi/chi/v5" 7 7 "tangled.org/core/appview/middleware" 8 + "tangled.org/core/appview/pipelines" 8 9 ) 9 10 10 11 func (s *Pulls) Router(mw *middleware.Middleware) http.Handler { 11 12 r := chi.NewRouter() 12 13 r.With(middleware.Paginate).Get("/", s.RepoPulls) 14 + r.Get("/pipeline-statuses", pipelines.StatusesHandler(s.oauth, s.repoResolver, s.pages, s.logger)) 13 15 r.With(middleware.AuthMiddleware(s.oauth)).Route("/new", func(r chi.Router) { 14 16 r.Get("/", s.NewPull) 15 17 r.Get("/refresh", s.RefreshCompose)
+2 -25
appview/repo/index.go
··· 23 23 "tangled.org/core/appview/models" 24 24 "tangled.org/core/appview/pages" 25 25 "tangled.org/core/appview/pages/markup" 26 + "tangled.org/core/appview/pipelines" 26 27 "tangled.org/core/types" 27 28 28 29 "github.com/go-chi/chi/v5" ··· 122 123 } 123 124 pipelineCh := make(chan pipelineResult, 1) 124 125 go func() { 125 - p, err := getPipelineStatuses(r.Context(), f, shas) 126 + p, err := pipelines.FetchStatuses(r.Context(), f, shas) 126 127 pipelineCh <- pipelineResult{p, err} 127 128 }() 128 129 ··· 198 199 EmailToDid: emailToDidMap, 199 200 VerifiedCommits: vc, 200 201 Languages: languageInfo, 201 - }) 202 - } 203 - 204 - func (rp *Repo) PipelineStatuses(w http.ResponseWriter, r *http.Request) { 205 - l := rp.logger.With("handler", "PipelineStatuses") 206 - 207 - f, err := rp.repoResolver.Resolve(r) 208 - if err != nil { 209 - l.Error("failed to resolve repo", "err", err) 210 - return 211 - } 212 - 213 - user := rp.oauth.GetMultiAccountUser(r) 214 - shas := r.URL.Query()["sha"] 215 - 216 - pipelines, err := getPipelineStatuses(r.Context(), f, shas) 217 - if err != nil { 218 - l.Error("failed to fetch pipeline statuses", "err", err) 219 - return 220 - } 221 - 222 - rp.pages.PipelineStatusesFragment(w, pages.PipelineStatusesParams{ 223 - RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 224 - Pipelines: pipelines, 225 202 }) 226 203 } 227 204
+4 -3
appview/repo/log.go
··· 12 12 "tangled.org/core/appview/commitverify" 13 13 "tangled.org/core/appview/db" 14 14 "tangled.org/core/appview/pages" 15 + "tangled.org/core/appview/pipelines" 15 16 "tangled.org/core/types" 16 17 xrpcclient "tangled.org/core/xrpc/xrpcclient" 17 18 ··· 238 239 } 239 240 240 241 user := rp.oauth.GetMultiAccountUser(r) 241 - pipelines, err := getPipelineStatuses(r.Context(), f, []string{result.Diff.Commit.This}) 242 + statuses, err := pipelines.FetchStatuses(r.Context(), f, []string{result.Diff.Commit.This}) 242 243 if err != nil { 243 - l.Error("failed to getPipelineStatuses", "err", err) 244 + l.Error("failed to FetchStatuses", "err", err) 244 245 // non-fatal 245 246 } 246 247 var pipeline *types.Pipeline 247 - if p, ok := pipelines[result.Diff.Commit.This]; ok { 248 + if p, ok := statuses[result.Diff.Commit.This]; ok { 248 249 pipeline = &p 249 250 } 250 251
-35
appview/repo/repo_util.go
··· 1 1 package repo 2 2 3 3 import ( 4 - "context" 5 4 "maps" 6 5 "slices" 7 6 "sort" 8 7 "strings" 9 8 10 - indigoxrpc "github.com/bluesky-social/indigo/xrpc" 11 - "tangled.org/core/api/tangled" 12 - "tangled.org/core/appview/models" 13 - "tangled.org/core/hostutil" 14 9 "tangled.org/core/types" 15 10 ) 16 11 ··· 87 82 88 83 return 89 84 } 90 - 91 - // fetch pipelines from spindle and map by commit sha 92 - func getPipelineStatuses( 93 - ctx context.Context, 94 - repo *models.Repo, 95 - shas []string, 96 - ) (map[string]types.Pipeline, error) { 97 - m := make(map[string]types.Pipeline) 98 - 99 - if len(shas) == 0 { 100 - return m, nil 101 - } 102 - 103 - if repo.Spindle == "" { 104 - return m, nil 105 - } 106 - 107 - spindleUrl, err := hostutil.EnsureHttpScheme(repo.Spindle) 108 - if err != nil { 109 - return m, nil 110 - } 111 - 112 - xrpcc := &indigoxrpc.Client{Host: spindleUrl} 113 - out, err := tangled.CiQueryPipelines(ctx, xrpcc, shas, "", nil, 0, repo.RepoDid) 114 - if err != nil { 115 - return nil, err 116 - } 117 - 118 - return types.PipelinesByCommit(out.Pipelines), nil 119 - }
+2 -1
appview/repo/router.go
··· 5 5 6 6 "github.com/go-chi/chi/v5" 7 7 "tangled.org/core/appview/middleware" 8 + "tangled.org/core/appview/pipelines" 8 9 ) 9 10 10 11 func (rp *Repo) Router(mw *middleware.Middleware) http.Handler { 11 12 r := chi.NewRouter() 12 13 r.Get("/", rp.Index) 13 - r.Get("/commit-statuses", rp.PipelineStatuses) 14 + r.Get("/commit-statuses", pipelines.StatusesHandler(rp.oauth, rp.repoResolver, rp.pages, rp.logger)) 14 15 r.Get("/opengraph", rp.Opengraph) 15 16 r.Get("/feed.atom", rp.AtomFeed) 16 17 r.Get("/commits/{ref}", rp.Log)