From 9d94a76fee788918cd4967308449bc3cbb88d80e Mon Sep 17 00:00:00 2001 From: ae-ou Date: Mon, 27 Mar 2023 01:27:46 +0100 Subject: [PATCH 1/4] Get filter patterns nested within the the On attribute of the workflow. --- pkg/model/workflow.go | 48 +++++++++ pkg/model/workflow_test.go | 204 +++++++++++++++++++++++++++++++++++++ 2 files changed, 252 insertions(+) diff --git a/pkg/model/workflow.go b/pkg/model/workflow.go index d7e2922b72b..121bcd21e2a 100644 --- a/pkg/model/workflow.go +++ b/pkg/model/workflow.go @@ -55,6 +55,54 @@ func (w *Workflow) On() []string { return nil } +//FindFilterPatterns searches for filter patterns relating to the specified event in the RawOn attribute +func (w *Workflow) FindFilterPatterns(event string) map[string][]string { + //TODO: We may want to return a custom struct that contains the filter types as attributes. + //Return immediately if the event type doesn't support filters + if event != "push" && event != "pull_request" { + return map[string][]string{} + } + + //If it isn't a mapping node, then the following traversal can't be performed + if w.RawOn.Kind != yaml.MappingNode { + return map[string][]string{} + } + + //Decode rawOn to a map of string=>interfaces + var topLevelMap map[string]interface{} + err := w.RawOn.Decode(&topLevelMap) + if err != nil { + log.Fatal(err) + } + + output := map[string][]string{} + + //topLevelMapKey correlates to the event type - e.g. "push" or "pull_request" + for topLevelMapKey, topLevelMapVal := range topLevelMap { + + //Skip to the next iteration if this isn't the event that we're looking for. + if topLevelMapKey != event { + continue + } + + if midLevelMap, ok := topLevelMapVal.(map[string]interface{}); ok { + //midLevelMapKey correlates to the filter type - e.g. "branches", "tags", or paths + for midLevelMapKey, midLevelMapVal := range midLevelMap { + if lowLevelMapVal, ok := midLevelMapVal.([]interface{}); ok { + //Leaf correlates to the actual filter pattern + for _, leaf := range lowLevelMapVal { + if leafString := leaf.(string); ok { + output[midLevelMapKey] = append(output[midLevelMapKey], leafString) + } + } + } + } + } + } + + return output +} + func (w *Workflow) OnEvent(event string) interface{} { if w.RawOn.Kind == yaml.MappingNode { var val map[string]interface{} diff --git a/pkg/model/workflow_test.go b/pkg/model/workflow_test.go index a789233857f..cfc861d79c3 100644 --- a/pkg/model/workflow_test.go +++ b/pkg/model/workflow_test.go @@ -71,6 +71,210 @@ jobs: assert.Contains(t, workflow.On(), "pull_request") } +func TestGetWorkflowFilterStrings(t *testing.T) { + testCases := []struct { + name string + yaml string + inputEvent string + expectedOutput map[string][]string + }{ + { + name: "on.push.branches", + yaml: ` +name: local-action-docker-url +on: + push: + branches: + - master + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: ./actions/docker-url +`, + inputEvent: "push", + expectedOutput: map[string][]string{"branches": {"master"}}, + }, + { + name: "on.push.tags", + yaml: ` +name: local-action-docker-url +on: + push: + tags: + - "*-release" + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: ./actions/docker-url +`, + inputEvent: "push", + expectedOutput: map[string][]string{"tags": {"*-release"}}, + }, + { + name: "on.push.paths", + yaml: ` +name: local-action-docker-url +on: + push: + paths: + - "**.go" + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: ./actions/docker-url +`, + inputEvent: "push", + expectedOutput: map[string][]string{"paths": {"**.go"}}, + }, + { + name: "on.pull_request.branches", + yaml: ` +name: local-action-docker-url +on: + pull_request: + branches: + - master + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: ./actions/docker-url +`, + inputEvent: "pull_request", + expectedOutput: map[string][]string{"branches": {"master"}}, + }, + { + name: "on.pull_request.paths", + yaml: ` +name: local-action-docker-url +on: + pull_request: + paths: + - "**.go" + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: ./actions/docker-url +`, + inputEvent: "pull_request", + expectedOutput: map[string][]string{"paths": {"**.go"}}, + }, + { + name: "on.push.tags AND on.push.paths", + yaml: ` +name: local-action-docker-url +on: + push: + tags: + - "*-release" + paths: + - "**.go" + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: ./actions/docker-url +`, + inputEvent: "push", + expectedOutput: map[string][]string{"tags": {"*-release"}, "paths": {"**.go"}}, + }, + { + name: "on.pull_request.branches AND on.pull_request.paths", + yaml: ` +name: local-action-docker-url +on: + pull_request: + branches: + - master + paths: + - "**.go" + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: ./actions/docker-url +`, + inputEvent: "pull_request", + expectedOutput: map[string][]string{"branches": {"master"}, "paths": {"**.go"}}, + }, + { + name: "on.pull_request.branches AND on.push.tags", + yaml: ` +name: local-action-docker-url +on: + pull_request: + branches: + - "master" + - "rc" + push: + tags: + - "*-release" + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: ./actions/docker-url +`, + inputEvent: "pull_request", + expectedOutput: map[string][]string{"branches": {"master", "rc"}}, + }, + { + name: "on.push - no filters supplied", + yaml: ` +name: local-action-docker-url +on: push + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: ./actions/docker-url +`, + inputEvent: "push", + expectedOutput: map[string][]string{}, + }, + + { + name: "on.schedule - filters not supported", + yaml: ` +name: local-action-docker-url +on: + schedule: + - cron: $cron-weekly + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: ./actions/docker-url +`, + inputEvent: "schedule", + expectedOutput: map[string][]string{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + workflow, err := ReadWorkflow(strings.NewReader(tc.yaml)) + assert.NoError(t, err, "read workflow should succeed") + + assert.Equal(t, tc.expectedOutput, workflow.FindFilterPatterns(tc.inputEvent)) + }) + } +} + func TestReadWorkflow_StringContainer(t *testing.T) { yaml := ` name: local-action-docker-url From 2a582fde0aed06ad5116ce71938427069f52150a Mon Sep 17 00:00:00 2001 From: ae-ou Date: Mon, 27 Mar 2023 02:44:21 +0100 Subject: [PATCH 2/4] Add a ShouldFilterWorkflow() function. Testing is still needed for this function, and we need to determine a way to pass the payload down to the function in order to check it using filters.@ --- pkg/model/workflow.go | 41 ++++++++++++++++++++++++++++++++++---- pkg/model/workflow_test.go | 1 + 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/pkg/model/workflow.go b/pkg/model/workflow.go index 121bcd21e2a..66df97bc3c2 100644 --- a/pkg/model/workflow.go +++ b/pkg/model/workflow.go @@ -2,6 +2,7 @@ package model import ( "fmt" + "github.com/nektos/act/pkg/workflowpattern" "io" "reflect" "regexp" @@ -55,11 +56,12 @@ func (w *Workflow) On() []string { return nil } -//FindFilterPatterns searches for filter patterns relating to the specified event in the RawOn attribute -func (w *Workflow) FindFilterPatterns(event string) map[string][]string { +//FindFilterPatterns searches for filter patterns relating to the specified eventName (e.g. "pull_request", or "push") +//in the RawOn attribute. +func (w *Workflow) FindFilterPatterns(eventName string) map[string][]string { //TODO: We may want to return a custom struct that contains the filter types as attributes. //Return immediately if the event type doesn't support filters - if event != "push" && event != "pull_request" { + if eventName != "push" && eventName != "pull_request" { return map[string][]string{} } @@ -81,7 +83,7 @@ func (w *Workflow) FindFilterPatterns(event string) map[string][]string { for topLevelMapKey, topLevelMapVal := range topLevelMap { //Skip to the next iteration if this isn't the event that we're looking for. - if topLevelMapKey != event { + if topLevelMapKey != eventName { continue } @@ -103,6 +105,37 @@ func (w *Workflow) FindFilterPatterns(event string) map[string][]string { return output } +//ShouldFilterWorkflow finds any filters relating to eventName in the Workflow definition (e.g. if the eventName is +//"pull_request", there may be a set of "branches" patterns). It then uses the found filters to determine whether the +//workflow should be skipped based on the data in the eventPayload. +func (w *Workflow) ShouldFilterWorkflow(eventName string, eventPayload string) bool { + //Find all filter patterns that relate to the input event + if fp := w.FindFilterPatterns(eventName); len(fp) > 0 { + tw := new(workflowpattern.StdOutTraceWriter) + + //TODO: Switch to a custom filter struct, have unique handling for each attribute (e.g. "branches", "tags", and "paths") + //Iterate over the different types of filters (e.g. "branches", "tags", and "paths") + for filterType, patterns := range fp { + log.Debugf("'%s' filters were found for '%s' workflow", filterType, w.File) + + regexFilters, err := workflowpattern.CompilePatterns(patterns...) + + if err != nil { + log.Fatalf("Failed to convert '%s' filter patterns to regex for '%s' workflow: %v", filterType, w.File, err) + } + + //TODO: We need to pass the event payload into this function in order to populate the inputs here. + // We don't have access to the payload within the Workflow struct, and we shouldn't store it on the struct, + // so we will have to pass it in as a parameter from the Planner (although this doesn't have the event payload either) + if workflowpattern.Filter(regexFilters, []string{}, tw) || workflowpattern.Skip(regexFilters, []string{}, tw) { + return true + } + } + } + + return false +} + func (w *Workflow) OnEvent(event string) interface{} { if w.RawOn.Kind == yaml.MappingNode { var val map[string]interface{} diff --git a/pkg/model/workflow_test.go b/pkg/model/workflow_test.go index cfc861d79c3..58488ada3c3 100644 --- a/pkg/model/workflow_test.go +++ b/pkg/model/workflow_test.go @@ -264,6 +264,7 @@ jobs: expectedOutput: map[string][]string{}, }, } + //TODO: add additional yaml in the format of `branches: [ main, 'release/v[0-9].[0-9]' ]` for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { From 5f88d0a801900350ccc50a2c7cbdf653bdefcb92 Mon Sep 17 00:00:00 2001 From: ae-ou Date: Wed, 29 Mar 2023 21:24:22 +0100 Subject: [PATCH 3/4] Switch to FilterStruct --- pkg/model/workflow.go | 79 +++++++++++++----- pkg/model/workflow_test.go | 164 ++++++++++++++++++++++++++++++++++--- 2 files changed, 208 insertions(+), 35 deletions(-) diff --git a/pkg/model/workflow.go b/pkg/model/workflow.go index 66df97bc3c2..2fdd268d438 100644 --- a/pkg/model/workflow.go +++ b/pkg/model/workflow.go @@ -24,6 +24,14 @@ type Workflow struct { Defaults Defaults `yaml:"defaults"` } +// FilterPatterns is a structure that contains filter patterns that were parsed from the On attribute in an event +// https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions +type FilterPatterns struct { + Branches []string + Paths []string + Tags []string +} + // On events for the workflow func (w *Workflow) On() []string { switch w.RawOn.Kind { @@ -58,16 +66,15 @@ func (w *Workflow) On() []string { //FindFilterPatterns searches for filter patterns relating to the specified eventName (e.g. "pull_request", or "push") //in the RawOn attribute. -func (w *Workflow) FindFilterPatterns(eventName string) map[string][]string { - //TODO: We may want to return a custom struct that contains the filter types as attributes. +func (w *Workflow) FindFilterPatterns(eventName string) *FilterPatterns { //Return immediately if the event type doesn't support filters if eventName != "push" && eventName != "pull_request" { - return map[string][]string{} + return nil } //If it isn't a mapping node, then the following traversal can't be performed if w.RawOn.Kind != yaml.MappingNode { - return map[string][]string{} + return nil } //Decode rawOn to a map of string=>interfaces @@ -77,7 +84,7 @@ func (w *Workflow) FindFilterPatterns(eventName string) map[string][]string { log.Fatal(err) } - output := map[string][]string{} + output := &FilterPatterns{} //topLevelMapKey correlates to the event type - e.g. "push" or "pull_request" for topLevelMapKey, topLevelMapVal := range topLevelMap { @@ -94,7 +101,14 @@ func (w *Workflow) FindFilterPatterns(eventName string) map[string][]string { //Leaf correlates to the actual filter pattern for _, leaf := range lowLevelMapVal { if leafString := leaf.(string); ok { - output[midLevelMapKey] = append(output[midLevelMapKey], leafString) + switch midLevelMapKey { + case "branches": + output.Branches = append(output.Branches, leafString) + case "paths": + output.Paths = append(output.Paths, leafString) + case "tags": + output.Tags = append(output.Tags, leafString) + } } } } @@ -110,26 +124,47 @@ func (w *Workflow) FindFilterPatterns(eventName string) map[string][]string { //workflow should be skipped based on the data in the eventPayload. func (w *Workflow) ShouldFilterWorkflow(eventName string, eventPayload string) bool { //Find all filter patterns that relate to the input event - if fp := w.FindFilterPatterns(eventName); len(fp) > 0 { - tw := new(workflowpattern.StdOutTraceWriter) + fp := w.FindFilterPatterns(eventName) + + if fp != nil { + return false + } - //TODO: Switch to a custom filter struct, have unique handling for each attribute (e.g. "branches", "tags", and "paths") - //Iterate over the different types of filters (e.g. "branches", "tags", and "paths") - for filterType, patterns := range fp { - log.Debugf("'%s' filters were found for '%s' workflow", filterType, w.File) + tw := new(workflowpattern.StdOutTraceWriter) - regexFilters, err := workflowpattern.CompilePatterns(patterns...) + //Function to build the relevant regex patterns and compare segments of the event payload against them + filtrationFunc := func(patterns []string, inputs []string) bool { + regexFilters, err := workflowpattern.CompilePatterns(patterns...) - if err != nil { - log.Fatalf("Failed to convert '%s' filter patterns to regex for '%s' workflow: %v", filterType, w.File, err) - } + if err != nil { + log.Fatalf("Failed to convert filter patterns to regex for '%s' workflow: %v", w.File, err) + } - //TODO: We need to pass the event payload into this function in order to populate the inputs here. - // We don't have access to the payload within the Workflow struct, and we shouldn't store it on the struct, - // so we will have to pass it in as a parameter from the Planner (although this doesn't have the event payload either) - if workflowpattern.Filter(regexFilters, []string{}, tw) || workflowpattern.Skip(regexFilters, []string{}, tw) { - return true - } + if workflowpattern.Filter(regexFilters, []string{}, tw) || workflowpattern.Skip(regexFilters, []string{}, tw) { + return true + } + + return false + } + + if len(fp.Branches) > 0 { + //TODO: Replace the slice with a list of branches from the event payload + if shouldSkip := filtrationFunc(fp.Branches, []string{}); shouldSkip { + return true + } + } + + if len(fp.Paths) > 0 { + //TODO: Replace the slice with a list of paths from the event payload + if shouldSkip := filtrationFunc(fp.Paths, []string{}); shouldSkip { + return true + } + } + + if len(fp.Tags) > 0 { + //TODO: Replace the slice with a list of Tags from the event payload + if shouldSkip := filtrationFunc(fp.Tags, []string{}); shouldSkip { + return true } } diff --git a/pkg/model/workflow_test.go b/pkg/model/workflow_test.go index 58488ada3c3..82e713d5b31 100644 --- a/pkg/model/workflow_test.go +++ b/pkg/model/workflow_test.go @@ -76,7 +76,7 @@ func TestGetWorkflowFilterStrings(t *testing.T) { name string yaml string inputEvent string - expectedOutput map[string][]string + expectedOutput *FilterPatterns }{ { name: "on.push.branches", @@ -94,7 +94,24 @@ jobs: - uses: ./actions/docker-url `, inputEvent: "push", - expectedOutput: map[string][]string{"branches": {"master"}}, + expectedOutput: &FilterPatterns{Branches: []string{"master"}}, + }, + { + name: "on.push.branches - alternate syntax", + yaml: ` +name: local-action-docker-url +on: + push: + branches: [master] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: ./actions/docker-url +`, + inputEvent: "push", + expectedOutput: &FilterPatterns{Branches: []string{"master"}}, }, { name: "on.push.tags", @@ -112,7 +129,24 @@ jobs: - uses: ./actions/docker-url `, inputEvent: "push", - expectedOutput: map[string][]string{"tags": {"*-release"}}, + expectedOutput: &FilterPatterns{Tags: []string{"*-release"}}, + }, + { + name: "on.push.tags - alternate syntax", + yaml: ` +name: local-action-docker-url +on: + push: + tags: ["*-release"] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: ./actions/docker-url +`, + inputEvent: "push", + expectedOutput: &FilterPatterns{Tags: []string{"*-release"}}, }, { name: "on.push.paths", @@ -130,7 +164,24 @@ jobs: - uses: ./actions/docker-url `, inputEvent: "push", - expectedOutput: map[string][]string{"paths": {"**.go"}}, + expectedOutput: &FilterPatterns{Paths: []string{"**.go"}}, + }, + { + name: "on.push.paths - alternate syntax", + yaml: ` +name: local-action-docker-url +on: + push: + paths: ["**.go"] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: ./actions/docker-url +`, + inputEvent: "push", + expectedOutput: &FilterPatterns{Paths: []string{"**.go"}}, }, { name: "on.pull_request.branches", @@ -148,7 +199,24 @@ jobs: - uses: ./actions/docker-url `, inputEvent: "pull_request", - expectedOutput: map[string][]string{"branches": {"master"}}, + expectedOutput: &FilterPatterns{Branches: []string{"master"}}, + }, + { + name: "on.pull_request.branches - alternate syntax", + yaml: ` +name: local-action-docker-url +on: + pull_request: + branches: [master] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: ./actions/docker-url +`, + inputEvent: "pull_request", + expectedOutput: &FilterPatterns{Branches: []string{"master"}}, }, { name: "on.pull_request.paths", @@ -166,7 +234,24 @@ jobs: - uses: ./actions/docker-url `, inputEvent: "pull_request", - expectedOutput: map[string][]string{"paths": {"**.go"}}, + expectedOutput: &FilterPatterns{Paths: []string{"**.go"}}, + }, + { + name: "on.pull_request.paths", + yaml: ` +name: local-action-docker-url +on: + pull_request: + paths: ["**.go"] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: ./actions/docker-url +`, + inputEvent: "pull_request", + expectedOutput: &FilterPatterns{Paths: []string{"**.go"}}, }, { name: "on.push.tags AND on.push.paths", @@ -186,7 +271,25 @@ jobs: - uses: ./actions/docker-url `, inputEvent: "push", - expectedOutput: map[string][]string{"tags": {"*-release"}, "paths": {"**.go"}}, + expectedOutput: &FilterPatterns{Paths: []string{"**.go"}, Tags: []string{"*-release"}}, + }, + { + name: "on.push.tags AND on.push.paths - alternate syntax", + yaml: ` +name: local-action-docker-url +on: + push: + tags: ["*-release"] + paths: ["**.go"] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: ./actions/docker-url +`, + inputEvent: "push", + expectedOutput: &FilterPatterns{Paths: []string{"**.go"}, Tags: []string{"*-release"}}, }, { name: "on.pull_request.branches AND on.pull_request.paths", @@ -206,7 +309,25 @@ jobs: - uses: ./actions/docker-url `, inputEvent: "pull_request", - expectedOutput: map[string][]string{"branches": {"master"}, "paths": {"**.go"}}, + expectedOutput: &FilterPatterns{Branches: []string{"master"}, Paths: []string{"**.go"}}, + }, + { + name: "on.pull_request.branches AND on.pull_request.paths - alternate syntax", + yaml: ` +name: local-action-docker-url +on: + pull_request: + branches: [master] + paths: ["**.go"] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: ./actions/docker-url +`, + inputEvent: "pull_request", + expectedOutput: &FilterPatterns{Branches: []string{"master"}, Paths: []string{"**.go"}}, }, { name: "on.pull_request.branches AND on.push.tags", @@ -228,7 +349,26 @@ jobs: - uses: ./actions/docker-url `, inputEvent: "pull_request", - expectedOutput: map[string][]string{"branches": {"master", "rc"}}, + expectedOutput: &FilterPatterns{Branches: []string{"master", "rc"}}, + }, + { + name: "on.pull_request.branches AND on.push.tags - alternate syntax", + yaml: ` +name: local-action-docker-url +on: + pull_request: + branches: ["master", "rc"] + push: + tags: ["*-release"] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: ./actions/docker-url +`, + inputEvent: "pull_request", + expectedOutput: &FilterPatterns{Branches: []string{"master", "rc"}}, }, { name: "on.push - no filters supplied", @@ -243,9 +383,8 @@ jobs: - uses: ./actions/docker-url `, inputEvent: "push", - expectedOutput: map[string][]string{}, + expectedOutput: nil, }, - { name: "on.schedule - filters not supported", yaml: ` @@ -261,10 +400,9 @@ jobs: - uses: ./actions/docker-url `, inputEvent: "schedule", - expectedOutput: map[string][]string{}, + expectedOutput: nil, }, } - //TODO: add additional yaml in the format of `branches: [ main, 'release/v[0-9].[0-9]' ]` for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { From e7702b9fc1a624666cc70e944a67c62e4e9ed8cc Mon Sep 17 00:00:00 2001 From: ae-ou Date: Sun, 2 Apr 2023 22:06:20 +0100 Subject: [PATCH 4/4] Handle the '*-ignore' filter paths, and return a log.Fatal() if a filter and the *ignore variant (e.g. 'paths' and 'paths-ignore') are both set. Define the function signature in workflow_pattern so that I can easily reference the signature for Skip()/Filter(). Add TODOs. --- pkg/model/planner.go | 7 +++ pkg/model/workflow.go | 71 ++++++++++++++++--------- pkg/model/workflow_test.go | 54 +++++++++++++++++++ pkg/workflowpattern/workflow_pattern.go | 3 ++ 4 files changed, 111 insertions(+), 24 deletions(-) diff --git a/pkg/model/planner.go b/pkg/model/planner.go index 1769b73212e..65d58198850 100644 --- a/pkg/model/planner.go +++ b/pkg/model/planner.go @@ -184,6 +184,8 @@ func (wp *workflowPlanner) PlanEvent(eventName string) (*Plan, error) { continue } + //TODO: filter out workflow using ShouldFilterWorkflow() - we will need to provide the GitHub/Event context + for _, e := range events { if e == eventName { stages, err := createStages(w, w.GetJobIDs()...) @@ -208,6 +210,9 @@ func (wp *workflowPlanner) PlanJob(jobName string) (*Plan, error) { var lastErr error for _, w := range wp.workflows { + //TODO: do we filter out workflows here? We want to run a specific job, not the full workflow - so should + // the workflow-level filters (e.g. "tags", "branches-ignore") still apply? + stages, err := createStages(w, jobName) if err != nil { log.Warn(err) @@ -229,6 +234,8 @@ func (wp *workflowPlanner) PlanAll() (*Plan, error) { var lastErr error for _, w := range wp.workflows { + //TODO: filter out workflow using ShouldFilterWorkflow() - we will need to provide the GitHub/Event context + stages, err := createStages(w, w.GetJobIDs()...) if err != nil { log.Warn(err) diff --git a/pkg/model/workflow.go b/pkg/model/workflow.go index 2fdd268d438..3848253a16d 100644 --- a/pkg/model/workflow.go +++ b/pkg/model/workflow.go @@ -27,9 +27,12 @@ type Workflow struct { // FilterPatterns is a structure that contains filter patterns that were parsed from the On attribute in an event // https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions type FilterPatterns struct { - Branches []string - Paths []string - Tags []string + Branches []string + BranchesIgnore []string + Paths []string + PathsIgnore []string + Tags []string + TagsIgnore []string } // On events for the workflow @@ -65,7 +68,8 @@ func (w *Workflow) On() []string { } //FindFilterPatterns searches for filter patterns relating to the specified eventName (e.g. "pull_request", or "push") -//in the RawOn attribute. +//in the RawOn attribute. This will error out if there are filters that can't be used simultaneously for the same event +//(e.g. "paths" and "paths-ignore") func (w *Workflow) FindFilterPatterns(eventName string) *FilterPatterns { //Return immediately if the event type doesn't support filters if eventName != "push" && eventName != "pull_request" { @@ -104,10 +108,16 @@ func (w *Workflow) FindFilterPatterns(eventName string) *FilterPatterns { switch midLevelMapKey { case "branches": output.Branches = append(output.Branches, leafString) + case "branches-ignore": + output.BranchesIgnore = append(output.BranchesIgnore, leafString) case "paths": output.Paths = append(output.Paths, leafString) + case "paths-ignore": + output.PathsIgnore = append(output.PathsIgnore, leafString) case "tags": output.Tags = append(output.Tags, leafString) + case "tags-ignore": + output.TagsIgnore = append(output.TagsIgnore, leafString) } } } @@ -116,55 +126,68 @@ func (w *Workflow) FindFilterPatterns(eventName string) *FilterPatterns { } } + //TODO: Should these all be migrated over to return an error (it would facilitate better testing) - see this PR: + // https://github.com/nektos/act/pull/1705 + if len(output.Branches) > 0 && len(output.BranchesIgnore) > 0 { + log.Fatal("branches and branches-ignore were both specified for the event, but they can't be used simultaneously") + } + + if len(output.Paths) > 0 && len(output.PathsIgnore) > 0 { + log.Fatal("paths and paths-ignore were both specified for the event, but they can't be used simultaneously") + } + + if len(output.Tags) > 0 && len(output.TagsIgnore) > 0 { + log.Fatal("tags and tags-ignore were both specified for the event, but they can't be used simultaneously") + } + return output } //ShouldFilterWorkflow finds any filters relating to eventName in the Workflow definition (e.g. if the eventName is //"pull_request", there may be a set of "branches" patterns). It then uses the found filters to determine whether the //workflow should be skipped based on the data in the eventPayload. -func (w *Workflow) ShouldFilterWorkflow(eventName string, eventPayload string) bool { +func (w *Workflow) ShouldFilterWorkflow(eventName string, ghc GithubContext) bool { //Find all filter patterns that relate to the input event - fp := w.FindFilterPatterns(eventName) + filterPatterns := w.FindFilterPatterns(eventName) - if fp != nil { + if filterPatterns == nil { return false } tw := new(workflowpattern.StdOutTraceWriter) //Function to build the relevant regex patterns and compare segments of the event payload against them - filtrationFunc := func(patterns []string, inputs []string) bool { + eventCheckFunc := func(filterFunc workflowpattern.FilterInputsFunc, patterns []string, inputs []string) bool { regexFilters, err := workflowpattern.CompilePatterns(patterns...) if err != nil { log.Fatalf("Failed to convert filter patterns to regex for '%s' workflow: %v", w.File, err) } - if workflowpattern.Filter(regexFilters, []string{}, tw) || workflowpattern.Skip(regexFilters, []string{}, tw) { + if filterFunc(regexFilters, inputs, tw) { return true } return false } - if len(fp.Branches) > 0 { - //TODO: Replace the slice with a list of branches from the event payload - if shouldSkip := filtrationFunc(fp.Branches, []string{}); shouldSkip { - return true - } - } - - if len(fp.Paths) > 0 { - //TODO: Replace the slice with a list of paths from the event payload - if shouldSkip := filtrationFunc(fp.Paths, []string{}); shouldSkip { - return true + //Iterate over the patterns that we call Skip() for (the non *ignore attributes from FilterPatterns) + for _, fp := range [][]string{filterPatterns.Branches, filterPatterns.Paths, filterPatterns.Tags} { + if len(fp) > 0 { + //TODO: pass the relevant data instead of a slice of string - this depends on the GithubContext/event context + if shouldSkip := eventCheckFunc(workflowpattern.Skip, fp, []string{}); shouldSkip { + return true + } } } - if len(fp.Tags) > 0 { - //TODO: Replace the slice with a list of Tags from the event payload - if shouldSkip := filtrationFunc(fp.Tags, []string{}); shouldSkip { - return true + //Iterate over the *Ignore attributes from the FilterPatterns struct + for _, fp := range [][]string{filterPatterns.BranchesIgnore, filterPatterns.PathsIgnore, filterPatterns.TagsIgnore} { + if len(fp) > 0 { + //TODO: pass the relevant data instead of a slice of string - this depends on the GithubContext/event context + if shouldFilter := eventCheckFunc(workflowpattern.Filter, fp, []string{}); shouldFilter { + return true + } } } diff --git a/pkg/model/workflow_test.go b/pkg/model/workflow_test.go index 82e713d5b31..beb95957440 100644 --- a/pkg/model/workflow_test.go +++ b/pkg/model/workflow_test.go @@ -113,6 +113,24 @@ jobs: inputEvent: "push", expectedOutput: &FilterPatterns{Branches: []string{"master"}}, }, + { + name: "on.push.branches-ignore", + yaml: ` +name: local-action-docker-url +on: + push: + branches-ignore: + - "**test" + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: ./actions/docker-url +`, + inputEvent: "push", + expectedOutput: &FilterPatterns{BranchesIgnore: []string{"**test"}}, + }, { name: "on.push.tags", yaml: ` @@ -148,6 +166,24 @@ jobs: inputEvent: "push", expectedOutput: &FilterPatterns{Tags: []string{"*-release"}}, }, + { + name: "on.push.tags-ignore", + yaml: ` +name: local-action-docker-url +on: + push: + tags-ignore: + - "*-alpha" + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: ./actions/docker-url +`, + inputEvent: "push", + expectedOutput: &FilterPatterns{TagsIgnore: []string{"*-alpha"}}, + }, { name: "on.push.paths", yaml: ` @@ -183,6 +219,24 @@ jobs: inputEvent: "push", expectedOutput: &FilterPatterns{Paths: []string{"**.go"}}, }, + { + name: "on.push.paths-ignore", + yaml: ` +name: local-action-docker-url +on: + push: + paths-ignore: + - "**.md" + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: ./actions/docker-url +`, + inputEvent: "push", + expectedOutput: &FilterPatterns{PathsIgnore: []string{"**.md"}}, + }, { name: "on.pull_request.branches", yaml: ` diff --git a/pkg/workflowpattern/workflow_pattern.go b/pkg/workflowpattern/workflow_pattern.go index cc03e405626..5184ea64b6b 100644 --- a/pkg/workflowpattern/workflow_pattern.go +++ b/pkg/workflowpattern/workflow_pattern.go @@ -147,6 +147,9 @@ func CompilePatterns(patterns ...string) ([]*WorkflowPattern, error) { return ret, nil } +//FilterInputsFunc defines the signature that both Skip() and Filter() implement +type FilterInputsFunc func(sequence []*WorkflowPattern, input []string, traceWriter TraceWriter) bool + // returns true if the workflow should be skipped paths/branches func Skip(sequence []*WorkflowPattern, input []string, traceWriter TraceWriter) bool { if len(sequence) == 0 {