Repository: srvaroa/labeler
Pull request #15 (2020-09-12 20:04:24)

Support multiple matchers per label
Introduce a new config version that allows multiple matchers for the
same label. This will allow to combine conditions with OR semantics.

Details added to README.

Ref.: #11 

Signed-off-by: Galo Navarro <anglorvaroa@gmail.com>

srvaroa: The Files matcher has not changed so it'll work in the same way as before. That is, this:

```
Version: 1
Labels:
  - Label: "MyLabel"
    files: 
      - "cmd/.*.go"
      - "pkg/.*.go"
```

Applies the `MyLabel` label if a file that matches [any of those regexes](https://github.com/srvaroa/labeler/blame/master/pkg/labeler.go#L101).  So if I interpret your question correctly, no, you just need one matcher, and multiple regexes for all the files you want to associate the label to.

The change implemented here only affects how matchers are combined. That is:

```
Version: 1
Labels:
  - Label: "MyLabel"
    title: 
      - "^WIP.*"
  - Label: "MyLabel"
    files: 
      - "cmd/.*.go"
      - "pkg/.*.go"
```

This PR allows having both entries for the same `Label`.  And, it will combine them with an OR.  That is, it'll apply `MyLabel` if either of the matchers are satisfied (the title starts with "WIP", *or* if it modifies any file that matches any of those two regexes). It should remove `MyLabel` if none of the matchers are satisfied.

If you're using multiple conditions in a `files` matcher (e.g. "a modified file matches any of these paths") like this:

```
Version: 1
Labels:
  - Label: "MyLabel"
    files: 
      - "cmd/.*.go"
      - "pkg/.*.go"
```

This PR would indeed allow you to break the matcher in two, with exactly the same results:

```
Version: 1
Labels:
  - Label: "MyLabel"
    files: 
      - "cmd/.*.go"
  - Label: "MyLabel"
    files: 
      - "pkg/.*.go"
```

Its' equivalent, but more verbose so you probably want to keep the shorter form (1 matcher, multiple files).

Does that make sense?

Patch:
diff --git a/.github/labeler.yml b/.github/labeler.yml
index 7b49b57..8c8b8ca 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -1,7 +1,9 @@
-TestLabel:
-  title: ".*"
-TestFileMatch:
-  files: 
-    - "cmd/.*.go"
-    - "pkg/.*.go"
-  
\ No newline at end of file
+Version: 1
+Labels:
+  - Label: "TestLabel"
+    title: ".*"
+  - Label: "TestFileMatch"
+    files: 
+      - "cmd/.*.go"
+      - "pkg/.*.go"
+  
diff --git a/README.md b/README.md
index cfc5808..fad0b8f 100644
--- a/README.md
+++ b/README.md
@@ -42,31 +42,40 @@ Typical errors are:
 
 ## Configuration
 
-Configuration can be stored at `./github/labeler.yml` as a plain list of labels
-and a set of conditions for each.  When *all* conditions for a label match,
-then the Action will set the given label.  When *any* condition for a label
-does not match, then the Action will unset the given label.
+Configuration can be stored at `./github/labeler.yml` as a plain list of
+label matchers, which consist of a label and a set of conditions for
+each.  When *all* conditions for a label match, then the Action will set
+the given label.  When *any* condition for a label does not match, then
+the Action will unset the given label.
 
-    <label>:
+Here is an example of a matcher for label "Example":
+
+      <label>: "Example"
       <condition_name>: <condition_parameters>
       <condition_name>: <condition_parameters>
 
-For example, given this `./github/labeler.yml`:
+For example, this `./github/labeler.yml` contains a single matcher with
+a single condition:
 
-      WIP:
-        title: "^WIP:.*"
+    version: 2
+    labels:
+      - label: "WIP"
+	title: "^WIP:.*"
 
 A Pull Request with title "WIP: this is work in progress" would be labelled as
 `WIP`.  If the Pull Request title changes to "This is done", then the `WIP`
 label would be removed.
 
-Each label may combine multiple conditions.  The label will be applied if *all*
-conditions are satisfied, removed otherwise.
+Each label may combine multiple conditions.  The action combines all
+conditions with an AND operation.  That is, the label will be applied if
+*all* conditions are satisfied, removed otherwise.
 
 For example, given this `./github/labeler.yml`:
 
-      WIP:
-        title: "^WIP:.*"
+    version: 2
+    labels:
+      - label: "WIP"
+	title: "^WIP:.*"
         mergeable: false
 
 A Pull Request with title "WIP: this is work in progress" *and* not in a
@@ -74,38 +83,47 @@ mergeable state would be labelled as `WIP`.  If the Pull Request title changes
 to "This is done", or it becomes mergeable, then the `WIP` label would be
 removed.
 
+If you wish to apply an OR, you may set multiple matchers for the same
+label. For example:
+
+    version: 2
+    labels:
+      - label: "WIP"
+	title: "^WIP:.*"
+      - label: "WIP"
+        mergeable: false
+
+The `WIP` label will be set if the title matches `^WIP:.*` OR the label
+is not in a mergeable state.
+
 ## Conditions
 
-Below are the conditions currently supported.
+Below are the conditions currently supported in label matchers.
 
 ### Regex on title
 
 This condition is satisfied when the PR title matches on the given regex.
 
-    WIP:
-      title: "^WIP:.*"
+    title: "^WIP:.*"
 
 ### Regex on branch
 
 This condition is satisfied when the PR branch matches on the given regex.
 
-    Feature:
-      branch: "^feature/.*"
+    branch: "^feature/.*"
 
 ### Regex on PR files
 
 This condition is satisfied when any of the PR files matches on the given regexs.
 
-    Tests:
-      files: 
-        - "cmd/.*_tests.go"
+    files: 
+      - "cmd/.*_tests.go"
 
 ### Mergeable status
 
 This condition is satisfied when the PR is in a [mergeable state](https://developer.github.com/v3/pulls/#response-1).
 
-    MyLabel:
-      mergeable: true
+    mergeable: true
 
 ### PR size
 
@@ -117,12 +135,12 @@ deletions` in the PR.
 
 For example, given this `./github/labeler.yml`:
 
-    S:
+    - label: "S"
       size-below: 10
-    M:
+    - label: "M":
       size-above: 9
       size-below: 100
-    L:
+    - label: "L":
       size-above: 100
 
 These would be the labels assigned to some PRs, based on their size as
diff --git a/cmd/action.go b/cmd/action.go
index 9401a00..6ab7639 100644
--- a/cmd/action.go
+++ b/cmd/action.go
@@ -81,16 +81,45 @@ func getRepoFile(gh *github.Client, repo, file, sha string) (*[]byte, error) {
 	return &raw, err
 }
 
-// getLabelerConfig builds a LabelerConfig from a raw yaml
-func getLabelerConfig(configRaw *[]byte) (*labeler.LabelerConfig, error) {
-	var c labeler.LabelerConfig
+// getLabelerConfig builds a LabelerConfigV1 from a raw yaml
+func getLabelerConfig(configRaw *[]byte) (*labeler.LabelerConfigV1, error) {
+	var c labeler.LabelerConfigV1
 	err := yaml.Unmarshal(*configRaw, &c)
 	if err != nil {
-		log.Printf("Unable to unmarshall config --\n%s\n--, %s", configRaw, err)
+		log.Printf("Unable to unmarshall config %s: ", err)
+	}
+	if c.Version == 0 {
+		c, err = getLabelerConfigV1(configRaw)
+		if err != nil {
+			log.Printf("Unable to unmarshall legacy config %s: ", err)
+		}
 	}
 	return &c, err
 }
 
+func getLabelerConfigV1(configRaw *[]byte) (labeler.LabelerConfigV1, error) {
+
+	// Load v0
+	var oldCfg map[string]labeler.LabelMatcher
+	err := yaml.Unmarshal(*configRaw, &oldCfg)
+	if err != nil {
+		log.Printf("Unable to unmarshall legacy config: %s", err)
+		return labeler.LabelerConfigV1{}, err
+	}
+
+	// Convert
+	var matchers = []labeler.LabelMatcher{}
+	for label, matcher := range oldCfg {
+		matcher.Label = label
+		matchers = append(matchers, matcher)
+	}
+
+	return labeler.LabelerConfigV1{
+		Version: 0,
+		Labels:  matchers,
+	}, err
+}
+
 func getGithubClient() *github.Client {
 	ghToken := os.Getenv("GITHUB_TOKEN")
 	ctx := context.Background()
@@ -114,10 +143,10 @@ func getEventPayload() *[]byte {
 	return &eventPayload
 }
 
-func newLabeler(gh *github.Client, config *labeler.LabelerConfig) *labeler.Labeler {
+func newLabeler(gh *github.Client, config *labeler.LabelerConfigV1) *labeler.Labeler {
 	l := labeler.Labeler{
 
-		FetchRepoConfig: func(owner string, repoName string) (*labeler.LabelerConfig, error) {
+		FetchRepoConfig: func(owner string, repoName string) (*labeler.LabelerConfigV1, error) {
 			return config, nil
 		},
 
diff --git a/pkg/labeler.go b/pkg/labeler.go
index 9a84c2d..d1cea65 100644
--- a/pkg/labeler.go
+++ b/pkg/labeler.go
@@ -15,8 +15,8 @@ import (
 	"github.com/waigani/diffparser"
 )
 
-type LabelerConfig map[string]LabelMatcher
 type LabelMatcher struct {
+	Label     string
 	Title     string
 	Branch    string
 	Files     []string
@@ -25,13 +25,20 @@ type LabelMatcher struct {
 	SizeAbove string `yaml:"size-above"`
 }
 
+type LabelerConfigV0 map[string]LabelMatcher
+
+type LabelerConfigV1 struct {
+	Version int32
+	Labels  []LabelMatcher
+}
+
 // LabelUpdates Represents a request to update the set of labels
 type LabelUpdates struct {
 	set map[string]bool
 }
 
 type Labeler struct {
-	FetchRepoConfig    func(owner string, repoName string) (*LabelerConfig, error)
+	FetchRepoConfig    func(owner string, repoName string) (*LabelerConfigV1, error)
 	ReplaceLabelsForPr func(owner string, repoName string, prNumber int, labels []string) error
 	GetCurrentLabels   func(owner string, repoName string, prNumber int) ([]string, error)
 }
@@ -215,7 +222,7 @@ func (l *Labeler) executeOn(pr *gh.PullRequest) error {
 }
 
 // findMatches returns all updates to be made to labels for the given PR
-func (l *Labeler) findMatches(pr *gh.PullRequest, config *LabelerConfig) (LabelUpdates, error) {
+func (l *Labeler) findMatches(pr *gh.PullRequest, config *LabelerConfigV1) (LabelUpdates, error) {
 
 	labelUpdates := LabelUpdates{
 		set: map[string]bool{},
@@ -228,7 +235,23 @@ func (l *Labeler) findMatches(pr *gh.PullRequest, config *LabelerConfig) (LabelU
 		NewFilesCondition(),
 	}
 
-	for label, matcher := range *config {
+	for _, matcher := range config.Labels {
+		label := matcher.Label
+
+		if labelUpdates.set[label] {
+			// This label was already matched in another matcher
+			// so we already decided to apply it and need to
+			// evaluate no more matchers.
+			//
+			// Note that multiple matchers for the same label
+			// are combined with an OR.
+			continue
+		}
+
+		// Reset the label as we're going to re-evaluate it in a new
+		// condition
+		delete(labelUpdates.set, label)
+
 		for _, c := range conditions {
 			isMatched, err := c.Evaluate(pr, matcher)
 			if err != nil {
@@ -237,7 +260,6 @@ func (l *Labeler) findMatches(pr *gh.PullRequest, config *LabelerConfig) (LabelU
 			}
 
 			prev, ok := labelUpdates.set[label]
-
 			if ok { // Other conditions were evaluated for the label
 				labelUpdates.set[label] = prev && isMatched
 			} else { // First condition evaluated for this label


Test patch:
diff --git a/cmd/action_test.go b/cmd/action_test.go
index 12c7bcd..01e050f 100644
--- a/cmd/action_test.go
+++ b/cmd/action_test.go
@@ -9,9 +9,9 @@ import (
 	l "github.com/srvaroa/labeler/pkg"
 )
 
-func TestGetLabelerConfig(t *testing.T) {
+func TestGetLabelerConfigV0(t *testing.T) {
 
-	file, err := os.Open("../test_data/config.yml")
+	file, err := os.Open("../test_data/config_v0.yml")
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -21,31 +21,106 @@ func TestGetLabelerConfig(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	var c *l.LabelerConfig
+	var c *l.LabelerConfigV1
 	c, err = getLabelerConfig(&contents)
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	expect := l.LabelerConfig{
+	if 0 != c.Version {
+		t.Fatalf("Expect version: %+v Got: %+v", 0, c.Version)
+	}
+
+	expectMatchers := map[string]l.LabelMatcher{
 		"WIP": l.LabelMatcher{
+			Label: "WIP",
 			Title: "^WIP:.*",
 		},
 		"WOP": l.LabelMatcher{
+			Label: "WOP",
 			Title: "^WOP:.*",
 		},
 		"S": l.LabelMatcher{
+			Label:     "S",
 			SizeBelow: "10",
 		},
 		"M": l.LabelMatcher{
+			Label:     "M",
 			SizeAbove: "9",
 			SizeBelow: "100",
 		},
 		"L": l.LabelMatcher{
+			Label:     "L",
 			SizeAbove: "100",
 		},
 	}
 
+	if !cmp.Equal(len(expectMatchers), len(c.Labels)) {
+		t.Fatalf("Expect same number of matchers: %+v Got: %+v",
+			len(expectMatchers),
+			len(c.Labels))
+	}
+
+	for _, actualMatcher := range c.Labels {
+		expectMatcher := expectMatchers[actualMatcher.Label]
+		if !cmp.Equal(expectMatcher, actualMatcher) {
+			t.Fatalf("Expect matcher: %+v Got: %+v",
+				expectMatcher,
+				actualMatcher)
+		}
+	}
+
+}
+
+func TestGetLabelerConfigV1(t *testing.T) {
+
+	file, err := os.Open("../test_data/config_v1.yml")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	contents, err := ioutil.ReadAll(file)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	var c *l.LabelerConfigV1
+	c, err = getLabelerConfig(&contents)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	expect := l.LabelerConfigV1{
+		Version: 1,
+		Labels: []l.LabelMatcher{
+			l.LabelMatcher{
+				Label:  "WIP",
+				Branch: "wip",
+			},
+			l.LabelMatcher{
+				Label: "WIP",
+				Title: "^WIP:.*",
+			},
+			l.LabelMatcher{
+				Label: "WOP",
+				Title: "^WOP:.*",
+			},
+			l.LabelMatcher{
+				Label:     "S",
+				SizeBelow: "10",
+			},
+			l.LabelMatcher{
+				Label:     "M",
+				SizeAbove: "9",
+				SizeBelow: "100",
+			},
+			l.LabelMatcher{
+				Label:     "L",
+				SizeAbove: "100",
+			},
+		},
+	}
+
 	if !cmp.Equal(expect, *c) {
 		t.Fatalf("Expect: %+v Got: %+v", expect, c)
 	}
diff --git a/pkg/labeler_test.go b/pkg/labeler_test.go
index 3d7f758..a5c356d 100644
--- a/pkg/labeler_test.go
+++ b/pkg/labeler_test.go
@@ -20,7 +20,7 @@ func loadPayload(name string) ([]byte, error) {
 type TestCase struct {
 	payloads       []string
 	name           string
-	config         LabelerConfig
+	config         LabelerConfigV1
 	initialLabels  []string
 	expectedLabels []string
 }
@@ -32,15 +32,20 @@ func TestHandleEvent(t *testing.T) {
 		TestCase{
 			payloads:       []string{"create_pr", "reopen_pr"},
 			name:           "Empty config",
-			config:         LabelerConfig{},
+			config:         LabelerConfigV1{},
 			initialLabels:  []string{"Fix"},
 			expectedLabels: []string{"Fix"},
 		},
 		TestCase{
 			payloads: []string{"create_pr", "reopen_pr"},
 			name:     "Config with no rules",
-			config: LabelerConfig{
-				"WIP": LabelMatcher{},
+			config: LabelerConfigV1{
+				Version: 1,
+				Labels: []LabelMatcher{
+					LabelMatcher{
+						Label: "WIP",
+					},
+				},
 			},
 			initialLabels:  []string{"Fix"},
 			expectedLabels: []string{"Fix"},
@@ -48,9 +53,13 @@ func TestHandleEvent(t *testing.T) {
 		TestCase{
 			payloads: []string{"create_pr", "reopen_pr"},
 			name:     "Add a label when not set and config matches",
-			config: LabelerConfig{
-				"WIP": LabelMatcher{
-					Title: "^WIP:.*",
+			config: LabelerConfigV1{
+				Version: 1,
+				Labels: []LabelMatcher{
+					LabelMatcher{
+						Label: "WIP",
+						Title: "^WIP:.*",
+					},
 				},
 			},
 			initialLabels:  []string{},
@@ -59,9 +68,13 @@ func TestHandleEvent(t *testing.T) {
 		TestCase{
 			payloads: []string{"create_pr", "reopen_pr"},
 			name:     "Remove a label when set and config does not match",
-			config: LabelerConfig{
-				"Fix": LabelMatcher{
-					Title: "Fix: .*",
+			config: LabelerConfigV1{
+				Version: 1,
+				Labels: []LabelMatcher{
+					LabelMatcher{
+						Label: "Fix",
+						Title: "Fix: .*",
+					},
 				},
 			},
 			initialLabels:  []string{"Fix"},
@@ -70,9 +83,13 @@ func TestHandleEvent(t *testing.T) {
 		TestCase{
 			payloads: []string{"create_pr", "reopen_pr"},
 			name:     "Respect a label when set, and not present in config",
-			config: LabelerConfig{
-				"Fix": LabelMatcher{
-					Title: "^Fix.*",
+			config: LabelerConfigV1{
+				Version: 1,
+				Labels: []LabelMatcher{
+					LabelMatcher{
+						Label: "Fix",
+						Title: "^Fix.*",
+					},
 				},
 			},
 			initialLabels:  []string{"SomeLabel"},
@@ -81,12 +98,17 @@ func TestHandleEvent(t *testing.T) {
 		TestCase{
 			payloads: []string{"create_pr", "reopen_pr"},
 			name:     "A combination of all cases",
-			config: LabelerConfig{
-				"WIP": LabelMatcher{
-					Title: "^WIP:.*",
-				},
-				"ShouldRemove": LabelMatcher{
-					Title: "^MEH.*",
+			config: LabelerConfigV1{
+				Version: 1,
+				Labels: []LabelMatcher{
+					LabelMatcher{
+						Label: "WIP",
+						Title: "^WIP:.*",
+					},
+					LabelMatcher{
+						Label: "ShouldRemove",
+						Title: "^MEH.*",
+					},
 				},
 			},
 			initialLabels:  []string{"ShouldRemove", "ShouldRespect"},
@@ -95,10 +117,14 @@ func TestHandleEvent(t *testing.T) {
 		TestCase{
 			payloads: []string{"create_pr", "reopen_pr"},
 			name:     "Add a label with two conditions, both matching",
-			config: LabelerConfig{
-				"WIP": LabelMatcher{
-					Title:     "^WIP:.*",
-					Mergeable: "False",
+			config: LabelerConfigV1{
+				Version: 1,
+				Labels: []LabelMatcher{
+					LabelMatcher{
+						Label:     "WIP",
+						Title:     "^WIP:.*",
+						Mergeable: "False",
+					},
 				},
 			},
 			initialLabels:  []string{},
@@ -107,10 +133,14 @@ func TestHandleEvent(t *testing.T) {
 		TestCase{
 			payloads: []string{"create_pr", "reopen_pr"},
 			name:     "Add a label with two conditions, one not matching (1)",
-			config: LabelerConfig{
-				"WIP": LabelMatcher{
-					Title:     "^WIP:.*",
-					Mergeable: "True",
+			config: LabelerConfigV1{
+				Version: 1,
+				Labels: []LabelMatcher{
+					LabelMatcher{
+						Label:     "WIP",
+						Title:     "^WIP:.*",
+						Mergeable: "True",
+					},
 				},
 			},
 			initialLabels:  []string{},
@@ -121,10 +151,14 @@ func TestHandleEvent(t *testing.T) {
 			// condition, while previous ones are false
 			payloads: []string{"create_pr", "reopen_pr"},
 			name:     "Add a label with two conditions, one not matching (2)",
-			config: LabelerConfig{
-				"WIP": LabelMatcher{
-					Title:     "^DOES NOT MATCH:.*",
-					Mergeable: "False",
+			config: LabelerConfigV1{
+				Version: 1,
+				Labels: []LabelMatcher{
+					LabelMatcher{
+						Label:     "WIP",
+						Title:     "^DOES NOT MATCH:.*",
+						Mergeable: "False",
+					},
 				},
 			},
 			initialLabels:  []string{},
@@ -133,9 +167,13 @@ func TestHandleEvent(t *testing.T) {
 		TestCase{
 			payloads: []string{"small_pr"},
 			name:     "Test the size_below rule",
-			config: LabelerConfig{
-				"S": LabelMatcher{
-					SizeBelow: "10",
+			config: LabelerConfigV1{
+				Version: 1,
+				Labels: []LabelMatcher{
+					LabelMatcher{
+						Label:     "S",
+						SizeBelow: "10",
+					},
 				},
 			},
 			initialLabels:  []string{},
@@ -144,10 +182,14 @@ func TestHandleEvent(t *testing.T) {
 		TestCase{
 			payloads: []string{"mid_pr"},
 			name:     "Test the size_below and size_above rules",
-			config: LabelerConfig{
-				"M": LabelMatcher{
-					SizeAbove: "9",
-					SizeBelow: "100",
+			config: LabelerConfigV1{
+				Version: 1,
+				Labels: []LabelMatcher{
+					LabelMatcher{
+						Label:     "M",
+						SizeAbove: "9",
+						SizeBelow: "100",
+					},
 				},
 			},
 			initialLabels:  []string{},
@@ -156,9 +198,13 @@ func TestHandleEvent(t *testing.T) {
 		TestCase{
 			payloads: []string{"big_pr"},
 			name:     "Test the size_above rule",
-			config: LabelerConfig{
-				"L": LabelMatcher{
-					SizeAbove: "100",
+			config: LabelerConfigV1{
+				Version: 1,
+				Labels: []LabelMatcher{
+					LabelMatcher{
+						Label:     "L",
+						SizeAbove: "100",
+					},
 				},
 			},
 			initialLabels:  []string{},
@@ -167,9 +213,13 @@ func TestHandleEvent(t *testing.T) {
 		TestCase{
 			payloads: []string{"small_pr"},
 			name:     "Test the branch rule (matching)",
-			config: LabelerConfig{
-				"Branch": LabelMatcher{
-					Branch: "^srvaroa-patch.*",
+			config: LabelerConfigV1{
+				Version: 1,
+				Labels: []LabelMatcher{
+					LabelMatcher{
+						Label:  "Branch",
+						Branch: "^srvaroa-patch.*",
+					},
 				},
 			},
 			initialLabels:  []string{},
@@ -178,9 +228,13 @@ func TestHandleEvent(t *testing.T) {
 		TestCase{
 			payloads: []string{"small_pr"},
 			name:     "Test the branch rule (not matching)",
-			config: LabelerConfig{
-				"Branch": LabelMatcher{
-					Branch: "^does/not-match/*",
+			config: LabelerConfigV1{
+				Version: 1,
+				Labels: []LabelMatcher{
+					LabelMatcher{
+						Label:  "Branch",
+						Branch: "^does/not-match/*",
+					},
 				},
 			},
 			initialLabels:  []string{},
@@ -188,17 +242,40 @@ func TestHandleEvent(t *testing.T) {
 		},
 		TestCase{
 			payloads: []string{"diff_pr"},
-			name:     "Test the branch rule",
-			config: LabelerConfig{
-				"Files": LabelMatcher{
-					Files: []string{
-						"^pkg/.*_test.go",
+			name:     "Test the files rule",
+			config: LabelerConfigV1{
+				Version: 1,
+				Labels: []LabelMatcher{
+					LabelMatcher{
+						Label: "Files",
+						Files: []string{
+							"^pkg/.*_test.go",
+						},
 					},
 				},
 			},
 			initialLabels:  []string{},
 			expectedLabels: []string{"Files"},
 		},
+		TestCase{
+			payloads: []string{"small_pr"},
+			name:     "Multiple conditions for the same tag function as OR",
+			config: LabelerConfigV1{
+				Version: 1,
+				Labels: []LabelMatcher{
+					LabelMatcher{
+						Label:  "Branch",
+						Branch: "^srvaroa-patch.*",
+					},
+					LabelMatcher{
+						Label:  "Branch",
+						Branch: "WONT MATCH",
+					},
+				},
+			},
+			initialLabels:  []string{},
+			expectedLabels: []string{"Branch"},
+		},
 	}
 
 	for _, tc := range testCases {
@@ -220,7 +297,7 @@ func TestHandleEvent(t *testing.T) {
 
 func NewTestLabeler(t *testing.T, tc TestCase) Labeler {
 	return Labeler{
-		FetchRepoConfig: func(owner, repoName string) (*LabelerConfig, error) {
+		FetchRepoConfig: func(owner, repoName string) (*LabelerConfigV1, error) {
 			return &tc.config, nil
 		},
 		GetCurrentLabels: func(owner, repoName string, prNumber int) ([]string, error) {
diff --git a/test_data/config.yml b/test_data/config_v0.yml
similarity index 100%
rename from test_data/config.yml
rename to test_data/config_v0.yml
diff --git a/test_data/config_v1.yml b/test_data/config_v1.yml
new file mode 100644
index 0000000..8781597
--- /dev/null
+++ b/test_data/config_v1.yml
@@ -0,0 +1,15 @@
+version: 1
+labels:
+  - label: WIP
+    branch: "wip"
+  - label: WIP
+    title: "^WIP:.*"
+  - label: WOP
+    title: "^WOP:.*"
+  - label: S
+    size-below: 10
+  - label: M
+    size-above: 9
+    size-below: 100
+  - label: L
+    size-above: 100