Skip to content

Commit

Permalink
Add block-paths munger functionality to prow (blockade).
Browse files Browse the repository at this point in the history
Besides migrating this commit makes the behavior configurable by repo, org, or set of org and
repos. Additionally it generates explanations of why each blocked PR was
blocked.
  • Loading branch information
cjwagner committed Oct 10, 2017
1 parent e79d9fc commit 183b6c2
Show file tree
Hide file tree
Showing 5 changed files with 674 additions and 0 deletions.
1 change: 1 addition & 0 deletions prow/plugins/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ filegroup(
srcs = [
":package-srcs",
"//prow/plugins/assign:all-srcs",
"//prow/plugins/blockade:all-srcs",
"//prow/plugins/cla:all-srcs",
"//prow/plugins/close:all-srcs",
"//prow/plugins/docs-no-retest:all-srcs",
Expand Down
38 changes: 38 additions & 0 deletions prow/plugins/blockade/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "go_default_library",
srcs = ["blockade.go"],
visibility = ["//visibility:public"],
deps = [
"//prow/github:go_default_library",
"//prow/plugins:go_default_library",
"//vendor/github.com/sirupsen/logrus:go_default_library",
],
)

filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)

filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

go_test(
name = "go_default_test",
srcs = ["blockade_test.go"],
library = ":go_default_library",
deps = [
"//prow/github:go_default_library",
"//prow/github/fakegithub:go_default_library",
"//prow/plugins:go_default_library",
"//vendor/github.com/sirupsen/logrus:go_default_library",
],
)
238 changes: 238 additions & 0 deletions prow/plugins/blockade/blockade.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
/*
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 blockade defines a plugin that adds the 'do-not-merge/blocked-paths' label to PRs that
// modify protected file paths.
// Protected file paths are defined with the plugins.Blockade struct. A PR is blocked if any file
// it changes is blocked by any Blockade. The process for determining if a file is blocked by a
// Blockade is as follows:
// By default, allow the file. Block if the file path matches any of block regexps, and does not
// match any of the exception regexps.
package blockade

import (
"bytes"
"fmt"
"regexp"
"strings"

"github.com/sirupsen/logrus"
"k8s.io/test-infra/prow/github"
"k8s.io/test-infra/prow/plugins"
)

const (
pluginName = "blockade"
blockedPathsLabel = "do-not-merge/blocked-paths"
)

var blockedPathsBody = fmt.Sprintf("Adding label: `%s` because PR changes a protected file.", blockedPathsLabel)

type githubClient interface {
GetPullRequestChanges(org, repo string, number int) ([]github.PullRequestChange, error)
GetIssueLabels(org, repo string, number int) ([]github.Label, error)
AddLabel(owner, repo string, number int, label string) error
RemoveLabel(owner, repo string, number int, label string) error
CreateComment(org, repo string, number int, comment string) error
}

type pruneClient interface {
PruneComments(func(ic github.IssueComment) bool)
}

func init() {
plugins.RegisterPullRequestHandler(pluginName, handlePullRequest)
}

type blockCalc func([]github.PullRequestChange, []blockade) summary

type client struct {
ghc githubClient
log *logrus.Entry
pruner pruneClient

blockCalc blockCalc
}

func handlePullRequest(pc plugins.PluginClient, pre github.PullRequestEvent) error {
c := &client{
ghc: pc.GitHubClient,
log: pc.Logger,
pruner: pc.CommentPruner,

blockCalc: calculateBlocks,
}
return handle(c, pc.PluginConfig.Blockades, &pre)
}

// blockade is a compiled version of a plugins.Blockade config struct.
type blockade struct {
blockRegexps, exceptionRegexps []*regexp.Regexp
explanation string
}

func (bd *blockade) isBlocked(file string) bool {
return matchesAny(file, bd.blockRegexps) && !matchesAny(file, bd.exceptionRegexps)
}

type summary map[string][]github.PullRequestChange

func (s summary) String() string {
if len(s) == 0 {
return ""
}
var buf bytes.Buffer
fmt.Fprint(&buf, "#### Reasons for blocking this PR:\n")
for reason, files := range s {
fmt.Fprintf(&buf, "[%s]\n", reason)
for _, file := range files {
fmt.Fprintf(&buf, "- [%s](%s)\n\n", file.Filename, file.BlobURL)
}
}
return buf.String()
}

func handle(c *client, config []plugins.Blockade, pre *github.PullRequestEvent) error {
if pre.Action != github.PullRequestActionSynchronize &&
pre.Action != github.PullRequestActionOpened &&
pre.Action != github.PullRequestActionReopened {
return nil
}

org := pre.Repo.Owner.Login
repo := pre.Repo.Name
labels, err := c.ghc.GetIssueLabels(org, repo, pre.Number)
if err != nil {
return err
}

labelPresent := hasBlockedLabel(labels)
blockades := compileApplicableBlockades(org, repo, c.log, config)
if len(blockades) == 0 && !labelPresent {
// Since the label is missing, we assume that we removed any associated comments.
return nil
}

var sum summary
if len(blockades) > 0 {
changes, err := c.ghc.GetPullRequestChanges(org, repo, pre.Number)
if err != nil {
return err
}
sum = c.blockCalc(changes, blockades)
}

shouldBlock := len(sum) > 0
if shouldBlock && !labelPresent {
// Add the label and leave a comment explaining why the label was added.
if err := c.ghc.AddLabel(org, repo, pre.Number, blockedPathsLabel); err != nil {
return err
}
msg := plugins.FormatResponse(pre.PullRequest.User.Login, blockedPathsBody, sum.String())
return c.ghc.CreateComment(org, repo, pre.Number, msg)
} else if !shouldBlock && labelPresent {
// Remove the label and delete any comments created by this plugin.
if err := c.ghc.RemoveLabel(org, repo, pre.Number, blockedPathsLabel); err != nil {
return err
}
c.pruner.PruneComments(func(ic github.IssueComment) bool {
return strings.Contains(ic.Body, blockedPathsBody)
})
}
return nil
}

// compileApplicableBlockades filters the specified blockades and compiles those that apply to the repo.
func compileApplicableBlockades(org, repo string, log *logrus.Entry, blockades []plugins.Blockade) []blockade {
if len(blockades) == 0 {
return nil
}

orgRepo := fmt.Sprintf("%s/%s", org, repo)
var compiled []blockade
for _, raw := range blockades {
// Only consider blockades that apply to this repo.
if !stringInSlice(org, raw.Repos) && !stringInSlice(orgRepo, raw.Repos) {
continue
}
b := blockade{}
for _, str := range raw.BlockRegexps {
if reg, err := regexp.Compile(str); err != nil {
log.WithError(err).Errorf("Failed to compile the blockade regexp '%s'.", str)
} else {
b.blockRegexps = append(b.blockRegexps, reg)
}
}
if len(b.blockRegexps) == 0 {
continue
}
if raw.Explanation == "" {
b.explanation = "Files are protected"
} else {
b.explanation = raw.Explanation
}
for _, str := range raw.ExceptionRegexps {
if reg, err := regexp.Compile(str); err != nil {
log.WithError(err).Errorf("Failed to compile the blockade regexp '%s'.", str)
} else {
b.exceptionRegexps = append(b.exceptionRegexps, reg)
}
}
compiled = append(compiled, b)
}
return compiled
}

// calculateBlocks determines if a PR should be blocked and returns the summary describing the block.
func calculateBlocks(changes []github.PullRequestChange, blockades []blockade) summary {
sum := make(summary)
for _, change := range changes {
for _, b := range blockades {
if b.isBlocked(change.Filename) {
sum[b.explanation] = append(sum[b.explanation], change)
}
}
}
return sum
}

func hasBlockedLabel(labels []github.Label) bool {
label := strings.ToLower(blockedPathsLabel)
for _, elem := range labels {
if strings.ToLower(elem.Name) == label {
return true
}
}
return false
}

func matchesAny(str string, regexps []*regexp.Regexp) bool {
for _, reg := range regexps {
if reg.MatchString(str) {
return true
}
}
return false
}

func stringInSlice(str string, slice []string) bool {
for _, elem := range slice {
if elem == str {
return true
}
}
return false
}
Loading

0 comments on commit 183b6c2

Please sign in to comment.