Repository: jenkins-x/lighthouse
Pull request #1384 (2021-11-10 18:35:01)

feat: Support commit context match pattern
If there are multiple CI jobs running against the same repository, poller may think that the Lighthouse job has run, when actually it was another system (e.g. Jenkins). This PR adds support for a flag to supply a regex pattern for matching the commit status context so that poller is looking for signs of the correct job.

jenkins-x-bot: Hi @dippynark. Thanks for your PR.

I'm waiting for a [jenkins-x](https://github.com/orgs/jenkins-x/people) or [todo](https://github.com/orgs/todo/people) member to verify that this patch is reasonable to test. If it is, they should reply with `/ok-to-test` on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should [join the org](https://github.com/orgs/todo/people) to skip this step.

Once the patch is verified, the new status will be reflected by the `ok-to-test` label.

I understand the commands that are listed [here](https://jenkins-x.io/v3/develop/reference/chatops/?repo=jenkins-x%2Flighthouse).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [jenkins-x/lighthouse](https://github.com/jenkins-x/lighthouse/issues/new?title=Command%20issue:) repository.
</details>

jenkins-x-bot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by:
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **dgozalo**
You can assign the PR to them by writing `/assign @dgozalo` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://jenkins-x.io/v3/develop/reference/chatops/?repo=jenkins-x%2Flighthouse).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/jenkins-x/lighthouse/blob/main/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["dgozalo"]} -->
dippynark: /assign @dgozalo
rawlingsj: /lgtm
rawlingsj: /ok-to-test
jenkins-x-bot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *[rawlingsj](https://github.com/jenkins-x/lighthouse/pull/1384#issuecomment-970585539 "LGTM")*

The full list of commands accepted by this bot can be found [here](https://jenkins-x.io/v3/develop/reference/chatops/?repo=jenkins-x%2Flighthouse).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/jenkins-x/lighthouse/blob/main/OWNERS)~~ [rawlingsj]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
jenkins-x-bot: @dippynark: The following test **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
pr | 3e1aa2209b811f092dc4632aae7d74ad362483c3 | [link](http://dashboard-jx.infra.jenkins-x.rocks/namespaces/jx/pipelineruns/jenkins-x-lighthouse-pr-1384-pr-27495) | `/test`



<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [jenkins-x/lighthouse](https://github.com/jenkins-x/lighthouse/issues/new?title=Command%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->

Patch:
diff --git a/charts/lighthouse/templates/poller-deployment.yaml b/charts/lighthouse/templates/poller-deployment.yaml
index 6b0004f2..f48b2e79 100644
--- a/charts/lighthouse/templates/poller-deployment.yaml
+++ b/charts/lighthouse/templates/poller-deployment.yaml
@@ -38,6 +38,9 @@ spec:
         imagePullPolicy: {{ tpl .Values.poller.image.pullPolicy . }}
         args:
           - "--namespace={{ .Release.Namespace }}"
+          {{- if .Values.poller.contextMatchPattern }}
+          - "--context-match-pattern={{ .Values.poller.contextMatchPattern }}"
+          {{- end }}
         ports:
           - name: http
             containerPort: {{ .Values.poller.internalPort }}
diff --git a/charts/lighthouse/values.yaml b/charts/lighthouse/values.yaml
index 2abfbbf4..889623ba 100644
--- a/charts/lighthouse/values.yaml
+++ b/charts/lighthouse/values.yaml
@@ -427,6 +427,9 @@ poller:
     # poller.env.POLL_HOOK_ENDPOINT the hook service endpoint to post webhooks to
     POLL_HOOK_ENDPOINT: http://hook/hook/poll
 
+  # poller.contextMatchPattern -- Regex pattern to use to match commit status context
+  contextMatchPattern: ""
+
   resources:
     # poller.resources.limits -- Resource limits applied to the poller pods
     limits:
diff --git a/cmd/poller/main.go b/cmd/poller/main.go
index b7071e4c..d4f3612d 100644
--- a/cmd/poller/main.go
+++ b/cmd/poller/main.go
@@ -8,6 +8,7 @@ import (
 	"net/http"
 	"net/url"
 	"os"
+	"regexp"
 	"strconv"
 	"strings"
 	"time"
@@ -40,6 +41,7 @@ type options struct {
 	namespace              string
 	repoNames              string
 	hookEndpoint           string
+	contextMatchPattern    string
 	runOnce                bool
 	dryRun                 bool
 	disablePollRelease     bool
@@ -62,6 +64,7 @@ func gatherOptions(fs *flag.FlagSet, args ...string) options {
 	fs.StringVar(&o.botName, "bot-name", "", "The bot name")
 	fs.StringVar(&o.gitServerURL, "git-url", "", "The git provider URL")
 	fs.StringVar(&o.gitKind, "git-kind", "", "The git provider kind (e.g. github, gitlab, bitbucketserver")
+	fs.StringVar(&o.contextMatchPattern, "context-match-pattern", "", "Regex pattern to use to match commit status context.")
 	fs.BoolVar(&o.runOnce, "run-once", false, "If true, run only once then quit.")
 	fs.BoolVar(&o.dryRun, "dry-run", false, "Disable POSTing to the webhook service and just log the webhooks instead.")
 	fs.BoolVar(&o.disablePollRelease, "no-release", false, "Disable polling for new commits on the main branch (releases) - mostly used for easier testing/debugging.")
@@ -154,6 +157,14 @@ func main() {
 		logrus.WithError(err).Fatalf("failed to parse git server %s", serverURL)
 	}
 
+	var contextMatchPatternCompiled *regexp.Regexp
+	if o.contextMatchPattern != "" {
+		contextMatchPatternCompiled, err = regexp.Compile(o.contextMatchPattern)
+		if err != nil {
+			logrus.WithError(err).Fatalf("failed to compile context match pattern \"%s\"", o.contextMatchPattern)
+		}
+	}
+
 	configureOpts := func(opts *gitv2.ClientFactoryOpts) {
 		opts.Token = func() []byte {
 			return []byte(o.gitToken)
@@ -194,7 +205,7 @@ func main() {
 		logrus.WithError(err).Fatal("failed to create scm client")
 	}
 
-	c, err := poller.NewPollingController(repoNames, serverURL, scmClient, fb, o.notifier)
+	c, err := poller.NewPollingController(repoNames, serverURL, scmClient, contextMatchPatternCompiled, fb, o.notifier)
 	if err != nil {
 		logrus.WithError(err).Fatal("Error creating Poller controller.")
 	}
diff --git a/pkg/poller/poller.go b/pkg/poller/poller.go
index 32389dc1..fb9ddd04 100644
--- a/pkg/poller/poller.go
+++ b/pkg/poller/poller.go
@@ -3,6 +3,7 @@ package poller
 import (
 	"context"
 	"net/http"
+	"regexp"
 	"strconv"
 	"strings"
 
@@ -19,34 +20,36 @@ var (
 )
 
 type pollingController struct {
-	DisablePollRelease     bool
-	DisablePollPullRequest bool
-	repositoryNames        []string
-	gitServer              string
-	scmClient              *scm.Client
-	fb                     filebrowser.Interface
-	pollstate              pollstate.Interface
-	logger                 *logrus.Entry
-	notifier               func(webhook *scm.WebhookWrapper) error
+	DisablePollRelease          bool
+	DisablePollPullRequest      bool
+	repositoryNames             []string
+	gitServer                   string
+	scmClient                   *scm.Client
+	fb                          filebrowser.Interface
+	pollstate                   pollstate.Interface
+	logger                      *logrus.Entry
+	contextMatchPatternCompiled *regexp.Regexp
+	notifier                    func(webhook *scm.WebhookWrapper) error
 }
 
 func (c *pollingController) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	w.Write([]byte("hello from lighthouse poller\n"))
 }
 
-func NewPollingController(repositoryNames []string, gitServer string, scmClient *scm.Client, fb filebrowser.Interface, notifier func(webhook *scm.WebhookWrapper) error) (*pollingController, error) {
+func NewPollingController(repositoryNames []string, gitServer string, scmClient *scm.Client, contextMatchPatternCompiled *regexp.Regexp, fb filebrowser.Interface, notifier func(webhook *scm.WebhookWrapper) error) (*pollingController, error) {
 	logger := logrus.NewEntry(logrus.StandardLogger())
 	if gitServer == "" {
 		gitServer = "https://github.com"
 	}
 	return &pollingController{
-		repositoryNames: repositoryNames,
-		gitServer:       gitServer,
-		logger:          logger,
-		scmClient:       scmClient,
-		fb:              fb,
-		notifier:        notifier,
-		pollstate:       pollstate.NewMemoryPollState(),
+		repositoryNames:             repositoryNames,
+		gitServer:                   gitServer,
+		logger:                      logger,
+		scmClient:                   scmClient,
+		contextMatchPatternCompiled: contextMatchPatternCompiled,
+		fb:                          fb,
+		notifier:                    notifier,
+		pollstate:                   pollstate.NewMemoryPollState(),
 	}, nil
 }
 
@@ -278,7 +281,7 @@ func (c *pollingController) hasStatusForSHA(ctx context.Context, l *logrus.Entry
 		return false, errors.Wrapf(err, "failed to list status")
 	}
 	for _, s := range statuses {
-		if !strings.HasPrefix(s.Label, "Lighthouse") {
+		if c.isMatchingStatus(s) {
 			l.WithField("Statuses", statuses).Info("the SHA has CI statuses so not triggering")
 			return true, nil
 		}
@@ -286,6 +289,13 @@ func (c *pollingController) hasStatusForSHA(ctx context.Context, l *logrus.Entry
 	return false, nil
 }
 
+func (c *pollingController) isMatchingStatus(s *scm.Status) bool {
+	if c.contextMatchPatternCompiled != nil {
+		return c.contextMatchPatternCompiled.MatchString(s.Label)
+	}
+	return !strings.HasPrefix(s.Label, "Lighthouse")
+}
+
 func (c *pollingController) createPushHook(fullName, owner, repo, before, after, branch, refBranch string) (*scm.PushHook, error) {
 	return &scm.PushHook{
 		Ref: "refs/heads/" + refBranch,


Test patch:
diff --git a/pkg/poller/poller_test.go b/pkg/poller/poller_test.go
index 186a738b..027eb973 100644
--- a/pkg/poller/poller_test.go
+++ b/pkg/poller/poller_test.go
@@ -1,6 +1,9 @@
 package poller_test
 
 import (
+	"os/exec"
+	"regexp"
+	"strings"
 	"testing"
 
 	scmfake "github.com/jenkins-x/go-scm/scm/driver/fake"
@@ -14,9 +17,11 @@ import (
 )
 
 var (
-	repoNames = []string{"myorg/myrepo"}
-
-	gitServer = "https://github.com"
+	repoNames           = []string{"myorg/myrepo"}
+	gitServer           = "https://github.com"
+	testDataDir         = "test_data"
+	contextMatchPattern = "^Lighthouse$"
+	statusLabel         = "Jenkins"
 )
 
 func TestPollerReleases(t *testing.T) {
@@ -30,10 +35,21 @@ func TestPollerReleases(t *testing.T) {
 		}
 		return nil
 	}
-	scmClient, _ := scmfake.NewDefault()
-	fb := fbfake.NewFakeFileBrowser("test_data", true)
+	scmClient, fakeData := scmfake.NewDefault()
+	fb := fbfake.NewFakeFileBrowser(testDataDir, true)
+
+	contextMatchPatternCompiled, err := regexp.Compile(contextMatchPattern)
+	require.NoErrorf(t, err, "failed to compile context match pattern \"%s\"", contextMatchPattern)
 
-	p, err := poller.NewPollingController(repoNames, gitServer, scmClient, fb, fakeNotifier)
+	// Load fake status with label that doesn't match our context match pattern
+	c := exec.Command("git", "rev-parse", "HEAD")
+	c.Dir = testDataDir
+	out, err := c.CombinedOutput()
+	require.NoError(t, err, "failed to get latest git commit sha")
+	sha := strings.TrimSpace(string(out))
+	fakeData.Statuses = map[string][]*scm.Status{sha: {{Label: statusLabel}}}
+
+	p, err := poller.NewPollingController(repoNames, gitServer, scmClient, contextMatchPatternCompiled, fb, fakeNotifier)
 	require.NoError(t, err, "failed to create PollingController")
 
 	p.PollReleases()
@@ -62,6 +78,9 @@ func TestPollerPullRequests(t *testing.T) {
 	scmClient, fakeData := scmfake.NewDefault()
 	fb := fbfake.NewFakeFileBrowser("test_data", true)
 
+	contextMatchPatternCompiled, err := regexp.Compile(contextMatchPattern)
+	require.NoErrorf(t, err, "failed to compile context match pattern \"%s\"", contextMatchPattern)
+
 	prNumber := 123
 	fullName := repoNames[0]
 	owner, repo := scm.Split(fullName)
@@ -85,7 +104,10 @@ func TestPollerPullRequests(t *testing.T) {
 		State:  "open",
 		Sha:    sha,
 	}
-	p, err := poller.NewPollingController(repoNames, gitServer, scmClient, fb, fakeNotifier)
+	// Load fake status with label that doesn't match our context match pattern
+	fakeData.Statuses = map[string][]*scm.Status{sha: {{Label: statusLabel}}}
+
+	p, err := poller.NewPollingController(repoNames, gitServer, scmClient, contextMatchPatternCompiled, fb, fakeNotifier)
 	require.NoError(t, err, "failed to create PollingController")
 
 	p.PollPullRequests()