Compare changes

Choose any two refs to compare.

+34
api/tangled/pipelinecancelPipeline.go
···
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + package tangled 4 + 5 + // schema: sh.tangled.pipeline.cancelPipeline 6 + 7 + import ( 8 + "context" 9 + 10 + "github.com/bluesky-social/indigo/lex/util" 11 + ) 12 + 13 + const ( 14 + PipelineCancelPipelineNSID = "sh.tangled.pipeline.cancelPipeline" 15 + ) 16 + 17 + // PipelineCancelPipeline_Input is the input argument to a sh.tangled.pipeline.cancelPipeline call. 18 + type PipelineCancelPipeline_Input struct { 19 + // pipeline: pipeline at-uri 20 + Pipeline string `json:"pipeline" cborgen:"pipeline"` 21 + // repo: repo at-uri, spindle can't resolve repo from pipeline at-uri yet 22 + Repo string `json:"repo" cborgen:"repo"` 23 + // workflow: workflow name 24 + Workflow string `json:"workflow" cborgen:"workflow"` 25 + } 26 + 27 + // PipelineCancelPipeline calls the XRPC method "sh.tangled.pipeline.cancelPipeline". 28 + func PipelineCancelPipeline(ctx context.Context, c util.LexClient, input *PipelineCancelPipeline_Input) error { 29 + if err := c.LexDo(ctx, util.Procedure, "application/json", "sh.tangled.pipeline.cancelPipeline", nil, input, nil); err != nil { 30 + return err 31 + } 32 + 33 + return nil 34 + }
-2
appview/db/follow.go
··· 167 if err != nil { 168 return nil, err 169 } 170 - defer rows.Close() 171 - 172 for rows.Next() { 173 var follow models.Follow 174 var followedAt string
··· 167 if err != nil { 168 return nil, err 169 } 170 for rows.Next() { 171 var follow models.Follow 172 var followedAt string
-1
appview/db/issues.go
··· 452 if err != nil { 453 return nil, err 454 } 455 - defer rows.Close() 456 457 for rows.Next() { 458 var comment models.IssueComment
··· 452 if err != nil { 453 return nil, err 454 } 455 456 for rows.Next() { 457 var comment models.IssueComment
+1 -1
appview/db/language.go
··· 28 whereClause, 29 ) 30 rows, err := e.Query(query, args...) 31 if err != nil { 32 return nil, fmt.Errorf("failed to execute query: %w ", err) 33 } 34 - defer rows.Close() 35 36 var langs []models.RepoLanguage 37 for rows.Next() {
··· 28 whereClause, 29 ) 30 rows, err := e.Query(query, args...) 31 + 32 if err != nil { 33 return nil, fmt.Errorf("failed to execute query: %w ", err) 34 } 35 36 var langs []models.RepoLanguage 37 for rows.Next() {
+6 -6
appview/db/pipeline.go
··· 6 "strings" 7 "time" 8 9 "tangled.org/core/appview/models" 10 "tangled.org/core/orm" 11 ) ··· 216 } 217 defer rows.Close() 218 219 - pipelines := make(map[string]models.Pipeline) 220 for rows.Next() { 221 var p models.Pipeline 222 var t models.Trigger ··· 253 p.Trigger = &t 254 p.Statuses = make(map[string]models.WorkflowStatus) 255 256 - k := fmt.Sprintf("%s/%s", p.Knot, p.Rkey) 257 - pipelines[k] = p 258 } 259 260 // get all statuses ··· 314 return nil, fmt.Errorf("invalid status created timestamp %q: %w", created, err) 315 } 316 317 - key := fmt.Sprintf("%s/%s", ps.PipelineKnot, ps.PipelineRkey) 318 319 // extract 320 - pipeline, ok := pipelines[key] 321 if !ok { 322 continue 323 } ··· 331 332 // reassign 333 pipeline.Statuses[ps.Workflow] = statuses 334 - pipelines[key] = pipeline 335 } 336 337 var all []models.Pipeline
··· 6 "strings" 7 "time" 8 9 + "github.com/bluesky-social/indigo/atproto/syntax" 10 "tangled.org/core/appview/models" 11 "tangled.org/core/orm" 12 ) ··· 217 } 218 defer rows.Close() 219 220 + pipelines := make(map[syntax.ATURI]models.Pipeline) 221 for rows.Next() { 222 var p models.Pipeline 223 var t models.Trigger ··· 254 p.Trigger = &t 255 p.Statuses = make(map[string]models.WorkflowStatus) 256 257 + pipelines[p.AtUri()] = p 258 } 259 260 // get all statuses ··· 314 return nil, fmt.Errorf("invalid status created timestamp %q: %w", created, err) 315 } 316 317 + pipelineAt := ps.PipelineAt() 318 319 // extract 320 + pipeline, ok := pipelines[pipelineAt] 321 if !ok { 322 continue 323 } ··· 331 332 // reassign 333 pipeline.Statuses[ps.Workflow] = statuses 334 + pipelines[pipelineAt] = pipeline 335 } 336 337 var all []models.Pipeline
-5
appview/db/profile.go
··· 230 if err != nil { 231 return nil, err 232 } 233 - defer rows.Close() 234 235 profileMap := make(map[string]*models.Profile) 236 for rows.Next() { ··· 271 if err != nil { 272 return nil, err 273 } 274 - defer rows.Close() 275 - 276 idxs := make(map[string]int) 277 for did := range profileMap { 278 idxs[did] = 0 ··· 293 if err != nil { 294 return nil, err 295 } 296 - defer rows.Close() 297 - 298 idxs = make(map[string]int) 299 for did := range profileMap { 300 idxs[did] = 0
··· 230 if err != nil { 231 return nil, err 232 } 233 234 profileMap := make(map[string]*models.Profile) 235 for rows.Next() { ··· 270 if err != nil { 271 return nil, err 272 } 273 idxs := make(map[string]int) 274 for did := range profileMap { 275 idxs[did] = 0 ··· 290 if err != nil { 291 return nil, err 292 } 293 idxs = make(map[string]int) 294 for did := range profileMap { 295 idxs[did] = 0
-1
appview/db/registration.go
··· 38 if err != nil { 39 return nil, err 40 } 41 - defer rows.Close() 42 43 for rows.Next() { 44 var createdAt string
··· 38 if err != nil { 39 return nil, err 40 } 41 42 for rows.Next() { 43 var createdAt string
+1 -11
appview/db/repos.go
··· 56 limitClause, 57 ) 58 rows, err := e.Query(repoQuery, args...) 59 if err != nil { 60 return nil, fmt.Errorf("failed to execute repo query: %w ", err) 61 } 62 - defer rows.Close() 63 64 for rows.Next() { 65 var repo models.Repo ··· 128 if err != nil { 129 return nil, fmt.Errorf("failed to execute labels query: %w ", err) 130 } 131 - defer rows.Close() 132 - 133 for rows.Next() { 134 var repoat, labelat string 135 if err := rows.Scan(&repoat, &labelat); err != nil { ··· 167 if err != nil { 168 return nil, fmt.Errorf("failed to execute lang query: %w ", err) 169 } 170 - defer rows.Close() 171 - 172 for rows.Next() { 173 var repoat, lang string 174 if err := rows.Scan(&repoat, &lang); err != nil { ··· 195 if err != nil { 196 return nil, fmt.Errorf("failed to execute star-count query: %w ", err) 197 } 198 - defer rows.Close() 199 - 200 for rows.Next() { 201 var repoat string 202 var count int ··· 226 if err != nil { 227 return nil, fmt.Errorf("failed to execute issue-count query: %w ", err) 228 } 229 - defer rows.Close() 230 - 231 for rows.Next() { 232 var repoat string 233 var open, closed int ··· 269 if err != nil { 270 return nil, fmt.Errorf("failed to execute pulls-count query: %w ", err) 271 } 272 - defer rows.Close() 273 - 274 for rows.Next() { 275 var repoat string 276 var open, merged, closed, deleted int
··· 56 limitClause, 57 ) 58 rows, err := e.Query(repoQuery, args...) 59 + 60 if err != nil { 61 return nil, fmt.Errorf("failed to execute repo query: %w ", err) 62 } 63 64 for rows.Next() { 65 var repo models.Repo ··· 128 if err != nil { 129 return nil, fmt.Errorf("failed to execute labels query: %w ", err) 130 } 131 for rows.Next() { 132 var repoat, labelat string 133 if err := rows.Scan(&repoat, &labelat); err != nil { ··· 165 if err != nil { 166 return nil, fmt.Errorf("failed to execute lang query: %w ", err) 167 } 168 for rows.Next() { 169 var repoat, lang string 170 if err := rows.Scan(&repoat, &lang); err != nil { ··· 191 if err != nil { 192 return nil, fmt.Errorf("failed to execute star-count query: %w ", err) 193 } 194 for rows.Next() { 195 var repoat string 196 var count int ··· 220 if err != nil { 221 return nil, fmt.Errorf("failed to execute issue-count query: %w ", err) 222 } 223 for rows.Next() { 224 var repoat string 225 var open, closed int ··· 261 if err != nil { 262 return nil, fmt.Errorf("failed to execute pulls-count query: %w ", err) 263 } 264 for rows.Next() { 265 var repoat string 266 var open, merged, closed, deleted int
-1
appview/db/star.go
··· 165 if err != nil { 166 return nil, err 167 } 168 - defer rows.Close() 169 170 starMap := make(map[string][]models.Star) 171 for rows.Next() {
··· 165 if err != nil { 166 return nil, err 167 } 168 169 starMap := make(map[string][]models.Star) 170 for rows.Next() {
+10
appview/models/pipeline.go
··· 1 package models 2 3 import ( 4 "slices" 5 "time" 6 7 "github.com/bluesky-social/indigo/atproto/syntax" 8 "github.com/go-git/go-git/v5/plumbing" 9 spindle "tangled.org/core/spindle/models" 10 "tangled.org/core/workflow" 11 ) ··· 23 // populate when querying for reverse mappings 24 Trigger *Trigger 25 Statuses map[string]WorkflowStatus 26 } 27 28 type WorkflowStatus struct { ··· 128 Error *string 129 ExitCode int 130 }
··· 1 package models 2 3 import ( 4 + "fmt" 5 "slices" 6 "time" 7 8 "github.com/bluesky-social/indigo/atproto/syntax" 9 "github.com/go-git/go-git/v5/plumbing" 10 + "tangled.org/core/api/tangled" 11 spindle "tangled.org/core/spindle/models" 12 "tangled.org/core/workflow" 13 ) ··· 25 // populate when querying for reverse mappings 26 Trigger *Trigger 27 Statuses map[string]WorkflowStatus 28 + } 29 + 30 + func (p *Pipeline) AtUri() syntax.ATURI { 31 + return syntax.ATURI(fmt.Sprintf("at://did:web:%s/%s/%s", p.Knot, tangled.PipelineNSID, p.Rkey)) 32 } 33 34 type WorkflowStatus struct { ··· 134 Error *string 135 ExitCode int 136 } 137 + 138 + func (ps *PipelineStatus) PipelineAt() syntax.ATURI { 139 + return syntax.ATURI(fmt.Sprintf("at://did:web:%s/%s/%s", ps.PipelineKnot, tangled.PipelineNSID, ps.PipelineRkey)) 140 + }
+1
appview/notify/merged_notifier.go
··· 39 v.Call(in) 40 }(n) 41 } 42 } 43 44 func (m *mergedNotifier) NewRepo(ctx context.Context, repo *models.Repo) {
··· 39 v.Call(in) 40 }(n) 41 } 42 + wg.Wait() 43 } 44 45 func (m *mergedNotifier) NewRepo(ctx context.Context, repo *models.Repo) {
+1 -6
appview/pages/funcmap.go
··· 25 "github.com/dustin/go-humanize" 26 "github.com/go-enry/go-enry/v2" 27 "github.com/yuin/goldmark" 28 - emoji "github.com/yuin/goldmark-emoji" 29 "tangled.org/core/appview/filetree" 30 "tangled.org/core/appview/models" 31 "tangled.org/core/appview/pages/markup" ··· 262 }, 263 "description": func(text string) template.HTML { 264 p.rctx.RendererType = markup.RendererTypeDefault 265 - htmlString := p.rctx.RenderMarkdownWith(text, goldmark.New( 266 - goldmark.WithExtensions( 267 - emoji.Emoji, 268 - ), 269 - )) 270 sanitized := p.rctx.SanitizeDescription(htmlString) 271 return template.HTML(sanitized) 272 },
··· 25 "github.com/dustin/go-humanize" 26 "github.com/go-enry/go-enry/v2" 27 "github.com/yuin/goldmark" 28 "tangled.org/core/appview/filetree" 29 "tangled.org/core/appview/models" 30 "tangled.org/core/appview/pages/markup" ··· 261 }, 262 "description": func(text string) template.HTML { 263 p.rctx.RendererType = markup.RendererTypeDefault 264 + htmlString := p.rctx.RenderMarkdownWith(text, goldmark.New()) 265 sanitized := p.rctx.SanitizeDescription(htmlString) 266 return template.HTML(sanitized) 267 },
-2
appview/pages/markup/markdown.go
··· 13 chromahtml "github.com/alecthomas/chroma/v2/formatters/html" 14 "github.com/alecthomas/chroma/v2/styles" 15 "github.com/yuin/goldmark" 16 - "github.com/yuin/goldmark-emoji" 17 highlighting "github.com/yuin/goldmark-highlighting/v2" 18 "github.com/yuin/goldmark/ast" 19 "github.com/yuin/goldmark/extension" ··· 67 ), 68 callout.CalloutExtention, 69 textension.AtExt, 70 - emoji.Emoji, 71 ), 72 goldmark.WithParserOptions( 73 parser.WithAutoHeadingID(),
··· 13 chromahtml "github.com/alecthomas/chroma/v2/formatters/html" 14 "github.com/alecthomas/chroma/v2/styles" 15 "github.com/yuin/goldmark" 16 highlighting "github.com/yuin/goldmark-highlighting/v2" 17 "github.com/yuin/goldmark/ast" 18 "github.com/yuin/goldmark/extension" ··· 66 ), 67 callout.CalloutExtention, 68 textension.AtExt, 69 ), 70 goldmark.WithParserOptions( 71 parser.WithAutoHeadingID(),
+1 -1
appview/pages/pages.go
··· 640 } 641 642 func (p *Pages) StarBtnFragment(w io.Writer, params StarBtnFragmentParams) error { 643 - return p.executePlain("fragments/starBtn-oob", w, params) 644 } 645 646 type RepoIndexParams struct {
··· 640 } 641 642 func (p *Pages) StarBtnFragment(w io.Writer, params StarBtnFragmentParams) error { 643 + return p.executePlain("fragments/starBtn", w, params) 644 } 645 646 type RepoIndexParams struct {
-5
appview/pages/templates/fragments/starBtn-oob.html
··· 1 - {{ define "fragments/starBtn-oob" }} 2 - <div hx-swap-oob='outerHTML:#starBtn[data-star-subject-at="{{ .SubjectAt }}"]'> 3 - {{ template "fragments/starBtn" . }} 4 - </div> 5 - {{ end }}
···
+3 -1
appview/pages/templates/fragments/starBtn.html
··· 1 {{ define "fragments/starBtn" }} 2 - {{/* NOTE: this fragment is always replaced with hx-swap-oob */}} 3 <button 4 id="starBtn" 5 class="btn disabled:opacity-50 disabled:cursor-not-allowed flex gap-2 items-center group" ··· 11 {{ end }} 12 13 hx-trigger="click" 14 hx-disabled-elt="#starBtn" 15 > 16 {{ if .IsStarred }}
··· 1 {{ define "fragments/starBtn" }} 2 <button 3 id="starBtn" 4 class="btn disabled:opacity-50 disabled:cursor-not-allowed flex gap-2 items-center group" ··· 10 {{ end }} 11 12 hx-trigger="click" 13 + hx-target="this" 14 + hx-swap="outerHTML" 15 + hx-swap-oob='outerHTML:#starBtn[data-star-subject-at="{{ .SubjectAt }}"]' 16 hx-disabled-elt="#starBtn" 17 > 18 {{ if .IsStarred }}
+1 -1
appview/pages/templates/knots/index.html
··· 105 {{ define "docsButton" }} 106 <a 107 class="btn flex items-center gap-2" 108 - href="https://tangled.org/@tangled.org/core/blob/master/docs/knot-hosting.md"> 109 {{ i "book" "size-4" }} 110 docs 111 </a>
··· 105 {{ define "docsButton" }} 106 <a 107 class="btn flex items-center gap-2" 108 + href="https://tangled.org/@tangled.org/core/blob/master/docs/spindle/hosting.md"> 109 {{ i "book" "size-4" }} 110 docs 111 </a>
+6 -6
appview/pages/templates/repo/fragments/backlinks.html
··· 14 <div class="flex gap-2 items-center"> 15 {{ if .State.IsClosed }} 16 <span class="text-gray-500 dark:text-gray-400"> 17 - {{ i "ban" "size-3" }} 18 </span> 19 {{ else if eq .Kind.String "issues" }} 20 <span class="text-green-600 dark:text-green-500"> 21 - {{ i "circle-dot" "size-3" }} 22 </span> 23 {{ else if .State.IsOpen }} 24 <span class="text-green-600 dark:text-green-500"> 25 - {{ i "git-pull-request" "size-3" }} 26 </span> 27 {{ else if .State.IsMerged }} 28 <span class="text-purple-600 dark:text-purple-500"> 29 - {{ i "git-merge" "size-3" }} 30 </span> 31 {{ else }} 32 <span class="text-gray-600 dark:text-gray-300"> 33 - {{ i "git-pull-request-closed" "size-3" }} 34 </span> 35 {{ end }} 36 - <a href="{{ . }}" class="line-clamp-1 text-sm"><span class="text-gray-500 dark:text-gray-400">#{{ .SubjectId }}</span> {{ .Title }}</a> 37 </div> 38 {{ if not (eq $.RepoInfo.FullName $repoUrl) }} 39 <div>
··· 14 <div class="flex gap-2 items-center"> 15 {{ if .State.IsClosed }} 16 <span class="text-gray-500 dark:text-gray-400"> 17 + {{ i "ban" "w-4 h-4" }} 18 </span> 19 {{ else if eq .Kind.String "issues" }} 20 <span class="text-green-600 dark:text-green-500"> 21 + {{ i "circle-dot" "w-4 h-4" }} 22 </span> 23 {{ else if .State.IsOpen }} 24 <span class="text-green-600 dark:text-green-500"> 25 + {{ i "git-pull-request" "w-4 h-4" }} 26 </span> 27 {{ else if .State.IsMerged }} 28 <span class="text-purple-600 dark:text-purple-500"> 29 + {{ i "git-merge" "w-4 h-4" }} 30 </span> 31 {{ else }} 32 <span class="text-gray-600 dark:text-gray-300"> 33 + {{ i "git-pull-request-closed" "w-4 h-4" }} 34 </span> 35 {{ end }} 36 + <a href="{{ . }}"><span class="text-gray-500 dark:text-gray-400">#{{ .SubjectId }}</span> {{ .Title }}</a> 37 </div> 38 {{ if not (eq $.RepoInfo.FullName $repoUrl) }} 39 <div>
+10
appview/pages/templates/repo/pipelines/workflow.html
··· 12 {{ block "sidebar" . }} {{ end }} 13 </div> 14 <div class="col-span-1 md:col-span-3"> 15 {{ block "logs" . }} {{ end }} 16 </div> 17 </section>
··· 12 {{ block "sidebar" . }} {{ end }} 13 </div> 14 <div class="col-span-1 md:col-span-3"> 15 + <div class="flex justify-end mb-2"> 16 + <button 17 + class="btn" 18 + hx-post="/{{ $.RepoInfo.FullName }}/pipelines/{{ .Pipeline.Id }}/workflow/{{ .Workflow }}/cancel" 19 + hx-swap="none" 20 + {{ if (index .Pipeline.Statuses .Workflow).Latest.Status.IsFinish -}} 21 + disabled 22 + {{- end }} 23 + >Cancel</button> 24 + </div> 25 {{ block "logs" . }} {{ end }} 26 </div> 27 </section>
+1 -1
appview/pages/templates/strings/string.html
··· 17 <span class="select-none">/</span> 18 <a href="/strings/{{ $ownerId }}/{{ .String.Rkey }}" class="font-bold">{{ .String.Filename }}</a> 19 </div> 20 - <div class="flex gap-2 items-stretch text-base"> 21 {{ if and .LoggedInUser (eq .LoggedInUser.Did .String.Did) }} 22 <a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group" 23 hx-boost="true"
··· 17 <span class="select-none">/</span> 18 <a href="/strings/{{ $ownerId }}/{{ .String.Rkey }}" class="font-bold">{{ .String.Filename }}</a> 19 </div> 20 + <div class="flex gap-2 text-base"> 21 {{ if and .LoggedInUser (eq .LoggedInUser.Did .String.Did) }} 22 <a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group" 23 hx-boost="true"
+82
appview/pipelines/pipelines.go
··· 4 "bytes" 5 "context" 6 "encoding/json" 7 "log/slog" 8 "net/http" 9 "strings" 10 "time" 11 12 "tangled.org/core/appview/config" 13 "tangled.org/core/appview/db" 14 "tangled.org/core/appview/oauth" 15 "tangled.org/core/appview/pages" 16 "tangled.org/core/appview/reporesolver" ··· 41 r.Get("/", p.Index) 42 r.Get("/{pipeline}/workflow/{workflow}", p.Workflow) 43 r.Get("/{pipeline}/workflow/{workflow}/logs", p.Logs) 44 45 return r 46 } ··· 314 } 315 } 316 } 317 } 318 319 // either a message or an error
··· 4 "bytes" 5 "context" 6 "encoding/json" 7 + "fmt" 8 "log/slog" 9 "net/http" 10 "strings" 11 "time" 12 13 + "tangled.org/core/api/tangled" 14 "tangled.org/core/appview/config" 15 "tangled.org/core/appview/db" 16 + "tangled.org/core/appview/models" 17 "tangled.org/core/appview/oauth" 18 "tangled.org/core/appview/pages" 19 "tangled.org/core/appview/reporesolver" ··· 44 r.Get("/", p.Index) 45 r.Get("/{pipeline}/workflow/{workflow}", p.Workflow) 46 r.Get("/{pipeline}/workflow/{workflow}/logs", p.Logs) 47 + r.Post("/{pipeline}/workflow/{workflow}/cancel", p.Cancel) 48 49 return r 50 } ··· 318 } 319 } 320 } 321 + } 322 + 323 + func (p *Pipelines) Cancel(w http.ResponseWriter, r *http.Request) { 324 + l := p.logger.With("handler", "Cancel") 325 + 326 + var ( 327 + pipelineId = chi.URLParam(r, "pipeline") 328 + workflow = chi.URLParam(r, "workflow") 329 + ) 330 + if pipelineId == "" || workflow == "" { 331 + http.Error(w, "missing pipeline ID or workflow", http.StatusBadRequest) 332 + return 333 + } 334 + 335 + f, err := p.repoResolver.Resolve(r) 336 + if err != nil { 337 + l.Error("failed to get repo and knot", "err", err) 338 + http.Error(w, "bad repo/knot", http.StatusBadRequest) 339 + return 340 + } 341 + 342 + pipeline, err := func() (models.Pipeline, error) { 343 + ps, err := db.GetPipelineStatuses( 344 + p.db, 345 + 1, 346 + orm.FilterEq("repo_owner", f.Did), 347 + orm.FilterEq("repo_name", f.Name), 348 + orm.FilterEq("knot", f.Knot), 349 + orm.FilterEq("id", pipelineId), 350 + ) 351 + if err != nil { 352 + return models.Pipeline{}, err 353 + } 354 + if len(ps) != 1 { 355 + return models.Pipeline{}, fmt.Errorf("wrong pipeline count %d", len(ps)) 356 + } 357 + return ps[0], nil 358 + }() 359 + if err != nil { 360 + l.Error("pipeline query failed", "err", err) 361 + http.Error(w, "pipeline not found", http.StatusNotFound) 362 + } 363 + var ( 364 + spindle = f.Spindle 365 + knot = f.Knot 366 + rkey = pipeline.Rkey 367 + ) 368 + 369 + if spindle == "" || knot == "" || rkey == "" { 370 + http.Error(w, "invalid repo info", http.StatusBadRequest) 371 + return 372 + } 373 + 374 + spindleClient, err := p.oauth.ServiceClient( 375 + r, 376 + oauth.WithService(f.Spindle), 377 + oauth.WithLxm(tangled.PipelineCancelPipelineNSID), 378 + oauth.WithExp(60), 379 + oauth.WithDev(false), 380 + oauth.WithTimeout(time.Second*30), // workflow cleanup usually takes time 381 + ) 382 + 383 + err = tangled.PipelineCancelPipeline( 384 + r.Context(), 385 + spindleClient, 386 + &tangled.PipelineCancelPipeline_Input{ 387 + Repo: string(f.RepoAt()), 388 + Pipeline: pipeline.AtUri().String(), 389 + Workflow: workflow, 390 + }, 391 + ) 392 + errorId := "pipeline-action" 393 + if err != nil { 394 + l.Error("failed to cancel pipeline", "err", err) 395 + p.pages.Notice(w, errorId, "Failed to add secret.") 396 + return 397 + } 398 + l.Debug("canceled pipeline", "uri", pipeline.AtUri()) 399 } 400 401 // either a message or an error
-8
appview/pulls/pulls.go
··· 1366 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1367 return 1368 } 1369 - 1370 } 1371 1372 if err = tx.Commit(); err != nil { 1373 log.Println("failed to create pull request", err) 1374 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1375 return 1376 - } 1377 - 1378 - // notify about each pull 1379 - // 1380 - // this is performed after tx.Commit, because it could result in a locked DB otherwise 1381 - for _, p := range stack { 1382 - s.notifier.NewPull(r.Context(), p) 1383 } 1384 1385 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo)
··· 1366 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1367 return 1368 } 1369 } 1370 1371 if err = tx.Commit(); err != nil { 1372 log.Println("failed to create pull request", err) 1373 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1374 return 1375 } 1376 1377 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo)
-1
go.mod
··· 190 github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 191 github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 192 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 193 - github.com/yuin/goldmark-emoji v1.0.6 // indirect 194 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 195 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 196 go.etcd.io/bbolt v1.4.0 // indirect
··· 190 github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 191 github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 192 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 193 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 194 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 195 go.etcd.io/bbolt v1.4.0 // indirect
-2
go.sum
··· 505 github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 506 github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= 507 github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= 508 - github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= 509 - github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= 510 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= 511 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= 512 gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab h1:gK9tS6QJw5F0SIhYJnGG2P83kuabOdmWBbSmZhJkz2A=
··· 505 github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 506 github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= 507 github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= 508 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= 509 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= 510 gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab h1:gK9tS6QJw5F0SIhYJnGG2P83kuabOdmWBbSmZhJkz2A=
+33
lexicons/pipeline/cancelPipeline.json
···
··· 1 + { 2 + "lexicon": 1, 3 + "id": "sh.tangled.pipeline.cancelPipeline", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Cancel a running pipeline", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["repo", "pipeline", "workflow"], 13 + "properties": { 14 + "repo": { 15 + "type": "string", 16 + "format": "at-uri", 17 + "description": "repo at-uri, spindle can't resolve repo from pipeline at-uri yet" 18 + }, 19 + "pipeline": { 20 + "type": "string", 21 + "format": "at-uri", 22 + "description": "pipeline at-uri" 23 + }, 24 + "workflow": { 25 + "type": "string", 26 + "description": "workflow name" 27 + } 28 + } 29 + } 30 + } 31 + } 32 + } 33 + }
+6 -18
spindle/db/events.go
··· 18 EventJson string `json:"event"` 19 } 20 21 - func (d *DB) InsertEvent(event Event, notifier *notifier.Notifier) error { 22 _, err := d.Exec( 23 `insert into events (rkey, nsid, event, created) values (?, ?, ?, ?)`, 24 event.Rkey, ··· 70 return evts, nil 71 } 72 73 - func (d *DB) CreateStatusEvent(rkey string, s tangled.PipelineStatus, n *notifier.Notifier) error { 74 - eventJson, err := json.Marshal(s) 75 - if err != nil { 76 - return err 77 - } 78 - 79 - event := Event{ 80 - Rkey: rkey, 81 - Nsid: tangled.PipelineStatusNSID, 82 - Created: time.Now().UnixNano(), 83 - EventJson: string(eventJson), 84 - } 85 - 86 - return d.InsertEvent(event, n) 87 - } 88 - 89 func (d *DB) createStatusEvent( 90 workflowId models.WorkflowId, 91 statusKind models.StatusKind, ··· 116 EventJson: string(eventJson), 117 } 118 119 - return d.InsertEvent(event, n) 120 121 } 122 ··· 164 165 func (d *DB) StatusFailed(workflowId models.WorkflowId, workflowError string, exitCode int64, n *notifier.Notifier) error { 166 return d.createStatusEvent(workflowId, models.StatusKindFailed, &workflowError, &exitCode, n) 167 } 168 169 func (d *DB) StatusSuccess(workflowId models.WorkflowId, n *notifier.Notifier) error {
··· 18 EventJson string `json:"event"` 19 } 20 21 + func (d *DB) insertEvent(event Event, notifier *notifier.Notifier) error { 22 _, err := d.Exec( 23 `insert into events (rkey, nsid, event, created) values (?, ?, ?, ?)`, 24 event.Rkey, ··· 70 return evts, nil 71 } 72 73 func (d *DB) createStatusEvent( 74 workflowId models.WorkflowId, 75 statusKind models.StatusKind, ··· 100 EventJson: string(eventJson), 101 } 102 103 + return d.insertEvent(event, n) 104 105 } 106 ··· 148 149 func (d *DB) StatusFailed(workflowId models.WorkflowId, workflowError string, exitCode int64, n *notifier.Notifier) error { 150 return d.createStatusEvent(workflowId, models.StatusKindFailed, &workflowError, &exitCode, n) 151 + } 152 + 153 + func (d *DB) StatusCancelled(workflowId models.WorkflowId, workflowError string, exitCode int64, n *notifier.Notifier) error { 154 + return d.createStatusEvent(workflowId, models.StatusKindCancelled, &workflowError, &exitCode, n) 155 } 156 157 func (d *DB) StatusSuccess(workflowId models.WorkflowId, n *notifier.Notifier) error {
-1
spindle/db/repos.go
··· 16 if err != nil { 17 return nil, err 18 } 19 - defer rows.Close() 20 21 var knots []string 22 for rows.Next() {
··· 16 if err != nil { 17 return nil, err 18 } 19 20 var knots []string 21 for rows.Next() {
+24 -10
spindle/engines/nixery/engine.go
··· 179 return err 180 } 181 e.registerCleanup(wid, func(ctx context.Context) error { 182 - return e.docker.NetworkRemove(ctx, networkName(wid)) 183 }) 184 185 addl := wf.Data.(addlFields) ··· 229 return fmt.Errorf("creating container: %w", err) 230 } 231 e.registerCleanup(wid, func(ctx context.Context) error { 232 - err = e.docker.ContainerStop(ctx, resp.ID, container.StopOptions{}) 233 if err != nil { 234 - return err 235 } 236 237 - return e.docker.ContainerRemove(ctx, resp.ID, container.RemoveOptions{ 238 RemoveVolumes: true, 239 RemoveLinks: false, 240 Force: false, 241 }) 242 }) 243 244 err = e.docker.ContainerStart(ctx, resp.ID, container.StartOptions{}) ··· 394 } 395 396 func (e *Engine) DestroyWorkflow(ctx context.Context, wid models.WorkflowId) error { 397 - e.cleanupMu.Lock() 398 - key := wid.String() 399 - 400 - fns := e.cleanup[key] 401 - delete(e.cleanup, key) 402 - e.cleanupMu.Unlock() 403 404 for _, fn := range fns { 405 if err := fn(ctx); err != nil { ··· 415 416 key := wid.String() 417 e.cleanup[key] = append(e.cleanup[key], fn) 418 } 419 420 func networkName(wid models.WorkflowId) string {
··· 179 return err 180 } 181 e.registerCleanup(wid, func(ctx context.Context) error { 182 + err := e.docker.NetworkRemove(ctx, networkName(wid)) 183 + if err != nil { 184 + return fmt.Errorf("removing network: %w", err) 185 + } 186 + return nil 187 }) 188 189 addl := wf.Data.(addlFields) ··· 233 return fmt.Errorf("creating container: %w", err) 234 } 235 e.registerCleanup(wid, func(ctx context.Context) error { 236 + err := e.docker.ContainerStop(ctx, resp.ID, container.StopOptions{}) 237 if err != nil { 238 + return fmt.Errorf("stopping container: %w", err) 239 } 240 241 + err = e.docker.ContainerRemove(ctx, resp.ID, container.RemoveOptions{ 242 RemoveVolumes: true, 243 RemoveLinks: false, 244 Force: false, 245 }) 246 + if err != nil { 247 + return fmt.Errorf("removing container: %w", err) 248 + } 249 + return nil 250 }) 251 252 err = e.docker.ContainerStart(ctx, resp.ID, container.StartOptions{}) ··· 402 } 403 404 func (e *Engine) DestroyWorkflow(ctx context.Context, wid models.WorkflowId) error { 405 + fns := e.drainCleanups(wid) 406 407 for _, fn := range fns { 408 if err := fn(ctx); err != nil { ··· 418 419 key := wid.String() 420 e.cleanup[key] = append(e.cleanup[key], fn) 421 + } 422 + 423 + func (e *Engine) drainCleanups(wid models.WorkflowId) []cleanupFunc { 424 + e.cleanupMu.Lock() 425 + key := wid.String() 426 + 427 + fns := e.cleanup[key] 428 + delete(e.cleanup, key) 429 + e.cleanupMu.Unlock() 430 + 431 + return fns 432 } 433 434 func networkName(wid models.WorkflowId) string {
+1 -1
spindle/models/pipeline_env.go
··· 20 // Standard CI environment variable 21 env["CI"] = "true" 22 23 - env["TANGLED_PIPELINE_ID"] = pipelineId.Rkey 24 25 // Repo info 26 if tr.Repo != nil {
··· 20 // Standard CI environment variable 21 env["CI"] = "true" 22 23 + env["TANGLED_PIPELINE_ID"] = pipelineId.AtUri().String() 24 25 // Repo info 26 if tr.Repo != nil {
+5 -4
spindle/server.go
··· 268 Config: s.cfg, 269 Resolver: s.res, 270 Vault: s.vault, 271 ServiceAuth: serviceAuth, 272 } 273 ··· 302 tpl.TriggerMetadata.Repo.Repo, 303 ) 304 if err != nil { 305 - return err 306 } 307 308 pipelineId := models.PipelineId{ ··· 323 Name: w.Name, 324 }, fmt.Sprintf("unknown engine %#v", w.Engine), -1, s.n) 325 if err != nil { 326 - return err 327 } 328 329 continue ··· 337 338 ewf, err := s.engs[w.Engine].InitWorkflow(*w, tpl) 339 if err != nil { 340 - return err 341 } 342 343 // inject TANGLED_* env vars after InitWorkflow ··· 354 Name: w.Name, 355 }, s.n) 356 if err != nil { 357 - return err 358 } 359 } 360 }
··· 268 Config: s.cfg, 269 Resolver: s.res, 270 Vault: s.vault, 271 + Notifier: s.Notifier(), 272 ServiceAuth: serviceAuth, 273 } 274 ··· 303 tpl.TriggerMetadata.Repo.Repo, 304 ) 305 if err != nil { 306 + return fmt.Errorf("failed to get repo: %w", err) 307 } 308 309 pipelineId := models.PipelineId{ ··· 324 Name: w.Name, 325 }, fmt.Sprintf("unknown engine %#v", w.Engine), -1, s.n) 326 if err != nil { 327 + return fmt.Errorf("db.StatusFailed: %w", err) 328 } 329 330 continue ··· 338 339 ewf, err := s.engs[w.Engine].InitWorkflow(*w, tpl) 340 if err != nil { 341 + return fmt.Errorf("init workflow: %w", err) 342 } 343 344 // inject TANGLED_* env vars after InitWorkflow ··· 355 Name: w.Name, 356 }, s.n) 357 if err != nil { 358 + return fmt.Errorf("db.StatusPending: %w", err) 359 } 360 } 361 }
+97
spindle/xrpc/pipeline_cancelPipeline.go
···
··· 1 + package xrpc 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "net/http" 7 + "strings" 8 + 9 + "github.com/bluesky-social/indigo/api/atproto" 10 + "github.com/bluesky-social/indigo/atproto/syntax" 11 + "github.com/bluesky-social/indigo/xrpc" 12 + securejoin "github.com/cyphar/filepath-securejoin" 13 + "tangled.org/core/api/tangled" 14 + "tangled.org/core/rbac" 15 + "tangled.org/core/spindle/models" 16 + xrpcerr "tangled.org/core/xrpc/errors" 17 + ) 18 + 19 + func (x *Xrpc) CancelPipeline(w http.ResponseWriter, r *http.Request) { 20 + l := x.Logger 21 + fail := func(e xrpcerr.XrpcError) { 22 + l.Error("failed", "kind", e.Tag, "error", e.Message) 23 + writeError(w, e, http.StatusBadRequest) 24 + } 25 + l.Debug("cancel pipeline") 26 + 27 + actorDid, ok := r.Context().Value(ActorDid).(syntax.DID) 28 + if !ok { 29 + fail(xrpcerr.MissingActorDidError) 30 + return 31 + } 32 + 33 + var input tangled.PipelineCancelPipeline_Input 34 + if err := json.NewDecoder(r.Body).Decode(&input); err != nil { 35 + fail(xrpcerr.GenericError(err)) 36 + return 37 + } 38 + 39 + aturi := syntax.ATURI(input.Pipeline) 40 + wid := models.WorkflowId{ 41 + PipelineId: models.PipelineId{ 42 + Knot: strings.TrimPrefix(aturi.Authority().String(), "did:web:"), 43 + Rkey: aturi.RecordKey().String(), 44 + }, 45 + Name: input.Workflow, 46 + } 47 + l.Debug("cancel pipeline", "wid", wid) 48 + 49 + // unfortunately we have to resolve repo-at here 50 + repoAt, err := syntax.ParseATURI(input.Repo) 51 + if err != nil { 52 + fail(xrpcerr.InvalidRepoError(input.Repo)) 53 + return 54 + } 55 + 56 + ident, err := x.Resolver.ResolveIdent(r.Context(), repoAt.Authority().String()) 57 + if err != nil || ident.Handle.IsInvalidHandle() { 58 + fail(xrpcerr.GenericError(fmt.Errorf("failed to resolve handle: %w", err))) 59 + return 60 + } 61 + 62 + xrpcc := xrpc.Client{Host: ident.PDSEndpoint()} 63 + resp, err := atproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) 64 + if err != nil { 65 + fail(xrpcerr.GenericError(err)) 66 + return 67 + } 68 + 69 + repo := resp.Value.Val.(*tangled.Repo) 70 + didSlashRepo, err := securejoin.SecureJoin(ident.DID.String(), repo.Name) 71 + if err != nil { 72 + fail(xrpcerr.GenericError(err)) 73 + return 74 + } 75 + 76 + // TODO: fine-grained role based control 77 + isRepoOwner, err := x.Enforcer.IsRepoOwner(actorDid.String(), rbac.ThisServer, didSlashRepo) 78 + if err != nil || !isRepoOwner { 79 + fail(xrpcerr.AccessControlError(actorDid.String())) 80 + return 81 + } 82 + for _, engine := range x.Engines { 83 + l.Debug("destorying workflow", "wid", wid) 84 + err = engine.DestroyWorkflow(r.Context(), wid) 85 + if err != nil { 86 + fail(xrpcerr.GenericError(fmt.Errorf("dailed to destroy workflow: %w", err))) 87 + return 88 + } 89 + err = x.Db.StatusCancelled(wid, "User canceled the workflow", -1, x.Notifier) 90 + if err != nil { 91 + fail(xrpcerr.GenericError(fmt.Errorf("dailed to emit status failed: %w", err))) 92 + return 93 + } 94 + } 95 + 96 + w.WriteHeader(http.StatusOK) 97 + }
+3
spindle/xrpc/xrpc.go
··· 10 11 "tangled.org/core/api/tangled" 12 "tangled.org/core/idresolver" 13 "tangled.org/core/rbac" 14 "tangled.org/core/spindle/config" 15 "tangled.org/core/spindle/db" ··· 29 Config *config.Config 30 Resolver *idresolver.Resolver 31 Vault secrets.Manager 32 ServiceAuth *serviceauth.ServiceAuth 33 } 34 ··· 41 r.Post("/"+tangled.RepoAddSecretNSID, x.AddSecret) 42 r.Post("/"+tangled.RepoRemoveSecretNSID, x.RemoveSecret) 43 r.Get("/"+tangled.RepoListSecretsNSID, x.ListSecrets) 44 }) 45 46 // service query endpoints (no auth required)
··· 10 11 "tangled.org/core/api/tangled" 12 "tangled.org/core/idresolver" 13 + "tangled.org/core/notifier" 14 "tangled.org/core/rbac" 15 "tangled.org/core/spindle/config" 16 "tangled.org/core/spindle/db" ··· 30 Config *config.Config 31 Resolver *idresolver.Resolver 32 Vault secrets.Manager 33 + Notifier *notifier.Notifier 34 ServiceAuth *serviceauth.ServiceAuth 35 } 36 ··· 43 r.Post("/"+tangled.RepoAddSecretNSID, x.AddSecret) 44 r.Post("/"+tangled.RepoRemoveSecretNSID, x.RemoveSecret) 45 r.Get("/"+tangled.RepoListSecretsNSID, x.ListSecrets) 46 + r.Post("/"+tangled.PipelineCancelPipelineNSID, x.CancelPipeline) 47 }) 48 49 // service query endpoints (no auth required)