···109109 setup := &setupSteps{}110110111111 setup.addStep(nixConfStep())112112- setup.addStep(cloneStep(twf, *tpl.TriggerMetadata, e.cfg.Server.Dev))112112+ setup.addStep(models.BuildCloneStep(twf, *tpl.TriggerMetadata, e.cfg.Server.Dev))113113 // this step could be empty114114 if s := dependencyStep(dwf.Dependencies); s != nil {115115 setup.addStep(*s)
-73
spindle/engines/nixery/setup_steps.go
···2233import (44 "fmt"55- "path"65 "strings"77-88- "tangled.org/core/api/tangled"99- "tangled.org/core/workflow"106)117128func nixConfStep() Step {···1317 command: setupCmd,1418 name: "Configure Nix",1519 }1616-}1717-1818-// cloneOptsAsSteps processes clone options and adds corresponding steps1919-// to the beginning of the workflow's step list if cloning is not skipped.2020-//2121-// the steps to do here are:2222-// - git init2323-// - git remote add origin <url>2424-// - git fetch --depth=<d> --recurse-submodules=<yes|no> <sha>2525-// - git checkout FETCH_HEAD2626-func cloneStep(twf tangled.Pipeline_Workflow, tr tangled.Pipeline_TriggerMetadata, dev bool) Step {2727- if twf.Clone.Skip {2828- return Step{}2929- }3030-3131- var commands []string3232-3333- // initialize git repo in workspace3434- commands = append(commands, "git init")3535-3636- // add repo as git remote3737- scheme := "https://"3838- if dev {3939- scheme = "http://"4040- tr.Repo.Knot = strings.ReplaceAll(tr.Repo.Knot, "localhost", "host.docker.internal")4141- }4242- url := scheme + path.Join(tr.Repo.Knot, tr.Repo.Did, tr.Repo.Repo)4343- commands = append(commands, fmt.Sprintf("git remote add origin %s", url))4444-4545- // run git fetch4646- {4747- var fetchArgs []string4848-4949- // default clone depth is 15050- depth := 15151- if twf.Clone.Depth > 1 {5252- depth = int(twf.Clone.Depth)5353- }5454- fetchArgs = append(fetchArgs, fmt.Sprintf("--depth=%d", depth))5555-5656- // optionally recurse submodules5757- if twf.Clone.Submodules {5858- fetchArgs = append(fetchArgs, "--recurse-submodules=yes")5959- }6060-6161- // set remote to fetch from6262- fetchArgs = append(fetchArgs, "origin")6363-6464- // set revision to checkout6565- switch workflow.TriggerKind(tr.Kind) {6666- case workflow.TriggerKindManual:6767- // TODO: unimplemented6868- case workflow.TriggerKindPush:6969- fetchArgs = append(fetchArgs, tr.Push.NewSha)7070- case workflow.TriggerKindPullRequest:7171- fetchArgs = append(fetchArgs, tr.PullRequest.SourceSha)7272- }7373-7474- commands = append(commands, fmt.Sprintf("git fetch %s", strings.Join(fetchArgs, " ")))7575- }7676-7777- // run git checkout7878- commands = append(commands, "git checkout FETCH_HEAD")7979-8080- cloneStep := Step{8181- command: strings.Join(commands, "\n"),8282- name: "Clone repository into workspace",8383- }8484- return cloneStep8520}86218722// dependencyStep processes dependencies defined in the workflow.
+151
spindle/models/clone.go
···11+package models22+33+import (44+ "fmt"55+ "strings"66+77+ "tangled.org/core/api/tangled"88+ "tangled.org/core/workflow"99+)1010+1111+type CloneStep struct {1212+ name string1313+ kind StepKind1414+ commands []string1515+}1616+1717+func (s CloneStep) Name() string {1818+ return s.name1919+}2020+2121+func (s CloneStep) Commands() []string {2222+ return s.commands2323+}2424+2525+func (s CloneStep) Command() string {2626+ return strings.Join(s.commands, "\n")2727+}2828+2929+func (s CloneStep) Kind() StepKind {3030+ return s.kind3131+}3232+3333+// BuildCloneStep generates git clone commands.3434+// The caller must ensure the current working directory is set to the desired3535+// workspace directory before executing these commands.3636+//3737+// The generated commands are:3838+// - git init3939+// - git remote add origin <url>4040+// - git fetch --depth=<d> --recurse-submodules=<yes|no> <sha>4141+// - git checkout FETCH_HEAD4242+//4343+// Supports all trigger types (push, PR, manual) and clone options.4444+func BuildCloneStep(twf tangled.Pipeline_Workflow, tr tangled.Pipeline_TriggerMetadata, dev bool) CloneStep {4545+ if twf.Clone != nil && twf.Clone.Skip {4646+ return CloneStep{}4747+ }4848+4949+ commitSHA, err := extractCommitSHA(tr)5050+ if err != nil {5151+ return CloneStep{5252+ kind: StepKindSystem,5353+ name: "Clone repository into workspace (error)",5454+ commands: []string{fmt.Sprintf("echo 'Failed to get clone info: %s' && exit 1", err.Error())},5555+ }5656+ }5757+5858+ repoURL := buildRepoURL(tr, dev)5959+6060+ var cloneOpts tangled.Pipeline_CloneOpts6161+ if twf.Clone != nil {6262+ cloneOpts = *twf.Clone6363+ }6464+ fetchArgs := buildFetchArgs(cloneOpts, commitSHA)6565+6666+ return CloneStep{6767+ kind: StepKindSystem,6868+ name: "Clone repository into workspace",6969+ commands: []string{7070+ "git init",7171+ fmt.Sprintf("git remote add origin %s", repoURL),7272+ fmt.Sprintf("git fetch %s", strings.Join(fetchArgs, " ")),7373+ "git checkout FETCH_HEAD",7474+ },7575+ }7676+}7777+7878+// extractCommitSHA extracts the commit SHA from trigger metadata based on trigger type7979+func extractCommitSHA(tr tangled.Pipeline_TriggerMetadata) (string, error) {8080+ switch workflow.TriggerKind(tr.Kind) {8181+ case workflow.TriggerKindPush:8282+ if tr.Push == nil {8383+ return "", fmt.Errorf("push trigger metadata is nil")8484+ }8585+ return tr.Push.NewSha, nil8686+8787+ case workflow.TriggerKindPullRequest:8888+ if tr.PullRequest == nil {8989+ return "", fmt.Errorf("pull request trigger metadata is nil")9090+ }9191+ return tr.PullRequest.SourceSha, nil9292+9393+ case workflow.TriggerKindManual:9494+ // Manual triggers don't have an explicit SHA in the metadata9595+ // For now, return empty string - could be enhanced to fetch from default branch9696+ // TODO: Implement manual trigger SHA resolution (fetch default branch HEAD)9797+ return "", nil9898+9999+ default:100100+ return "", fmt.Errorf("unknown trigger kind: %s", tr.Kind)101101+ }102102+}103103+104104+// buildRepoURL constructs the repository URL from trigger metadata105105+func buildRepoURL(tr tangled.Pipeline_TriggerMetadata, devMode bool) string {106106+ if tr.Repo == nil {107107+ return ""108108+ }109109+110110+ // Determine protocol111111+ scheme := "https://"112112+ if devMode {113113+ scheme = "http://"114114+ }115115+116116+ // Get host from knot117117+ host := tr.Repo.Knot118118+119119+ // In dev mode, replace localhost with host.docker.internal for Docker networking120120+ if devMode && strings.Contains(host, "localhost") {121121+ host = strings.ReplaceAll(host, "localhost", "host.docker.internal")122122+ }123123+124124+ // Build URL: {scheme}{knot}/{did}/{repo}125125+ return fmt.Sprintf("%s%s/%s/%s", scheme, host, tr.Repo.Did, tr.Repo.Repo)126126+}127127+128128+// buildFetchArgs constructs the arguments for git fetch based on clone options129129+func buildFetchArgs(clone tangled.Pipeline_CloneOpts, sha string) []string {130130+ args := []string{}131131+132132+ // Set fetch depth (default to 1 for shallow clone)133133+ depth := clone.Depth134134+ if depth == 0 {135135+ depth = 1136136+ }137137+ args = append(args, fmt.Sprintf("--depth=%d", depth))138138+139139+ // Add submodules if requested140140+ if clone.Submodules {141141+ args = append(args, "--recurse-submodules=yes")142142+ }143143+144144+ // Add remote and SHA145145+ args = append(args, "origin")146146+ if sha != "" {147147+ args = append(args, sha)148148+ }149149+150150+ return args151151+}
+371
spindle/models/clone_test.go
···11+package models22+33+import (44+ "strings"55+ "testing"66+77+ "tangled.org/core/api/tangled"88+ "tangled.org/core/workflow"99+)1010+1111+func TestBuildCloneStep_PushTrigger(t *testing.T) {1212+ twf := tangled.Pipeline_Workflow{1313+ Clone: &tangled.Pipeline_CloneOpts{1414+ Depth: 1,1515+ Submodules: false,1616+ Skip: false,1717+ },1818+ }1919+ tr := tangled.Pipeline_TriggerMetadata{2020+ Kind: string(workflow.TriggerKindPush),2121+ Push: &tangled.Pipeline_PushTriggerData{2222+ NewSha: "abc123",2323+ OldSha: "def456",2424+ Ref: "refs/heads/main",2525+ },2626+ Repo: &tangled.Pipeline_TriggerRepo{2727+ Knot: "example.com",2828+ Did: "did:plc:user123",2929+ Repo: "my-repo",3030+ },3131+ }3232+3333+ step := BuildCloneStep(twf, tr, false)3434+3535+ if step.Kind() != StepKindSystem {3636+ t.Errorf("Expected StepKindSystem, got %v", step.Kind())3737+ }3838+3939+ if step.Name() != "Clone repository into workspace" {4040+ t.Errorf("Expected 'Clone repository into workspace', got '%s'", step.Name())4141+ }4242+4343+ commands := step.Commands()4444+ if len(commands) != 4 {4545+ t.Errorf("Expected 4 commands, got %d", len(commands))4646+ }4747+4848+ // Verify commands contain expected git operations4949+ allCmds := strings.Join(commands, " ")5050+ if !strings.Contains(allCmds, "git init") {5151+ t.Error("Commands should contain 'git init'")5252+ }5353+ if !strings.Contains(allCmds, "git remote add origin") {5454+ t.Error("Commands should contain 'git remote add origin'")5555+ }5656+ if !strings.Contains(allCmds, "git fetch") {5757+ t.Error("Commands should contain 'git fetch'")5858+ }5959+ if !strings.Contains(allCmds, "abc123") {6060+ t.Error("Commands should contain commit SHA")6161+ }6262+ if !strings.Contains(allCmds, "git checkout FETCH_HEAD") {6363+ t.Error("Commands should contain 'git checkout FETCH_HEAD'")6464+ }6565+ if !strings.Contains(allCmds, "https://example.com/did:plc:user123/my-repo") {6666+ t.Error("Commands should contain expected repo URL")6767+ }6868+}6969+7070+func TestBuildCloneStep_PullRequestTrigger(t *testing.T) {7171+ twf := tangled.Pipeline_Workflow{7272+ Clone: &tangled.Pipeline_CloneOpts{7373+ Depth: 1,7474+ Skip: false,7575+ },7676+ }7777+ tr := tangled.Pipeline_TriggerMetadata{7878+ Kind: string(workflow.TriggerKindPullRequest),7979+ PullRequest: &tangled.Pipeline_PullRequestTriggerData{8080+ SourceSha: "pr-sha-789",8181+ SourceBranch: "feature-branch",8282+ TargetBranch: "main",8383+ Action: "opened",8484+ },8585+ Repo: &tangled.Pipeline_TriggerRepo{8686+ Knot: "example.com",8787+ Did: "did:plc:user123",8888+ Repo: "my-repo",8989+ },9090+ }9191+9292+ step := BuildCloneStep(twf, tr, false)9393+9494+ allCmds := strings.Join(step.Commands(), " ")9595+ if !strings.Contains(allCmds, "pr-sha-789") {9696+ t.Error("Commands should contain PR commit SHA")9797+ }9898+}9999+100100+func TestBuildCloneStep_ManualTrigger(t *testing.T) {101101+ twf := tangled.Pipeline_Workflow{102102+ Clone: &tangled.Pipeline_CloneOpts{103103+ Depth: 1,104104+ Skip: false,105105+ },106106+ }107107+ tr := tangled.Pipeline_TriggerMetadata{108108+ Kind: string(workflow.TriggerKindManual),109109+ Manual: &tangled.Pipeline_ManualTriggerData{110110+ Inputs: nil,111111+ },112112+ Repo: &tangled.Pipeline_TriggerRepo{113113+ Knot: "example.com",114114+ Did: "did:plc:user123",115115+ Repo: "my-repo",116116+ },117117+ }118118+119119+ step := BuildCloneStep(twf, tr, false)120120+121121+ // Manual triggers don't have a SHA yet (TODO), so git fetch won't include a SHA122122+ allCmds := strings.Join(step.Commands(), " ")123123+ // Should still have basic git commands124124+ if !strings.Contains(allCmds, "git init") {125125+ t.Error("Commands should contain 'git init'")126126+ }127127+ if !strings.Contains(allCmds, "git fetch") {128128+ t.Error("Commands should contain 'git fetch'")129129+ }130130+}131131+132132+func TestBuildCloneStep_SkipFlag(t *testing.T) {133133+ twf := tangled.Pipeline_Workflow{134134+ Clone: &tangled.Pipeline_CloneOpts{135135+ Skip: true,136136+ },137137+ }138138+ tr := tangled.Pipeline_TriggerMetadata{139139+ Kind: string(workflow.TriggerKindPush),140140+ Push: &tangled.Pipeline_PushTriggerData{141141+ NewSha: "abc123",142142+ },143143+ Repo: &tangled.Pipeline_TriggerRepo{144144+ Knot: "example.com",145145+ Did: "did:plc:user123",146146+ Repo: "my-repo",147147+ },148148+ }149149+150150+ step := BuildCloneStep(twf, tr, false)151151+152152+ // Empty step when skip is true153153+ if step.Name() != "" {154154+ t.Error("Expected empty step name when Skip is true")155155+ }156156+ if len(step.Commands()) != 0 {157157+ t.Errorf("Expected no commands when Skip is true, got %d commands", len(step.Commands()))158158+ }159159+}160160+161161+func TestBuildCloneStep_DevMode(t *testing.T) {162162+ twf := tangled.Pipeline_Workflow{163163+ Clone: &tangled.Pipeline_CloneOpts{164164+ Depth: 1,165165+ Skip: false,166166+ },167167+ }168168+ tr := tangled.Pipeline_TriggerMetadata{169169+ Kind: string(workflow.TriggerKindPush),170170+ Push: &tangled.Pipeline_PushTriggerData{171171+ NewSha: "abc123",172172+ },173173+ Repo: &tangled.Pipeline_TriggerRepo{174174+ Knot: "localhost:3000",175175+ Did: "did:plc:user123",176176+ Repo: "my-repo",177177+ },178178+ }179179+180180+ step := BuildCloneStep(twf, tr, true)181181+182182+ // In dev mode, should use http:// and replace localhost with host.docker.internal183183+ allCmds := strings.Join(step.Commands(), " ")184184+ expectedURL := "http://host.docker.internal:3000/did:plc:user123/my-repo"185185+ if !strings.Contains(allCmds, expectedURL) {186186+ t.Errorf("Expected dev mode URL '%s' in commands", expectedURL)187187+ }188188+}189189+190190+func TestBuildCloneStep_DepthAndSubmodules(t *testing.T) {191191+ twf := tangled.Pipeline_Workflow{192192+ Clone: &tangled.Pipeline_CloneOpts{193193+ Depth: 10,194194+ Submodules: true,195195+ Skip: false,196196+ },197197+ }198198+ tr := tangled.Pipeline_TriggerMetadata{199199+ Kind: string(workflow.TriggerKindPush),200200+ Push: &tangled.Pipeline_PushTriggerData{201201+ NewSha: "abc123",202202+ },203203+ Repo: &tangled.Pipeline_TriggerRepo{204204+ Knot: "example.com",205205+ Did: "did:plc:user123",206206+ Repo: "my-repo",207207+ },208208+ }209209+210210+ step := BuildCloneStep(twf, tr, false)211211+212212+ allCmds := strings.Join(step.Commands(), " ")213213+ if !strings.Contains(allCmds, "--depth=10") {214214+ t.Error("Commands should contain '--depth=10'")215215+ }216216+217217+ if !strings.Contains(allCmds, "--recurse-submodules=yes") {218218+ t.Error("Commands should contain '--recurse-submodules=yes'")219219+ }220220+}221221+222222+func TestBuildCloneStep_DefaultDepth(t *testing.T) {223223+ twf := tangled.Pipeline_Workflow{224224+ Clone: &tangled.Pipeline_CloneOpts{225225+ Depth: 0, // Default should be 1226226+ Skip: false,227227+ },228228+ }229229+ tr := tangled.Pipeline_TriggerMetadata{230230+ Kind: string(workflow.TriggerKindPush),231231+ Push: &tangled.Pipeline_PushTriggerData{232232+ NewSha: "abc123",233233+ },234234+ Repo: &tangled.Pipeline_TriggerRepo{235235+ Knot: "example.com",236236+ Did: "did:plc:user123",237237+ Repo: "my-repo",238238+ },239239+ }240240+241241+ step := BuildCloneStep(twf, tr, false)242242+243243+ allCmds := strings.Join(step.Commands(), " ")244244+ if !strings.Contains(allCmds, "--depth=1") {245245+ t.Error("Commands should default to '--depth=1'")246246+ }247247+}248248+249249+func TestBuildCloneStep_NilPushData(t *testing.T) {250250+ twf := tangled.Pipeline_Workflow{251251+ Clone: &tangled.Pipeline_CloneOpts{252252+ Depth: 1,253253+ Skip: false,254254+ },255255+ }256256+ tr := tangled.Pipeline_TriggerMetadata{257257+ Kind: string(workflow.TriggerKindPush),258258+ Push: nil, // Nil push data should create error step259259+ Repo: &tangled.Pipeline_TriggerRepo{260260+ Knot: "example.com",261261+ Did: "did:plc:user123",262262+ Repo: "my-repo",263263+ },264264+ }265265+266266+ step := BuildCloneStep(twf, tr, false)267267+268268+ // Should return an error step269269+ if !strings.Contains(step.Name(), "error") {270270+ t.Error("Expected error in step name when push data is nil")271271+ }272272+273273+ allCmds := strings.Join(step.Commands(), " ")274274+ if !strings.Contains(allCmds, "Failed to get clone info") {275275+ t.Error("Commands should contain error message")276276+ }277277+ if !strings.Contains(allCmds, "exit 1") {278278+ t.Error("Commands should exit with error")279279+ }280280+}281281+282282+func TestBuildCloneStep_NilPRData(t *testing.T) {283283+ twf := tangled.Pipeline_Workflow{284284+ Clone: &tangled.Pipeline_CloneOpts{285285+ Depth: 1,286286+ Skip: false,287287+ },288288+ }289289+ tr := tangled.Pipeline_TriggerMetadata{290290+ Kind: string(workflow.TriggerKindPullRequest),291291+ PullRequest: nil, // Nil PR data should create error step292292+ Repo: &tangled.Pipeline_TriggerRepo{293293+ Knot: "example.com",294294+ Did: "did:plc:user123",295295+ Repo: "my-repo",296296+ },297297+ }298298+299299+ step := BuildCloneStep(twf, tr, false)300300+301301+ // Should return an error step302302+ if !strings.Contains(step.Name(), "error") {303303+ t.Error("Expected error in step name when pull request data is nil")304304+ }305305+306306+ allCmds := strings.Join(step.Commands(), " ")307307+ if !strings.Contains(allCmds, "Failed to get clone info") {308308+ t.Error("Commands should contain error message")309309+ }310310+}311311+312312+func TestBuildCloneStep_UnknownTriggerKind(t *testing.T) {313313+ twf := tangled.Pipeline_Workflow{314314+ Clone: &tangled.Pipeline_CloneOpts{315315+ Depth: 1,316316+ Skip: false,317317+ },318318+ }319319+ tr := tangled.Pipeline_TriggerMetadata{320320+ Kind: "unknown_trigger",321321+ Repo: &tangled.Pipeline_TriggerRepo{322322+ Knot: "example.com",323323+ Did: "did:plc:user123",324324+ Repo: "my-repo",325325+ },326326+ }327327+328328+ step := BuildCloneStep(twf, tr, false)329329+330330+ // Should return an error step331331+ if !strings.Contains(step.Name(), "error") {332332+ t.Error("Expected error in step name for unknown trigger kind")333333+ }334334+335335+ allCmds := strings.Join(step.Commands(), " ")336336+ if !strings.Contains(allCmds, "unknown trigger kind") {337337+ t.Error("Commands should contain error message about unknown trigger kind")338338+ }339339+}340340+341341+func TestBuildCloneStep_NilCloneOpts(t *testing.T) {342342+ twf := tangled.Pipeline_Workflow{343343+ Clone: nil, // Nil clone options should use defaults344344+ }345345+ tr := tangled.Pipeline_TriggerMetadata{346346+ Kind: string(workflow.TriggerKindPush),347347+ Push: &tangled.Pipeline_PushTriggerData{348348+ NewSha: "abc123",349349+ },350350+ Repo: &tangled.Pipeline_TriggerRepo{351351+ Knot: "example.com",352352+ Did: "did:plc:user123",353353+ Repo: "my-repo",354354+ },355355+ }356356+357357+ step := BuildCloneStep(twf, tr, false)358358+359359+ // Should still work with default options360360+ if step.Kind() != StepKindSystem {361361+ t.Errorf("Expected StepKindSystem, got %v", step.Kind())362362+ }363363+364364+ allCmds := strings.Join(step.Commands(), " ")365365+ if !strings.Contains(allCmds, "--depth=1") {366366+ t.Error("Commands should default to '--depth=1' when Clone is nil")367367+ }368368+ if !strings.Contains(allCmds, "git init") {369369+ t.Error("Commands should contain 'git init'")370370+ }371371+}