github.com/shashidharatd/test-infra@v0.0.0-20171006011030-71304e1ca560/prow/plugins/cla/cla.go (about)

     1  /*
     2  Copyright 2016 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package cla
    18  
    19  import (
    20  	"fmt"
    21  	"time"
    22  
    23  	"github.com/sirupsen/logrus"
    24  
    25  	"k8s.io/test-infra/prow/github"
    26  	"k8s.io/test-infra/prow/plugins"
    27  )
    28  
    29  const (
    30  	pluginName             = "cla"
    31  	claContextName         = "cla/linuxfoundation"
    32  	claYesLabel            = "cncf-cla: yes"
    33  	claNoLabel             = "cncf-cla: no"
    34  	cncfclaNotFoundMessage = `Thanks for your pull request. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).
    35  
    36  :memo: **Please follow instructions at <https://github.com/kubernetes/kubernetes/wiki/CLA-FAQ> to sign the CLA.**
    37  
    38  It may take a couple minutes for the CLA signature to be fully registered; after that, please reply here with a new comment and we'll verify.  Thanks.
    39  
    40  ---
    41  
    42  - If you've already signed a CLA, it's possible we don't have your GitHub username or you're using a different email address.  Check your existing CLA data and verify that your [email is set on your git commits](https://help.github.com/articles/setting-your-email-in-git/).
    43  - If you signed the CLA as a corporation, please sign in with your organization's credentials at <https://identity.linuxfoundation.org/projects/cncf> to be authorized.
    44  - If you have done the above and are still having issues with the CLA being reported as unsigned, please email the CNCF helpdesk: helpdesk@rt.linuxfoundation.org
    45  
    46  <!-- need_sender_cla -->
    47  
    48  <details>
    49  
    50  %s
    51  </details>
    52  	`
    53  	maxRetries = 5
    54  )
    55  
    56  func init() {
    57  	plugins.RegisterStatusEventHandler(pluginName, handleStatusEvent)
    58  }
    59  
    60  type gitHubClient interface {
    61  	CreateComment(owner, repo string, number int, comment string) error
    62  	AddLabel(owner, repo string, number int, label string) error
    63  	RemoveLabel(owner, repo string, number int, label string) error
    64  	GetPullRequest(owner, repo string, number int) (*github.PullRequest, error)
    65  	FindIssues(query, sort string, asc bool) ([]github.Issue, error)
    66  }
    67  
    68  func handleStatusEvent(pc plugins.PluginClient, se github.StatusEvent) error {
    69  	return handle(pc.GitHubClient, pc.Logger, se)
    70  }
    71  
    72  // 1. Check that the status event received from the webhook is for the CNCF-CLA.
    73  // 2. Use the github search API to search for the PRs which match the commit hash corresponding to the status event.
    74  // 3. For each issue that matches, check that the PR's HEAD commit hash against the commit hash for which the status
    75  //    was received. This is because we only care about the status associated with the last (latest) commit in a PR.
    76  // 4. Set the corresponding CLA label if needed.
    77  func handle(gc gitHubClient, log *logrus.Entry, se github.StatusEvent) error {
    78  	if se.State == "" || se.Context == "" {
    79  		return fmt.Errorf("invalid status event delivered with empty state/context")
    80  	}
    81  
    82  	if se.Context != claContextName {
    83  		// Not the CNCF CLA context, do not process this.
    84  		return nil
    85  	}
    86  
    87  	if se.State == github.StatusPending {
    88  		// do nothing and wait for state to be updated.
    89  		return nil
    90  	}
    91  
    92  	org := se.Repo.Owner.Login
    93  	repo := se.Repo.Name
    94  	log.Info("Searching for PRs matching the commit.")
    95  
    96  	var issues []github.Issue
    97  	var err error
    98  	for i := 0; i < maxRetries; i++ {
    99  		issues, err = gc.FindIssues(fmt.Sprintf("%s repo:%s/%s type:pr state:open", se.SHA, org, repo), "", false)
   100  		if err != nil {
   101  			return fmt.Errorf("error searching for issues matching commit: %v", err)
   102  		}
   103  		if len(issues) > 0 {
   104  			break
   105  		}
   106  		time.Sleep(10 * time.Second)
   107  	}
   108  	log.Infof("Found %d PRs matching commit.", len(issues))
   109  
   110  	for _, issue := range issues {
   111  		l := log.WithField("pr", issue.Number)
   112  		hasCncfYes := issue.HasLabel(claYesLabel)
   113  		hasCncfNo := issue.HasLabel(claNoLabel)
   114  		if hasCncfYes && se.State == github.StatusSuccess {
   115  			// Nothing to update.
   116  			l.Infof("PR has up-to-date %s label.", claYesLabel)
   117  			continue
   118  		}
   119  
   120  		if hasCncfNo && (se.State == github.StatusFailure || se.State == github.StatusError) {
   121  			// Nothing to update.
   122  			l.Infof("PR has up-to-date %s label.", claNoLabel)
   123  			continue
   124  		}
   125  
   126  		l.Info("PR labels may be out of date. Getting pull request info.")
   127  		pr, err := gc.GetPullRequest(org, repo, issue.Number)
   128  		if err != nil {
   129  			l.WithError(err).Warningf("Unable to fetch PR-%d from %s/%s.", issue.Number, org, repo)
   130  			continue
   131  		}
   132  
   133  		// Check if this is the latest commit in the PR.
   134  		if pr.Head.SHA != se.SHA {
   135  			l.Info("Event is not for PR HEAD, skipping.")
   136  			continue
   137  		}
   138  
   139  		number := pr.Number
   140  		if se.State == github.StatusSuccess {
   141  			if hasCncfNo {
   142  				if err := gc.RemoveLabel(org, repo, number, claNoLabel); err != nil {
   143  					l.WithError(err).Warningf("Could not remove %s label.", claNoLabel)
   144  				}
   145  			}
   146  			if err := gc.AddLabel(org, repo, number, claYesLabel); err != nil {
   147  				l.WithError(err).Warningf("Could not add %s label.", claYesLabel)
   148  			}
   149  			continue
   150  		}
   151  
   152  		// If we end up here, the status is a failure/error.
   153  		if hasCncfYes {
   154  			if err := gc.RemoveLabel(org, repo, number, claYesLabel); err != nil {
   155  				l.WithError(err).Warningf("Could not remove %s label.", claYesLabel)
   156  			}
   157  		}
   158  		if err := gc.CreateComment(org, repo, number, fmt.Sprintf(cncfclaNotFoundMessage, plugins.AboutThisBot)); err != nil {
   159  			l.WithError(err).Warning("Could not create CLA not found comment.")
   160  		}
   161  		if err := gc.AddLabel(org, repo, number, claNoLabel); err != nil {
   162  			l.WithError(err).Warningf("Could not add %s label.", claNoLabel)
   163  		}
   164  	}
   165  	return nil
   166  }