+1
-1
spindle/engines/nixery/engine.go
+1
-1
spindle/engines/nixery/engine.go
···
109
109
setup := &setupSteps{}
110
110
111
111
setup.addStep(nixConfStep())
112
-
setup.addStep(cloneStep(twf, *tpl.TriggerMetadata, e.cfg.Server.Dev))
112
+
setup.addStep(models.BuildCloneStep(twf, *tpl.TriggerMetadata, e.cfg.Server.Dev))
113
113
// this step could be empty
114
114
if s := dependencyStep(dwf.Dependencies); s != nil {
115
115
setup.addStep(*s)
-73
spindle/engines/nixery/setup_steps.go
-73
spindle/engines/nixery/setup_steps.go
···
2
2
3
3
import (
4
4
"fmt"
5
-
"path"
6
5
"strings"
7
-
8
-
"tangled.org/core/api/tangled"
9
-
"tangled.org/core/workflow"
10
6
)
11
7
12
8
func nixConfStep() Step {
···
17
13
command: setupCmd,
18
14
name: "Configure Nix",
19
15
}
20
-
}
21
-
22
-
// cloneOptsAsSteps processes clone options and adds corresponding steps
23
-
// to the beginning of the workflow's step list if cloning is not skipped.
24
-
//
25
-
// the steps to do here are:
26
-
// - git init
27
-
// - git remote add origin <url>
28
-
// - git fetch --depth=<d> --recurse-submodules=<yes|no> <sha>
29
-
// - git checkout FETCH_HEAD
30
-
func cloneStep(twf tangled.Pipeline_Workflow, tr tangled.Pipeline_TriggerMetadata, dev bool) Step {
31
-
if twf.Clone.Skip {
32
-
return Step{}
33
-
}
34
-
35
-
var commands []string
36
-
37
-
// initialize git repo in workspace
38
-
commands = append(commands, "git init")
39
-
40
-
// add repo as git remote
41
-
scheme := "https://"
42
-
if dev {
43
-
scheme = "http://"
44
-
tr.Repo.Knot = strings.ReplaceAll(tr.Repo.Knot, "localhost", "host.docker.internal")
45
-
}
46
-
url := scheme + path.Join(tr.Repo.Knot, tr.Repo.Did, tr.Repo.Repo)
47
-
commands = append(commands, fmt.Sprintf("git remote add origin %s", url))
48
-
49
-
// run git fetch
50
-
{
51
-
var fetchArgs []string
52
-
53
-
// default clone depth is 1
54
-
depth := 1
55
-
if twf.Clone.Depth > 1 {
56
-
depth = int(twf.Clone.Depth)
57
-
}
58
-
fetchArgs = append(fetchArgs, fmt.Sprintf("--depth=%d", depth))
59
-
60
-
// optionally recurse submodules
61
-
if twf.Clone.Submodules {
62
-
fetchArgs = append(fetchArgs, "--recurse-submodules=yes")
63
-
}
64
-
65
-
// set remote to fetch from
66
-
fetchArgs = append(fetchArgs, "origin")
67
-
68
-
// set revision to checkout
69
-
switch workflow.TriggerKind(tr.Kind) {
70
-
case workflow.TriggerKindManual:
71
-
// TODO: unimplemented
72
-
case workflow.TriggerKindPush:
73
-
fetchArgs = append(fetchArgs, tr.Push.NewSha)
74
-
case workflow.TriggerKindPullRequest:
75
-
fetchArgs = append(fetchArgs, tr.PullRequest.SourceSha)
76
-
}
77
-
78
-
commands = append(commands, fmt.Sprintf("git fetch %s", strings.Join(fetchArgs, " ")))
79
-
}
80
-
81
-
// run git checkout
82
-
commands = append(commands, "git checkout FETCH_HEAD")
83
-
84
-
cloneStep := Step{
85
-
command: strings.Join(commands, "\n"),
86
-
name: "Clone repository into workspace",
87
-
}
88
-
return cloneStep
89
16
}
90
17
91
18
// dependencyStep processes dependencies defined in the workflow.
+151
spindle/models/clone.go
+151
spindle/models/clone.go
···
1
+
package models
2
+
3
+
import (
4
+
"fmt"
5
+
"strings"
6
+
7
+
"tangled.org/core/api/tangled"
8
+
"tangled.org/core/workflow"
9
+
)
10
+
11
+
type CloneStep struct {
12
+
name string
13
+
kind StepKind
14
+
commands []string
15
+
}
16
+
17
+
func (s CloneStep) Name() string {
18
+
return s.name
19
+
}
20
+
21
+
func (s CloneStep) Commands() []string {
22
+
return s.commands
23
+
}
24
+
25
+
func (s CloneStep) Command() string {
26
+
return strings.Join(s.commands, "\n")
27
+
}
28
+
29
+
func (s CloneStep) Kind() StepKind {
30
+
return s.kind
31
+
}
32
+
33
+
// BuildCloneStep generates git clone commands.
34
+
// The caller must ensure the current working directory is set to the desired
35
+
// workspace directory before executing these commands.
36
+
//
37
+
// The generated commands are:
38
+
// - git init
39
+
// - git remote add origin <url>
40
+
// - git fetch --depth=<d> --recurse-submodules=<yes|no> <sha>
41
+
// - git checkout FETCH_HEAD
42
+
//
43
+
// Supports all trigger types (push, PR, manual) and clone options.
44
+
func BuildCloneStep(twf tangled.Pipeline_Workflow, tr tangled.Pipeline_TriggerMetadata, dev bool) CloneStep {
45
+
if twf.Clone != nil && twf.Clone.Skip {
46
+
return CloneStep{}
47
+
}
48
+
49
+
commitSHA, err := extractCommitSHA(tr)
50
+
if err != nil {
51
+
return CloneStep{
52
+
kind: StepKindSystem,
53
+
name: "Clone repository into workspace (error)",
54
+
commands: []string{fmt.Sprintf("echo 'Failed to get clone info: %s' && exit 1", err.Error())},
55
+
}
56
+
}
57
+
58
+
repoURL := buildRepoURL(tr, dev)
59
+
60
+
var cloneOpts tangled.Pipeline_CloneOpts
61
+
if twf.Clone != nil {
62
+
cloneOpts = *twf.Clone
63
+
}
64
+
fetchArgs := buildFetchArgs(cloneOpts, commitSHA)
65
+
66
+
return CloneStep{
67
+
kind: StepKindSystem,
68
+
name: "Clone repository into workspace",
69
+
commands: []string{
70
+
"git init",
71
+
fmt.Sprintf("git remote add origin %s", repoURL),
72
+
fmt.Sprintf("git fetch %s", strings.Join(fetchArgs, " ")),
73
+
"git checkout FETCH_HEAD",
74
+
},
75
+
}
76
+
}
77
+
78
+
// extractCommitSHA extracts the commit SHA from trigger metadata based on trigger type
79
+
func extractCommitSHA(tr tangled.Pipeline_TriggerMetadata) (string, error) {
80
+
switch workflow.TriggerKind(tr.Kind) {
81
+
case workflow.TriggerKindPush:
82
+
if tr.Push == nil {
83
+
return "", fmt.Errorf("push trigger metadata is nil")
84
+
}
85
+
return tr.Push.NewSha, nil
86
+
87
+
case workflow.TriggerKindPullRequest:
88
+
if tr.PullRequest == nil {
89
+
return "", fmt.Errorf("pull request trigger metadata is nil")
90
+
}
91
+
return tr.PullRequest.SourceSha, nil
92
+
93
+
case workflow.TriggerKindManual:
94
+
// Manual triggers don't have an explicit SHA in the metadata
95
+
// For now, return empty string - could be enhanced to fetch from default branch
96
+
// TODO: Implement manual trigger SHA resolution (fetch default branch HEAD)
97
+
return "", nil
98
+
99
+
default:
100
+
return "", fmt.Errorf("unknown trigger kind: %s", tr.Kind)
101
+
}
102
+
}
103
+
104
+
// buildRepoURL constructs the repository URL from trigger metadata
105
+
func buildRepoURL(tr tangled.Pipeline_TriggerMetadata, devMode bool) string {
106
+
if tr.Repo == nil {
107
+
return ""
108
+
}
109
+
110
+
// Determine protocol
111
+
scheme := "https://"
112
+
if devMode {
113
+
scheme = "http://"
114
+
}
115
+
116
+
// Get host from knot
117
+
host := tr.Repo.Knot
118
+
119
+
// In dev mode, replace localhost with host.docker.internal for Docker networking
120
+
if devMode && strings.Contains(host, "localhost") {
121
+
host = strings.ReplaceAll(host, "localhost", "host.docker.internal")
122
+
}
123
+
124
+
// Build URL: {scheme}{knot}/{did}/{repo}
125
+
return fmt.Sprintf("%s%s/%s/%s", scheme, host, tr.Repo.Did, tr.Repo.Repo)
126
+
}
127
+
128
+
// buildFetchArgs constructs the arguments for git fetch based on clone options
129
+
func buildFetchArgs(clone tangled.Pipeline_CloneOpts, sha string) []string {
130
+
args := []string{}
131
+
132
+
// Set fetch depth (default to 1 for shallow clone)
133
+
depth := clone.Depth
134
+
if depth == 0 {
135
+
depth = 1
136
+
}
137
+
args = append(args, fmt.Sprintf("--depth=%d", depth))
138
+
139
+
// Add submodules if requested
140
+
if clone.Submodules {
141
+
args = append(args, "--recurse-submodules=yes")
142
+
}
143
+
144
+
// Add remote and SHA
145
+
args = append(args, "origin")
146
+
if sha != "" {
147
+
args = append(args, sha)
148
+
}
149
+
150
+
return args
151
+
}
+371
spindle/models/clone_test.go
+371
spindle/models/clone_test.go
···
1
+
package models
2
+
3
+
import (
4
+
"strings"
5
+
"testing"
6
+
7
+
"tangled.org/core/api/tangled"
8
+
"tangled.org/core/workflow"
9
+
)
10
+
11
+
func TestBuildCloneStep_PushTrigger(t *testing.T) {
12
+
twf := tangled.Pipeline_Workflow{
13
+
Clone: &tangled.Pipeline_CloneOpts{
14
+
Depth: 1,
15
+
Submodules: false,
16
+
Skip: false,
17
+
},
18
+
}
19
+
tr := tangled.Pipeline_TriggerMetadata{
20
+
Kind: string(workflow.TriggerKindPush),
21
+
Push: &tangled.Pipeline_PushTriggerData{
22
+
NewSha: "abc123",
23
+
OldSha: "def456",
24
+
Ref: "refs/heads/main",
25
+
},
26
+
Repo: &tangled.Pipeline_TriggerRepo{
27
+
Knot: "example.com",
28
+
Did: "did:plc:user123",
29
+
Repo: "my-repo",
30
+
},
31
+
}
32
+
33
+
step := BuildCloneStep(twf, tr, false)
34
+
35
+
if step.Kind() != StepKindSystem {
36
+
t.Errorf("Expected StepKindSystem, got %v", step.Kind())
37
+
}
38
+
39
+
if step.Name() != "Clone repository into workspace" {
40
+
t.Errorf("Expected 'Clone repository into workspace', got '%s'", step.Name())
41
+
}
42
+
43
+
commands := step.Commands()
44
+
if len(commands) != 4 {
45
+
t.Errorf("Expected 4 commands, got %d", len(commands))
46
+
}
47
+
48
+
// Verify commands contain expected git operations
49
+
allCmds := strings.Join(commands, " ")
50
+
if !strings.Contains(allCmds, "git init") {
51
+
t.Error("Commands should contain 'git init'")
52
+
}
53
+
if !strings.Contains(allCmds, "git remote add origin") {
54
+
t.Error("Commands should contain 'git remote add origin'")
55
+
}
56
+
if !strings.Contains(allCmds, "git fetch") {
57
+
t.Error("Commands should contain 'git fetch'")
58
+
}
59
+
if !strings.Contains(allCmds, "abc123") {
60
+
t.Error("Commands should contain commit SHA")
61
+
}
62
+
if !strings.Contains(allCmds, "git checkout FETCH_HEAD") {
63
+
t.Error("Commands should contain 'git checkout FETCH_HEAD'")
64
+
}
65
+
if !strings.Contains(allCmds, "https://example.com/did:plc:user123/my-repo") {
66
+
t.Error("Commands should contain expected repo URL")
67
+
}
68
+
}
69
+
70
+
func TestBuildCloneStep_PullRequestTrigger(t *testing.T) {
71
+
twf := tangled.Pipeline_Workflow{
72
+
Clone: &tangled.Pipeline_CloneOpts{
73
+
Depth: 1,
74
+
Skip: false,
75
+
},
76
+
}
77
+
tr := tangled.Pipeline_TriggerMetadata{
78
+
Kind: string(workflow.TriggerKindPullRequest),
79
+
PullRequest: &tangled.Pipeline_PullRequestTriggerData{
80
+
SourceSha: "pr-sha-789",
81
+
SourceBranch: "feature-branch",
82
+
TargetBranch: "main",
83
+
Action: "opened",
84
+
},
85
+
Repo: &tangled.Pipeline_TriggerRepo{
86
+
Knot: "example.com",
87
+
Did: "did:plc:user123",
88
+
Repo: "my-repo",
89
+
},
90
+
}
91
+
92
+
step := BuildCloneStep(twf, tr, false)
93
+
94
+
allCmds := strings.Join(step.Commands(), " ")
95
+
if !strings.Contains(allCmds, "pr-sha-789") {
96
+
t.Error("Commands should contain PR commit SHA")
97
+
}
98
+
}
99
+
100
+
func TestBuildCloneStep_ManualTrigger(t *testing.T) {
101
+
twf := tangled.Pipeline_Workflow{
102
+
Clone: &tangled.Pipeline_CloneOpts{
103
+
Depth: 1,
104
+
Skip: false,
105
+
},
106
+
}
107
+
tr := tangled.Pipeline_TriggerMetadata{
108
+
Kind: string(workflow.TriggerKindManual),
109
+
Manual: &tangled.Pipeline_ManualTriggerData{
110
+
Inputs: nil,
111
+
},
112
+
Repo: &tangled.Pipeline_TriggerRepo{
113
+
Knot: "example.com",
114
+
Did: "did:plc:user123",
115
+
Repo: "my-repo",
116
+
},
117
+
}
118
+
119
+
step := BuildCloneStep(twf, tr, false)
120
+
121
+
// Manual triggers don't have a SHA yet (TODO), so git fetch won't include a SHA
122
+
allCmds := strings.Join(step.Commands(), " ")
123
+
// Should still have basic git commands
124
+
if !strings.Contains(allCmds, "git init") {
125
+
t.Error("Commands should contain 'git init'")
126
+
}
127
+
if !strings.Contains(allCmds, "git fetch") {
128
+
t.Error("Commands should contain 'git fetch'")
129
+
}
130
+
}
131
+
132
+
func TestBuildCloneStep_SkipFlag(t *testing.T) {
133
+
twf := tangled.Pipeline_Workflow{
134
+
Clone: &tangled.Pipeline_CloneOpts{
135
+
Skip: true,
136
+
},
137
+
}
138
+
tr := tangled.Pipeline_TriggerMetadata{
139
+
Kind: string(workflow.TriggerKindPush),
140
+
Push: &tangled.Pipeline_PushTriggerData{
141
+
NewSha: "abc123",
142
+
},
143
+
Repo: &tangled.Pipeline_TriggerRepo{
144
+
Knot: "example.com",
145
+
Did: "did:plc:user123",
146
+
Repo: "my-repo",
147
+
},
148
+
}
149
+
150
+
step := BuildCloneStep(twf, tr, false)
151
+
152
+
// Empty step when skip is true
153
+
if step.Name() != "" {
154
+
t.Error("Expected empty step name when Skip is true")
155
+
}
156
+
if len(step.Commands()) != 0 {
157
+
t.Errorf("Expected no commands when Skip is true, got %d commands", len(step.Commands()))
158
+
}
159
+
}
160
+
161
+
func TestBuildCloneStep_DevMode(t *testing.T) {
162
+
twf := tangled.Pipeline_Workflow{
163
+
Clone: &tangled.Pipeline_CloneOpts{
164
+
Depth: 1,
165
+
Skip: false,
166
+
},
167
+
}
168
+
tr := tangled.Pipeline_TriggerMetadata{
169
+
Kind: string(workflow.TriggerKindPush),
170
+
Push: &tangled.Pipeline_PushTriggerData{
171
+
NewSha: "abc123",
172
+
},
173
+
Repo: &tangled.Pipeline_TriggerRepo{
174
+
Knot: "localhost:3000",
175
+
Did: "did:plc:user123",
176
+
Repo: "my-repo",
177
+
},
178
+
}
179
+
180
+
step := BuildCloneStep(twf, tr, true)
181
+
182
+
// In dev mode, should use http:// and replace localhost with host.docker.internal
183
+
allCmds := strings.Join(step.Commands(), " ")
184
+
expectedURL := "http://host.docker.internal:3000/did:plc:user123/my-repo"
185
+
if !strings.Contains(allCmds, expectedURL) {
186
+
t.Errorf("Expected dev mode URL '%s' in commands", expectedURL)
187
+
}
188
+
}
189
+
190
+
func TestBuildCloneStep_DepthAndSubmodules(t *testing.T) {
191
+
twf := tangled.Pipeline_Workflow{
192
+
Clone: &tangled.Pipeline_CloneOpts{
193
+
Depth: 10,
194
+
Submodules: true,
195
+
Skip: false,
196
+
},
197
+
}
198
+
tr := tangled.Pipeline_TriggerMetadata{
199
+
Kind: string(workflow.TriggerKindPush),
200
+
Push: &tangled.Pipeline_PushTriggerData{
201
+
NewSha: "abc123",
202
+
},
203
+
Repo: &tangled.Pipeline_TriggerRepo{
204
+
Knot: "example.com",
205
+
Did: "did:plc:user123",
206
+
Repo: "my-repo",
207
+
},
208
+
}
209
+
210
+
step := BuildCloneStep(twf, tr, false)
211
+
212
+
allCmds := strings.Join(step.Commands(), " ")
213
+
if !strings.Contains(allCmds, "--depth=10") {
214
+
t.Error("Commands should contain '--depth=10'")
215
+
}
216
+
217
+
if !strings.Contains(allCmds, "--recurse-submodules=yes") {
218
+
t.Error("Commands should contain '--recurse-submodules=yes'")
219
+
}
220
+
}
221
+
222
+
func TestBuildCloneStep_DefaultDepth(t *testing.T) {
223
+
twf := tangled.Pipeline_Workflow{
224
+
Clone: &tangled.Pipeline_CloneOpts{
225
+
Depth: 0, // Default should be 1
226
+
Skip: false,
227
+
},
228
+
}
229
+
tr := tangled.Pipeline_TriggerMetadata{
230
+
Kind: string(workflow.TriggerKindPush),
231
+
Push: &tangled.Pipeline_PushTriggerData{
232
+
NewSha: "abc123",
233
+
},
234
+
Repo: &tangled.Pipeline_TriggerRepo{
235
+
Knot: "example.com",
236
+
Did: "did:plc:user123",
237
+
Repo: "my-repo",
238
+
},
239
+
}
240
+
241
+
step := BuildCloneStep(twf, tr, false)
242
+
243
+
allCmds := strings.Join(step.Commands(), " ")
244
+
if !strings.Contains(allCmds, "--depth=1") {
245
+
t.Error("Commands should default to '--depth=1'")
246
+
}
247
+
}
248
+
249
+
func TestBuildCloneStep_NilPushData(t *testing.T) {
250
+
twf := tangled.Pipeline_Workflow{
251
+
Clone: &tangled.Pipeline_CloneOpts{
252
+
Depth: 1,
253
+
Skip: false,
254
+
},
255
+
}
256
+
tr := tangled.Pipeline_TriggerMetadata{
257
+
Kind: string(workflow.TriggerKindPush),
258
+
Push: nil, // Nil push data should create error step
259
+
Repo: &tangled.Pipeline_TriggerRepo{
260
+
Knot: "example.com",
261
+
Did: "did:plc:user123",
262
+
Repo: "my-repo",
263
+
},
264
+
}
265
+
266
+
step := BuildCloneStep(twf, tr, false)
267
+
268
+
// Should return an error step
269
+
if !strings.Contains(step.Name(), "error") {
270
+
t.Error("Expected error in step name when push data is nil")
271
+
}
272
+
273
+
allCmds := strings.Join(step.Commands(), " ")
274
+
if !strings.Contains(allCmds, "Failed to get clone info") {
275
+
t.Error("Commands should contain error message")
276
+
}
277
+
if !strings.Contains(allCmds, "exit 1") {
278
+
t.Error("Commands should exit with error")
279
+
}
280
+
}
281
+
282
+
func TestBuildCloneStep_NilPRData(t *testing.T) {
283
+
twf := tangled.Pipeline_Workflow{
284
+
Clone: &tangled.Pipeline_CloneOpts{
285
+
Depth: 1,
286
+
Skip: false,
287
+
},
288
+
}
289
+
tr := tangled.Pipeline_TriggerMetadata{
290
+
Kind: string(workflow.TriggerKindPullRequest),
291
+
PullRequest: nil, // Nil PR data should create error step
292
+
Repo: &tangled.Pipeline_TriggerRepo{
293
+
Knot: "example.com",
294
+
Did: "did:plc:user123",
295
+
Repo: "my-repo",
296
+
},
297
+
}
298
+
299
+
step := BuildCloneStep(twf, tr, false)
300
+
301
+
// Should return an error step
302
+
if !strings.Contains(step.Name(), "error") {
303
+
t.Error("Expected error in step name when pull request data is nil")
304
+
}
305
+
306
+
allCmds := strings.Join(step.Commands(), " ")
307
+
if !strings.Contains(allCmds, "Failed to get clone info") {
308
+
t.Error("Commands should contain error message")
309
+
}
310
+
}
311
+
312
+
func TestBuildCloneStep_UnknownTriggerKind(t *testing.T) {
313
+
twf := tangled.Pipeline_Workflow{
314
+
Clone: &tangled.Pipeline_CloneOpts{
315
+
Depth: 1,
316
+
Skip: false,
317
+
},
318
+
}
319
+
tr := tangled.Pipeline_TriggerMetadata{
320
+
Kind: "unknown_trigger",
321
+
Repo: &tangled.Pipeline_TriggerRepo{
322
+
Knot: "example.com",
323
+
Did: "did:plc:user123",
324
+
Repo: "my-repo",
325
+
},
326
+
}
327
+
328
+
step := BuildCloneStep(twf, tr, false)
329
+
330
+
// Should return an error step
331
+
if !strings.Contains(step.Name(), "error") {
332
+
t.Error("Expected error in step name for unknown trigger kind")
333
+
}
334
+
335
+
allCmds := strings.Join(step.Commands(), " ")
336
+
if !strings.Contains(allCmds, "unknown trigger kind") {
337
+
t.Error("Commands should contain error message about unknown trigger kind")
338
+
}
339
+
}
340
+
341
+
func TestBuildCloneStep_NilCloneOpts(t *testing.T) {
342
+
twf := tangled.Pipeline_Workflow{
343
+
Clone: nil, // Nil clone options should use defaults
344
+
}
345
+
tr := tangled.Pipeline_TriggerMetadata{
346
+
Kind: string(workflow.TriggerKindPush),
347
+
Push: &tangled.Pipeline_PushTriggerData{
348
+
NewSha: "abc123",
349
+
},
350
+
Repo: &tangled.Pipeline_TriggerRepo{
351
+
Knot: "example.com",
352
+
Did: "did:plc:user123",
353
+
Repo: "my-repo",
354
+
},
355
+
}
356
+
357
+
step := BuildCloneStep(twf, tr, false)
358
+
359
+
// Should still work with default options
360
+
if step.Kind() != StepKindSystem {
361
+
t.Errorf("Expected StepKindSystem, got %v", step.Kind())
362
+
}
363
+
364
+
allCmds := strings.Join(step.Commands(), " ")
365
+
if !strings.Contains(allCmds, "--depth=1") {
366
+
t.Error("Commands should default to '--depth=1' when Clone is nil")
367
+
}
368
+
if !strings.Contains(allCmds, "git init") {
369
+
t.Error("Commands should contain 'git init'")
370
+
}
371
+
}