+34
api/tangled/pipelinecancelPipeline.go
+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
-2
appview/db/follow.go
-1
appview/db/issues.go
-1
appview/db/issues.go
+1
-1
appview/db/language.go
+1
-1
appview/db/language.go
+6
-6
appview/db/pipeline.go
+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
-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
-1
appview/db/registration.go
+1
-11
appview/db/repos.go
+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
-1
appview/db/star.go
+10
appview/models/pipeline.go
+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
+1
appview/notify/merged_notifier.go
+1
-6
appview/pages/funcmap.go
+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
-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
+1
-1
appview/pages/pages.go
-5
appview/pages/templates/fragments/starBtn-oob.html
-5
appview/pages/templates/fragments/starBtn-oob.html
+3
-1
appview/pages/templates/fragments/starBtn.html
+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
+1
-1
appview/pages/templates/knots/index.html
+6
-6
appview/pages/templates/repo/fragments/backlinks.html
+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
+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
+
<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
+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
+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
-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
-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
-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
+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
+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
-1
spindle/db/repos.go
+24
-10
spindle/engines/nixery/engine.go
+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
+1
-1
spindle/models/pipeline_env.go
+5
-4
spindle/server.go
+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
+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
+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)