Skip to content

Commit 53bc2dc

Browse files
authored
Add error wrapping to compiler_yaml.go generateYAML function (#14584)
1 parent 84109fa commit 53bc2dc

2 files changed

Lines changed: 202 additions & 1 deletion

File tree

pkg/workflow/compiler_yaml.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ func (c *Compiler) generateYAML(data *WorkflowData, markdownPath string) (string
161161

162162
// Build all jobs and validate dependencies
163163
if err := c.buildJobsAndValidate(data, markdownPath); err != nil {
164-
return "", err
164+
return "", fmt.Errorf("failed to build and validate jobs: %w", err)
165165
}
166166

167167
// Compute frontmatter hash before generating YAML
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
//go:build !integration
2+
3+
package workflow
4+
5+
import (
6+
"errors"
7+
"os"
8+
"path/filepath"
9+
"testing"
10+
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
// TestGenerateYAML_ErrorWrapping verifies that errors from buildJobsAndValidate
16+
// are properly wrapped when returned from generateYAML
17+
func TestGenerateYAML_ErrorWrapping(t *testing.T) {
18+
tests := []struct {
19+
name string
20+
workflowContent string
21+
wantErrContains []string
22+
}{
23+
{
24+
name: "circular job dependency wraps error",
25+
workflowContent: `---
26+
engine: copilot
27+
on:
28+
issues:
29+
types: [opened]
30+
permissions:
31+
issues: read
32+
pull-requests: read
33+
jobs:
34+
job1:
35+
runs-on: ubuntu-latest
36+
needs: [job2]
37+
steps:
38+
- run: echo "test"
39+
job2:
40+
runs-on: ubuntu-latest
41+
needs: [job1]
42+
steps:
43+
- run: echo "test"
44+
---
45+
# Test Workflow`,
46+
wantErrContains: []string{
47+
"failed to generate YAML",
48+
"failed to build and validate jobs",
49+
},
50+
},
51+
}
52+
53+
for _, tt := range tests {
54+
t.Run(tt.name, func(t *testing.T) {
55+
// Create a temporary test file
56+
tmpDir := t.TempDir()
57+
testFile := filepath.Join(tmpDir, "test.md")
58+
err := os.WriteFile(testFile, []byte(tt.workflowContent), 0644)
59+
require.NoError(t, err, "Failed to create test file")
60+
61+
// Create compiler and try to compile
62+
compiler := NewCompilerWithVersion("1.0.0")
63+
err = compiler.CompileWorkflow(testFile)
64+
65+
// Should return an error
66+
require.Error(t, err, "Expected error from invalid workflow")
67+
68+
// Error should contain all wrapping messages
69+
for _, wantStr := range tt.wantErrContains {
70+
assert.Contains(t, err.Error(), wantStr,
71+
"Error should contain: %s", wantStr)
72+
}
73+
74+
// Verify error can be unwrapped to find the root cause
75+
// The error chain should be preserved
76+
var unwrappedErr error
77+
for unwrappedErr = err; errors.Unwrap(unwrappedErr) != nil; unwrappedErr = errors.Unwrap(unwrappedErr) {
78+
// Walk the error chain
79+
}
80+
assert.Error(t, unwrappedErr, "Should have an unwrapped error at the end of the chain")
81+
})
82+
}
83+
}
84+
85+
// TestGenerateYAML_ErrorChainPreservation validates that the error chain
86+
// is preserved through multiple layers of error wrapping
87+
func TestGenerateYAML_ErrorChainPreservation(t *testing.T) {
88+
// Create a workflow with a validation error that will propagate through
89+
// buildJobsAndValidate -> generateYAML -> generateAndValidateYAML
90+
workflowContent := `---
91+
engine: copilot
92+
on:
93+
issues:
94+
types: [opened]
95+
permissions:
96+
issues: read
97+
pull-requests: read
98+
jobs:
99+
test_job:
100+
runs-on: ubuntu-latest
101+
needs: [nonexistent_job]
102+
steps:
103+
- run: echo "test"
104+
---
105+
# Test Workflow`
106+
107+
tmpDir := t.TempDir()
108+
testFile := filepath.Join(tmpDir, "test.md")
109+
err := os.WriteFile(testFile, []byte(workflowContent), 0644)
110+
require.NoError(t, err)
111+
112+
compiler := NewCompilerWithVersion("1.0.0")
113+
err = compiler.CompileWorkflow(testFile)
114+
115+
require.Error(t, err, "Expected error from invalid job dependency")
116+
117+
// The error should be wrapped multiple times:
118+
// 1. Original error from job validation
119+
// 2. Wrapped by buildJobsAndValidate
120+
// 3. Wrapped by generateYAML
121+
// 4. Wrapped by generateAndValidateYAML with formatCompilerError
122+
123+
// Count the layers by unwrapping
124+
layers := 0
125+
for currentErr := err; currentErr != nil; currentErr = errors.Unwrap(currentErr) {
126+
layers++
127+
if layers > 10 {
128+
t.Fatal("Too many error layers - possible infinite loop")
129+
}
130+
}
131+
132+
// Should have at least 2 layers of wrapping (generateYAML + formatCompilerError)
133+
assert.GreaterOrEqual(t, layers, 2, "Error should be wrapped multiple times")
134+
135+
// The error message should contain file path (from formatCompilerError)
136+
assert.Contains(t, err.Error(), "test.md", "Error should contain file path")
137+
138+
// The error message should contain context from wrapping
139+
errMsg := err.Error()
140+
assert.NotEmpty(t, errMsg, "Error message should not be empty")
141+
assert.Greater(t, len(errMsg), 50, "Error message should be descriptive")
142+
}
143+
144+
// TestBuildJobsAndValidate_ErrorWrapping verifies that buildJobsAndValidate
145+
// properly wraps errors with context messages
146+
func TestBuildJobsAndValidate_ErrorWrapping(t *testing.T) {
147+
tests := []struct {
148+
name string
149+
workflowContent string
150+
wantErrContains []string // Multiple strings that should all be present
151+
}{
152+
{
153+
name: "dependency validation error is wrapped",
154+
workflowContent: `---
155+
engine: copilot
156+
on:
157+
issues:
158+
types: [opened]
159+
permissions:
160+
issues: read
161+
pull-requests: read
162+
jobs:
163+
job1:
164+
runs-on: ubuntu-latest
165+
needs: [missing_job]
166+
steps:
167+
- run: echo "test"
168+
---
169+
# Test`,
170+
wantErrContains: []string{
171+
"failed to generate YAML",
172+
"failed to build and validate jobs",
173+
"job dependency validation failed",
174+
},
175+
},
176+
}
177+
178+
for _, tt := range tests {
179+
t.Run(tt.name, func(t *testing.T) {
180+
tmpDir := t.TempDir()
181+
testFile := filepath.Join(tmpDir, "test.md")
182+
err := os.WriteFile(testFile, []byte(tt.workflowContent), 0644)
183+
require.NoError(t, err)
184+
185+
compiler := NewCompilerWithVersion("1.0.0")
186+
err = compiler.CompileWorkflow(testFile)
187+
188+
require.Error(t, err, "Expected error")
189+
190+
// All expected strings should be present in the error
191+
for _, wantStr := range tt.wantErrContains {
192+
assert.Contains(t, err.Error(), wantStr,
193+
"Error should contain: %s", wantStr)
194+
}
195+
196+
// Error should be wrapped (errors.Unwrap should return non-nil)
197+
unwrapped := errors.Unwrap(err)
198+
assert.Error(t, unwrapped, "Error should be wrapped")
199+
})
200+
}
201+
}

0 commit comments

Comments
 (0)