Skip to content

Commit

Permalink
Use branch protection setting in tide
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastienvas committed Apr 18, 2018
1 parent 16f5fc2 commit 63d1e7c
Show file tree
Hide file tree
Showing 9 changed files with 532 additions and 40 deletions.
81 changes: 81 additions & 0 deletions prow/cmd/tide/README.md
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
```
5 changes: 5 additions & 0 deletions prow/config/branch_protection.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,8 @@ type Branch struct {
Contexts []string `json:"require-contexts,omitempty"`
Pushers []string `json:"allow-push,omitempty"`
}

func (c *Config) GetBranchProtection(org, repo, branch string) (*Branch, error) {
// Place holder. Implemented in #7680
return nil, nil
}
10 changes: 10 additions & 0 deletions prow/config/jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ type Presubmit struct {
Spec *v1.PodSpec `json:"spec,omitempty"`
// Run these jobs after successfully running this one.
RunAfterSuccess []Presubmit `json:"run_after_success"`
// Defines if a test is a required check as defined in the Github Branch protection.
Optional bool `json:"optional,omitempty"`

Brancher

Expand Down Expand Up @@ -207,6 +209,14 @@ func (ps Presubmit) TriggerMatches(body string) bool {
return ps.re.MatchString(body)
}

// ContextRequired checks whether a context is required from github points of view (required check).
func (ps Presubmit) ContextRequired() bool {
if ps.Optional || ps.SkipReport {
return false
}
return true
}

type ChangedFilesProvider func() ([]string, error)

func matching(j Presubmit, body string, testAll bool) []Presubmit {
Expand Down
4 changes: 4 additions & 0 deletions prow/config/tide.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ type Tide struct {
// controller to handle org/repo:branch pools. Defaults to 20. Needs to be a
// positive number.
MaxGoroutines int `json:"max_goroutines,omitempty"`

// SkipOptionalContexts will use BranchProtection configuration options to know
// which contexts can be skipped in order to merge a PR.
SkipOptionalContexts bool `json:"skip_optional_contexts,omitempty"`
}

// MergeMethod returns the merge method to use for a repo. The default of merge is
Expand Down
10 changes: 8 additions & 2 deletions prow/tide/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "go_default_library",
srcs = ["tide.go"],
srcs = [
"contextregister.go",
"tide.go",
],
importpath = "k8s.io/test-infra/prow/tide",
visibility = ["//visibility:public"],
deps = [
Expand Down Expand Up @@ -33,7 +36,10 @@ filegroup(

go_test(
name = "go_default_test",
srcs = ["tide_test.go"],
srcs = [
"contextregister_test.go",
"tide_test.go",
],
embed = [":go_default_library"],
deps = [
"//prow/config:go_default_library",
Expand Down
102 changes: 102 additions & 0 deletions prow/tide/contextregister.go
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...)
}
130 changes: 130 additions & 0 deletions prow/tide/contextregister_test.go
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)
}
}
}
Loading

0 comments on commit 63d1e7c

Please sign in to comment.