Monorepo for Tangled tangled.org

workflow: add paths filter and ChangedFiles support #1261

open opened by anirudh.fi targeting master from icy/xmmrnx
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:hwevmowznbiukdf6uk5dwrrq/sh.tangled.repo.pull/3miioqmkhyr22
+271 -6
Diff #0
+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
··· 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
··· 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
··· 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
sign up or login to add to the discussion
1 commit
expand
workflow: add paths filter and ChangedFiles support
3/3 failed
expand
no conflicts, ready to merge
expand 0 comments
anirudh.fi submitted #0
1 commit
expand
workflow: add paths filter and ChangedFiles support
expand 0 comments