+271
-6
Diff
round #0
+4
-3
workflow/compile.go
+4
-3
workflow/compile.go
···
15
15
type RawPipeline = []RawWorkflow
16
16
17
17
type Compiler struct {
18
-
Trigger tangled.Pipeline_TriggerMetadata
19
-
Diagnostics Diagnostics
18
+
Trigger tangled.Pipeline_TriggerMetadata
19
+
ChangedFiles []string
20
+
Diagnostics Diagnostics
20
21
}
21
22
22
23
type Diagnostics struct {
···
113
114
func (compiler *Compiler) compileWorkflow(w Workflow) *tangled.Pipeline_Workflow {
114
115
cw := &tangled.Pipeline_Workflow{}
115
116
116
-
matched, err := w.Match(compiler.Trigger)
117
+
matched, err := w.Match(compiler.Trigger, compiler.ChangedFiles)
117
118
if err != nil {
118
119
compiler.Diagnostics.AddError(
119
120
w.Name,
+65
workflow/compile_test.go
+65
workflow/compile_test.go
···
96
96
assert.Equal(t, MissingEngine, c.Diagnostics.Errors[0].Error)
97
97
}
98
98
99
+
func TestCompileWorkflow_ChangedFilesMatchesPaths(t *testing.T) {
100
+
wf := Workflow{
101
+
Name: ".tangled/workflows/test.yml",
102
+
Engine: "nixery",
103
+
When: []Constraint{
104
+
{
105
+
Event: []string{"push"},
106
+
Branch: []string{"main"},
107
+
Paths: []string{"src/**"},
108
+
},
109
+
},
110
+
}
111
+
112
+
c := Compiler{
113
+
Trigger: trigger,
114
+
ChangedFiles: []string{"src/main.go", "src/util.go"},
115
+
}
116
+
cp := c.Compile([]Workflow{wf})
117
+
118
+
assert.Len(t, cp.Workflows, 1)
119
+
assert.Equal(t, wf.Name, cp.Workflows[0].Name)
120
+
assert.False(t, c.Diagnostics.IsErr())
121
+
}
122
+
123
+
func TestCompileWorkflow_ChangedFilesNoMatch(t *testing.T) {
124
+
wf := Workflow{
125
+
Name: ".tangled/workflows/test.yml",
126
+
Engine: "nixery",
127
+
When: []Constraint{
128
+
{
129
+
Event: []string{"push"},
130
+
Branch: []string{"main"},
131
+
Paths: []string{"src/**"},
132
+
},
133
+
},
134
+
}
135
+
136
+
c := Compiler{
137
+
Trigger: trigger,
138
+
ChangedFiles: []string{"docs/guide.md", "README.md"},
139
+
}
140
+
cp := c.Compile([]Workflow{wf})
141
+
142
+
assert.Len(t, cp.Workflows, 0)
143
+
assert.Len(t, c.Diagnostics.Warnings, 1)
144
+
assert.Equal(t, WorkflowSkipped, c.Diagnostics.Warnings[0].Type)
145
+
}
146
+
147
+
func TestCompileWorkflow_NoPaths_ChangedFilesIgnored(t *testing.T) {
148
+
wf := Workflow{
149
+
Name: ".tangled/workflows/test.yml",
150
+
Engine: "nixery",
151
+
When: when, // no Paths constraint
152
+
}
153
+
154
+
c := Compiler{
155
+
Trigger: trigger,
156
+
ChangedFiles: []string{"docs/guide.md"},
157
+
}
158
+
cp := c.Compile([]Workflow{wf})
159
+
160
+
assert.Len(t, cp.Workflows, 1)
161
+
assert.False(t, c.Diagnostics.IsErr())
162
+
}
163
+
99
164
func TestCompileWorkflow_MultipleBranchAndTag(t *testing.T) {
100
165
wf := Workflow{
101
166
Name: ".tangled/workflows/branch_and_tag.yml",
+27
-3
workflow/def.go
+27
-3
workflow/def.go
···
36
36
Event StringList `yaml:"event"`
37
37
Branch StringList `yaml:"branch"` // required for pull_request; for push, either branch or tag must be specified
38
38
Tag StringList `yaml:"tag"` // optional; only applies to push events
39
+
Paths StringList `yaml:"paths"` // optional; only run if any changed file matches a glob pattern
39
40
}
40
41
41
42
CloneOpts struct {
···
93
94
}
94
95
95
96
// if any of the constraints on a workflow is true, return true
96
-
func (w *Workflow) Match(trigger tangled.Pipeline_TriggerMetadata) (bool, error) {
97
+
func (w *Workflow) Match(trigger tangled.Pipeline_TriggerMetadata, changedFiles []string) (bool, error) {
97
98
// manual triggers always run the workflow
98
99
if trigger.Manual != nil {
99
100
return true, nil
···
101
102
102
103
// if not manual, run through the constraint list and see if any one matches
103
104
for _, c := range w.When {
104
-
matched, err := c.Match(trigger)
105
+
matched, err := c.Match(trigger, changedFiles)
105
106
if err != nil {
106
107
return false, err
107
108
}
···
118
119
return false, nil
119
120
}
120
121
121
-
func (c *Constraint) Match(trigger tangled.Pipeline_TriggerMetadata) (bool, error) {
122
+
func (c *Constraint) Match(trigger tangled.Pipeline_TriggerMetadata, changedFiles []string) (bool, error) {
122
123
match := true
123
124
124
125
// manual triggers always pass this constraint
···
147
148
match = match && matched
148
149
}
149
150
151
+
// apply paths filter: if specified, at least one changed file must match
152
+
if len(c.Paths) > 0 {
153
+
matched, err := matchesAnyFile(changedFiles, c.Paths)
154
+
if err != nil {
155
+
return false, err
156
+
}
157
+
match = match && matched
158
+
}
159
+
150
160
return match, nil
151
161
}
152
162
163
+
// matchesAnyFile returns true if any file in files matches any of the glob patterns.
164
+
func matchesAnyFile(files []string, patterns []string) (bool, error) {
165
+
for _, f := range files {
166
+
matched, err := matchesPattern(f, patterns)
167
+
if err != nil {
168
+
return false, err
169
+
}
170
+
if matched {
171
+
return true, nil
172
+
}
173
+
}
174
+
return false, nil
175
+
}
176
+
153
177
func (c *Constraint) MatchRef(ref string) (bool, error) {
154
178
refName := plumbing.ReferenceName(ref)
155
179
shortName := refName.Short()
+175
workflow/def_test.go
+175
workflow/def_test.go
···
4
4
"testing"
5
5
6
6
"github.com/stretchr/testify/assert"
7
+
"tangled.org/core/api/tangled"
7
8
)
8
9
9
10
func TestUnmarshalWorkflowWithBranch(t *testing.T) {
···
275
276
}
276
277
}
277
278
279
+
func TestMatchesAnyFile(t *testing.T) {
280
+
tests := []struct {
281
+
name string
282
+
files []string
283
+
patterns []string
284
+
expected bool
285
+
}{
286
+
{
287
+
name: "exact file match",
288
+
files: []string{"src/main.go"},
289
+
patterns: []string{"src/main.go"},
290
+
expected: true,
291
+
},
292
+
{
293
+
name: "glob match single star",
294
+
files: []string{"src/main.go"},
295
+
patterns: []string{"src/*.go"},
296
+
expected: true,
297
+
},
298
+
{
299
+
name: "glob match double star",
300
+
files: []string{"src/pkg/util.go"},
301
+
patterns: []string{"src/**/*.go"},
302
+
expected: true,
303
+
},
304
+
{
305
+
name: "any file in list matches",
306
+
files: []string{"README.md", "src/main.go", "docs/guide.md"},
307
+
patterns: []string{"src/**"},
308
+
expected: true,
309
+
},
310
+
{
311
+
name: "no file matches",
312
+
files: []string{"README.md", "docs/guide.md"},
313
+
patterns: []string{"src/**"},
314
+
expected: false,
315
+
},
316
+
{
317
+
name: "empty files list",
318
+
files: []string{},
319
+
patterns: []string{"src/**"},
320
+
expected: false,
321
+
},
322
+
{
323
+
name: "nil files list",
324
+
files: nil,
325
+
patterns: []string{"src/**"},
326
+
expected: false,
327
+
},
328
+
{
329
+
name: "multiple patterns, second matches",
330
+
files: []string{"docs/guide.md"},
331
+
patterns: []string{"src/**", "docs/**"},
332
+
expected: true,
333
+
},
334
+
{
335
+
name: "single star does not cross directory boundary",
336
+
files: []string{"src/pkg/util.go"},
337
+
patterns: []string{"src/*.go"},
338
+
expected: false,
339
+
},
340
+
}
341
+
342
+
for _, tt := range tests {
343
+
t.Run(tt.name, func(t *testing.T) {
344
+
result, err := matchesAnyFile(tt.files, tt.patterns)
345
+
assert.NoError(t, err)
346
+
assert.Equal(t, tt.expected, result)
347
+
})
348
+
}
349
+
}
350
+
351
+
func TestConstraintMatch_PathsFilter(t *testing.T) {
352
+
pushTrigger := tangled.Pipeline_TriggerMetadata{
353
+
Kind: string(TriggerKindPush),
354
+
Push: &tangled.Pipeline_PushTriggerData{
355
+
Ref: "refs/heads/main",
356
+
},
357
+
}
358
+
359
+
tests := []struct {
360
+
name string
361
+
constraint Constraint
362
+
changedFiles []string
363
+
expected bool
364
+
}{
365
+
{
366
+
name: "paths match - workflow runs",
367
+
constraint: Constraint{
368
+
Event: []string{"push"},
369
+
Branch: []string{"main"},
370
+
Paths: []string{"src/**"},
371
+
},
372
+
changedFiles: []string{"src/main.go"},
373
+
expected: true,
374
+
},
375
+
{
376
+
name: "paths no match - workflow skipped",
377
+
constraint: Constraint{
378
+
Event: []string{"push"},
379
+
Branch: []string{"main"},
380
+
Paths: []string{"src/**"},
381
+
},
382
+
changedFiles: []string{"docs/guide.md"},
383
+
expected: false,
384
+
},
385
+
{
386
+
name: "no paths filter - all files pass",
387
+
constraint: Constraint{
388
+
Event: []string{"push"},
389
+
Branch: []string{"main"},
390
+
},
391
+
changedFiles: []string{"docs/guide.md"},
392
+
expected: true,
393
+
},
394
+
{
395
+
name: "paths filter with empty changed files - skipped",
396
+
constraint: Constraint{
397
+
Event: []string{"push"},
398
+
Branch: []string{"main"},
399
+
Paths: []string{"src/**"},
400
+
},
401
+
changedFiles: []string{},
402
+
expected: false,
403
+
},
404
+
{
405
+
name: "paths glob matches one of many changed files",
406
+
constraint: Constraint{
407
+
Event: []string{"push"},
408
+
Branch: []string{"main"},
409
+
Paths: []string{"**/*.go"},
410
+
},
411
+
changedFiles: []string{"README.md", "go.mod", "src/main.go"},
412
+
expected: true,
413
+
},
414
+
}
415
+
416
+
for _, tt := range tests {
417
+
t.Run(tt.name, func(t *testing.T) {
418
+
result, err := tt.constraint.Match(pushTrigger, tt.changedFiles)
419
+
assert.NoError(t, err)
420
+
assert.Equal(t, tt.expected, result)
421
+
})
422
+
}
423
+
}
424
+
425
+
func TestUnmarshalWorkflowWithPaths(t *testing.T) {
426
+
yamlData := `
427
+
when:
428
+
- event: push
429
+
branch: main
430
+
paths:
431
+
- "src/**"
432
+
- "**.go"`
433
+
434
+
wf, err := FromFile("test.yml", []byte(yamlData))
435
+
assert.NoError(t, err)
436
+
assert.Len(t, wf.When, 1)
437
+
assert.ElementsMatch(t, []string{"src/**", "**.go"}, wf.When[0].Paths)
438
+
}
439
+
440
+
func TestUnmarshalWorkflowWithPathsSingleString(t *testing.T) {
441
+
yamlData := `
442
+
when:
443
+
- event: push
444
+
branch: main
445
+
paths: "src/**"`
446
+
447
+
wf, err := FromFile("test.yml", []byte(yamlData))
448
+
assert.NoError(t, err)
449
+
assert.Len(t, wf.When, 1)
450
+
assert.ElementsMatch(t, []string{"src/**"}, wf.When[0].Paths)
451
+
}
452
+
278
453
func TestConstraintMatchTag_GlobPatterns(t *testing.T) {
279
454
tests := []struct {
280
455
name string
History
2 rounds
0 comments
anirudh.fi
submitted
#1
1 commit
expand
collapse
workflow: add paths filter and ChangedFiles support
3/3 failed
expand
collapse
no conflicts, ready to merge
expand 0 comments
anirudh.fi
submitted
#0
1 commit
expand
collapse
workflow: add paths filter and ChangedFiles support