-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
6
"strings"
7
7
"time"
8
8
9
-
"github.com/bluesky-social/indigo/atproto/syntax"
10
9
"tangled.org/core/appview/models"
11
10
"tangled.org/core/orm"
12
11
)
···
217
216
}
218
217
defer rows.Close()
219
218
220
-
pipelines := make(map[syntax.ATURI]models.Pipeline)
219
+
pipelines := make(map[string]models.Pipeline)
221
220
for rows.Next() {
222
221
var p models.Pipeline
223
222
var t models.Trigger
···
254
253
p.Trigger = &t
255
254
p.Statuses = make(map[string]models.WorkflowStatus)
256
255
257
-
pipelines[p.AtUri()] = p
256
+
k := fmt.Sprintf("%s/%s", p.Knot, p.Rkey)
257
+
pipelines[k] = p
258
258
}
259
259
260
260
// get all statuses
···
314
314
return nil, fmt.Errorf("invalid status created timestamp %q: %w", created, err)
315
315
}
316
316
317
-
pipelineAt := ps.PipelineAt()
317
+
key := fmt.Sprintf("%s/%s", ps.PipelineKnot, ps.PipelineRkey)
318
318
319
319
// extract
320
-
pipeline, ok := pipelines[pipelineAt]
320
+
pipeline, ok := pipelines[key]
321
321
if !ok {
322
322
continue
323
323
}
···
331
331
332
332
// reassign
333
333
pipeline.Statuses[ps.Workflow] = statuses
334
-
pipelines[pipelineAt] = pipeline
334
+
pipelines[key] = pipeline
335
335
}
336
336
337
337
var all []models.Pipeline
+5
appview/db/profile.go
+5
appview/db/profile.go
···
230
230
if err != nil {
231
231
return nil, err
232
232
}
233
+
defer rows.Close()
233
234
234
235
profileMap := make(map[string]*models.Profile)
235
236
for rows.Next() {
···
270
271
if err != nil {
271
272
return nil, err
272
273
}
274
+
defer rows.Close()
275
+
273
276
idxs := make(map[string]int)
274
277
for did := range profileMap {
275
278
idxs[did] = 0
···
290
293
if err != nil {
291
294
return nil, err
292
295
}
296
+
defer rows.Close()
297
+
293
298
idxs = make(map[string]int)
294
299
for did := range profileMap {
295
300
idxs[did] = 0
+1
appview/db/registration.go
+1
appview/db/registration.go
+11
-1
appview/db/repos.go
+11
-1
appview/db/repos.go
···
56
56
limitClause,
57
57
)
58
58
rows, err := e.Query(repoQuery, args...)
59
-
60
59
if err != nil {
61
60
return nil, fmt.Errorf("failed to execute repo query: %w ", err)
62
61
}
62
+
defer rows.Close()
63
63
64
64
for rows.Next() {
65
65
var repo models.Repo
···
128
128
if err != nil {
129
129
return nil, fmt.Errorf("failed to execute labels query: %w ", err)
130
130
}
131
+
defer rows.Close()
132
+
131
133
for rows.Next() {
132
134
var repoat, labelat string
133
135
if err := rows.Scan(&repoat, &labelat); err != nil {
···
165
167
if err != nil {
166
168
return nil, fmt.Errorf("failed to execute lang query: %w ", err)
167
169
}
170
+
defer rows.Close()
171
+
168
172
for rows.Next() {
169
173
var repoat, lang string
170
174
if err := rows.Scan(&repoat, &lang); err != nil {
···
191
195
if err != nil {
192
196
return nil, fmt.Errorf("failed to execute star-count query: %w ", err)
193
197
}
198
+
defer rows.Close()
199
+
194
200
for rows.Next() {
195
201
var repoat string
196
202
var count int
···
220
226
if err != nil {
221
227
return nil, fmt.Errorf("failed to execute issue-count query: %w ", err)
222
228
}
229
+
defer rows.Close()
230
+
223
231
for rows.Next() {
224
232
var repoat string
225
233
var open, closed int
···
261
269
if err != nil {
262
270
return nil, fmt.Errorf("failed to execute pulls-count query: %w ", err)
263
271
}
272
+
defer rows.Close()
273
+
264
274
for rows.Next() {
265
275
var repoat string
266
276
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
1
package models
2
2
3
3
import (
4
-
"fmt"
5
4
"slices"
6
5
"time"
7
6
8
7
"github.com/bluesky-social/indigo/atproto/syntax"
9
8
"github.com/go-git/go-git/v5/plumbing"
10
-
"tangled.org/core/api/tangled"
11
9
spindle "tangled.org/core/spindle/models"
12
10
"tangled.org/core/workflow"
13
11
)
···
25
23
// populate when querying for reverse mappings
26
24
Trigger *Trigger
27
25
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
26
}
33
27
34
28
type WorkflowStatus struct {
···
134
128
Error *string
135
129
ExitCode int
136
130
}
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
+6
-1
appview/pages/funcmap.go
+6
-1
appview/pages/funcmap.go
···
25
25
"github.com/dustin/go-humanize"
26
26
"github.com/go-enry/go-enry/v2"
27
27
"github.com/yuin/goldmark"
28
+
emoji "github.com/yuin/goldmark-emoji"
28
29
"tangled.org/core/appview/filetree"
29
30
"tangled.org/core/appview/models"
30
31
"tangled.org/core/appview/pages/markup"
···
261
262
},
262
263
"description": func(text string) template.HTML {
263
264
p.rctx.RendererType = markup.RendererTypeDefault
264
-
htmlString := p.rctx.RenderMarkdownWith(text, goldmark.New())
265
+
htmlString := p.rctx.RenderMarkdownWith(text, goldmark.New(
266
+
goldmark.WithExtensions(
267
+
emoji.Emoji,
268
+
),
269
+
))
265
270
sanitized := p.rctx.SanitizeDescription(htmlString)
266
271
return template.HTML(sanitized)
267
272
},
+2
appview/pages/markup/markdown.go
+2
appview/pages/markup/markdown.go
···
13
13
chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
14
14
"github.com/alecthomas/chroma/v2/styles"
15
15
"github.com/yuin/goldmark"
16
+
"github.com/yuin/goldmark-emoji"
16
17
highlighting "github.com/yuin/goldmark-highlighting/v2"
17
18
"github.com/yuin/goldmark/ast"
18
19
"github.com/yuin/goldmark/extension"
···
66
67
),
67
68
callout.CalloutExtention,
68
69
textension.AtExt,
70
+
emoji.Emoji,
69
71
),
70
72
goldmark.WithParserOptions(
71
73
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
+1
-3
appview/pages/templates/fragments/starBtn.html
+1
-3
appview/pages/templates/fragments/starBtn.html
···
1
1
{{ define "fragments/starBtn" }}
2
+
{{/* NOTE: this fragment is always replaced with hx-swap-oob */}}
2
3
<button
3
4
id="starBtn"
4
5
class="btn disabled:opacity-50 disabled:cursor-not-allowed flex gap-2 items-center group"
···
10
11
{{ end }}
11
12
12
13
hx-trigger="click"
13
-
hx-target="this"
14
-
hx-swap="outerHTML"
15
-
hx-swap-oob='outerHTML:#starBtn[data-star-subject-at="{{ .SubjectAt }}"]'
16
14
hx-disabled-elt="#starBtn"
17
15
>
18
16
{{ if .IsStarred }}
+1
-1
appview/pages/templates/knots/index.html
+1
-1
appview/pages/templates/knots/index.html
···
105
105
{{ define "docsButton" }}
106
106
<a
107
107
class="btn flex items-center gap-2"
108
-
href="https://tangled.org/@tangled.org/core/blob/master/docs/spindle/hosting.md">
108
+
href="https://tangled.org/@tangled.org/core/blob/master/docs/knot-hosting.md">
109
109
{{ i "book" "size-4" }}
110
110
docs
111
111
</a>
+1
-1
appview/pages/templates/repo/empty.html
+1
-1
appview/pages/templates/repo/empty.html
···
26
26
{{ else if (and .LoggedInUser (eq .LoggedInUser.Did .RepoInfo.OwnerDid)) }}
27
27
{{ $knot := .RepoInfo.Knot }}
28
28
{{ if eq $knot "knot1.tangled.sh" }}
29
-
{{ $knot = "tangled.sh" }}
29
+
{{ $knot = "tangled.org" }}
30
30
{{ end }}
31
31
<div class="w-full flex place-content-center">
32
32
<div class="py-6 w-fit flex flex-col gap-4">
+6
-6
appview/pages/templates/repo/fragments/backlinks.html
+6
-6
appview/pages/templates/repo/fragments/backlinks.html
···
14
14
<div class="flex gap-2 items-center">
15
15
{{ if .State.IsClosed }}
16
16
<span class="text-gray-500 dark:text-gray-400">
17
-
{{ i "ban" "w-4 h-4" }}
17
+
{{ i "ban" "size-3" }}
18
18
</span>
19
19
{{ else if eq .Kind.String "issues" }}
20
20
<span class="text-green-600 dark:text-green-500">
21
-
{{ i "circle-dot" "w-4 h-4" }}
21
+
{{ i "circle-dot" "size-3" }}
22
22
</span>
23
23
{{ else if .State.IsOpen }}
24
24
<span class="text-green-600 dark:text-green-500">
25
-
{{ i "git-pull-request" "w-4 h-4" }}
25
+
{{ i "git-pull-request" "size-3" }}
26
26
</span>
27
27
{{ else if .State.IsMerged }}
28
28
<span class="text-purple-600 dark:text-purple-500">
29
-
{{ i "git-merge" "w-4 h-4" }}
29
+
{{ i "git-merge" "size-3" }}
30
30
</span>
31
31
{{ else }}
32
32
<span class="text-gray-600 dark:text-gray-300">
33
-
{{ i "git-pull-request-closed" "w-4 h-4" }}
33
+
{{ i "git-pull-request-closed" "size-3" }}
34
34
</span>
35
35
{{ end }}
36
-
<a href="{{ . }}"><span class="text-gray-500 dark:text-gray-400">#{{ .SubjectId }}</span> {{ .Title }}</a>
36
+
<a href="{{ . }}" class="line-clamp-1 text-sm"><span class="text-gray-500 dark:text-gray-400">#{{ .SubjectId }}</span> {{ .Title }}</a>
37
37
</div>
38
38
{{ if not (eq $.RepoInfo.FullName $repoUrl) }}
39
39
<div>
-10
appview/pages/templates/repo/pipelines/workflow.html
-10
appview/pages/templates/repo/pipelines/workflow.html
···
12
12
{{ block "sidebar" . }} {{ end }}
13
13
</div>
14
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
15
{{ block "logs" . }} {{ end }}
26
16
</div>
27
17
</section>
+1
-1
appview/pages/templates/strings/string.html
+1
-1
appview/pages/templates/strings/string.html
···
17
17
<span class="select-none">/</span>
18
18
<a href="/strings/{{ $ownerId }}/{{ .String.Rkey }}" class="font-bold">{{ .String.Filename }}</a>
19
19
</div>
20
-
<div class="flex gap-2 text-base">
20
+
<div class="flex gap-2 items-stretch text-base">
21
21
{{ if and .LoggedInUser (eq .LoggedInUser.Did .String.Did) }}
22
22
<a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group"
23
23
hx-boost="true"
+2
-2
appview/pages/templates/user/fragments/followCard.html
+2
-2
appview/pages/templates/user/fragments/followCard.html
···
6
6
<img class="object-cover rounded-full p-2" src="{{ fullAvatar $userIdent }}" alt="{{ $userIdent }}" />
7
7
</div>
8
8
9
-
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-2 w-full">
9
+
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-2 w-full min-w-0">
10
10
<div class="flex-1 min-h-0 justify-around flex flex-col">
11
11
<a href="/{{ $userIdent }}">
12
12
<span class="font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full">{{ $userIdent | truncateAt30 }}</span>
13
13
</a>
14
14
{{ with .Profile }}
15
-
<p class="text-sm pb-2 md:pb-2">{{.Description}}</p>
15
+
<p class="text-sm pb-2 md:pb-2 break-words">{{.Description}}</p>
16
16
{{ end }}
17
17
<div class="text-sm flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
18
18
<span class="flex-shrink-0">{{ i "users" "size-4" }}</span>
-82
appview/pipelines/pipelines.go
-82
appview/pipelines/pipelines.go
···
4
4
"bytes"
5
5
"context"
6
6
"encoding/json"
7
-
"fmt"
8
7
"log/slog"
9
8
"net/http"
10
9
"strings"
11
10
"time"
12
11
13
-
"tangled.org/core/api/tangled"
14
12
"tangled.org/core/appview/config"
15
13
"tangled.org/core/appview/db"
16
-
"tangled.org/core/appview/models"
17
14
"tangled.org/core/appview/oauth"
18
15
"tangled.org/core/appview/pages"
19
16
"tangled.org/core/appview/reporesolver"
···
44
41
r.Get("/", p.Index)
45
42
r.Get("/{pipeline}/workflow/{workflow}", p.Workflow)
46
43
r.Get("/{pipeline}/workflow/{workflow}/logs", p.Logs)
47
-
r.Post("/{pipeline}/workflow/{workflow}/cancel", p.Cancel)
48
44
49
45
return r
50
46
}
···
318
314
}
319
315
}
320
316
}
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
317
}
400
318
401
319
// either a message or an error
+8
appview/pulls/pulls.go
+8
appview/pulls/pulls.go
···
1366
1366
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1367
1367
return
1368
1368
}
1369
+
1369
1370
}
1370
1371
1371
1372
if err = tx.Commit(); err != nil {
1372
1373
log.Println("failed to create pull request", err)
1373
1374
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1374
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)
1375
1383
}
1376
1384
1377
1385
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo)
+9
-9
flake.lock
+9
-9
flake.lock
···
35
35
"systems": "systems"
36
36
},
37
37
"locked": {
38
-
"lastModified": 1694529238,
39
-
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
38
+
"lastModified": 1731533236,
39
+
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
40
40
"owner": "numtide",
41
41
"repo": "flake-utils",
42
-
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
42
+
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
43
43
"type": "github"
44
44
},
45
45
"original": {
···
56
56
]
57
57
},
58
58
"locked": {
59
-
"lastModified": 1754078208,
60
-
"narHash": "sha256-YVoIFDCDpYuU3riaDEJ3xiGdPOtsx4sR5eTzHTytPV8=",
59
+
"lastModified": 1763982521,
60
+
"narHash": "sha256-ur4QIAHwgFc0vXiaxn5No/FuZicxBr2p0gmT54xZkUQ=",
61
61
"owner": "nix-community",
62
62
"repo": "gomod2nix",
63
-
"rev": "7f963246a71626c7fc70b431a315c4388a0c95cf",
63
+
"rev": "02e63a239d6eabd595db56852535992c898eba72",
64
64
"type": "github"
65
65
},
66
66
"original": {
···
150
150
},
151
151
"nixpkgs": {
152
152
"locked": {
153
-
"lastModified": 1765186076,
154
-
"narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=",
153
+
"lastModified": 1766070988,
154
+
"narHash": "sha256-G/WVghka6c4bAzMhTwT2vjLccg/awmHkdKSd2JrycLc=",
155
155
"owner": "nixos",
156
156
"repo": "nixpkgs",
157
-
"rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8",
157
+
"rev": "c6245e83d836d0433170a16eb185cefe0572f8b8",
158
158
"type": "github"
159
159
},
160
160
"original": {
+2
-1
go.mod
+2
-1
go.mod
···
45
45
github.com/urfave/cli/v3 v3.3.3
46
46
github.com/whyrusleeping/cbor-gen v0.3.1
47
47
github.com/yuin/goldmark v1.7.13
48
+
github.com/yuin/goldmark-emoji v1.0.6
48
49
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
49
50
gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab
50
51
golang.org/x/crypto v0.40.0
51
52
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
52
53
golang.org/x/image v0.31.0
53
54
golang.org/x/net v0.42.0
54
-
golang.org/x/sync v0.17.0
55
55
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da
56
56
gopkg.in/yaml.v3 v3.0.1
57
57
)
···
203
203
go.uber.org/atomic v1.11.0 // indirect
204
204
go.uber.org/multierr v1.11.0 // indirect
205
205
go.uber.org/zap v1.27.0 // indirect
206
+
golang.org/x/sync v0.17.0 // indirect
206
207
golang.org/x/sys v0.34.0 // indirect
207
208
golang.org/x/text v0.29.0 // indirect
208
209
golang.org/x/time v0.12.0 // indirect
+2
go.sum
+2
go.sum
···
505
505
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
506
506
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
507
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=
508
510
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
509
511
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
510
512
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
-
}
+3
nix/gomod2nix.toml
+3
nix/gomod2nix.toml
···
530
530
[mod."github.com/yuin/goldmark"]
531
531
version = "v1.7.13"
532
532
hash = "sha256-vBCxZrPYPc8x/nvAAv3Au59dCCyfS80Vw3/a9EXK7TE="
533
+
[mod."github.com/yuin/goldmark-emoji"]
534
+
version = "v1.0.6"
535
+
hash = "sha256-+d6bZzOPE+JSFsZbQNZMCWE+n3jgcQnkPETVk47mxSY="
533
536
[mod."github.com/yuin/goldmark-highlighting/v2"]
534
537
version = "v2.0.0-20230729083705-37449abec8cc"
535
538
hash = "sha256-HpiwU7jIeDUAg2zOpTIiviQir8dpRPuXYh2nqFFccpg="
+18
-6
spindle/db/events.go
+18
-6
spindle/db/events.go
···
18
18
EventJson string `json:"event"`
19
19
}
20
20
21
-
func (d *DB) insertEvent(event Event, notifier *notifier.Notifier) error {
21
+
func (d *DB) InsertEvent(event Event, notifier *notifier.Notifier) error {
22
22
_, err := d.Exec(
23
23
`insert into events (rkey, nsid, event, created) values (?, ?, ?, ?)`,
24
24
event.Rkey,
···
70
70
return evts, nil
71
71
}
72
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
+
73
89
func (d *DB) createStatusEvent(
74
90
workflowId models.WorkflowId,
75
91
statusKind models.StatusKind,
···
100
116
EventJson: string(eventJson),
101
117
}
102
118
103
-
return d.insertEvent(event, n)
119
+
return d.InsertEvent(event, n)
104
120
105
121
}
106
122
···
148
164
149
165
func (d *DB) StatusFailed(workflowId models.WorkflowId, workflowError string, exitCode int64, n *notifier.Notifier) error {
150
166
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
167
}
156
168
157
169
func (d *DB) StatusSuccess(workflowId models.WorkflowId, n *notifier.Notifier) error {
+1
spindle/db/repos.go
+1
spindle/db/repos.go
+5
-1
spindle/engine/engine.go
+5
-1
spindle/engine/engine.go
···
70
70
}
71
71
defer eng.DestroyWorkflow(ctx, wid)
72
72
73
-
wfLogger, err := models.NewWorkflowLogger(cfg.Server.LogDir, wid)
73
+
secretValues := make([]string, len(allSecrets))
74
+
for i, s := range allSecrets {
75
+
secretValues[i] = s.Value
76
+
}
77
+
wfLogger, err := models.NewWorkflowLogger(cfg.Server.LogDir, wid, secretValues)
74
78
if err != nil {
75
79
l.Warn("failed to setup step logger; logs will not be persisted", "error", err)
76
80
wfLogger = nil
+10
-24
spindle/engines/nixery/engine.go
+10
-24
spindle/engines/nixery/engine.go
···
179
179
return err
180
180
}
181
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
182
+
return e.docker.NetworkRemove(ctx, networkName(wid))
187
183
})
188
184
189
185
addl := wf.Data.(addlFields)
···
233
229
return fmt.Errorf("creating container: %w", err)
234
230
}
235
231
e.registerCleanup(wid, func(ctx context.Context) error {
236
-
err := e.docker.ContainerStop(ctx, resp.ID, container.StopOptions{})
232
+
err = e.docker.ContainerStop(ctx, resp.ID, container.StopOptions{})
237
233
if err != nil {
238
-
return fmt.Errorf("stopping container: %w", err)
234
+
return err
239
235
}
240
236
241
-
err = e.docker.ContainerRemove(ctx, resp.ID, container.RemoveOptions{
237
+
return e.docker.ContainerRemove(ctx, resp.ID, container.RemoveOptions{
242
238
RemoveVolumes: true,
243
239
RemoveLinks: false,
244
240
Force: false,
245
241
})
246
-
if err != nil {
247
-
return fmt.Errorf("removing container: %w", err)
248
-
}
249
-
return nil
250
242
})
251
243
252
244
err = e.docker.ContainerStart(ctx, resp.ID, container.StartOptions{})
···
402
394
}
403
395
404
396
func (e *Engine) DestroyWorkflow(ctx context.Context, wid models.WorkflowId) error {
405
-
fns := e.drainCleanups(wid)
397
+
e.cleanupMu.Lock()
398
+
key := wid.String()
399
+
400
+
fns := e.cleanup[key]
401
+
delete(e.cleanup, key)
402
+
e.cleanupMu.Unlock()
406
403
407
404
for _, fn := range fns {
408
405
if err := fn(ctx); err != nil {
···
418
415
419
416
key := wid.String()
420
417
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
418
}
433
419
434
420
func networkName(wid models.WorkflowId) string {
+6
-1
spindle/models/logger.go
+6
-1
spindle/models/logger.go
···
12
12
type WorkflowLogger struct {
13
13
file *os.File
14
14
encoder *json.Encoder
15
+
mask *SecretMask
15
16
}
16
17
17
-
func NewWorkflowLogger(baseDir string, wid WorkflowId) (*WorkflowLogger, error) {
18
+
func NewWorkflowLogger(baseDir string, wid WorkflowId, secretValues []string) (*WorkflowLogger, error) {
18
19
path := LogFilePath(baseDir, wid)
19
20
20
21
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
···
25
26
return &WorkflowLogger{
26
27
file: file,
27
28
encoder: json.NewEncoder(file),
29
+
mask: NewSecretMask(secretValues),
28
30
}, nil
29
31
}
30
32
···
62
64
63
65
func (w *dataWriter) Write(p []byte) (int, error) {
64
66
line := strings.TrimRight(string(p), "\r\n")
67
+
if w.logger.mask != nil {
68
+
line = w.logger.mask.Mask(line)
69
+
}
65
70
entry := NewDataLogLine(w.idx, line, w.stream)
66
71
if err := w.logger.encoder.Encode(entry); err != nil {
67
72
return 0, err
+1
-1
spindle/models/pipeline_env.go
+1
-1
spindle/models/pipeline_env.go
+51
spindle/models/secret_mask.go
+51
spindle/models/secret_mask.go
···
1
+
package models
2
+
3
+
import (
4
+
"encoding/base64"
5
+
"strings"
6
+
)
7
+
8
+
// SecretMask replaces secret values in strings with "***".
9
+
type SecretMask struct {
10
+
replacer *strings.Replacer
11
+
}
12
+
13
+
// NewSecretMask creates a mask for the given secret values.
14
+
// Also registers base64-encoded variants of each secret.
15
+
func NewSecretMask(values []string) *SecretMask {
16
+
var pairs []string
17
+
18
+
for _, value := range values {
19
+
if value == "" {
20
+
continue
21
+
}
22
+
23
+
pairs = append(pairs, value, "***")
24
+
25
+
b64 := base64.StdEncoding.EncodeToString([]byte(value))
26
+
if b64 != value {
27
+
pairs = append(pairs, b64, "***")
28
+
}
29
+
30
+
b64NoPad := strings.TrimRight(b64, "=")
31
+
if b64NoPad != b64 && b64NoPad != value {
32
+
pairs = append(pairs, b64NoPad, "***")
33
+
}
34
+
}
35
+
36
+
if len(pairs) == 0 {
37
+
return nil
38
+
}
39
+
40
+
return &SecretMask{
41
+
replacer: strings.NewReplacer(pairs...),
42
+
}
43
+
}
44
+
45
+
// Mask replaces all registered secret values with "***".
46
+
func (m *SecretMask) Mask(input string) string {
47
+
if m == nil || m.replacer == nil {
48
+
return input
49
+
}
50
+
return m.replacer.Replace(input)
51
+
}
+135
spindle/models/secret_mask_test.go
+135
spindle/models/secret_mask_test.go
···
1
+
package models
2
+
3
+
import (
4
+
"encoding/base64"
5
+
"testing"
6
+
)
7
+
8
+
func TestSecretMask_BasicMasking(t *testing.T) {
9
+
mask := NewSecretMask([]string{"mysecret123"})
10
+
11
+
input := "The password is mysecret123 in this log"
12
+
expected := "The password is *** in this log"
13
+
14
+
result := mask.Mask(input)
15
+
if result != expected {
16
+
t.Errorf("expected %q, got %q", expected, result)
17
+
}
18
+
}
19
+
20
+
func TestSecretMask_Base64Encoded(t *testing.T) {
21
+
secret := "mysecret123"
22
+
mask := NewSecretMask([]string{secret})
23
+
24
+
b64 := base64.StdEncoding.EncodeToString([]byte(secret))
25
+
input := "Encoded: " + b64
26
+
expected := "Encoded: ***"
27
+
28
+
result := mask.Mask(input)
29
+
if result != expected {
30
+
t.Errorf("expected %q, got %q", expected, result)
31
+
}
32
+
}
33
+
34
+
func TestSecretMask_Base64NoPadding(t *testing.T) {
35
+
// "test" encodes to "dGVzdA==" with padding
36
+
secret := "test"
37
+
mask := NewSecretMask([]string{secret})
38
+
39
+
b64NoPad := "dGVzdA" // base64 without padding
40
+
input := "Token: " + b64NoPad
41
+
expected := "Token: ***"
42
+
43
+
result := mask.Mask(input)
44
+
if result != expected {
45
+
t.Errorf("expected %q, got %q", expected, result)
46
+
}
47
+
}
48
+
49
+
func TestSecretMask_MultipleSecrets(t *testing.T) {
50
+
mask := NewSecretMask([]string{"password1", "apikey123"})
51
+
52
+
input := "Using password1 and apikey123 for auth"
53
+
expected := "Using *** and *** for auth"
54
+
55
+
result := mask.Mask(input)
56
+
if result != expected {
57
+
t.Errorf("expected %q, got %q", expected, result)
58
+
}
59
+
}
60
+
61
+
func TestSecretMask_MultipleOccurrences(t *testing.T) {
62
+
mask := NewSecretMask([]string{"secret"})
63
+
64
+
input := "secret appears twice: secret"
65
+
expected := "*** appears twice: ***"
66
+
67
+
result := mask.Mask(input)
68
+
if result != expected {
69
+
t.Errorf("expected %q, got %q", expected, result)
70
+
}
71
+
}
72
+
73
+
func TestSecretMask_ShortValues(t *testing.T) {
74
+
mask := NewSecretMask([]string{"abc", "xy", ""})
75
+
76
+
if mask == nil {
77
+
t.Fatal("expected non-nil mask")
78
+
}
79
+
80
+
input := "abc xy test"
81
+
expected := "*** *** test"
82
+
result := mask.Mask(input)
83
+
if result != expected {
84
+
t.Errorf("expected %q, got %q", expected, result)
85
+
}
86
+
}
87
+
88
+
func TestSecretMask_NilMask(t *testing.T) {
89
+
var mask *SecretMask
90
+
91
+
input := "some input text"
92
+
result := mask.Mask(input)
93
+
if result != input {
94
+
t.Errorf("expected %q, got %q", input, result)
95
+
}
96
+
}
97
+
98
+
func TestSecretMask_EmptyInput(t *testing.T) {
99
+
mask := NewSecretMask([]string{"secret"})
100
+
101
+
result := mask.Mask("")
102
+
if result != "" {
103
+
t.Errorf("expected empty string, got %q", result)
104
+
}
105
+
}
106
+
107
+
func TestSecretMask_NoMatch(t *testing.T) {
108
+
mask := NewSecretMask([]string{"secretvalue"})
109
+
110
+
input := "nothing to mask here"
111
+
result := mask.Mask(input)
112
+
if result != input {
113
+
t.Errorf("expected %q, got %q", input, result)
114
+
}
115
+
}
116
+
117
+
func TestSecretMask_EmptySecretsList(t *testing.T) {
118
+
mask := NewSecretMask([]string{})
119
+
120
+
if mask != nil {
121
+
t.Error("expected nil mask for empty secrets list")
122
+
}
123
+
}
124
+
125
+
func TestSecretMask_EmptySecretsFiltered(t *testing.T) {
126
+
mask := NewSecretMask([]string{"ab", "validpassword", "", "xyz"})
127
+
128
+
input := "Using validpassword here"
129
+
expected := "Using *** here"
130
+
131
+
result := mask.Mask(input)
132
+
if result != expected {
133
+
t.Errorf("expected %q, got %q", expected, result)
134
+
}
135
+
}
+4
-5
spindle/server.go
+4
-5
spindle/server.go
···
268
268
Config: s.cfg,
269
269
Resolver: s.res,
270
270
Vault: s.vault,
271
-
Notifier: s.Notifier(),
272
271
ServiceAuth: serviceAuth,
273
272
}
274
273
···
303
302
tpl.TriggerMetadata.Repo.Repo,
304
303
)
305
304
if err != nil {
306
-
return fmt.Errorf("failed to get repo: %w", err)
305
+
return err
307
306
}
308
307
309
308
pipelineId := models.PipelineId{
···
324
323
Name: w.Name,
325
324
}, fmt.Sprintf("unknown engine %#v", w.Engine), -1, s.n)
326
325
if err != nil {
327
-
return fmt.Errorf("db.StatusFailed: %w", err)
326
+
return err
328
327
}
329
328
330
329
continue
···
338
337
339
338
ewf, err := s.engs[w.Engine].InitWorkflow(*w, tpl)
340
339
if err != nil {
341
-
return fmt.Errorf("init workflow: %w", err)
340
+
return err
342
341
}
343
342
344
343
// inject TANGLED_* env vars after InitWorkflow
···
355
354
Name: w.Name,
356
355
}, s.n)
357
356
if err != nil {
358
-
return fmt.Errorf("db.StatusPending: %w", err)
357
+
return err
359
358
}
360
359
}
361
360
}
-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
10
11
11
"tangled.org/core/api/tangled"
12
12
"tangled.org/core/idresolver"
13
-
"tangled.org/core/notifier"
14
13
"tangled.org/core/rbac"
15
14
"tangled.org/core/spindle/config"
16
15
"tangled.org/core/spindle/db"
···
30
29
Config *config.Config
31
30
Resolver *idresolver.Resolver
32
31
Vault secrets.Manager
33
-
Notifier *notifier.Notifier
34
32
ServiceAuth *serviceauth.ServiceAuth
35
33
}
36
34
···
43
41
r.Post("/"+tangled.RepoAddSecretNSID, x.AddSecret)
44
42
r.Post("/"+tangled.RepoRemoveSecretNSID, x.RemoveSecret)
45
43
r.Get("/"+tangled.RepoListSecretsNSID, x.ListSecrets)
46
-
r.Post("/"+tangled.PipelineCancelPipelineNSID, x.CancelPipeline)
47
44
})
48
45
49
46
// service query endpoints (no auth required)