Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions pkg/workflow/compiler_jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}

Expand Down
81 changes: 81 additions & 0 deletions pkg/workflow/compiler_jobs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
}
Comment on lines +2205 to +2232
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new runs-on handling returns an error when YAML marshaling fails, but this test suite never exercises that path (and shouldErr is unused). Add a test case with an unmarshalable runs-on value (e.g., a function or channel) and assert that buildCustomJobs returns an error mentioning the job name; or remove shouldErr if the error-path test is intentionally out of scope.

Copilot uses AI. Check for mistakes.

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)
}
}
})
}
}
Loading