-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use branch protection setting in tide
- Loading branch information
1 parent
16f5fc2
commit 63d1e7c
Showing
9 changed files
with
532 additions
and
40 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
# Tide Documentation | ||
|
||
Tide merges PR that match a given sets of criteria | ||
|
||
## Tide configuration | ||
|
||
Extend the primary prow [`config.yaml`] document to include a top-level | ||
`tide` key that looks like the following: | ||
|
||
```yaml | ||
|
||
tide: | ||
queries: | ||
... | ||
branchProtection: | ||
... | ||
merge_method: | ||
... | ||
|
||
|
||
presubmits: | ||
kubernetes/test-infra: | ||
- name: fancy-job-name | ||
context: fancy-job-name | ||
always_run: true | ||
spec: # podspec that runs job | ||
``` | ||
### Merging Options | ||
Tide supports all 3 github merging options: | ||
* squash | ||
* merge | ||
* rebase | ||
A merge method can be set for repo or per org. | ||
Example: | ||
```yaml | ||
tide: | ||
... | ||
merge_method: | ||
org1: squash | ||
org2/repo1: rebase | ||
org2/repo2: merge | ||
``` | ||
### Queries Configuration | ||
Queries are using github queries to find PRs to be merged. Multiple queries can be defined for a single repo. Queries support filtering per existing and missing labels. In order to filter PRs that have been approved, use the reviewApprovedRequired. | ||
```yaml | ||
tide: | ||
queries: | ||
... | ||
- repos: | ||
- org1/repo1 | ||
- org2/repo2 | ||
labels: | ||
- labelThatMustsExists | ||
- OtherLabelThatMustsExist | ||
missingLabels: | ||
- labelThatShouldNotBePresent | ||
# If you want github approval | ||
reviewApprovedRequired: true | ||
``` | ||
### Branch Protection Options | ||
Branch Protection options are use to enforce github branch protection. | ||
A PR will be merged when all required checks are passing, meaning we will skip optional contexts. | ||
Example: | ||
```yaml | ||
tide: | ||
... | ||
skip_optional_contexts: true | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
/* | ||
Copyright 2017 The Kubernetes Authors. | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
package tide | ||
|
||
import ( | ||
"sync" | ||
|
||
"github.com/shurcooL/githubql" | ||
"k8s.io/apimachinery/pkg/util/sets" | ||
) | ||
|
||
type ContextChecker interface { | ||
// IgnoreContext tells whether a context is optional. | ||
IgnoreContext(context Context) bool | ||
// MissingRequiredContexts tells if required contexts are missing from the list of contexts provided. | ||
MissingRequiredContexts([]Context) []Context | ||
} | ||
|
||
// newExpectedContext creates a Context with Expected state. | ||
// This should not only be used when contexts are missing. | ||
func newExpectedContext(c string) Context { | ||
return Context{ | ||
Context: githubql.String(c), | ||
State: githubql.StatusStateExpected, | ||
Description: githubql.String(""), | ||
} | ||
} | ||
|
||
// ContextRegister implements ContextChecker and allow registering of required and optional contexts. | ||
type ContextRegister struct { | ||
lock sync.RWMutex | ||
required, optional sets.String | ||
} | ||
|
||
// NewContextRegister instantiates a new ContextRegister and register the optional contexts provided. | ||
func NewContextRegister(optional ...string) *ContextRegister { | ||
r := ContextRegister{ | ||
required: sets.NewString(), | ||
optional: sets.NewString(), | ||
} | ||
r.RegisterOptionalContexts(optional...) | ||
return &r | ||
} | ||
|
||
// IgnoreContext checks whether a context can be ignored. | ||
// Will return true if | ||
// - context is registered as optional | ||
// - required contexts are registered and the context provided is not required | ||
// Will return false otherwise. Every context is required. | ||
func (r *ContextRegister) IgnoreContext(c Context) bool { | ||
if r.optional.Has(string(c.Context)) { | ||
return true | ||
} | ||
if r.required.Len() > 0 && !r.required.Has(string(c.Context)) { | ||
return true | ||
} | ||
return false | ||
} | ||
|
||
// MissingRequiredContexts discard the optional contexts and only look of extra required contexts that are not provided. | ||
func (r *ContextRegister) MissingRequiredContexts(contexts []Context) []Context { | ||
if r.required.Len() == 0 { | ||
return nil | ||
} | ||
existingContexts := sets.NewString() | ||
for _, c := range contexts { | ||
existingContexts.Insert(string(c.Context)) | ||
} | ||
var missingContexts []Context | ||
for c := range r.required.Difference(existingContexts) { | ||
missingContexts = append(missingContexts, newExpectedContext(c)) | ||
} | ||
return missingContexts | ||
} | ||
|
||
// RegisterOptionalContexts registers optional contexts | ||
func (r *ContextRegister) RegisterOptionalContexts(c ...string) { | ||
r.lock.Lock() | ||
defer r.lock.Unlock() | ||
r.optional.Insert(c...) | ||
} | ||
|
||
// RegisterRequiredContexts register required contexts. | ||
// Once required contexts are registered other contexts will be considered optional. | ||
func (r *ContextRegister) RegisterRequiredContexts(c ...string) { | ||
r.lock.Lock() | ||
defer r.lock.Unlock() | ||
r.required.Insert(c...) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
/* | ||
Copyright 2017 The Kubernetes Authors. | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
package tide | ||
|
||
import ( | ||
"testing" | ||
|
||
"k8s.io/apimachinery/pkg/util/sets" | ||
) | ||
|
||
func TestContextRegisterIgnoreContext(t *testing.T) { | ||
testCases := []struct { | ||
name string | ||
required, optional []string | ||
contexts []string | ||
results []bool | ||
}{ | ||
{ | ||
name: "only optional contexts registered", | ||
contexts: []string{"c1", "o1", "o2"}, | ||
optional: []string{"o1", "o2"}, | ||
results: []bool{false, true, true}, | ||
}, | ||
{ | ||
name: "no contexts registered", | ||
contexts: []string{"t2"}, | ||
results: []bool{false}, | ||
}, | ||
{ | ||
name: "only required contexts registered", | ||
required: []string{"c1", "c2", "c3"}, | ||
contexts: []string{"c1", "c2", "c3", "t1"}, | ||
results: []bool{false, false, false, true}, | ||
}, | ||
{ | ||
name: "optional and required contexts registered", | ||
optional: []string{"o1", "o2"}, | ||
required: []string{"c1", "c2", "c3"}, | ||
contexts: []string{"o1", "o2", "c1", "c2", "c3", "t1"}, | ||
results: []bool{true, true, false, false, false, true}, | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
cr := NewContextRegister(tc.optional...) | ||
cr.RegisterRequiredContexts(tc.required...) | ||
for i, c := range tc.contexts { | ||
if cr.IgnoreContext(newExpectedContext(c)) != tc.results[i] { | ||
t.Errorf("%s - ignoreContext for %s should return %t", tc.name, c, tc.results[i]) | ||
} | ||
} | ||
} | ||
} | ||
|
||
func contextsToSet(contexts []Context) sets.String { | ||
s := sets.NewString() | ||
for _, c := range contexts { | ||
s.Insert(string(c.Context)) | ||
} | ||
return s | ||
} | ||
|
||
func TestContextRegisterMissingContexts(t *testing.T) { | ||
testCases := []struct { | ||
name string | ||
required, optional []string | ||
existingContexts, expectedContexts []string | ||
}{ | ||
{ | ||
name: "no contexts registered", | ||
existingContexts: []string{"c1", "c2"}, | ||
}, | ||
{ | ||
name: "optional contexts registered / no missing contexts", | ||
optional: []string{"o1", "o2", "o3"}, | ||
existingContexts: []string{"c1", "c2"}, | ||
}, | ||
{ | ||
name: "required contexts registered / missing contexts", | ||
required: []string{"c1", "c2", "c3"}, | ||
existingContexts: []string{"c1", "c2"}, | ||
expectedContexts: []string{"c3"}, | ||
}, | ||
{ | ||
name: "required contexts registered / no missing contexts", | ||
required: []string{"c1", "c2", "c3"}, | ||
existingContexts: []string{"c1", "c2", "c3"}, | ||
}, | ||
{ | ||
name: "optional and required contexts registered / missing contexts", | ||
optional: []string{"o1", "o2", "o3"}, | ||
required: []string{"c1", "c2", "c3"}, | ||
existingContexts: []string{"c1", "c2"}, | ||
expectedContexts: []string{"c3"}, | ||
}, | ||
{ | ||
name: "optional and required contexts registered / no missing contexts", | ||
optional: []string{"o1", "o2", "o3"}, | ||
required: []string{"c1", "c2"}, | ||
existingContexts: []string{"c1", "c2", "c4"}, | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
cr := NewContextRegister(tc.optional...) | ||
cr.RegisterRequiredContexts(tc.required...) | ||
var contexts []Context | ||
for _, c := range tc.existingContexts { | ||
contexts = append(contexts, newExpectedContext(c)) | ||
} | ||
missingContexts := cr.MissingRequiredContexts(contexts) | ||
m := contextsToSet(missingContexts) | ||
if !m.Equal(sets.NewString(tc.expectedContexts...)) { | ||
t.Errorf("%s - expected %v got %v", tc.name, tc.expectedContexts, missingContexts) | ||
} | ||
} | ||
} |
Oops, something went wrong.