Repository: jenkins-x/lighthouse
Pull request #1024 (2020-09-09 10:59:43)

refactor: manage commands in Plugin descriptor
This PR changes the way plugin commands are managed by the plugin engine.

Before, the commands were registered separately from the help provider, it was simply a `GenericCommentEventHandler` and there was no guarantee that the command and the help provider were consistent.

Now, the commands are part of the definition of a plugin, the help is moved in the command definition, this makes it easier to keep the command and its help consistent.
The command registration can specify the regex to use and a filter func, these parameters will be taken care of by the plugin engine automatically and the regex match will be passed to the handler when being invoked by the plugin engine.

If the command definition does not specify a regex or a filter func, it will behave like before (receive all events and the filtering/matching has to be done by the handler).


eddycharly: /hold

must come after https://github.com/jenkins-x/lighthouse/pull/1023
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"]} -->
jenkins-x-bot: @eddycharly: The following test **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
ghe | 386fc9e24d882ec2238ee56a4927b73d93097a8d | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/2) | `/test ghe`

[View all Builds for this Pull Request](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024) 

<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 -->
jenkins-x-bot: @eddycharly: The following tests **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
ghe | 386fc9e24d882ec2238ee56a4927b73d93097a8d | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/2) | `/test ghe`
gitlab | 386fc9e24d882ec2238ee56a4927b73d93097a8d | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/5) | `/test gitlab`

[View all Builds for this Pull Request](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024) 

<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 -->
jenkins-x-bot: @eddycharly: The following tests **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
ghe | 386fc9e24d882ec2238ee56a4927b73d93097a8d | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/2) | `/test ghe`
gitlab | 386fc9e24d882ec2238ee56a4927b73d93097a8d | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/5) | `/test gitlab`
github | 386fc9e24d882ec2238ee56a4927b73d93097a8d | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/3) | `/test github`

[View all Builds for this Pull Request](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024) 

<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 -->
eddycharly: Ok, i probably broke something...
jenkins-x-bot: @eddycharly: The following tests **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
ghe | 386fc9e24d882ec2238ee56a4927b73d93097a8d | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/2) | `/test ghe`
gitlab | 386fc9e24d882ec2238ee56a4927b73d93097a8d | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/5) | `/test gitlab`
github | 386fc9e24d882ec2238ee56a4927b73d93097a8d | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/3) | `/test github`
bbs | 386fc9e24d882ec2238ee56a4927b73d93097a8d | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/4) | `/test bbs`

[View all Builds for this Pull Request](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024) 

<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 -->
abayer: The Jenkins X contexts never merged the test PR - everything before then worked, so I'm not sure what you changed could have caused that...
eddycharly: Ok, it's probably around the lgtm or approve plugins then...
jenkins-x-bot: @eddycharly: The following tests **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
ghe | 386fc9e24d882ec2238ee56a4927b73d93097a8d | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/2) | `/test ghe`
gitlab | 386fc9e24d882ec2238ee56a4927b73d93097a8d | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/5) | `/test gitlab`
github | 386fc9e24d882ec2238ee56a4927b73d93097a8d | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/3) | `/test github`
bbs | 386fc9e24d882ec2238ee56a4927b73d93097a8d | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/4) | `/test bbs`
tekton | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/12) | `/test tekton`

[View all Builds for this Pull Request](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024) 

<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 -->
eddycharly: /test tekton

this one was passing...
abayer: lgtm and approve are working - the last thing that happens is that it `/override`s, which it did successfully, and then waits for Keeper to merge the PR. That's what's weird - but I'll dig into logs if it continues.
abayer: /retest
abayer: Tekton failed on `/uncc` - but since everything else has passed that cleanly before, I think it may have just been random noise.
abayer: Ah-ha, it's `/override` - it briefly sets the status correctly but then it gets overwritten with a failure and so things can't merge. 
abayer: override has a history of weird behavior like this, for what it's worth.
abayer: aaaaaah `/override` is _also_ triggering a run, not just creating a `LighthouseJob` that just sets a status and doesn't do anything else.
jenkins-x-bot: @eddycharly: The following tests **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
ghe | 386fc9e24d882ec2238ee56a4927b73d93097a8d | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/2) | `/test ghe`
github | 386fc9e24d882ec2238ee56a4927b73d93097a8d | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/3) | `/test github`
bbs | 386fc9e24d882ec2238ee56a4927b73d93097a8d | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/4) | `/test bbs`
tekton | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/12) | `/test tekton`
gitlab | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/11) | `/test gitlab`

[View all Builds for this Pull Request](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024) 

<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 -->
jenkins-x-bot: @eddycharly: The following tests **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
github | 386fc9e24d882ec2238ee56a4927b73d93097a8d | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/3) | `/test github`
bbs | 386fc9e24d882ec2238ee56a4927b73d93097a8d | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/4) | `/test bbs`
tekton | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/12) | `/test tekton`
gitlab | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/11) | `/test gitlab`
ghe | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/7) | `/test ghe`

[View all Builds for this Pull Request](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024) 

<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 -->
jenkins-x-bot: @eddycharly: The following tests **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
bbs | 386fc9e24d882ec2238ee56a4927b73d93097a8d | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/4) | `/test bbs`
tekton | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/12) | `/test tekton`
gitlab | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/11) | `/test gitlab`
ghe | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/7) | `/test ghe`
github | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/9) | `/test github`

[View all Builds for this Pull Request](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024) 

<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 -->
jenkins-x-bot: @eddycharly: The following tests **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
tekton | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/12) | `/test tekton`
gitlab | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/11) | `/test gitlab`
ghe | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/7) | `/test ghe`
github | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/9) | `/test github`
bbs | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/10) | `/test bbs`

[View all Builds for this Pull Request](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024) 

<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 -->
jenkins-x-bot: @eddycharly: The following tests **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
gitlab | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/11) | `/test gitlab`
ghe | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/7) | `/test ghe`
github | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/9) | `/test github`
bbs | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/10) | `/test bbs`
tekton | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/13) | `/test tekton`

[View all Builds for this Pull Request](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024) 

<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 -->
eddycharly: /retest
abayer: Actually, it might not be `override`, but there are 7 runs being created for the PR, and there should be 3, so it looks like we're spawning more runs than there should be, and while one of the three that seemed to be running at the same time failed and the test saw that and proceeded to override, another of the remaining two runs failed after the override triggered and confused things.
abayer: Verified - it looks like `/test this` and `/retest` each triggered 3 runs.
eddycharly: Ok, understood, there are 3 commands in the trigger plugin...
eddycharly: Each command gets the event but there is no logic to acknowledge the event was consumed... gotta change that, the unit tests would have failed but they didn't. I was surprised but didn't pay more attention.

Thanks, gonna fix that.
jenkins-x-bot: @eddycharly: The following tests **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
gitlab | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/11) | `/test gitlab`
github | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/9) | `/test github`
bbs | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/10) | `/test bbs`
tekton | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/13) | `/test tekton`
ghe | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/14) | `/test ghe`

[View all Builds for this Pull Request](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024) 

<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 -->
jenkins-x-bot: @eddycharly: The following tests **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
gitlab | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/11) | `/test gitlab`
github | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/9) | `/test github`
bbs | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/10) | `/test bbs`
ghe | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/14) | `/test ghe`
tekton | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/18) | `/test tekton`

[View all Builds for this Pull Request](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024) 

<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 -->
jenkins-x-bot: @eddycharly: The following tests **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
gitlab | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/11) | `/test gitlab`
ghe | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/14) | `/test ghe`
tekton | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/18) | `/test tekton`
github | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/15) | `/test github`

[View all Builds for this Pull Request](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024) 

<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 -->
jenkins-x-bot: @eddycharly: The following tests **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
ghe | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/14) | `/test ghe`
tekton | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/18) | `/test tekton`
github | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/15) | `/test github`
gitlab | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/17) | `/test gitlab`

[View all Builds for this Pull Request](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024) 

<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 -->
jenkins-x-bot: @eddycharly: The following tests **failed**, say `/retest` to rerun them all:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
ghe | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/14) | `/test ghe`
tekton | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/18) | `/test tekton`
github | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/15) | `/test github`
gitlab | 62da2295fb28e7778420fdb8a364ba953c25e45a | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/17) | `/test gitlab`
pr-build | 825c534845ba584273ff4f3d1c687e96b6bed3fd | [link](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024/19) | `/test pr-build`

[View all Builds for this Pull Request](https://dashboard-jx.jenkins-x.live/teams/jx/projects/jenkins-x/lighthouse/PR-1024) 

<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 -->
eddycharly: It should be fixed this time.

abayer: Failure is in linting:

```
LINTING
/home/jenkins/go/bin/golint -set_exit_status ./...
pkg/plugins/plugins.go:89:1: exported method Plugin.InvokeCommand should have comment or be unexported
Found 1 lint suggestions; failing.
make: *** [lint] Error 1
```
eddycharly: yep, forgot those...
hopefully other pipelines will be green 🤞 
abayer: /lgtm

Yup, looks all good now. Woo!
jenkins-x-bot: [APPROVALNOTIFIER] This PR is **APPROVED**

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

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?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#issuecomment-689635300/jenkins-x/lighthouse/blob/master/OWNERS)~~ [abayer]

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: /hold cancel

I'll try to harden the plugin contract, making the regex mandatory first, then rework the command/command help relations.


Patch:
diff --git a/pkg/plugins/approve/approve.go b/pkg/plugins/approve/approve.go
index 35757c04..9724a1c2 100644
--- a/pkg/plugins/approve/approve.go
+++ b/pkg/plugins/approve/approve.go
@@ -94,20 +94,33 @@ type state struct {
 	htmlURL   string
 }
 
-func init() {
-	plugins.RegisterPlugin(
-		PluginName,
-		plugins.Plugin{
-			Description: `The approve plugin implements a pull request approval process that manages the '` + labels.Approved + `' label and an approval notification comment. Approval is achieved when the set of users that have approved the PR is capable of approving every file changed by the PR. A user is able to approve a file if their username or an alias they belong to is listed in the 'approvers' section of an OWNERS file in the directory of the file or higher in the directory tree.
+var (
+	plugin = plugins.Plugin{
+		Description: `The approve plugin implements a pull request approval process that manages the '` + labels.Approved + `' label and an approval notification comment. Approval is achieved when the set of users that have approved the PR is capable of approving every file changed by the PR. A user is able to approve a file if their username or an alias they belong to is listed in the 'approvers' section of an OWNERS file in the directory of the file or higher in the directory tree.
 <br>
 <br>Per-repo configuration may be used to require that PRs link to an associated issue before approval is granted. It may also be used to specify that the PR authors implicitly approve their own PRs.
 <br>For more information see <a href="https://git.github.com/jenkins-x/lighthouse/pkg/prow/plugins/approve/approvers/README.md">here</a>.`,
-			HelpProvider:          helpProvider,
+		HelpProvider:       helpProvider,
+		ReviewEventHandler: handleReviewEvent,
+		PullRequestHandler: handlePullRequestEvent,
+		Commands: []plugins.Command{{
 			GenericCommentHandler: handleGenericCommentEvent,
-			ReviewEventHandler:    handleReviewEvent,
-			PullRequestHandler:    handlePullRequestEvent,
-		},
-	)
+			Filter: func(e scmprovider.GenericCommentEvent) bool {
+				return !(e.Action != scm.ActionCreate || !e.IsPR || e.IssueState == "closed")
+			},
+			Help: []pluginhelp.Command{{
+				Usage:       "/approve [no-issue|cancel]",
+				Description: "Approves a pull request",
+				Featured:    true,
+				WhoCanUse:   "Users listed as 'approvers' in appropriate OWNERS files.",
+				Examples:    []string{"/approve", "/approve no-issue", "/lh-approve"},
+			}},
+		}},
+	}
+)
+
+func init() {
+	plugins.RegisterPlugin(PluginName, plugin)
 }
 
 func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) {
@@ -138,20 +151,10 @@ func helpProvider(config *plugins.Configuration, enabledRepos []string) (*plugin
 		}
 		approveConfig[repo] = fmt.Sprintf("Pull requests %s require an associated issue.<br>Pull request authors %s implicitly approve their own PRs.<br>The /lgtm [cancel] command(s) %s act as approval.<br>A GitHub approved or changes requested review %s act as approval or cancel respectively.", doNot(opts.IssueRequired), doNot(opts.HasSelfApproval()), willNot(opts.LgtmActsAsApprove), willNot(opts.ConsiderReviewState()))
 	}
-	pluginHelp := &pluginhelp.PluginHelp{
-		Config: approveConfig,
-	}
-	pluginHelp.AddCommand(pluginhelp.Command{
-		Usage:       "/approve [no-issue|cancel]",
-		Description: "Approves a pull request",
-		Featured:    true,
-		WhoCanUse:   "Users listed as 'approvers' in appropriate OWNERS files.",
-		Examples:    []string{"/approve", "/approve no-issue", "/lh-approve"},
-	})
-	return pluginHelp, nil
+	return &pluginhelp.PluginHelp{Config: approveConfig}, nil
 }
 
-func handleGenericCommentEvent(pc plugins.Agent, ce scmprovider.GenericCommentEvent) error {
+func handleGenericCommentEvent(_ []string, pc plugins.Agent, ce scmprovider.GenericCommentEvent) error {
 	baseURL, err := url.Parse(ce.IssueLink)
 	if err != nil {
 		return errors.Wrapf(err, "failed to parse URL %s", ce.Link)
@@ -169,10 +172,6 @@ func handleGenericCommentEvent(pc plugins.Agent, ce scmprovider.GenericCommentEv
 }
 
 func handleGenericComment(log *logrus.Entry, spc scmProviderClient, oc ownersClient, serverURL *url.URL, config *plugins.Configuration, ce *scmprovider.GenericCommentEvent) error {
-	if ce.Action != scm.ActionCreate || !ce.IsPR || ce.IssueState == "closed" {
-		return nil
-	}
-
 	botName, err := spc.BotName()
 	if err != nil {
 		return err
diff --git a/pkg/plugins/assign/assign.go b/pkg/plugins/assign/assign.go
index 0d1b773b..a732e85a 100644
--- a/pkg/plugins/assign/assign.go
+++ b/pkg/plugins/assign/assign.go
@@ -37,35 +37,37 @@ var (
 	CCRegexp = regexp.MustCompile(`(?mi)^/(?:lh-)?(un)?cc(( +@?(?:")?[-/\w]+?)*(?:")?)\s*$`)
 )
 
-func init() {
-	plugins.RegisterPlugin(
-		pluginName,
-		plugins.Plugin{
-			Description:           "The assign plugin assigns or requests reviews from users. Specific users can be assigned with the command '/assign @user1' or have reviews requested of them with the command '/cc @user1'. If no user is specified the commands default to targeting the user who created the command. Assignments and requested reviews can be removed in the same way that they are added by prefixing the commands with 'un'.",
-			HelpProvider:          helpProvider,
+var (
+	plugin = plugins.Plugin{
+		Description:  "The assign plugin assigns or requests reviews from users. Specific users can be assigned with the command '/assign @user1' or have reviews requested of them with the command '/cc @user1'. If no user is specified the commands default to targeting the user who created the command. Assignments and requested reviews can be removed in the same way that they are added by prefixing the commands with 'un'.",
+		HelpProvider: helpProvider,
+		Commands: []plugins.Command{{
 			GenericCommentHandler: handleGenericComment,
-		},
-	)
+			Filter:                func(e scmprovider.GenericCommentEvent) bool { return e.Action == scm.ActionCreate },
+			Help: []pluginhelp.Command{{
+				Usage:       "/[un]assign [[@]<username>...]",
+				Description: "Assigns an assignee to the PR",
+				Featured:    true,
+				WhoCanUse:   "Anyone can use the command, but the target user must be an org member, a repo collaborator, or should have previously commented on the issue or PR.",
+				Examples:    []string{"/assign", "/unassign", "/assign @k8s-ci-robot", "/lh-assign"},
+			}, {
+				Usage:       "/[un]cc [[@]<username>...]",
+				Description: "Requests a review from the user(s).",
+				Featured:    true,
+				WhoCanUse:   "Anyone can use the command, but the target user must be a member of the org that owns the repository.",
+				Examples:    []string{"/cc", "/uncc", "/cc @k8s-ci-robot", "/lh-cc"},
+			}},
+		}},
+	}
+)
+
+func init() {
+	plugins.RegisterPlugin(pluginName, plugin)
 }
 
 func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) {
 	// The Config field is omitted because this plugin is not configurable.
-	pluginHelp := &pluginhelp.PluginHelp{}
-	pluginHelp.AddCommand(pluginhelp.Command{
-		Usage:       "/[un]assign [[@]<username>...]",
-		Description: "Assigns an assignee to the PR",
-		Featured:    true,
-		WhoCanUse:   "Anyone can use the command, but the target user must be an org member, a repo collaborator, or should have previously commented on the issue or PR.",
-		Examples:    []string{"/assign", "/unassign", "/assign @k8s-ci-robot", "/lh-assign"},
-	})
-	pluginHelp.AddCommand(pluginhelp.Command{
-		Usage:       "/[un]cc [[@]<username>...]",
-		Description: "Requests a review from the user(s).",
-		Featured:    true,
-		WhoCanUse:   "Anyone can use the command, but the target user must be a member of the org that owns the repository.",
-		Examples:    []string{"/cc", "/uncc", "/cc @k8s-ci-robot", "/lh-cc"},
-	})
-	return pluginHelp, nil
+	return &pluginhelp.PluginHelp{}, nil
 }
 
 type scmProviderClient interface {
@@ -79,10 +81,7 @@ type scmProviderClient interface {
 	QuoteAuthorForComment(string) string
 }
 
-func handleGenericComment(pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
-	if e.Action != scm.ActionCreate {
-		return nil
-	}
+func handleGenericComment(_ []string, pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
 	err := handle(newAssignHandler(e, pc.SCMProviderClient, pc.Logger))
 	if e.IsPR {
 		err = combineErrors(err, handle(newReviewHandler(e, pc.SCMProviderClient, pc.Logger)))
diff --git a/pkg/plugins/cat/cat.go b/pkg/plugins/cat/cat.go
index 907a4fb9..e6298ccf 100644
--- a/pkg/plugins/cat/cat.go
+++ b/pkg/plugins/cat/cat.go
@@ -30,11 +30,10 @@ import (
 	"time"
 
 	"github.com/jenkins-x/go-scm/scm"
-	"github.com/jenkins-x/lighthouse/pkg/scmprovider"
-	"github.com/sirupsen/logrus"
-
 	"github.com/jenkins-x/lighthouse/pkg/pluginhelp"
 	"github.com/jenkins-x/lighthouse/pkg/plugins"
+	"github.com/jenkins-x/lighthouse/pkg/scmprovider"
+	"github.com/sirupsen/logrus"
 )
 
 var (
@@ -50,31 +49,36 @@ const (
 	grumpyURL  = "https://upload.wikimedia.org/wikipedia/commons/e/ee/Grumpy_Cat_by_Gage_Skidmore.jpg"
 )
 
-func init() {
-	plugins.RegisterPlugin(
-		pluginName,
-		plugins.Plugin{
-			Description:           "The cat plugin adds a cat image to an issue or PR in response to the `/meow` command.",
-			HelpProvider:          helpProvider,
+var (
+	plugin = plugins.Plugin{
+		Description:  "The cat plugin adds a cat image to an issue or PR in response to the `/meow` command.",
+		HelpProvider: helpProvider,
+		Commands: []plugins.Command{{
 			GenericCommentHandler: handleGenericComment,
-		},
-	)
+			Filter:                func(e scmprovider.GenericCommentEvent) bool { return e.Action == scm.ActionCreate },
+			Regex:                 match,
+			Help: []pluginhelp.Command{{
+				Description: "Add a cat image to the issue or PR",
+				Usage:       "/meow(vie) [CATegory]",
+				Featured:    false,
+				WhoCanUse:   "Anyone",
+				Examples:    []string{"/meow", "/meow caturday", "/meowvie clothes", "/lh-meow"},
+			}},
+		}},
+	}
+)
+
+func init() {
+	plugins.RegisterPlugin(pluginName, plugin)
 }
 
 func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) {
-	pluginHelp := &pluginhelp.PluginHelp{
-		Config: map[string]string{
-			"": fmt.Sprintf("The cat plugin uses an api key for thecatapi.com stored in %s.", config.Cat.KeyPath),
+	return &pluginhelp.PluginHelp{
+			Config: map[string]string{
+				"": fmt.Sprintf("The cat plugin uses an api key for thecatapi.com stored in %s.", config.Cat.KeyPath),
+			},
 		},
-	}
-	pluginHelp.AddCommand(pluginhelp.Command{
-		Usage:       "/meow(vie) [CATegory]",
-		Description: "Add a cat image to the issue or PR",
-		Featured:    false,
-		WhoCanUse:   "Anyone",
-		Examples:    []string{"/meow", "/meow caturday", "/meowvie clothes", "/lh-meow"},
-	})
-	return pluginHelp, nil
+		nil
 }
 
 type scmProviderClient interface {
@@ -181,8 +185,9 @@ func (c *realClowder) readCat(category string, movieCat bool) (string, error) {
 	return a.Format()
 }
 
-func handleGenericComment(pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
+func handleGenericComment(match []string, pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
 	return handle(
+		match,
 		pc.SCMProviderClient,
 		pc.Logger,
 		&e,
@@ -191,18 +196,9 @@ func handleGenericComment(pc plugins.Agent, e scmprovider.GenericCommentEvent) e
 	)
 }
 
-func handle(spc scmProviderClient, log *logrus.Entry, e *scmprovider.GenericCommentEvent, c clowder, setKey func()) error {
-	// Only consider new comments.
-	if e.Action != scm.ActionCreate {
-		return nil
-	}
-	// Make sure they are requesting a cat
-	mat := match.FindStringSubmatch(e.Body)
-	if mat == nil {
-		return nil
-	}
-
-	category, movieCat, err := parseMatch(mat)
+func handle(match []string, spc scmProviderClient, log *logrus.Entry, e *scmprovider.GenericCommentEvent, c clowder, setKey func()) error {
+	// Parse match
+	category, movieCat, err := parseMatch(match)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/plugins/dog/dog.go b/pkg/plugins/dog/dog.go
index 09460234..25ecfe8e 100644
--- a/pkg/plugins/dog/dog.go
+++ b/pkg/plugins/dog/dog.go
@@ -49,28 +49,31 @@ const (
 	pluginName    = "dog"
 )
 
-func init() {
-	plugins.RegisterPlugin(
-		pluginName,
-		plugins.Plugin{
-			Description:           "The dog plugin adds a dog image to an issue or PR in response to the `/woof` command.",
-			HelpProvider:          helpProvider,
+var (
+	plugin = plugins.Plugin{
+		Description:  "The dog plugin adds a dog image to an issue or PR in response to the `/woof` command.",
+		HelpProvider: helpProvider,
+		Commands: []plugins.Command{{
 			GenericCommentHandler: handleGenericComment,
-		},
-	)
+			Filter:                func(e scmprovider.GenericCommentEvent) bool { return e.Action == scm.ActionCreate },
+			Help: []pluginhelp.Command{{
+				Usage:       "/(lh-)?(woof|bark|this-is-{fine|not-fine|unbearable})",
+				Description: "Add a dog image to the issue or PR",
+				Featured:    false,
+				WhoCanUse:   "Anyone",
+				Examples:    []string{"/woof", "/bark", "this-is-{fine|not-fine|unbearable}"},
+			}},
+		}},
+	}
+)
+
+func init() {
+	plugins.RegisterPlugin(pluginName, plugin)
 }
 
 func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) {
 	// The Config field is omitted because this plugin is not configurable.
-	pluginHelp := &pluginhelp.PluginHelp{}
-	pluginHelp.AddCommand(pluginhelp.Command{
-		Usage:       "/(lh-)?(woof|bark|this-is-{fine|not-fine|unbearable})",
-		Description: "Add a dog image to the issue or PR",
-		Featured:    false,
-		WhoCanUse:   "Anyone",
-		Examples:    []string{"/woof", "/bark", "this-is-{fine|not-fine|unbearable}"},
-	})
-	return pluginHelp, nil
+	return &pluginhelp.PluginHelp{}, nil
 }
 
 type scmProviderClient interface {
@@ -136,15 +139,11 @@ func (u realPack) readDog(dogURL string) (string, error) {
 	return FormatURL(dogURL)
 }
 
-func handleGenericComment(pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
-	return handle(pc.SCMProviderClient, pc.Logger, &e, dogURL)
+func handleGenericComment(match []string, pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
+	return handle(match, pc.SCMProviderClient, pc.Logger, &e, dogURL)
 }
 
-func handle(spc scmProviderClient, log *logrus.Entry, e *scmprovider.GenericCommentEvent, p pack) error {
-	// Only consider new comments.
-	if e.Action != scm.ActionCreate {
-		return nil
-	}
+func handle(_ []string, spc scmProviderClient, log *logrus.Entry, e *scmprovider.GenericCommentEvent, p pack) error {
 	// Make sure they are requesting a dog
 	mat := match.FindStringSubmatch(e.Body)
 	url := ""
diff --git a/pkg/plugins/help/help.go b/pkg/plugins/help/help.go
index f1dbb3cc..c9001d3c 100644
--- a/pkg/plugins/help/help.go
+++ b/pkg/plugins/help/help.go
@@ -56,28 +56,33 @@ by commenting with the ` + "`/remove-good-first-issue`" + ` command.
 `
 )
 
-func init() {
-	plugins.RegisterPlugin(
-		pluginName,
-		plugins.Plugin{
-			Description:           "The help plugin provides commands that add or remove the '" + labels.Help + "' and the '" + labels.GoodFirstIssue + "' labels from issues.",
-			HelpProvider:          helpProvider,
+var (
+	plugin = plugins.Plugin{
+		Description:  "The help plugin provides commands that add or remove the '" + labels.Help + "' and the '" + labels.GoodFirstIssue + "' labels from issues.",
+		HelpProvider: helpProvider,
+		Commands: []plugins.Command{{
 			GenericCommentHandler: handleGenericComment,
-		},
-	)
+			Filter: func(e scmprovider.GenericCommentEvent) bool {
+				return !(e.IsPR || e.IssueState != "open" || e.Action != scm.ActionCreate)
+			},
+			Help: []pluginhelp.Command{{
+				Usage:       "/[remove-](help|good-first-issue)",
+				Description: "Applies or removes the '" + labels.Help + "' and '" + labels.GoodFirstIssue + "' labels to an issue.",
+				Featured:    false,
+				WhoCanUse:   "Anyone can trigger this command on a PR.",
+				Examples:    []string{"/help", "/remove-help", "/good-first-issue", "/remove-good-first-issue"},
+			}},
+		}},
+	}
+)
+
+func init() {
+	plugins.RegisterPlugin(pluginName, plugin)
 }
 
 func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) {
 	// The Config field is omitted because this plugin is not configurable.
-	pluginHelp := &pluginhelp.PluginHelp{}
-	pluginHelp.AddCommand(pluginhelp.Command{
-		Usage:       "/[remove-](help|good-first-issue)",
-		Description: "Applies or removes the '" + labels.Help + "' and '" + labels.GoodFirstIssue + "' labels to an issue.",
-		Featured:    false,
-		WhoCanUse:   "Anyone can trigger this command on a PR.",
-		Examples:    []string{"/help", "/remove-help", "/good-first-issue", "/remove-good-first-issue"},
-	})
-	return pluginHelp, nil
+	return &pluginhelp.PluginHelp{}, nil
 }
 
 type scmProviderClient interface {
@@ -93,20 +98,15 @@ type commentPruner interface {
 	PruneComments(pr bool, shouldPrune func(*scm.Comment) bool)
 }
 
-func handleGenericComment(pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
+func handleGenericComment(match []string, pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
 	cp, err := pc.CommentPruner()
 	if err != nil {
 		return err
 	}
-	return handle(pc.SCMProviderClient, pc.Logger, cp, &e)
+	return handle(match, pc.SCMProviderClient, pc.Logger, cp, &e)
 }
 
-func handle(spc scmProviderClient, log *logrus.Entry, cp commentPruner, e *scmprovider.GenericCommentEvent) error {
-	// Only consider open issues and new comments.
-	if e.IsPR || e.IssueState != "open" || e.Action != scm.ActionCreate {
-		return nil
-	}
-
+func handle(_ []string, spc scmProviderClient, log *logrus.Entry, cp commentPruner, e *scmprovider.GenericCommentEvent) error {
 	org := e.Repo.Namespace
 	repo := e.Repo.Name
 	commentAuthor := e.Author.Login
diff --git a/pkg/plugins/hold/hold.go b/pkg/plugins/hold/hold.go
index b0b71a47..ec4242b0 100644
--- a/pkg/plugins/hold/hold.go
+++ b/pkg/plugins/hold/hold.go
@@ -44,28 +44,31 @@ var (
 
 type hasLabelFunc func(label string, issueLabels []*scm.Label) bool
 
-func init() {
-	plugins.RegisterPlugin(
-		pluginName,
-		plugins.Plugin{
-			Description:           "The hold plugin allows anyone to add or remove the '" + labels.Hold + "' Label from a pull request in order to temporarily prevent the PR from merging without withholding approval.",
-			HelpProvider:          helpProvider,
+var (
+	plugin = plugins.Plugin{
+		Description:  "The hold plugin allows anyone to add or remove the '" + labels.Hold + "' Label from a pull request in order to temporarily prevent the PR from merging without withholding approval.",
+		HelpProvider: helpProvider,
+		Commands: []plugins.Command{{
 			GenericCommentHandler: handleGenericComment,
-		},
-	)
+			Filter:                func(e scmprovider.GenericCommentEvent) bool { return e.Action == scm.ActionCreate },
+			Help: []pluginhelp.Command{{
+				Usage:       "/hold [cancel]",
+				Description: "Adds or removes the `" + labels.Hold + "` Label which is used to indicate that the PR should not be automatically merged.",
+				Featured:    false,
+				WhoCanUse:   "Anyone can use the /hold command to add or remove the '" + labels.Hold + "' Label.",
+				Examples:    []string{"/hold", "/hold cancel"},
+			}},
+		}},
+	}
+)
+
+func init() {
+	plugins.RegisterPlugin(pluginName, plugin)
 }
 
 func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) {
 	// The Config field is omitted because this plugin is not configurable.
-	pluginHelp := &pluginhelp.PluginHelp{}
-	pluginHelp.AddCommand(pluginhelp.Command{
-		Usage:       "/hold [cancel]",
-		Description: "Adds or removes the `" + labels.Hold + "` Label which is used to indicate that the PR should not be automatically merged.",
-		Featured:    false,
-		WhoCanUse:   "Anyone can use the /hold command to add or remove the '" + labels.Hold + "' Label.",
-		Examples:    []string{"/hold", "/hold cancel"},
-	})
-	return pluginHelp, nil
+	return &pluginhelp.PluginHelp{}, nil
 }
 
 type scmProviderClient interface {
@@ -74,20 +77,17 @@ type scmProviderClient interface {
 	GetIssueLabels(org, repo string, number int, pr bool) ([]*scm.Label, error)
 }
 
-func handleGenericComment(pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
+func handleGenericComment(match []string, pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
 	hasLabel := func(label string, labels []*scm.Label) bool {
 		return scmprovider.HasLabel(label, labels)
 	}
-	return handle(pc.SCMProviderClient, pc.Logger, &e, hasLabel)
+	return handle(match, pc.SCMProviderClient, pc.Logger, &e, hasLabel)
 }
 
 // handle drives the pull request to the desired state. If any user adds
 // a /hold directive, we want to add a label if one does not already exist.
 // If they add /hold cancel, we want to remove the label if it exists.
-func handle(spc scmProviderClient, log *logrus.Entry, e *scmprovider.GenericCommentEvent, f hasLabelFunc) error {
-	if e.Action != scm.ActionCreate {
-		return nil
-	}
+func handle(_ []string, spc scmProviderClient, log *logrus.Entry, e *scmprovider.GenericCommentEvent, f hasLabelFunc) error {
 	needsLabel := false
 	if labelRe.MatchString(e.Body) {
 		needsLabel = true
diff --git a/pkg/plugins/label/label.go b/pkg/plugins/label/label.go
index 68514073..62986a07 100644
--- a/pkg/plugins/label/label.go
+++ b/pkg/plugins/label/label.go
@@ -40,15 +40,25 @@ var (
 	nonExistentLabelOnIssue = "Those labels are not set on the issue: `%v`"
 )
 
-func init() {
-	plugins.RegisterPlugin(
-		pluginName,
-		plugins.Plugin{
-			Description:           "The label plugin provides commands that add or remove certain types of labels. Labels of the following types can be manipulated: 'area/*', 'committee/*', 'kind/*', 'language/*', 'priority/*', 'sig/*', 'triage/*', and 'wg/*'. More labels can be configured to be used via the /label command.",
-			HelpProvider:          helpProvider,
+var (
+	plugin = plugins.Plugin{
+		Description:  "The label plugin provides commands that add or remove certain types of labels. Labels of the following types can be manipulated: 'area/*', 'committee/*', 'kind/*', 'language/*', 'priority/*', 'sig/*', 'triage/*', and 'wg/*'. More labels can be configured to be used via the /label command.",
+		HelpProvider: helpProvider,
+		Commands: []plugins.Command{{
 			GenericCommentHandler: handleGenericComment,
-		},
-	)
+			Help: []pluginhelp.Command{{
+				Usage:       "/[remove-](area|committee|kind|language|priority|sig|triage|wg|label) <target>",
+				Description: "Applies or removes a label from one of the recognized types of labels.",
+				Featured:    false,
+				WhoCanUse:   "Anyone can trigger this command on a PR.",
+				Examples:    []string{"/kind bug", "/remove-area prow", "/sig testing", "/language zh"},
+			}},
+		}},
+	}
+)
+
+func init() {
+	plugins.RegisterPlugin(pluginName, plugin)
 }
 
 func configString(labels []string) string {
@@ -68,17 +78,10 @@ func helpProvider(config *plugins.Configuration, enabledRepos []string) (*plugin
 			"": configString(labels),
 		},
 	}
-	pluginHelp.AddCommand(pluginhelp.Command{
-		Usage:       "/[remove-](area|committee|kind|language|priority|sig|triage|wg|label) <target>",
-		Description: "Applies or removes a label from one of the recognized types of labels.",
-		Featured:    false,
-		WhoCanUse:   "Anyone can trigger this command on a PR.",
-		Examples:    []string{"/kind bug", "/remove-area prow", "/sig testing", "/language zh"},
-	})
 	return pluginHelp, nil
 }
 
-func handleGenericComment(pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
+func handleGenericComment(_ []string, pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
 	return handle(pc.SCMProviderClient, pc.Logger, pc.PluginConfig.Label.AdditionalLabels, &e)
 }
 
diff --git a/pkg/plugins/lgtm/lgtm.go b/pkg/plugins/lgtm/lgtm.go
index 17c0f6e7..cd038838 100644
--- a/pkg/plugins/lgtm/lgtm.go
+++ b/pkg/plugins/lgtm/lgtm.go
@@ -57,19 +57,33 @@ type commentPruner interface {
 	PruneComments(pr bool, shouldPrune func(*scm.Comment) bool)
 }
 
-func init() {
-	plugins.RegisterPlugin(
-		pluginName,
-		plugins.Plugin{
-			Description:           "The lgtm plugin manages the application and removal of the 'lgtm' (Looks Good To Me) label which is typically used to gate merging.",
-			HelpProvider:          helpProvider,
-			GenericCommentHandler: handleGenericCommentEvent,
-			PullRequestHandler: func(pc plugins.Agent, pe scm.PullRequestHook) error {
-				return handlePullRequestEvent(pc, pe)
-			},
-			ReviewEventHandler: handlePullRequestReviewEvent,
+var (
+	plugin = plugins.Plugin{
+		Description:  "The lgtm plugin manages the application and removal of the 'lgtm' (Looks Good To Me) label which is typically used to gate merging.",
+		HelpProvider: helpProvider,
+		PullRequestHandler: func(pc plugins.Agent, pe scm.PullRequestHook) error {
+			return handlePullRequestEvent(pc, pe)
 		},
-	)
+		ReviewEventHandler: handlePullRequestReviewEvent,
+		Commands: []plugins.Command{{
+			Filter: func(e scmprovider.GenericCommentEvent) bool {
+				return !(!e.IsPR || e.IssueState != "open" || e.Action != scm.ActionCreate)
+			},
+			// No regex, the handler will take care of it
+			GenericCommentHandler: handleGenericCommentEvent,
+			Help: []pluginhelp.Command{{
+				Usage:       "/lgtm [cancel] or GitHub Review action",
+				Description: "Adds or removes the 'lgtm' label which is typically used to gate merging.",
+				Featured:    true,
+				WhoCanUse:   "Collaborators on the repository. '/lgtm cancel' can be used additionally by the PR author.",
+				Examples:    []string{"/lgtm", "/lgtm cancel", "<a href=\"https://help.github.com/articles/about-pull-request-reviews/\">'Approve' or 'Request Changes'</a>"},
+			}},
+		}},
+	}
+)
+
+func init() {
+	plugins.RegisterPlugin(pluginName, plugin)
 }
 
 func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) {
@@ -106,13 +120,6 @@ func helpProvider(config *plugins.Configuration, enabledRepos []string) (*plugin
 		}
 	}
 	pluginHelp := &pluginhelp.PluginHelp{Config: configInfo}
-	pluginHelp.AddCommand(pluginhelp.Command{
-		Usage:       "/lgtm [cancel] or GitHub Review action",
-		Description: "Adds or removes the 'lgtm' label which is typically used to gate merging.",
-		Featured:    true,
-		WhoCanUse:   "Collaborators on the repository. '/lgtm cancel' can be used additionally by the PR author.",
-		Examples:    []string{"/lgtm", "/lgtm cancel", "<a href=\"https://help.github.com/articles/about-pull-request-reviews/\">'Approve' or 'Request Changes'</a>"},
-	})
 	return pluginHelp, nil
 }
 
@@ -162,7 +169,7 @@ type reviewCtx struct {
 	number                             int
 }
 
-func handleGenericCommentEvent(pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
+func handleGenericCommentEvent(_ []string, pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
 	cp, err := pc.CommentPruner()
 	if err != nil {
 		return err
@@ -203,11 +210,6 @@ func handleGenericComment(spc scmProviderClient, config *plugins.Configuration,
 		number:      e.Number,
 	}
 
-	// Only consider open PRs and new comments.
-	if !e.IsPR || e.IssueState != "open" || e.Action != scm.ActionCreate {
-		return nil
-	}
-
 	// If we create an "/lgtm" comment, add lgtm if necessary.
 	// If we create a "/lgtm cancel" comment, remove lgtm if necessary.
 	wantLGTM := false
diff --git a/pkg/plugins/lifecycle/lifecycle.go b/pkg/plugins/lifecycle/lifecycle.go
index 9a833775..2c012282 100644
--- a/pkg/plugins/lifecycle/lifecycle.go
+++ b/pkg/plugins/lifecycle/lifecycle.go
@@ -35,41 +35,42 @@ var (
 
 const pluginName = "lifecycle"
 
-func init() {
-	plugins.RegisterPlugin(
-		pluginName,
-		plugins.Plugin{
-			Description:           "Close, reopen, flag and/or unflag an issue or PR as frozen/stale/rotten",
-			HelpProvider:          help,
+var (
+	plugin = plugins.Plugin{
+		Description:  "Close, reopen, flag and/or unflag an issue or PR as frozen/stale/rotten",
+		HelpProvider: help,
+		Commands: []plugins.Command{{
+			Filter:                func(e scmprovider.GenericCommentEvent) bool { return e.Action == scm.ActionCreate },
 			GenericCommentHandler: lifecycleHandleGenericComment,
-		},
-	)
+			Help: []pluginhelp.Command{{
+				Usage:       "/close",
+				Description: "Closes an issue or PR.",
+				Featured:    false,
+				WhoCanUse:   "Authors and collaborators on the repository can trigger this command.",
+				Examples:    []string{"/close", "/lh-close"},
+			}, {
+				Usage:       "/reopen",
+				Description: "Reopens an issue or PR",
+				Featured:    false,
+				WhoCanUse:   "Authors and collaborators on the repository can trigger this command.",
+				Examples:    []string{"/reopen", "/lh-reopen"},
+			}, {
+				Usage:       "/[remove-]lifecycle <frozen|stale|rotten>",
+				Description: "Flags an issue or PR as frozen/stale/rotten",
+				Featured:    false,
+				WhoCanUse:   "Anyone can trigger this command.",
+				Examples:    []string{"/lifecycle frozen", "/remove-lifecycle stale", "/lh-lifecyle rotten"},
+			}},
+		}},
+	}
+)
+
+func init() {
+	plugins.RegisterPlugin(pluginName, plugin)
 }
 
 func help(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) {
-	pluginHelp := &pluginhelp.PluginHelp{}
-	pluginHelp.AddCommand(pluginhelp.Command{
-		Usage:       "/close",
-		Description: "Closes an issue or PR.",
-		Featured:    false,
-		WhoCanUse:   "Authors and collaborators on the repository can trigger this command.",
-		Examples:    []string{"/close", "/lh-close"},
-	})
-	pluginHelp.AddCommand(pluginhelp.Command{
-		Usage:       "/reopen",
-		Description: "Reopens an issue or PR",
-		Featured:    false,
-		WhoCanUse:   "Authors and collaborators on the repository can trigger this command.",
-		Examples:    []string{"/reopen", "/lh-reopen"},
-	})
-	pluginHelp.AddCommand(pluginhelp.Command{
-		Usage:       "/[remove-]lifecycle <frozen|stale|rotten>",
-		Description: "Flags an issue or PR as frozen/stale/rotten",
-		Featured:    false,
-		WhoCanUse:   "Anyone can trigger this command.",
-		Examples:    []string{"/lifecycle frozen", "/remove-lifecycle stale", "/lh-lifecyle rotten"},
-	})
-	return pluginHelp, nil
+	return &pluginhelp.PluginHelp{}, nil
 }
 
 type lifecycleClient interface {
@@ -78,7 +79,7 @@ type lifecycleClient interface {
 	GetIssueLabels(org, repo string, number int, pr bool) ([]*scm.Label, error)
 }
 
-func lifecycleHandleGenericComment(pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
+func lifecycleHandleGenericComment(_ []string, pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
 	gc := pc.SCMProviderClient
 	log := pc.Logger
 	if err := handleReopen(gc, log, &e); err != nil {
@@ -91,11 +92,6 @@ func lifecycleHandleGenericComment(pc plugins.Agent, e scmprovider.GenericCommen
 }
 
 func handle(gc lifecycleClient, log *logrus.Entry, e *scmprovider.GenericCommentEvent) error {
-	// Only consider new comments.
-	if e.Action != scm.ActionCreate {
-		return nil
-	}
-
 	for _, mat := range lifecycleRe.FindAllStringSubmatch(e.Body, -1) {
 		if err := handleOne(gc, log, e, mat); err != nil {
 			return err
diff --git a/pkg/plugins/milestone/milestone.go b/pkg/plugins/milestone/milestone.go
index 69dd1688..1b1e4890 100644
--- a/pkg/plugins/milestone/milestone.go
+++ b/pkg/plugins/milestone/milestone.go
@@ -51,15 +51,27 @@ type scmProviderClient interface {
 	QuoteAuthorForComment(string) string
 }
 
-func init() {
-	plugins.RegisterPlugin(
-		pluginName,
-		plugins.Plugin{
-			Description:           "The milestone plugin allows members of a configurable GitHub team to set the milestone on an issue or pull request.",
-			HelpProvider:          helpProvider,
+var (
+	plugin = plugins.Plugin{
+		Description:  "The milestone plugin allows members of a configurable GitHub team to set the milestone on an issue or pull request.",
+		HelpProvider: helpProvider,
+		Commands: []plugins.Command{{
 			GenericCommentHandler: handleGenericComment,
-		},
-	)
+			Filter:                func(e scmprovider.GenericCommentEvent) bool { return e.Action == scm.ActionCreate },
+			Regex:                 milestoneRegex,
+			Help: []pluginhelp.Command{{
+				Usage:       "/milestone <version> or /milestone clear",
+				Description: "Updates the milestone for an issue or PR",
+				Featured:    false,
+				WhoCanUse:   "Members of the milestone maintainers GitHub team can use the '/milestone' command.",
+				Examples:    []string{"/milestone v1.10", "/milestone v1.9", "/milestone clear", "/lh-milestone clear"},
+			}},
+		}},
+	}
+)
+
+func init() {
+	plugins.RegisterPlugin(pluginName, plugin)
 }
 
 func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) {
@@ -80,18 +92,11 @@ func helpProvider(config *plugins.Configuration, enabledRepos []string) (*plugin
 			return configMap
 		}(enabledRepos),
 	}
-	pluginHelp.AddCommand(pluginhelp.Command{
-		Usage:       "/milestone <version> or /milestone clear",
-		Description: "Updates the milestone for an issue or PR",
-		Featured:    false,
-		WhoCanUse:   "Members of the milestone maintainers GitHub team can use the '/milestone' command.",
-		Examples:    []string{"/milestone v1.10", "/milestone v1.9", "/milestone clear", "/lh-milestone clear"},
-	})
 	return pluginHelp, nil
 }
 
-func handleGenericComment(pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
-	return handle(pc.SCMProviderClient, pc.Logger, &e, pc.PluginConfig.RepoMilestone)
+func handleGenericComment(match []string, pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
+	return handle(match, pc.SCMProviderClient, pc.Logger, &e, pc.PluginConfig.RepoMilestone)
 }
 
 func buildMilestoneMap(milestones []scmprovider.Milestone) map[string]int {
@@ -101,16 +106,8 @@ func buildMilestoneMap(milestones []scmprovider.Milestone) map[string]int {
 	}
 	return m
 }
-func handle(spc scmProviderClient, log *logrus.Entry, e *scmprovider.GenericCommentEvent, repoMilestone map[string]plugins.Milestone) error {
-	if e.Action != scm.ActionCreate {
-		return nil
-	}
-
-	milestoneMatch := milestoneRegex.FindStringSubmatch(e.Body)
-	if len(milestoneMatch) != 2 {
-		return nil
-	}
 
+func handle(match []string, spc scmProviderClient, log *logrus.Entry, e *scmprovider.GenericCommentEvent, repoMilestone map[string]plugins.Milestone) error {
 	org := e.Repo.Namespace
 	repo := e.Repo.Name
 
@@ -143,7 +140,7 @@ func handle(spc scmProviderClient, log *logrus.Entry, e *scmprovider.GenericComm
 		log.WithError(err).Errorf("Error listing the milestones in the %s/%s repo", org, repo)
 		return err
 	}
-	proposedMilestone := milestoneMatch[1]
+	proposedMilestone := match[1]
 
 	// special case, if the clear keyword is used
 	if proposedMilestone == clearKeyword {
diff --git a/pkg/plugins/milestonestatus/milestonestatus.go b/pkg/plugins/milestonestatus/milestonestatus.go
index 39a65c68..1df6847d 100644
--- a/pkg/plugins/milestonestatus/milestonestatus.go
+++ b/pkg/plugins/milestonestatus/milestonestatus.go
@@ -44,6 +44,25 @@ var (
 	}
 )
 
+var (
+	plugin = plugins.Plugin{
+		Description:  "The milestonestatus plugin allows members of the milestone maintainers GitHub team to specify the 'status/*' label that should apply to a pull request.",
+		HelpProvider: helpProvider,
+		Commands: []plugins.Command{{
+			GenericCommentHandler: handleGenericComment,
+			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.",
+				Featured:    false,
+				WhoCanUse:   "Members of the milestone maintainers GitHub team can use the '/status' command. This team is specified in the config by providing the GitHub team's ID.",
+				Examples:    []string{"/status approved-for-milestone", "/status in-progress", "/status in-review", "/lh-status in-review"},
+			}},
+		}},
+	}
+)
+
 type scmProviderClient interface {
 	CreateComment(owner, repo string, number int, pr bool, comment string) error
 	AddLabel(owner, repo string, number int, label string, pr bool) error
@@ -51,14 +70,7 @@ type scmProviderClient interface {
 }
 
 func init() {
-	plugins.RegisterPlugin(
-		pluginName,
-		plugins.Plugin{
-			Description:           "The milestonestatus plugin allows members of the milestone maintainers GitHub team to specify the 'status/*' label that should apply to a pull request.",
-			HelpProvider:          helpProvider,
-			GenericCommentHandler: handleGenericComment,
-		},
-	)
+	plugins.RegisterPlugin(pluginName, plugin)
 }
 
 func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) {
@@ -79,30 +91,14 @@ func helpProvider(config *plugins.Configuration, enabledRepos []string) (*plugin
 			return configMap
 		}(),
 	}
-	pluginHelp.AddCommand(pluginhelp.Command{
-		Usage:       "/status (approved-for-milestone|in-progress|in-review)",
-		Description: "Applies the 'status/' label to a PR.",
-		Featured:    false,
-		WhoCanUse:   "Members of the milestone maintainers GitHub team can use the '/status' command. This team is specified in the config by providing the GitHub team's ID.",
-		Examples:    []string{"/status approved-for-milestone", "/status in-progress", "/status in-review", "/lh-status in-review"},
-	})
 	return pluginHelp, nil
 }
 
-func handleGenericComment(pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
-	return handle(pc.SCMProviderClient, pc.Logger, &e, pc.PluginConfig.RepoMilestone)
+func handleGenericComment(match []string, pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
+	return handle(match, pc.SCMProviderClient, pc.Logger, &e, pc.PluginConfig.RepoMilestone)
 }
 
-func handle(spc scmProviderClient, log *logrus.Entry, e *scmprovider.GenericCommentEvent, repoMilestone map[string]plugins.Milestone) error {
-	if e.Action != scm.ActionCreate {
-		return nil
-	}
-
-	statusMatches := statusRegex.FindAllStringSubmatch(e.Body, -1)
-	if len(statusMatches) == 0 {
-		return nil
-	}
-
+func handle(match []string, spc scmProviderClient, log *logrus.Entry, e *scmprovider.GenericCommentEvent, repoMilestone map[string]plugins.Milestone) error {
 	org := e.Repo.Namespace
 	repo := e.Repo.Name
 
@@ -130,14 +126,12 @@ func handle(spc scmProviderClient, log *logrus.Entry, e *scmprovider.GenericComm
 		return spc.CreateComment(org, repo, e.Number, e.IsPR, msg)
 	}
 
-	for _, statusMatch := range statusMatches {
-		sLabel, validStatus := statusMap[strings.TrimSpace(statusMatch[1])]
-		if !validStatus {
-			continue
-		}
+	sLabel, validStatus := statusMap[strings.TrimSpace(match[1])]
+	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)
 		}
 	}
+
 	return nil
 }
diff --git a/pkg/plugins/override/override.go b/pkg/plugins/override/override.go
index c4c77deb..d5ed23e3 100644
--- a/pkg/plugins/override/override.go
+++ b/pkg/plugins/override/override.go
@@ -27,14 +27,13 @@ import (
 	lighthouseclient "github.com/jenkins-x/lighthouse/pkg/client/clientset/versioned/typed/lighthouse/v1alpha1"
 	"github.com/jenkins-x/lighthouse/pkg/config/job"
 	"github.com/jenkins-x/lighthouse/pkg/jobutil"
+	"github.com/jenkins-x/lighthouse/pkg/pluginhelp"
+	"github.com/jenkins-x/lighthouse/pkg/plugins"
 	"github.com/jenkins-x/lighthouse/pkg/scmprovider"
 	"github.com/jenkins-x/lighthouse/pkg/util"
 	"github.com/sirupsen/logrus"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/sets"
-
-	"github.com/jenkins-x/lighthouse/pkg/pluginhelp"
-	"github.com/jenkins-x/lighthouse/pkg/plugins"
 )
 
 const pluginName = "override"
@@ -126,30 +125,35 @@ func (c client) presubmitForContext(org, repo, context string) *job.Presubmit {
 	return nil
 }
 
-func init() {
-	plugins.RegisterPlugin(
-		pluginName,
-		plugins.Plugin{
-			Description:           "The override plugin allows repo admins to force a github status context to pass",
-			HelpProvider:          helpProvider,
+var (
+	plugin = plugins.Plugin{
+		Description:  "The override plugin allows repo admins to force a github status context to pass",
+		HelpProvider: helpProvider,
+		Commands: []plugins.Command{{
 			GenericCommentHandler: handleGenericComment,
-		},
-	)
+			Filter: func(e scmprovider.GenericCommentEvent) bool {
+				return !(!e.IsPR || e.IssueState != "open" || e.Action != scm.ActionCreate)
+			},
+			Help: []pluginhelp.Command{{
+				Usage:       "/override [context]",
+				Description: "Forces a github status context to green (one per line).",
+				Featured:    false,
+				WhoCanUse:   "Repo administrators",
+				Examples:    []string{"/override pull-repo-whatever", "/override ci/circleci", "/override deleted-job", "/lh-override some-job"},
+			}},
+		}},
+	}
+)
+
+func init() {
+	plugins.RegisterPlugin(pluginName, plugin)
 }
 
 func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) {
-	pluginHelp := &pluginhelp.PluginHelp{}
-	pluginHelp.AddCommand(pluginhelp.Command{
-		Usage:       "/override [context]",
-		Description: "Forces a github status context to green (one per line).",
-		Featured:    false,
-		WhoCanUse:   "Repo administrators",
-		Examples:    []string{"/override pull-repo-whatever", "/override ci/circleci", "/override deleted-job", "/lh-override some-job"},
-	})
-	return pluginHelp, nil
+	return &pluginhelp.PluginHelp{}, nil
 }
 
-func handleGenericComment(pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
+func handleGenericComment(_ []string, pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
 	c := client{
 		spc:      pc.SCMProviderClient,
 		lhClient: pc.LighthouseClient,
@@ -189,11 +193,6 @@ func formatList(list []string) string {
 }
 
 func handle(oc overrideClient, log *logrus.Entry, e *scmprovider.GenericCommentEvent) error {
-
-	if !e.IsPR || e.IssueState != "open" || e.Action != scm.ActionCreate {
-		return nil
-	}
-
 	mat := overrideRe.FindAllStringSubmatch(e.Body, -1)
 	if len(mat) == 0 {
 		return nil // no /override commands given in the comment
diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go
index c504d889..38e2af45 100644
--- a/pkg/plugins/plugins.go
+++ b/pkg/plugins/plugins.go
@@ -22,6 +22,7 @@ import (
 	"io/ioutil"
 	"net/url"
 	"os"
+	"regexp"
 	"strings"
 	"sync"
 
@@ -29,7 +30,7 @@ import (
 	lighthouseclient "github.com/jenkins-x/lighthouse/pkg/client/clientset/versioned/typed/lighthouse/v1alpha1"
 	"github.com/jenkins-x/lighthouse/pkg/commentpruner"
 	"github.com/jenkins-x/lighthouse/pkg/config"
-	git2 "github.com/jenkins-x/lighthouse/pkg/git"
+	"github.com/jenkins-x/lighthouse/pkg/git"
 	"github.com/jenkins-x/lighthouse/pkg/launcher"
 	"github.com/jenkins-x/lighthouse/pkg/pluginhelp"
 	"github.com/jenkins-x/lighthouse/pkg/repoowners"
@@ -41,29 +42,58 @@ import (
 
 var (
 	plugins = map[string]Plugin{}
-	// pluginHelp                 = map[string]HelpProvider{}
-	// genericCommentHandlers     = map[string]GenericCommentHandler{}
-	// issueHandlers              = map[string]IssueHandler{}
-	// issueCommentHandlers       = map[string]IssueCommentHandler{}
-	// pullRequestHandlers        = map[string]PullRequestHandler{}
-	// pushEventHandlers          = map[string]PushEventHandler{}
-	// reviewEventHandlers        = map[string]ReviewEventHandler{}
-	// reviewCommentEventHandlers = map[string]ReviewCommentEventHandler{}
-	// statusEventHandlers        = map[string]StatusEventHandler{}
 )
 
+// Command defines a plugin command sent through a comment
+type Command struct {
+	Help                  []pluginhelp.Command
+	Regex                 *regexp.Regexp
+	MaxMatches            int
+	Filter                func(e scmprovider.GenericCommentEvent) bool
+	GenericCommentHandler GenericCommentHandler
+}
+
+// InvokeHandler performs command checks (filter, then regex if any) the calls the handler wiith the match (if any)
+func (cmd Command) InvokeHandler(ce *scmprovider.GenericCommentEvent, handler func([]string) error) error {
+	if cmd.GenericCommentHandler == nil || (cmd.Filter != nil && !cmd.Filter(*ce)) {
+		return nil
+	}
+	if cmd.Regex != nil {
+		max := cmd.MaxMatches
+		if max == 0 {
+			max = -1
+		}
+		for _, m := range cmd.Regex.FindAllStringSubmatch(ce.Body, max) {
+			if err := handler(m); err != nil {
+				return err
+			}
+		}
+	} else {
+		return handler(nil)
+	}
+	return nil
+}
+
 // Plugin defines a plugin and its handlers
 type Plugin struct {
-	Description               string
-	HelpProvider              HelpProvider
-	GenericCommentHandler     GenericCommentHandler
-	IssueHandler              IssueHandler
-	IssueCommentHandler       IssueCommentHandler
-	PullRequestHandler        PullRequestHandler
-	PushEventHandler          PushEventHandler
-	ReviewEventHandler        ReviewEventHandler
-	ReviewCommentEventHandler ReviewCommentEventHandler
-	StatusEventHandler        StatusEventHandler
+	Description        string
+	HelpProvider       HelpProvider
+	IssueHandler       IssueHandler
+	PullRequestHandler PullRequestHandler
+	PushEventHandler   PushEventHandler
+	ReviewEventHandler ReviewEventHandler
+	StatusEventHandler StatusEventHandler
+	Commands           []Command
+}
+
+// InvokeCommand calls InvokeHandler on all commands
+func (plugin Plugin) InvokeCommand(ce *scmprovider.GenericCommentEvent, handler func([]string) error) error {
+	for _, cmd := range plugin.Commands {
+		if err := cmd.InvokeHandler(ce, handler); err != nil {
+			return err
+		}
+	}
+	return nil
 }
 
 // RegisterPlugin registers a plugin.
@@ -97,7 +127,7 @@ type ReviewEventHandler func(Agent, scm.ReviewHook) error
 type ReviewCommentEventHandler func(Agent, scm.PullRequestCommentHook) error
 
 // GenericCommentHandler defines the function contract for a scm.Comment handler.
-type GenericCommentHandler func(Agent, scmprovider.GenericCommentEvent) error
+type GenericCommentHandler func([]string, Agent, scmprovider.GenericCommentEvent) error
 
 // HelpProviders returns the map of registered plugins with their associated HelpProvider.
 func HelpProviders() map[string]HelpProvider {
@@ -109,6 +139,11 @@ func HelpProviders() map[string]HelpProvider {
 				if h != nil {
 					h.Description = v.Description
 				}
+				for _, c := range v.Commands {
+					for _, hh := range c.Help {
+						h.AddCommand(hh)
+					}
+				}
 				return h, err
 			}
 		}
@@ -120,7 +155,7 @@ func HelpProviders() map[string]HelpProvider {
 type Agent struct {
 	SCMProviderClient *scmprovider.Client
 	LauncherClient    launcher.PipelineLauncher
-	GitClient         git2.Client
+	GitClient         git.Client
 	KubernetesClient  kubernetes.Interface
 	LighthouseClient  lighthouseclient.LighthouseJobInterface
 	ServerURL         *url.URL
@@ -193,7 +228,7 @@ type ClientAgent struct {
 	SCMProviderClient *scm.Client
 
 	KubernetesClient kubernetes.Interface
-	GitClient        git2.Client
+	GitClient        git.Client
 	LauncherClient   launcher.PipelineLauncher
 	LighthouseClient lighthouseclient.LighthouseJobInterface
 
@@ -332,9 +367,6 @@ func EventsForPlugin(name string) []string {
 	if p, ok := plugins[name]; ok && p.IssueHandler != nil {
 		events = append(events, "issue")
 	}
-	if p, ok := plugins[name]; ok && p.IssueCommentHandler != nil {
-		events = append(events, "issue_comment")
-	}
 	if p, ok := plugins[name]; ok && p.PullRequestHandler != nil {
 		events = append(events, "pull_request")
 	}
@@ -344,14 +376,8 @@ func EventsForPlugin(name string) []string {
 	if p, ok := plugins[name]; ok && p.ReviewEventHandler != nil {
 		events = append(events, "pull_request_review")
 	}
-	if p, ok := plugins[name]; ok && p.ReviewCommentEventHandler != nil {
-		events = append(events, "pull_request_review_comment")
-	}
 	if p, ok := plugins[name]; ok && p.StatusEventHandler != nil {
 		events = append(events, "status")
 	}
-	if p, ok := plugins[name]; ok && p.GenericCommentHandler != nil {
-		events = append(events, "GenericCommentEvent (any event for user text)")
-	}
 	return events
 }
diff --git a/pkg/plugins/pony/pony.go b/pkg/plugins/pony/pony.go
index 6c552006..6d46f2db 100644
--- a/pkg/plugins/pony/pony.go
+++ b/pkg/plugins/pony/pony.go
@@ -55,28 +55,31 @@ var (
 	match = regexp.MustCompile(`(?mi)^/(?:lh-)?(?:pony)(?: +(.+?))?\s*$`)
 )
 
-func init() {
-	plugins.RegisterPlugin(
-		pluginName,
-		plugins.Plugin{
-			Description:           "The pony plugin adds a pony image to an issue or PR in response to the `/pony` command.",
-			HelpProvider:          helpProvider,
+var (
+	plugin = plugins.Plugin{
+		Description:  "The pony plugin adds a pony image to an issue or PR in response to the `/pony` command.",
+		HelpProvider: helpProvider,
+		Commands: []plugins.Command{{
 			GenericCommentHandler: handleGenericComment,
-		},
-	)
+			Filter:                func(e scmprovider.GenericCommentEvent) bool { return e.Action == scm.ActionCreate },
+			Help: []pluginhelp.Command{{
+				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",
+				Examples:    []string{"/pony", "/pony Twilight Sparkle", "/lh-pony"},
+			}},
+		}},
+	}
+)
+
+func init() {
+	plugins.RegisterPlugin(pluginName, plugin)
 }
 
 func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) {
 	// The Config field is omitted because this plugin is not configurable.
-	pluginHelp := &pluginhelp.PluginHelp{}
-	pluginHelp.AddCommand(pluginhelp.Command{
-		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",
-		Examples:    []string{"/pony", "/pony Twilight Sparkle", "/lh-pony"},
-	})
-	return pluginHelp, nil
+	return &pluginhelp.PluginHelp{}, nil
 }
 
 var client = http.Client{}
@@ -122,15 +125,11 @@ func (h realHerd) readPony(tags string) (string, error) {
 	return formatURLs(a.Pony.Representations.Small, a.Pony.Representations.Full), nil
 }
 
-func handleGenericComment(pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
+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 {
-	// Only consider new comments.
-	if e.Action != scm.ActionCreate {
-		return nil
-	}
 	// Make sure they are requesting a pony
 	mat := match.FindStringSubmatch(e.Body)
 	if mat == nil {
diff --git a/pkg/plugins/shrug/shrug.go b/pkg/plugins/shrug/shrug.go
index 85dbded6..c28fc6ca 100644
--- a/pkg/plugins/shrug/shrug.go
+++ b/pkg/plugins/shrug/shrug.go
@@ -48,28 +48,31 @@ type event struct {
 	htmlurl       string
 }
 
-func init() {
-	plugins.RegisterPlugin(
-		pluginName,
-		plugins.Plugin{
-			Description:           labels.Shrug,
-			HelpProvider:          helpProvider,
+var (
+	plugin = plugins.Plugin{
+		Description:  labels.Shrug,
+		HelpProvider: helpProvider,
+		Commands: []plugins.Command{{
 			GenericCommentHandler: handleGenericComment,
-		},
-	)
+			Filter:                func(e scmprovider.GenericCommentEvent) bool { return e.Action == scm.ActionCreate },
+			Help: []pluginhelp.Command{{
+				Usage:       "/[un]shrug",
+				Description: labels.Shrug,
+				Featured:    false,
+				WhoCanUse:   "Anyone, " + labels.Shrug,
+				Examples:    []string{"/shrug", "/unshrug"},
+			}},
+		}},
+	}
+)
+
+func init() {
+	plugins.RegisterPlugin(pluginName, plugin)
 }
 
 func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) {
 	// The Config field is omitted because this plugin is not configurable.
-	pluginHelp := &pluginhelp.PluginHelp{}
-	pluginHelp.AddCommand(pluginhelp.Command{
-		Usage:       "/[un]shrug",
-		Description: labels.Shrug,
-		Featured:    false,
-		WhoCanUse:   "Anyone, " + labels.Shrug,
-		Examples:    []string{"/shrug", "/unshrug"},
-	})
-	return pluginHelp, nil
+	return &pluginhelp.PluginHelp{}, nil
 }
 
 type scmProviderClient interface {
@@ -80,15 +83,11 @@ type scmProviderClient interface {
 	QuoteAuthorForComment(string) string
 }
 
-func handleGenericComment(pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
+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 {
-	if e.Action != scm.ActionCreate {
-		return nil
-	}
-
 	wantShrug := false
 	if shrugRe.MatchString(e.Body) {
 		wantShrug = true
diff --git a/pkg/plugins/sigmention/sigmention.go b/pkg/plugins/sigmention/sigmention.go
index 95a194c8..22b145d2 100644
--- a/pkg/plugins/sigmention/sigmention.go
+++ b/pkg/plugins/sigmention/sigmention.go
@@ -56,15 +56,27 @@ type scmProviderClient interface {
 	QuoteAuthorForComment(string) string
 }
 
+var (
+	sigmentionCommand = plugins.Command{
+		// TODO help
+		Filter:                func(e scmprovider.GenericCommentEvent) bool { return e.Action == scm.ActionCreate },
+		GenericCommentHandler: handleGenericComment,
+	}
+)
+
 func init() {
+	description := `The sigmention plugin responds to SIG (Special Interest Group) GitHub team mentions like '@kubernetes/sig-testing-bugs'. The plugin responds in two ways:
+<ol><li> The appropriate 'sig/*' and 'kind/*' labels are applied to the issue or pull request. In this case 'sig/testing' and 'kind/bug'.</li>
+<li> If the user who mentioned the GitHub team is not a member of the organization that owns the repository the bot will create a comment that repeats the mention. This is necessary because non-member mentions do not trigger GitHub notifications.</li></ol>`
+
 	plugins.RegisterPlugin(
 		pluginName,
 		plugins.Plugin{
-			Description: `The sigmention plugin responds to SIG (Special Interest Group) GitHub team mentions like '@kubernetes/sig-testing-bugs'. The plugin responds in two ways:
-<ol><li> The appropriate 'sig/*' and 'kind/*' labels are applied to the issue or pull request. In this case 'sig/testing' and 'kind/bug'.</li>
-<li> If the user who mentioned the GitHub team is not a member of the organization that owns the repository the bot will create a comment that repeats the mention. This is necessary because non-member mentions do not trigger GitHub notifications.</li></ol>`,
-			HelpProvider:          helpProvider,
-			GenericCommentHandler: handleGenericComment,
+			Description:  description,
+			HelpProvider: helpProvider,
+			Commands: []plugins.Command{
+				sigmentionCommand,
+			},
 		},
 	)
 }
@@ -78,7 +90,7 @@ func helpProvider(config *plugins.Configuration, enabledRepos []string) (*plugin
 		nil
 }
 
-func handleGenericComment(pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
+func handleGenericComment(_ []string, pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
 	return handle(pc.SCMProviderClient, pc.Logger, &e, pc.PluginConfig.SigMention.Re)
 }
 
@@ -91,9 +103,6 @@ func handle(spc scmProviderClient, log *logrus.Entry, e *scmprovider.GenericComm
 	if e.Author.Login == botName {
 		return nil
 	}
-	if e.Action != scm.ActionCreate {
-		return nil
-	}
 
 	sigMatches := re.FindAllStringSubmatch(e.Body, -1)
 	if len(sigMatches) == 0 {
diff --git a/pkg/plugins/skip/skip.go b/pkg/plugins/skip/skip.go
index 6de7d1a9..3f486a2e 100644
--- a/pkg/plugins/skip/skip.go
+++ b/pkg/plugins/skip/skip.go
@@ -46,43 +46,41 @@ type scmProviderClient interface {
 	QuoteAuthorForComment(string) string
 }
 
-func init() {
-	plugins.RegisterPlugin(
-		pluginName,
-		plugins.Plugin{
-			Description:           "The skip plugin allows users to clean up GitHub stale commit statuses for non-blocking jobs on a PR.",
-			HelpProvider:          helpProvider,
+var (
+	plugin = plugins.Plugin{
+		Description:  "The skip plugin allows users to clean up GitHub stale commit statuses for non-blocking jobs on a PR.",
+		HelpProvider: helpProvider,
+		Commands: []plugins.Command{{
 			GenericCommentHandler: handleGenericComment,
-		},
-	)
+			Filter: func(e scmprovider.GenericCommentEvent) bool {
+				return !(!e.IsPR || e.IssueState != "open" || e.Action != scm.ActionCreate)
+			},
+			Regex: skipRe,
+			Help: []pluginhelp.Command{{
+				Usage:       "/skip",
+				Description: "Cleans up GitHub stale commit statuses for non-blocking jobs on a PR.",
+				Featured:    false,
+				WhoCanUse:   "Anyone can trigger this command on a PR.",
+				Examples:    []string{"/skip", "/lh-skip"},
+			}},
+		}},
+	}
+)
+
+func init() {
+	plugins.RegisterPlugin(pluginName, plugin)
 }
 
 func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) {
-	pluginHelp := &pluginhelp.PluginHelp{}
-	pluginHelp.AddCommand(pluginhelp.Command{
-		Usage:       "/skip",
-		Description: "Cleans up GitHub stale commit statuses for non-blocking jobs on a PR.",
-		Featured:    false,
-		WhoCanUse:   "Anyone can trigger this command on a PR.",
-		Examples:    []string{"/skip", "/lh-skip"},
-	})
-	return pluginHelp, nil
+	return &pluginhelp.PluginHelp{}, nil
 }
 
-func handleGenericComment(pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
+func handleGenericComment(_ []string, pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
 	honorOkToTest := trigger.HonorOkToTest(pc.PluginConfig.TriggerFor(e.Repo.Namespace, e.Repo.Name))
 	return handle(pc.SCMProviderClient, pc.Logger, &e, pc.Config.GetPresubmits(e.Repo), honorOkToTest)
 }
 
 func handle(spc scmProviderClient, log *logrus.Entry, e *scmprovider.GenericCommentEvent, presubmits []job.Presubmit, honorOkToTest bool) error {
-	if !e.IsPR || e.IssueState != "open" || e.Action != scm.ActionCreate {
-		return nil
-	}
-
-	if !skipRe.MatchString(e.Body) {
-		return nil
-	}
-
 	org := e.Repo.Namespace
 	repo := e.Repo.Name
 	number := e.Number
diff --git a/pkg/plugins/stage/stage.go b/pkg/plugins/stage/stage.go
index a4fdd26e..b25e5329 100644
--- a/pkg/plugins/stage/stage.go
+++ b/pkg/plugins/stage/stage.go
@@ -39,28 +39,32 @@ var (
 
 const pluginName = "stage"
 
-func init() {
-	plugins.RegisterPlugin(
-		pluginName,
-		plugins.Plugin{
-			Description:           "Label the stage of an issue as alpha/beta/stable",
-			HelpProvider:          help,
+var (
+	plugin = plugins.Plugin{
+		Description:  "Label the stage of an issue as alpha/beta/stable",
+		HelpProvider: help,
+		Commands: []plugins.Command{{
+			Filter:                func(e scmprovider.GenericCommentEvent) bool { return e.Action == scm.ActionCreate },
+			Regex:                 stageRe,
 			GenericCommentHandler: stageHandleGenericComment,
-		},
-	)
+			Help: []pluginhelp.Command{{
+				Usage:       "/[remove-]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"},
+			}},
+		}},
+	}
+)
+
+func init() {
+	plugins.RegisterPlugin(pluginName, plugin)
 }
 
 func help(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) {
 	// The Config field is omitted because this plugin is not configurable.
-	pluginHelp := &pluginhelp.PluginHelp{}
-	pluginHelp.AddCommand(pluginhelp.Command{
-		Usage:       "/[remove-]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"},
-	})
-	return pluginHelp, nil
+	return &pluginhelp.PluginHelp{}, nil
 }
 
 type stageClient interface {
@@ -69,22 +73,12 @@ type stageClient interface {
 	GetIssueLabels(org, repo string, number int, pr bool) ([]*scm.Label, error)
 }
 
-func stageHandleGenericComment(pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
-	return handle(pc.SCMProviderClient, pc.Logger, &e)
+func stageHandleGenericComment(match []string, pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
+	return handle(match, pc.SCMProviderClient, pc.Logger, &e)
 }
 
-func handle(gc stageClient, log *logrus.Entry, e *scmprovider.GenericCommentEvent) error {
-	// Only consider new comments.
-	if e.Action != scm.ActionCreate {
-		return nil
-	}
-
-	for _, mat := range stageRe.FindAllStringSubmatch(e.Body, -1) {
-		if err := handleOne(gc, log, e, mat); err != nil {
-			return err
-		}
-	}
-	return nil
+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 {
diff --git a/pkg/plugins/trigger/generic-comment.go b/pkg/plugins/trigger/generic-comment.go
index cd6cb57a..00d77183 100644
--- a/pkg/plugins/trigger/generic-comment.go
+++ b/pkg/plugins/trigger/generic-comment.go
@@ -35,12 +35,7 @@ func handleGenericComment(c Client, trigger *plugins.Trigger, gc scmprovider.Gen
 	repo := gc.Repo.Name
 	number := gc.Number
 	commentAuthor := gc.Author.Login
-	// Only take action when a comment is first created,
-	// when it belongs to a PR,
-	// and the PR is open.
-	if gc.Action != scm.ActionCreate || !gc.IsPR || gc.IssueState != "open" {
-		return nil
-	}
+
 	// Skip comments not germane to this plugin
 	if !jobutil.RetestRe.MatchString(gc.Body) && !jobutil.OkToTestRe.MatchString(gc.Body) && !jobutil.TestAllRe.MatchString(gc.Body) {
 		matched := false
diff --git a/pkg/plugins/trigger/trigger.go b/pkg/plugins/trigger/trigger.go
index 606d5d27..077a5a6a 100644
--- a/pkg/plugins/trigger/trigger.go
+++ b/pkg/plugins/trigger/trigger.go
@@ -38,19 +38,44 @@ const (
 	pluginName = "trigger"
 )
 
-func init() {
-	plugins.RegisterPlugin(
-		pluginName,
-		plugins.Plugin{
-			Description: `The trigger plugin starts tests in reaction to commands and pull request events. It is responsible for ensuring that test jobs are only run on trusted PRs. A PR is considered trusted if the author is a member of the 'trusted organization' for the repository or if such a member has left an '/ok-to-test' command on the PR.
+var (
+	plugin = plugins.Plugin{
+		Description: `The trigger plugin starts tests in reaction to commands and pull request events. It is responsible for ensuring that test jobs are only run on trusted PRs. A PR is considered trusted if the author is a member of the 'trusted organization' for the repository or if such a member has left an '/ok-to-test' command on the PR.
 <br>Trigger starts jobs automatically when a new trusted PR is created or when an untrusted PR becomes trusted, but it can also be used to start jobs manually via the '/test' command.
 <br>The '/retest' command can be used to rerun jobs that have reported failure.`,
-			HelpProvider:          helpProvider,
+		HelpProvider:       helpProvider,
+		PullRequestHandler: handlePullRequest,
+		PushEventHandler:   handlePush,
+		Commands: []plugins.Command{{
 			GenericCommentHandler: handleGenericCommentEvent,
-			PullRequestHandler:    handlePullRequest,
-			PushEventHandler:      handlePush,
-		},
-	)
+			Filter: func(e scmprovider.GenericCommentEvent) bool {
+				return !(e.Action != scm.ActionCreate || !e.IsPR || e.IssueState != "open")
+			},
+			Help: []pluginhelp.Command{{
+				Usage:       "/ok-to-test",
+				Description: "Marks a PR as 'trusted' and starts tests.",
+				Featured:    false,
+				WhoCanUse:   "Members of the trusted organization for the repo.",
+				Examples:    []string{"/ok-to-test", "/lh-ok-to-test"},
+			}, {
+				Usage:       "/test (<job name>|all)",
+				Description: "Manually starts a/all test job(s).",
+				Featured:    true,
+				WhoCanUse:   "Anyone can trigger this command on a trusted PR.",
+				Examples:    []string{"/test all", "/test pull-bazel-test", "/lh-test all"},
+			}, {
+				Usage:       "/retest",
+				Description: "Rerun test jobs that have failed.",
+				Featured:    true,
+				WhoCanUse:   "Anyone can trigger this command on a trusted PR.",
+				Examples:    []string{"/retest", "/lh-retest"},
+			}},
+		}},
+	}
+)
+
+func init() {
+	plugins.RegisterPlugin(pluginName, plugin)
 }
 
 func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) {
@@ -72,31 +97,7 @@ func helpProvider(config *plugins.Configuration, enabledRepos []string) (*plugin
 		}
 		configInfo[orgRepo] = fmt.Sprintf("The trusted GitHub organization for this repository is %q.", org)
 	}
-	pluginHelp := &pluginhelp.PluginHelp{
-		Config: configInfo,
-	}
-	pluginHelp.AddCommand(pluginhelp.Command{
-		Usage:       "/ok-to-test",
-		Description: "Marks a PR as 'trusted' and starts tests.",
-		Featured:    false,
-		WhoCanUse:   "Members of the trusted organization for the repo.",
-		Examples:    []string{"/ok-to-test", "/lh-ok-to-test"},
-	})
-	pluginHelp.AddCommand(pluginhelp.Command{
-		Usage:       "/test (<job name>|all)",
-		Description: "Manually starts a/all test job(s).",
-		Featured:    true,
-		WhoCanUse:   "Anyone can trigger this command on a trusted PR.",
-		Examples:    []string{"/test all", "/test pull-bazel-test", "/lh-test all"},
-	})
-	pluginHelp.AddCommand(pluginhelp.Command{
-		Usage:       "/retest",
-		Description: "Rerun test jobs that have failed.",
-		Featured:    true,
-		WhoCanUse:   "Anyone can trigger this command on a trusted PR.",
-		Examples:    []string{"/retest", "/lh-retest"},
-	})
-	return pluginHelp, nil
+	return &pluginhelp.PluginHelp{Config: configInfo}, nil
 }
 
 type scmProviderClient interface {
@@ -152,7 +153,7 @@ func handlePullRequest(pc plugins.Agent, pr scm.PullRequestHook) error {
 	return handlePR(getClient(pc), pc.PluginConfig.TriggerFor(org, repo), pr)
 }
 
-func handleGenericCommentEvent(pc plugins.Agent, gc scmprovider.GenericCommentEvent) error {
+func handleGenericCommentEvent(_ []string, pc plugins.Agent, gc scmprovider.GenericCommentEvent) error {
 	return handleGenericComment(getClient(pc), pc.PluginConfig.TriggerFor(gc.Repo.Namespace, gc.Repo.Name), gc)
 }
 
diff --git a/pkg/plugins/yuks/yuks.go b/pkg/plugins/yuks/yuks.go
index 15e908e1..3e4daa55 100644
--- a/pkg/plugins/yuks/yuks.go
+++ b/pkg/plugins/yuks/yuks.go
@@ -42,28 +42,32 @@ const (
 	pluginName = "yuks"
 )
 
-func init() {
-	plugins.RegisterPlugin(
-		pluginName,
-		plugins.Plugin{
-			Description:           "The yuks plugin comments with jokes in response to the `/joke` command.",
-			HelpProvider:          helpProvider,
+var (
+	plugin = plugins.Plugin{
+		Description:  "The yuks plugin comments with jokes in response to the `/joke` command.",
+		HelpProvider: helpProvider,
+		Commands: []plugins.Command{{
+			Filter:                func(e scmprovider.GenericCommentEvent) bool { return e.Action == scm.ActionCreate },
+			Regex:                 match,
 			GenericCommentHandler: handleGenericComment,
-		},
-	)
+			Help: []pluginhelp.Command{{
+				Usage:       "/joke",
+				Description: "Tells a joke.",
+				Featured:    false,
+				WhoCanUse:   "Anyone can use the `/joke` command.",
+				Examples:    []string{"/joke", "/lh-joke"},
+			}},
+		}},
+	}
+)
+
+func init() {
+	plugins.RegisterPlugin(pluginName, plugin)
 }
 
 func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) {
 	// The Config field is omitted because this plugin is not configurable.
-	pluginHelp := &pluginhelp.PluginHelp{}
-	pluginHelp.AddCommand(pluginhelp.Command{
-		Usage:       "/joke",
-		Description: "Tells a joke.",
-		Featured:    false,
-		WhoCanUse:   "Anyone can use the `/joke` command.",
-		Examples:    []string{"/joke", "/lh-joke"},
-	})
-	return pluginHelp, nil
+	return &pluginhelp.PluginHelp{}, nil
 }
 
 type scmProviderClient interface {
@@ -104,20 +108,11 @@ func (url realJoke) readJoke() (string, error) {
 	return a.Joke, nil
 }
 
-func handleGenericComment(pc plugins.Agent, e scmprovider.GenericCommentEvent) error {
+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 {
-	// Only consider new comments.
-	if e.Action != scm.ActionCreate {
-		return nil
-	}
-	// Make sure they are requesting a joke
-	if !match.MatchString(e.Body) {
-		return nil
-	}
-
 	org := e.Repo.Namespace
 	repo := e.Repo.Name
 	number := e.Number
diff --git a/pkg/webhook/events.go b/pkg/webhook/events.go
index 0aed4522..9f5c9f94 100644
--- a/pkg/webhook/events.go
+++ b/pkg/webhook/events.go
@@ -53,23 +53,6 @@ func (s *Server) handleIssueCommentEvent(l *logrus.Entry, ic scm.IssueCommentHoo
 		"url":                    ic.Comment.Link,
 	})
 	l.Infof("Issue comment %s.", ic.Action)
-	for p, h := range s.Plugins.GetPlugins(ic.Repo.Namespace, ic.Repo.Name) {
-		if h.IssueCommentHandler != nil {
-			s.wg.Add(1)
-			go func(p string, h plugins.IssueCommentHandler) {
-				defer s.wg.Done()
-				agent := plugins.NewAgent(s.ConfigAgent, s.Plugins, s.ClientAgent, s.ServerURL, l.WithField("plugin", p))
-				agent.InitializeCommentPruner(
-					ic.Repo.Namespace,
-					ic.Repo.Name,
-					ic.Issue.Number,
-				)
-				if err := h(agent, ic); err != nil {
-					agent.Logger.WithError(err).Error("Error handling IssueCommentEvent.")
-				}
-			}(p, h.IssueCommentHandler)
-		}
-	}
 
 	s.handleGenericComment(
 		l,
@@ -124,20 +107,26 @@ func (s *Server) handlePullRequestCommentEvent(l *logrus.Entry, pc scm.PullReque
 
 func (s *Server) handleGenericComment(l *logrus.Entry, ce *scmprovider.GenericCommentEvent) {
 	for p, h := range s.Plugins.GetPlugins(ce.Repo.Namespace, ce.Repo.Name) {
-		if h.GenericCommentHandler != nil {
-			s.wg.Add(1)
-			go func(p string, h plugins.GenericCommentHandler) {
-				defer s.wg.Done()
-				agent := plugins.NewAgent(s.ConfigAgent, s.Plugins, s.ClientAgent, s.ServerURL, l.WithField("plugin", p))
-				agent.InitializeCommentPruner(
-					ce.Repo.Namespace,
-					ce.Repo.Name,
-					ce.Number,
-				)
-				if err := h(agent, *ce); err != nil {
-					agent.Logger.WithError(err).Error("Error handling GenericCommentEvent.")
-				}
-			}(p, h.GenericCommentHandler)
+		for _, cmd := range h.Commands {
+			err := cmd.InvokeHandler(ce, func(match []string) error {
+				s.wg.Add(1)
+				go func(p string, h plugins.GenericCommentHandler, m []string) {
+					defer s.wg.Done()
+					agent := plugins.NewAgent(s.ConfigAgent, s.Plugins, s.ClientAgent, s.ServerURL, l.WithField("plugin", p))
+					agent.InitializeCommentPruner(
+						ce.Repo.Namespace,
+						ce.Repo.Name,
+						ce.Number,
+					)
+					if err := h(m, agent, *ce); err != nil {
+						agent.Logger.WithError(err).Error("Error handling GenericCommentEvent.")
+					}
+				}(p, cmd.GenericCommentHandler, match)
+				return nil
+			})
+			if err != nil {
+				l.WithError(err).Error("Error invoking command handler")
+			}
 		}
 	}
 }


Test patch:
diff --git a/pkg/plugins/approve/approve_test.go b/pkg/plugins/approve/approve_test.go
index 355d14d2..9fe85cc5 100644
--- a/pkg/plugins/approve/approve_test.go
+++ b/pkg/plugins/approve/approve_test.go
@@ -1493,17 +1493,19 @@ func TestHandleGenericComment(t *testing.T) {
 				Repos:             []string{test.commentEvent.Repo.Namespace},
 				LgtmActsAsApprove: test.lgtmActsAsApprove,
 			})
-			err := handleGenericComment(
-				logrus.WithField("plugin", "approve"),
-				fakeClient,
-				fakeOwnersClient{},
-				&url.URL{
-					Scheme: "https",
-					Host:   "github.com",
-				},
-				config,
-				&test.commentEvent,
-			)
+			err := plugin.InvokeCommand(&test.commentEvent, func(match []string) error {
+				return handleGenericComment(
+					logrus.WithField("plugin", "approve"),
+					fakeClient,
+					fakeOwnersClient{},
+					&url.URL{
+						Scheme: "https",
+						Host:   "github.com",
+					},
+					config,
+					&test.commentEvent,
+				)
+			})
 
 			if test.expectHandle && !handled {
 				t.Errorf("%s: expected call to handleFunc, but it wasn't called", test.name)
diff --git a/pkg/plugins/cat/cat_test.go b/pkg/plugins/cat/cat_test.go
index 770bbd7a..5137136b 100644
--- a/pkg/plugins/cat/cat_test.go
+++ b/pkg/plugins/cat/cat_test.go
@@ -363,7 +363,9 @@ Available variants:
 		Number:     5,
 		IssueState: "open",
 	}
-	if err := handle(fakeClient, logrus.WithField("plugin", pluginName), e, &realClowder{url: ts.URL + "/?format=json"}, func() {}); err != nil {
+	if err := plugin.InvokeCommand(e, func(match []string) error {
+		return handle(match, fakeClient, logrus.WithField("plugin", pluginName), e, &realClowder{url: ts.URL + "/?format=json"}, func() {})
+	}); err != nil {
 		t.Errorf("didn't expect error: %v", err)
 		return
 	}
@@ -482,7 +484,9 @@ func TestCats(t *testing.T) {
 				IssueState: tc.state,
 				IsPR:       tc.pr,
 			}
-			err := handle(fakeClient, logrus.WithField("plugin", pluginName), e, fakeClowder("tubbs"), func() {})
+			err := plugin.InvokeCommand(e, func(match []string) error {
+				return handle(match, fakeClient, logrus.WithField("plugin", pluginName), e, fakeClowder("tubbs"), func() {})
+			})
 			if !tc.shouldError && err != nil {
 				t.Fatalf("%s: didn't expect error: %v", tc.name, err)
 			} else if tc.shouldError && err == nil {
diff --git a/pkg/plugins/dog/dog_test.go b/pkg/plugins/dog/dog_test.go
index 4f981ab1..cb3ba7a2 100644
--- a/pkg/plugins/dog/dog_test.go
+++ b/pkg/plugins/dog/dog_test.go
@@ -228,7 +228,9 @@ func TestHttpResponse(t *testing.T) {
 			Number:     5,
 			IssueState: "open",
 		}
-		err = handle(fakeClient, logrus.WithField("plugin", pluginName), e, realPack(ts.URL))
+		err = plugin.InvokeCommand(e, func(match []string) error {
+			return handle(match, fakeClient, logrus.WithField("plugin", pluginName), e, realPack(ts.URL))
+		})
 		if err != nil {
 			t.Errorf("tc %s: For comment %s, didn't expect error: %v", testcase.name, testcase.comment, err)
 		}
@@ -348,7 +350,9 @@ func TestDogs(t *testing.T) {
 				IssueState: tc.state,
 				IsPR:       tc.pr,
 			}
-			err := handle(fakeClient, logrus.WithField("plugin", pluginName), e, fakePack("doge"))
+			err := plugin.InvokeCommand(e, func(match []string) error {
+				return handle(match, fakeClient, logrus.WithField("plugin", pluginName), e, fakePack("doge"))
+			})
 			if err != nil {
 				t.Errorf("For case %s, didn't expect error: %v", tc.name, err)
 			}
diff --git a/pkg/plugins/help/help_test.go b/pkg/plugins/help/help_test.go
index 3daf9ff9..c8ce61b3 100644
--- a/pkg/plugins/help/help_test.go
+++ b/pkg/plugins/help/help_test.go
@@ -215,7 +215,9 @@ func TestLabel(t *testing.T) {
 				Repo:       scm.Repository{Namespace: "org", Name: "repo"},
 				Author:     scm.User{Login: "Alice"},
 			}
-			err := handle(fakeSCMProviderClient, logrus.WithField("plugin", pluginName), &fakePruner{}, e)
+			err := plugin.InvokeCommand(e, func(match []string) error {
+				return handle(match, fakeSCMProviderClient, logrus.WithField("plugin", pluginName), &fakePruner{}, e)
+			})
 			if err != nil {
 				t.Fatalf("For case %s, didn't expect error from label test: %v", tc.name, err)
 			}
diff --git a/pkg/plugins/hold/hold_test.go b/pkg/plugins/hold/hold_test.go
index 03aff1da..11277f07 100644
--- a/pkg/plugins/hold/hold_test.go
+++ b/pkg/plugins/hold/hold_test.go
@@ -101,7 +101,9 @@ func TestHandle(t *testing.T) {
 				return tc.hasLabel
 			}
 
-			if err := handle(scmprovider.ToTestClient(client), logrus.WithField("plugin", pluginName), e, hasLabel); err != nil {
+			if err := plugin.InvokeCommand(e, func(match []string) error {
+				return handle(match, scmprovider.ToTestClient(client), logrus.WithField("plugin", pluginName), e, hasLabel)
+			}); err != nil {
 				t.Fatalf("For case %s, didn't expect error from hold: %v", tc.name, err)
 			}
 
diff --git a/pkg/plugins/milestone/milestone_test.go b/pkg/plugins/milestone/milestone_test.go
index 406554ac..8ccb0d07 100644
--- a/pkg/plugins/milestone/milestone_test.go
+++ b/pkg/plugins/milestone/milestone_test.go
@@ -145,7 +145,9 @@ func TestMilestoneStatus(t *testing.T) {
 				repoMilestone["org/repo"] = plugins.Milestone{MaintainersID: maintainersID, MaintainersTeam: maintainersName}
 			}
 
-			if err := handle(fakeClient, logrus.WithField("plugin", pluginName), e, repoMilestone); err != nil {
+			if err := plugin.InvokeCommand(e, func(match []string) error {
+				return handle(match, fakeClient, logrus.WithField("plugin", pluginName), e, repoMilestone)
+			}); err != nil {
 				t.Fatalf("(%s): Unexpected error from handle: %v.", tc.name, err)
 			}
 
diff --git a/pkg/plugins/milestonestatus/milestonestatus_test.go b/pkg/plugins/milestonestatus/milestonestatus_test.go
index 76496b6d..58750c0e 100644
--- a/pkg/plugins/milestonestatus/milestonestatus_test.go
+++ b/pkg/plugins/milestonestatus/milestonestatus_test.go
@@ -149,7 +149,9 @@ func TestMilestoneStatus(t *testing.T) {
 				repoMilestone["org/repo"] = plugins.Milestone{MaintainersID: 42}
 			}
 
-			if err := handle(fakeClient, logrus.WithField("plugin", pluginName), e, repoMilestone); err != nil {
+			if err := plugin.InvokeCommand(e, func(match []string) error {
+				return handle(match, fakeClient, logrus.WithField("plugin", pluginName), e, repoMilestone)
+			}); err != nil {
 				t.Fatalf("(%s): Unexpected error from handle: %v.", tc.name, err)
 			}
 
diff --git a/pkg/plugins/override/override_test.go b/pkg/plugins/override/override_test.go
index 5e82832d..3234a99f 100644
--- a/pkg/plugins/override/override_test.go
+++ b/pkg/plugins/override/override_test.go
@@ -560,7 +560,9 @@ func TestHandle(t *testing.T) {
 				tc.jobs = sets.String{}
 			}
 
-			err := handle(&fc, log, &event)
+			err := plugin.InvokeCommand(&event, func(match []string) error {
+				return handle(&fc, log, &event)
+			})
 			switch {
 			case err != nil:
 				if !tc.err {
diff --git a/pkg/plugins/pony/pony_test.go b/pkg/plugins/pony/pony_test.go
index 7de7fd21..d77037ac 100644
--- a/pkg/plugins/pony/pony_test.go
+++ b/pkg/plugins/pony/pony_test.go
@@ -224,7 +224,9 @@ func TestHttpResponse(t *testing.T) {
 				Number:     5,
 				IssueState: "open",
 			}
-			err = handle(fakeClient, logrus.WithField("plugin", pluginName), e, realHerd(ts.URL+testcase.path))
+			err = plugin.InvokeCommand(e, func(match []string) error {
+				return handle(fakeClient, logrus.WithField("plugin", pluginName), e, realHerd(ts.URL+testcase.path))
+			})
 			if err != nil {
 				t.Errorf("tc %s: For comment %s, didn't expect error: %v", testcase.name, testcase.comment, err)
 			}
@@ -318,7 +320,9 @@ func TestPonies(t *testing.T) {
 			IssueState: tc.state,
 			IsPR:       tc.pr,
 		}
-		err := handle(fakeClient, logrus.WithField("plugin", pluginName), e, fakeHerd("pone"))
+		err := plugin.InvokeCommand(e, func(match []string) error {
+			return handle(fakeClient, logrus.WithField("plugin", pluginName), e, fakeHerd("pone"))
+		})
 		if err != nil {
 			t.Errorf("For case %s, didn't expect error: %v", tc.name, err)
 		}
diff --git a/pkg/plugins/stage/stage_test.go b/pkg/plugins/stage/stage_test.go
index e356bd66..7e9bb59d 100644
--- a/pkg/plugins/stage/stage_test.go
+++ b/pkg/plugins/stage/stage_test.go
@@ -222,7 +222,9 @@ func TestStageLabels(t *testing.T) {
 				Body:   tc.body,
 				Action: scm.ActionCreate,
 			}
-			err := handle(fc, logrus.WithField("plugin", "fake-lifecyle"), e)
+			err := plugin.InvokeCommand(e, func(match []string) error {
+				return handle(match, fc, logrus.WithField("plugin", "fake-lifecyle"), e)
+			})
 			switch {
 			case err != nil:
 				t.Errorf("%s: unexpected error: %v", tc.name, err)
diff --git a/pkg/plugins/trigger/generic-comment_test.go b/pkg/plugins/trigger/generic-comment_test.go
index 446b6d58..cf11ed5e 100644
--- a/pkg/plugins/trigger/generic-comment_test.go
+++ b/pkg/plugins/trigger/generic-comment_test.go
@@ -937,11 +937,15 @@ func TestHandleGenericComment(t *testing.T) {
 			// In some cases handleGenericComment can be called twice for the same event.
 			// For instance on Issue/PR creation and modification.
 			// Let's call it twice to ensure idempotency.
-			if err := handleGenericComment(c, trigger, event); err != nil {
+			if err := plugin.InvokeCommand(&event, func(match []string) error {
+				return handleGenericComment(c, trigger, event)
+			}); err != nil {
 				t.Fatalf("%s: didn't expect error: %s", tc.name, err)
 			}
 			validate(tc.name, fakeLauncher, g, tc, t)
-			if err := handleGenericComment(c, trigger, event); err != nil {
+			if err := plugin.InvokeCommand(&event, func(match []string) error {
+				return handleGenericComment(c, trigger, event)
+			}); err != nil {
 				t.Fatalf("%s: didn't expect error: %s", tc.name, err)
 			}
 			validate(tc.name, fakeLauncher, g, tc, t)
diff --git a/pkg/plugins/yuks/yuks_test.go b/pkg/plugins/yuks/yuks_test.go
index 81ac08a2..a30abb85 100644
--- a/pkg/plugins/yuks/yuks_test.go
+++ b/pkg/plugins/yuks/yuks_test.go
@@ -67,7 +67,9 @@ func TestJokesMedium(t *testing.T) {
 		Number:     5,
 		IssueState: "open",
 	}
-	if err := handle(fakeClient, logrus.WithField("plugin", pluginName), e, realJoke(ts.URL)); err != nil {
+	if err := plugin.InvokeCommand(e, func(match []string) error {
+		return handle(fakeClient, logrus.WithField("plugin", pluginName), e, realJoke(ts.URL))
+	}); err != nil {
 		t.Errorf("didn't expect error: %v", err)
 		return
 	}
@@ -161,7 +163,9 @@ func TestJokes(t *testing.T) {
 				IssueState: tc.state,
 				IsPR:       tc.pr,
 			}
-			err := handle(fakeClient, logrus.WithField("plugin", pluginName), e, tc.joke)
+			err := plugin.InvokeCommand(e, func(match []string) error {
+				return handle(fakeClient, logrus.WithField("plugin", pluginName), e, tc.joke)
+			})
 			if !tc.shouldError && err != nil {
 				t.Fatalf("For case %s, didn't expect error: %v", tc.name, err)
 			} else if tc.shouldError && err == nil {