diff --git a/pkg/workflow/compiler_jobs.go b/pkg/workflow/compiler_jobs.go index f9590994e7..011bd0dcdc 100644 --- a/pkg/workflow/compiler_jobs.go +++ b/pkg/workflow/compiler_jobs.go @@ -465,6 +465,19 @@ func (c *Compiler) buildCustomJobs(data *WorkflowData, activationJobCreated bool if runsOn, hasRunsOn := configMap["runs-on"]; hasRunsOn { if runsOnStr, ok := runsOn.(string); ok { job.RunsOn = "runs-on: " + runsOnStr + } else { + // Array or object form: marshal the value and build indented YAML snippet + yamlBytes, err := yaml.Marshal(runsOn) + if err != nil { + return fmt.Errorf("failed to convert runs-on to YAML for job '%s': %w", jobName, err) + } + lines := strings.Split(strings.TrimSpace(string(yamlBytes)), "\n") + var b strings.Builder + b.WriteString("runs-on:\n") + for _, line := range lines { + b.WriteString(" " + line + "\n") + } + job.RunsOn = strings.TrimSuffix(b.String(), "\n") } } diff --git a/pkg/workflow/compiler_jobs_test.go b/pkg/workflow/compiler_jobs_test.go index ad431594dc..8f6164f347 100644 --- a/pkg/workflow/compiler_jobs_test.go +++ b/pkg/workflow/compiler_jobs_test.go @@ -2198,3 +2198,84 @@ func TestBuildCustomJobsSkipsPreActivationJob(t *testing.T) { t.Error("Expected normal_job to be added") } } + +// TestBuildCustomJobsRunsOnForms tests that runs-on string, array, and object forms +// are all correctly handled in buildCustomJobs. +func TestBuildCustomJobsRunsOnForms(t *testing.T) { + tests := []struct { + name string + runsOn any + expectedRunsOn string + expectedContains []string + shouldErr bool + }{ + { + name: "string form", + runsOn: "ubuntu-latest", + expectedRunsOn: "runs-on: ubuntu-latest", + }, + { + name: "array form", + runsOn: []any{"self-hosted", "linux", "large"}, + expectedContains: []string{"runs-on:", "- self-hosted", "- linux", "- large"}, + }, + { + name: "object form", + runsOn: map[string]any{"group": "my-runners"}, + expectedContains: []string{"runs-on:", "group: my-runners"}, + }, + { + name: "unmarshalable value returns error", + runsOn: make(chan int), // channels cannot be marshaled to YAML + shouldErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + compiler := NewCompiler() + compiler.jobManager = NewJobManager() + + data := &WorkflowData{ + Name: "Test Workflow", + AI: "copilot", + RunsOn: "runs-on: ubuntu-latest", + Jobs: map[string]any{ + "my_job": map[string]any{ + "runs-on": tt.runsOn, + "steps": []any{map[string]any{"run": "echo hi"}}, + }, + }, + } + + err := compiler.buildCustomJobs(data, false) + if tt.shouldErr { + if err == nil { + t.Error("Expected error but got none") + } else if !strings.Contains(err.Error(), "my_job") { + t.Errorf("Expected error to mention job name 'my_job', got: %v", err) + } + return + } + if err != nil { + t.Fatalf("buildCustomJobs() returned unexpected error: %v", err) + } + + job, exists := compiler.jobManager.GetJob("my_job") + if !exists { + t.Fatal("Expected my_job to be added") + } + + if tt.expectedRunsOn != "" { + if job.RunsOn != tt.expectedRunsOn { + t.Errorf("RunsOn = %q, want %q", job.RunsOn, tt.expectedRunsOn) + } + } + for _, want := range tt.expectedContains { + if !strings.Contains(job.RunsOn, want) { + t.Errorf("RunsOn %q does not contain %q", job.RunsOn, want) + } + } + }) + } +}