Repository: jenkins-x/lighthouse
Pull request #1035 (2020-09-10 14:15:38)

refactor: stage and pony plugins
This PR refactors stage and pony plugins

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 **eddycharly**
You can assign the PR to them by writing `/assign @eddycharly` in a comment when ready.

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

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

- **[OWNERS](https://github.com/jenkins-x/lighthouse/blob/master/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":["eddycharly"]} -->

Patch:
diff --git a/pkg/plugins/milestonestatus/milestonestatus.go b/pkg/plugins/milestonestatus/milestonestatus.go
index 3bae14e7..2df77be3 100644
--- a/pkg/plugins/milestonestatus/milestonestatus.go
+++ b/pkg/plugins/milestonestatus/milestonestatus.go
@@ -49,9 +49,11 @@ var (
 		Description:        "The milestonestatus plugin allows members of the milestone maintainers GitHub team to specify the 'status/*' label that should apply to a pull request.",
 		ConfigHelpProvider: configHelp,
 		Commands: []plugins.Command{{
-			GenericCommentHandler: handleGenericComment,
-			Filter:                func(e scmprovider.GenericCommentEvent) bool { return e.Action == scm.ActionCreate },
-			Regex:                 statusRegex,
+			GenericCommentHandler: func(match []string, pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
+				return handle(match[1], pc.SCMProviderClient, pc.Logger, &e, pc.PluginConfig.RepoMilestone)
+			},
+			Filter: func(e scmprovider.GenericCommentEvent) bool { return e.Action == scm.ActionCreate },
+			Regex:  statusRegex,
 			Help: []pluginhelp.Command{{
 				Usage:       "/status (approved-for-milestone|in-progress|in-review)",
 				Description: "Applies the 'status/' label to a PR.",
@@ -88,11 +90,7 @@ func configHelp(config *plugins.Configuration, enabledRepos []string) (map[strin
 	return configMap, nil
 }
 
-func handleGenericComment(match []string, pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
-	return handle(match, pc.SCMProviderClient, pc.Logger, &e, pc.PluginConfig.RepoMilestone)
-}
-
-func handle(match []string, spc scmProviderClient, log *logrus.Entry, e *scmprovider.GenericCommentEvent, repoMilestone map[string]plugins.Milestone) error {
+func handle(mileStone string, spc scmProviderClient, log *logrus.Entry, e *scmprovider.GenericCommentEvent, repoMilestone map[string]plugins.Milestone) error {
 	org := e.Repo.Namespace
 	repo := e.Repo.Name
 
@@ -120,7 +118,7 @@ func handle(match []string, spc scmProviderClient, log *logrus.Entry, e *scmprov
 		return spc.CreateComment(org, repo, e.Number, e.IsPR, msg)
 	}
 
-	sLabel, validStatus := statusMap[strings.TrimSpace(match[1])]
+	sLabel, validStatus := statusMap[strings.TrimSpace(mileStone)]
 	if validStatus {
 		if err := spc.AddLabel(org, repo, e.Number, sLabel, e.IsPR); err != nil {
 			log.WithError(err).Errorf("Error adding the label %q to %s/%s#%d.", sLabel, org, repo, e.Number)
diff --git a/pkg/plugins/pony/pony.go b/pkg/plugins/pony/pony.go
index 32a49530..01af8065 100644
--- a/pkg/plugins/pony/pony.go
+++ b/pkg/plugins/pony/pony.go
@@ -52,17 +52,20 @@ const (
 )
 
 var (
-	match = regexp.MustCompile(`(?mi)^/(?:lh-)?(?:pony)(?: +(.+?))?\s*$`)
+	match = regexp.MustCompile(`(?mi)^/(?:lh-)?pony(?: +(.+))?\s*$`)
 )
 
-var (
-	plugin = plugins.Plugin{
+func createPlugin(h herd) plugins.Plugin {
+	return plugins.Plugin{
 		Description: "The pony plugin adds a pony image to an issue or PR in response to the `/pony` command.",
 		Commands: []plugins.Command{{
-			GenericCommentHandler: handleGenericComment,
-			Filter:                func(e scmprovider.GenericCommentEvent) bool { return e.Action == scm.ActionCreate },
+			Filter: func(e scmprovider.GenericCommentEvent) bool { return e.Action == scm.ActionCreate },
+			Regex:  match,
+			GenericCommentHandler: func(match []string, pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
+				return handle(match[1], pc.SCMProviderClient, pc.Logger, &e, h)
+			},
 			Help: []pluginhelp.Command{{
-				Usage:       "/(pony) [pony]",
+				Usage:       "/pony [pony]",
 				Description: "Add a little pony image to the issue or PR. A particular pony can optionally be named for a picture of that specific pony.",
 				Featured:    false,
 				WhoCanUse:   "Anyone",
@@ -70,10 +73,10 @@ var (
 			}},
 		}},
 	}
-)
+}
 
 func init() {
-	plugins.RegisterPlugin(pluginName, plugin)
+	plugins.RegisterPlugin(pluginName, createPlugin(ponyURL))
 }
 
 var client = http.Client{}
@@ -119,29 +122,14 @@ func (h realHerd) readPony(tags string) (string, error) {
 	return formatURLs(a.Pony.Representations.Small, a.Pony.Representations.Full), nil
 }
 
-func handleGenericComment(_ []string, pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
-	return handle(pc.SCMProviderClient, pc.Logger, &e, ponyURL)
-}
-
-func handle(spc scmProviderClient, log *logrus.Entry, e *scmprovider.GenericCommentEvent, p herd) error {
-	// Make sure they are requesting a pony
-	mat := match.FindStringSubmatch(e.Body)
-	if mat == nil {
-		return nil
-	}
-
-	tag := mat[1]
-	org := e.Repo.Namespace
-	repo := e.Repo.Name
-	number := e.Number
-
+func handle(tag string, spc scmProviderClient, log *logrus.Entry, e *scmprovider.GenericCommentEvent, p herd) error {
 	for i := 0; i < 5; i++ {
 		resp, err := p.readPony(tag)
 		if err != nil {
 			log.WithError(err).Println("Failed to get a pony")
 			continue
 		}
-		return spc.CreateComment(org, repo, number, e.IsPR, plugins.FormatResponseRaw(e.Body, e.Link, spc.QuoteAuthorForComment(e.Author.Login), resp))
+		return spc.CreateComment(e.Repo.Namespace, e.Repo.Name, e.Number, e.IsPR, plugins.FormatResponseRaw(e.Body, e.Link, spc.QuoteAuthorForComment(e.Author.Login), resp))
 	}
 
 	var msg string
@@ -150,7 +138,7 @@ func handle(spc scmProviderClient, log *logrus.Entry, e *scmprovider.GenericComm
 	} else {
 		msg = "https://theponyapi.com appears to be down"
 	}
-	if err := spc.CreateComment(org, repo, number, e.IsPR, plugins.FormatResponseRaw(e.Body, e.Link, spc.QuoteAuthorForComment(e.Author.Login), msg)); err != nil {
+	if err := spc.CreateComment(e.Repo.Namespace, e.Repo.Name, e.Number, e.IsPR, plugins.FormatResponseRaw(e.Body, e.Link, spc.QuoteAuthorForComment(e.Author.Login), msg)); err != nil {
 		log.WithError(err).Error("Failed to leave comment")
 	}
 
diff --git a/pkg/plugins/shrug/shrug.go b/pkg/plugins/shrug/shrug.go
index 99da1a20..6af04d10 100644
--- a/pkg/plugins/shrug/shrug.go
+++ b/pkg/plugins/shrug/shrug.go
@@ -52,14 +52,30 @@ var (
 	plugin = plugins.Plugin{
 		Description: labels.Shrug,
 		Commands: []plugins.Command{{
-			GenericCommentHandler: handleGenericComment,
-			Filter:                func(e scmprovider.GenericCommentEvent) bool { return e.Action == scm.ActionCreate },
+			Filter: func(e scmprovider.GenericCommentEvent) bool { return e.Action == scm.ActionCreate },
+			Regex:  shrugRe,
+			GenericCommentHandler: func(_ []string, pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
+				return addLabel(pc.SCMProviderClient, pc.Logger, &e)
+			},
 			Help: []pluginhelp.Command{{
-				Usage:       "/[un]shrug",
-				Description: labels.Shrug,
+				Usage:       "/shrug",
+				Description: "Adds the " + labels.Shrug + " label",
 				Featured:    false,
 				WhoCanUse:   "Anyone, " + labels.Shrug,
-				Examples:    []string{"/shrug", "/unshrug"},
+				Examples:    []string{"/shrug", "/lh-shrug"},
+			}},
+		}, {
+			Filter: func(e scmprovider.GenericCommentEvent) bool { return e.Action == scm.ActionCreate },
+			Regex:  unshrugRe,
+			GenericCommentHandler: func(_ []string, pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
+				return removeLabel(pc.SCMProviderClient, pc.Logger, &e)
+			},
+			Help: []pluginhelp.Command{{
+				Usage:       "/unshrug",
+				Description: "Removes the " + labels.Shrug + " label",
+				Featured:    false,
+				WhoCanUse:   "Anyone, " + labels.Shrug,
+				Examples:    []string{"/unshrug", "/lh-unshrug"},
 			}},
 		}},
 	}
@@ -77,46 +93,40 @@ type scmProviderClient interface {
 	QuoteAuthorForComment(string) string
 }
 
-func handleGenericComment(_ []string, pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
-	return handle(pc.SCMProviderClient, pc.Logger, &e)
-}
-
-func handle(spc scmProviderClient, log *logrus.Entry, e *scmprovider.GenericCommentEvent) error {
-	wantShrug := false
-	if shrugRe.MatchString(e.Body) {
-		wantShrug = true
-	} else if unshrugRe.MatchString(e.Body) {
-		wantShrug = false
-	} else {
-		return nil
-	}
-
-	org := e.Repo.Namespace
-	repo := e.Repo.Name
-
-	// Only add the label if it doesn't have it yet.
-	hasShrug := false
-	issueLabels, err := spc.GetIssueLabels(org, repo, e.Number, e.IsPR)
+func hasLabel(spc scmProviderClient, log *logrus.Entry, e *scmprovider.GenericCommentEvent) (bool, error) {
+	issueLabels, err := spc.GetIssueLabels(e.Repo.Namespace, e.Repo.Name, e.Number, e.IsPR)
 	if err != nil {
-		log.WithError(err).Errorf("Failed to get the labels on %s/%s#%d.", org, repo, e.Number)
+		log.WithError(err).Errorf("Failed to get the labels on %s/%s#%d.", e.Repo.Namespace, e.Repo.Name, e.Number)
+		return false, err
 	}
 	for _, candidate := range issueLabels {
 		if candidate.Name == labels.Shrug {
-			hasShrug = true
-			break
+			return true, nil
 		}
 	}
-	if hasShrug && !wantShrug {
-		log.Info("Removing Shrug label.")
-		resp := "¯\\\\\\_(ツ)\\_/¯"
-		log.Infof("Commenting with \"%s\".", resp)
-		if err := spc.CreateComment(org, repo, e.Number, e.IsPR, plugins.FormatResponseRaw(e.Body, e.Link, spc.QuoteAuthorForComment(e.Author.Login), resp)); err != nil {
-			return fmt.Errorf("failed to comment on %s/%s#%d: %v", org, repo, e.Number, err)
-		}
-		return spc.RemoveLabel(org, repo, e.Number, labels.Shrug, e.IsPR)
-	} else if !hasShrug && wantShrug {
-		log.Info("Adding Shrug label.")
-		return spc.AddLabel(org, repo, e.Number, labels.Shrug, e.IsPR)
+	return false, nil
+}
+
+func addLabel(spc scmProviderClient, log *logrus.Entry, e *scmprovider.GenericCommentEvent) error {
+	hasLabel, err := hasLabel(spc, log, e)
+	if err != nil || hasLabel {
+		return err
+	}
+	log.Info("Adding Shrug label.")
+	return spc.AddLabel(e.Repo.Namespace, e.Repo.Name, e.Number, labels.Shrug, e.IsPR)
+
+}
+
+func removeLabel(spc scmProviderClient, log *logrus.Entry, e *scmprovider.GenericCommentEvent) error {
+	hasLabel, err := hasLabel(spc, log, e)
+	if err != nil || !hasLabel {
+		return err
+	}
+	log.Info("Removing Shrug label.")
+	resp := "¯\\\\\\_(ツ)\\_/¯"
+	log.Infof("Commenting with \"%s\".", resp)
+	if err := spc.CreateComment(e.Repo.Namespace, e.Repo.Name, e.Number, e.IsPR, plugins.FormatResponseRaw(e.Body, e.Link, spc.QuoteAuthorForComment(e.Author.Login), resp)); err != nil {
+		return fmt.Errorf("failed to comment on %s/%s#%d: %v", e.Repo.Namespace, e.Repo.Name, e.Number, err)
 	}
-	return nil
+	return spc.RemoveLabel(e.Repo.Namespace, e.Repo.Name, e.Number, labels.Shrug, e.IsPR)
 }
diff --git a/pkg/plugins/stage/stage.go b/pkg/plugins/stage/stage.go
index a0250a20..9ae95eb5 100644
--- a/pkg/plugins/stage/stage.go
+++ b/pkg/plugins/stage/stage.go
@@ -34,7 +34,8 @@ var (
 	stageBeta   = "stage/beta"
 	stageStable = "stage/stable"
 	stageLabels = []string{stageAlpha, stageBeta, stageStable}
-	stageRe     = regexp.MustCompile(`(?mi)^/(?:lh-)?(remove-)?stage (alpha|beta|stable)\s*$`)
+	stageRe     = regexp.MustCompile(`(?mi)^/(?:lh-)?stage (alpha|beta|stable)\s*$`)
+	unstageRe   = regexp.MustCompile(`(?mi)^/(?:lh-)?remove-stage (alpha|beta|stable)\s*$`)
 )
 
 const pluginName = "stage"
@@ -43,15 +44,30 @@ var (
 	plugin = plugins.Plugin{
 		Description: "Label the stage of an issue as alpha/beta/stable",
 		Commands: []plugins.Command{{
-			Filter:                func(e scmprovider.GenericCommentEvent) bool { return e.Action == scm.ActionCreate },
-			Regex:                 stageRe,
-			GenericCommentHandler: stageHandleGenericComment,
+			Filter: func(e scmprovider.GenericCommentEvent) bool { return e.Action == scm.ActionCreate },
+			Regex:  stageRe,
+			GenericCommentHandler: func(match []string, pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
+				return stage(pc.SCMProviderClient, pc.Logger, &e, match[1])
+			},
 			Help: []pluginhelp.Command{{
-				Usage:       "/[remove-]stage <alpha|beta|stable>",
+				Usage:       "/stage <alpha|beta|stable>",
 				Description: "Labels the stage of an issue as alpha/beta/stable",
 				Featured:    false,
 				WhoCanUse:   "Anyone can trigger this command.",
-				Examples:    []string{"/stage alpha", "/remove-stage alpha"},
+				Examples:    []string{"/stage alpha"},
+			}},
+		}, {
+			Filter: func(e scmprovider.GenericCommentEvent) bool { return e.Action == scm.ActionCreate },
+			Regex:  unstageRe,
+			GenericCommentHandler: func(match []string, pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
+				return unstage(pc.SCMProviderClient, pc.Logger, &e, match[1])
+			},
+			Help: []pluginhelp.Command{{
+				Usage:       "/remove-stage <alpha|beta|stable>",
+				Description: "Removes the stage label of an issue as alpha/beta/stable",
+				Featured:    false,
+				WhoCanUse:   "Anyone can trigger this command.",
+				Examples:    []string{"/remove-stage alpha"},
 			}},
 		}},
 	}
@@ -61,53 +77,52 @@ func init() {
 	plugins.RegisterPlugin(pluginName, plugin)
 }
 
-type stageClient interface {
+type scmProviderClient interface {
 	AddLabel(owner, repo string, number int, label string, pr bool) error
 	RemoveLabel(owner, repo string, number int, label string, pr bool) error
 	GetIssueLabels(org, repo string, number int, pr bool) ([]*scm.Label, error)
 }
 
-func stageHandleGenericComment(match []string, pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
-	return handle(match, pc.SCMProviderClient, pc.Logger, &e)
-}
-
-func handle(match []string, gc stageClient, log *logrus.Entry, e *scmprovider.GenericCommentEvent) error {
-	return handleOne(gc, log, e, match)
-}
-
-func handleOne(gc stageClient, log *logrus.Entry, e *scmprovider.GenericCommentEvent, mat []string) error {
-	org := e.Repo.Namespace
-	repo := e.Repo.Name
-	number := e.Number
-
-	remove := mat[1] != ""
-	cmd := mat[2]
-	lbl := "stage/" + cmd
+func unstage(gc scmProviderClient, log *logrus.Entry, e *scmprovider.GenericCommentEvent, stage string) error {
+	lbl := "stage/" + stage
 
 	// Let's start simple and allow anyone to add/remove alpha, beta and stable labels.
 	// Adjust if we find evidence of the community abusing these labels.
-	labels, err := gc.GetIssueLabels(org, repo, number, e.IsPR)
+	labels, err := gc.GetIssueLabels(e.Repo.Namespace, e.Repo.Name, e.Number, e.IsPR)
 	if err != nil {
 		log.WithError(err).Errorf("Failed to get labels.")
+		return err
 	}
 
 	// If the label exists and we asked for it to be removed, remove it.
-	if scmprovider.HasLabel(lbl, labels) && remove {
-		return gc.RemoveLabel(org, repo, number, lbl, e.IsPR)
+	if scmprovider.HasLabel(lbl, labels) {
+		return gc.RemoveLabel(e.Repo.Namespace, e.Repo.Name, e.Number, lbl, e.IsPR)
+	}
+	return nil
+}
+
+func stage(gc scmProviderClient, log *logrus.Entry, e *scmprovider.GenericCommentEvent, stage string) error {
+	lbl := "stage/" + stage
+
+	// Let's start simple and allow anyone to add/remove alpha, beta and stable labels.
+	// Adjust if we find evidence of the community abusing these labels.
+	labels, err := gc.GetIssueLabels(e.Repo.Namespace, e.Repo.Name, e.Number, e.IsPR)
+	if err != nil {
+		log.WithError(err).Errorf("Failed to get labels.")
 	}
 
 	// If the label does not exist and we asked for it to be added,
 	// remove other existing stage labels and add it.
-	if !scmprovider.HasLabel(lbl, labels) && !remove {
+	if !scmprovider.HasLabel(lbl, labels) {
 		for _, label := range stageLabels {
-			if label != lbl && scmprovider.HasLabel(label, labels) {
-				if err := gc.RemoveLabel(org, repo, number, label, e.IsPR); err != nil {
+			if lbl != label && scmprovider.HasLabel(label, labels) {
+				if err := gc.RemoveLabel(e.Repo.Namespace, e.Repo.Name, e.Number, label, e.IsPR); err != nil {
 					log.WithError(err).Errorf("GitHub failed to remove the following label: %s", label)
 				}
 			}
 		}
 
-		if err := gc.AddLabel(org, repo, number, lbl, e.IsPR); err != nil {
+		if err := gc.AddLabel(e.Repo.Namespace, e.Repo.Name, e.Number, lbl, e.IsPR); err != nil {
 			log.WithError(err).Errorf("GitHub failed to add the following label: %s", lbl)
 		}
 	}
diff --git a/pkg/plugins/yuks/yuks.go b/pkg/plugins/yuks/yuks.go
index c9e0c793..bea63eac 100644
--- a/pkg/plugins/yuks/yuks.go
+++ b/pkg/plugins/yuks/yuks.go
@@ -37,18 +37,19 @@ var (
 )
 
 const (
-	// Previously: https://tambal.azurewebsites.net/joke/random
 	jokeURL    = realJoke("https://icanhazdadjoke.com")
 	pluginName = "yuks"
 )
 
-var (
-	plugin = plugins.Plugin{
+func createPlugin(j joker) plugins.Plugin {
+	return plugins.Plugin{
 		Description: "The yuks plugin comments with jokes in response to the `/joke` command.",
 		Commands: []plugins.Command{{
-			Filter:                func(e scmprovider.GenericCommentEvent) bool { return e.Action == scm.ActionCreate },
-			Regex:                 match,
-			GenericCommentHandler: handleGenericComment,
+			Filter: func(e scmprovider.GenericCommentEvent) bool { return e.Action == scm.ActionCreate },
+			Regex:  match,
+			GenericCommentHandler: func(_ []string, pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
+				return joke(pc.SCMProviderClient, pc.Logger, &e, j)
+			},
 			Help: []pluginhelp.Command{{
 				Usage:       "/joke",
 				Description: "Tells a joke.",
@@ -58,10 +59,10 @@ var (
 			}},
 		}},
 	}
-)
+}
 
 func init() {
-	plugins.RegisterPlugin(pluginName, plugin)
+	plugins.RegisterPlugin(pluginName, createPlugin(jokeURL))
 }
 
 type scmProviderClient interface {
@@ -102,15 +103,7 @@ func (url realJoke) readJoke() (string, error) {
 	return a.Joke, nil
 }
 
-func handleGenericComment(_ []string, pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
-	return handle(pc.SCMProviderClient, pc.Logger, &e, jokeURL)
-}
-
-func handle(spc scmProviderClient, log *logrus.Entry, e *scmprovider.GenericCommentEvent, j joker) error {
-	org := e.Repo.Namespace
-	repo := e.Repo.Name
-	number := e.Number
-
+func joke(spc scmProviderClient, log *logrus.Entry, e *scmprovider.GenericCommentEvent, j joker) error {
 	for i := 0; i < 10; i++ {
 		resp, err := j.readJoke()
 		if err != nil {
@@ -118,7 +111,7 @@ func handle(spc scmProviderClient, log *logrus.Entry, e *scmprovider.GenericComm
 		}
 		if simple.MatchString(resp) {
 			log.Infof("Commenting with \"%s\".", resp)
-			return spc.CreateComment(org, repo, number, e.IsPR, plugins.FormatResponseRaw(e.Body, e.Link, spc.QuoteAuthorForComment(e.Author.Login), resp))
+			return spc.CreateComment(e.Repo.Namespace, e.Repo.Name, e.Number, e.IsPR, plugins.FormatResponseRaw(e.Body, e.Link, spc.QuoteAuthorForComment(e.Author.Login), resp))
 		}
 
 		log.Errorf("joke contains invalid characters: %v", resp)


Test patch:
diff --git a/pkg/plugins/milestonestatus/milestonestatus_test.go b/pkg/plugins/milestonestatus/milestonestatus_test.go
index 58750c0e..8234e818 100644
--- a/pkg/plugins/milestonestatus/milestonestatus_test.go
+++ b/pkg/plugins/milestonestatus/milestonestatus_test.go
@@ -23,8 +23,8 @@ import (
 	"testing"
 
 	"github.com/jenkins-x/go-scm/scm"
+	"github.com/jenkins-x/go-scm/scm/driver/fake"
 	"github.com/jenkins-x/lighthouse/pkg/scmprovider"
-	"github.com/jenkins-x/lighthouse/pkg/scmprovider/fake"
 	"github.com/sirupsen/logrus"
 
 	"github.com/jenkins-x/lighthouse/pkg/plugins"
@@ -35,9 +35,6 @@ func formatLabels(labels ...string) []string {
 	for _, l := range labels {
 		r = append(r, fmt.Sprintf("%s/%s#%d:%s", "org", "repo", 1, l))
 	}
-	if len(r) == 0 {
-		return nil
-	}
 	return r
 }
 
@@ -134,7 +131,9 @@ func TestMilestoneStatus(t *testing.T) {
 
 	for _, tc := range testcases {
 		t.Run(tc.name, func(t *testing.T) {
-			fakeClient := &fake.SCMClient{IssueComments: make(map[int][]*scm.Comment)}
+			fakeScmClient, fc := fake.NewDefault()
+			fakeClient := scmprovider.ToTestClient(fakeScmClient)
+
 			e := &scmprovider.GenericCommentEvent{
 				Action: scm.ActionCreate,
 				Body:   tc.body,
@@ -142,15 +141,19 @@ func TestMilestoneStatus(t *testing.T) {
 				Repo:   scm.Repository{Namespace: "org", Name: "repo"},
 				Author: scm.User{Login: tc.commenter},
 			}
-
 			repoMilestone := map[string]plugins.Milestone{"": {MaintainersID: 0}}
-
 			if !tc.noRepoMaintainer {
 				repoMilestone["org/repo"] = plugins.Milestone{MaintainersID: 42}
 			}
-
-			if err := plugin.InvokeCommand(e, func(match []string) error {
-				return handle(match, fakeClient, logrus.WithField("plugin", pluginName), e, repoMilestone)
+			agent := plugins.Agent{
+				SCMProviderClient: &fakeClient.Client,
+				Logger:            logrus.WithField("plugin", pluginName),
+				PluginConfig: &plugins.Configuration{
+					RepoMilestone: repoMilestone,
+				},
+			}
+			if err := plugin.InvokeCommandHandler(e, func(handler plugins.GenericCommentHandler, e *scmprovider.GenericCommentEvent, match []string) error {
+				return handler(match, agent, *e)
 			}); err != nil {
 				t.Fatalf("(%s): Unexpected error from handle: %v.", tc.name, err)
 			}
@@ -158,13 +161,13 @@ func TestMilestoneStatus(t *testing.T) {
 			// Check that the correct labels were added.
 			expectLabels := formatLabels(tc.expectedNewLabels...)
 			sort.Strings(expectLabels)
-			sort.Strings(fakeClient.IssueLabelsAdded)
-			if !reflect.DeepEqual(expectLabels, fakeClient.IssueLabelsAdded) {
-				t.Errorf("(%s): Expected issue to end with labels %q, but ended with %q.", tc.name, expectLabels, fakeClient.IssueLabelsAdded)
+			sort.Strings(fc.IssueLabelsAdded)
+			if !reflect.DeepEqual(expectLabels, fc.IssueLabelsAdded) {
+				t.Errorf("(%s): Expected issue to end with labels %q, but ended with %q.", tc.name, expectLabels, fc.IssueLabelsAdded)
 			}
 
 			// Check that a comment was left iff one should have been left.
-			comments := len(fakeClient.IssueComments[1])
+			comments := len(fc.IssueComments[1])
 			if tc.shouldComment && comments != 1 {
 				t.Errorf("(%s): 1 comment should have been made, but %d comments were made.", tc.name, comments)
 			} else if !tc.shouldComment && comments != 0 {
diff --git a/pkg/plugins/pony/pony_test.go b/pkg/plugins/pony/pony_test.go
index d77037ac..bf1c4459 100644
--- a/pkg/plugins/pony/pony_test.go
+++ b/pkg/plugins/pony/pony_test.go
@@ -28,6 +28,7 @@ import (
 
 	"github.com/jenkins-x/go-scm/scm"
 	"github.com/jenkins-x/go-scm/scm/driver/fake"
+	"github.com/jenkins-x/lighthouse/pkg/plugins"
 	"github.com/jenkins-x/lighthouse/pkg/scmprovider"
 	"github.com/sirupsen/logrus"
 )
@@ -224,8 +225,13 @@ func TestHttpResponse(t *testing.T) {
 				Number:     5,
 				IssueState: "open",
 			}
-			err = plugin.InvokeCommand(e, func(match []string) error {
-				return handle(fakeClient, logrus.WithField("plugin", pluginName), e, realHerd(ts.URL+testcase.path))
+			agent := plugins.Agent{
+				SCMProviderClient: &fakeClient.Client,
+				Logger:            logrus.WithField("plugin", pluginName),
+			}
+			plugin := createPlugin(realHerd(ts.URL + testcase.path))
+			err = plugin.InvokeCommandHandler(e, func(handler plugins.GenericCommentHandler, e *scmprovider.GenericCommentEvent, match []string) error {
+				return handler(match, agent, *e)
 			})
 			if err != nil {
 				t.Errorf("tc %s: For comment %s, didn't expect error: %v", testcase.name, testcase.comment, err)
@@ -320,8 +326,13 @@ func TestPonies(t *testing.T) {
 			IssueState: tc.state,
 			IsPR:       tc.pr,
 		}
-		err := plugin.InvokeCommand(e, func(match []string) error {
-			return handle(fakeClient, logrus.WithField("plugin", pluginName), e, fakeHerd("pone"))
+		agent := plugins.Agent{
+			SCMProviderClient: &fakeClient.Client,
+			Logger:            logrus.WithField("plugin", pluginName),
+		}
+		plugin := createPlugin(fakeHerd("pone"))
+		err := plugin.InvokeCommandHandler(e, func(handler plugins.GenericCommentHandler, e *scmprovider.GenericCommentEvent, match []string) error {
+			return handler(match, agent, *e)
 		})
 		if err != nil {
 			t.Errorf("For case %s, didn't expect error: %v", tc.name, err)
diff --git a/pkg/plugins/shrug/shurg_test.go b/pkg/plugins/shrug/shurg_test.go
index 7f7db27b..afeaa7fb 100644
--- a/pkg/plugins/shrug/shurg_test.go
+++ b/pkg/plugins/shrug/shurg_test.go
@@ -21,6 +21,7 @@ import (
 
 	"github.com/jenkins-x/go-scm/scm"
 	"github.com/jenkins-x/go-scm/scm/driver/fake"
+	"github.com/jenkins-x/lighthouse/pkg/plugins"
 	"github.com/jenkins-x/lighthouse/pkg/scmprovider"
 	"github.com/sirupsen/logrus"
 
@@ -81,10 +82,16 @@ func TestShrugComment(t *testing.T) {
 			Number: 5,
 			Repo:   scm.Repository{Namespace: "org", Name: "repo"},
 		}
+		agent := plugins.Agent{
+			SCMProviderClient: &fakeClient.Client,
+			Logger:            logrus.WithField("plugin", pluginName),
+		}
 		if tc.hasShrug {
 			fc.IssueLabelsAdded = []string{"org/repo#5:" + labels.Shrug}
 		}
-		if err := handle(fakeClient, logrus.WithField("plugin", pluginName), e); err != nil {
+		if err := plugin.InvokeCommandHandler(e, func(handler plugins.GenericCommentHandler, e *scmprovider.GenericCommentEvent, match []string) error {
+			return handler(match, agent, *e)
+		}); err != nil {
 			t.Errorf("For case %s, didn't expect error: %v", tc.name, err)
 			continue
 		}
diff --git a/pkg/plugins/stage/stage_test.go b/pkg/plugins/stage/stage_test.go
index 7e9bb59d..e72cc691 100644
--- a/pkg/plugins/stage/stage_test.go
+++ b/pkg/plugins/stage/stage_test.go
@@ -17,49 +17,23 @@ limitations under the License.
 package stage
 
 import (
+	"fmt"
 	"reflect"
 	"testing"
 
 	"github.com/jenkins-x/go-scm/scm"
+	"github.com/jenkins-x/go-scm/scm/driver/fake"
+	"github.com/jenkins-x/lighthouse/pkg/plugins"
 	"github.com/jenkins-x/lighthouse/pkg/scmprovider"
 	"github.com/sirupsen/logrus"
 )
 
-type fakeClient struct {
-	// current labels
-	labels []string
-	// labels that are added
-	added []string
-	// labels that are removed
-	removed []string
-}
-
-func (c *fakeClient) AddLabel(owner, repo string, number int, label string, _ bool) error {
-	c.added = append(c.added, label)
-	c.labels = append(c.labels, label)
-	return nil
-}
-
-func (c *fakeClient) RemoveLabel(owner, repo string, number int, label string, _ bool) error {
-	c.removed = append(c.removed, label)
-
-	// remove from existing labels
-	for k, v := range c.labels {
-		if label == v {
-			c.labels = append(c.labels[:k], c.labels[k+1:]...)
-			break
-		}
+func issueLabels(labels ...string) []string {
+	ls := []string{}
+	for _, label := range labels {
+		ls = append(ls, fmt.Sprintf("org/repo#0:%s", label))
 	}
-
-	return nil
-}
-
-func (c *fakeClient) GetIssueLabels(owner, repo string, number int, _ bool) ([]*scm.Label, error) {
-	la := []*scm.Label{}
-	for _, l := range c.labels {
-		la = append(la, &scm.Label{Name: l})
-	}
-	return la, nil
+	return ls
 }
 
 func TestStageLabels(t *testing.T) {
@@ -213,25 +187,32 @@ func TestStageLabels(t *testing.T) {
 	}
 	for _, tc := range testcases {
 		t.Run(tc.name, func(t *testing.T) {
-			fc := &fakeClient{
-				labels:  tc.labels,
-				added:   []string{},
-				removed: []string{},
-			}
+			fakeScmClient, fc := fake.NewDefault()
+			fakeClient := scmprovider.ToTestClient(fakeScmClient)
 			e := &scmprovider.GenericCommentEvent{
 				Body:   tc.body,
 				Action: scm.ActionCreate,
+				Repo: scm.Repository{
+					Namespace: "org",
+					Name:      "repo",
+					FullName:  "org/repo",
+				},
+			}
+			fc.IssueLabelsExisting = issueLabels(tc.labels...)
+			agent := plugins.Agent{
+				SCMProviderClient: &fakeClient.Client,
+				Logger:            logrus.WithField("plugin", pluginName),
 			}
-			err := plugin.InvokeCommand(e, func(match []string) error {
-				return handle(match, fc, logrus.WithField("plugin", "fake-lifecyle"), e)
+			err := plugin.InvokeCommandHandler(e, func(handler plugins.GenericCommentHandler, e *scmprovider.GenericCommentEvent, match []string) error {
+				return handler(match, agent, *e)
 			})
 			switch {
 			case err != nil:
 				t.Errorf("%s: unexpected error: %v", tc.name, err)
-			case !reflect.DeepEqual(tc.added, fc.added):
-				t.Errorf("%s: added %v != actual %v", tc.name, tc.added, fc.added)
-			case !reflect.DeepEqual(tc.removed, fc.removed):
-				t.Errorf("%s: removed %v != actual %v", tc.name, tc.removed, fc.removed)
+			case !reflect.DeepEqual(issueLabels(tc.added...), fc.IssueLabelsAdded):
+				t.Errorf("%s: added %v != actual %v", tc.name, tc.added, fc.IssueLabelsAdded)
+			case !reflect.DeepEqual(issueLabels(tc.removed...), fc.IssueLabelsRemoved):
+				t.Errorf("%s: removed %v != actual %v", tc.name, tc.removed, fc.IssueLabelsRemoved)
 			}
 		})
 	}
diff --git a/pkg/plugins/yuks/yuks_test.go b/pkg/plugins/yuks/yuks_test.go
index a30abb85..7717fa5f 100644
--- a/pkg/plugins/yuks/yuks_test.go
+++ b/pkg/plugins/yuks/yuks_test.go
@@ -26,6 +26,7 @@ import (
 
 	"github.com/jenkins-x/go-scm/scm"
 	"github.com/jenkins-x/go-scm/scm/driver/fake"
+	"github.com/jenkins-x/lighthouse/pkg/plugins"
 	"github.com/jenkins-x/lighthouse/pkg/scmprovider"
 	"github.com/sirupsen/logrus"
 )
@@ -67,8 +68,13 @@ func TestJokesMedium(t *testing.T) {
 		Number:     5,
 		IssueState: "open",
 	}
-	if err := plugin.InvokeCommand(e, func(match []string) error {
-		return handle(fakeClient, logrus.WithField("plugin", pluginName), e, realJoke(ts.URL))
+	agent := plugins.Agent{
+		SCMProviderClient: &fakeClient.Client,
+		Logger:            logrus.WithField("plugin", pluginName),
+	}
+	plugin := createPlugin(realJoke(ts.URL))
+	if err := plugin.InvokeCommandHandler(e, func(handler plugins.GenericCommentHandler, e *scmprovider.GenericCommentEvent, match []string) error {
+		return handler(match, agent, *e)
 	}); err != nil {
 		t.Errorf("didn't expect error: %v", err)
 		return
@@ -163,8 +169,13 @@ func TestJokes(t *testing.T) {
 				IssueState: tc.state,
 				IsPR:       tc.pr,
 			}
-			err := plugin.InvokeCommand(e, func(match []string) error {
-				return handle(fakeClient, logrus.WithField("plugin", pluginName), e, tc.joke)
+			agent := plugins.Agent{
+				SCMProviderClient: &fakeClient.Client,
+				Logger:            logrus.WithField("plugin", pluginName),
+			}
+			plugin := createPlugin(tc.joke)
+			err := plugin.InvokeCommandHandler(e, func(handler plugins.GenericCommentHandler, e *scmprovider.GenericCommentEvent, match []string) error {
+				return handler(match, agent, *e)
 			})
 			if !tc.shouldError && err != nil {
 				t.Fatalf("For case %s, didn't expect error: %v", tc.name, err)