Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f8c080f

Browse files
Merge pull request jenkinsci#90 from paschdan/review-trigger-with-integration
Review trigger with integration
2 parents 8e2f2b3 + f73357f commit f8c080f

File tree

5 files changed

+321
-1
lines changed

5 files changed

+321
-1
lines changed

README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ If you plan to use this plugin to add/modify/remove comments, labels, commit sta
4949

5050
This plugin adds the following pipeline triggers
5151

52+
* issueCommentTrigger
53+
* pullRequestReview
54+
5255
## issueCommentTrigger
5356

5457
### Requirements
@@ -120,6 +123,60 @@ The GitHub comment and author that triggered the build are exposed as environmen
120123
* `GITHUB_COMMENT`
121124
* `GITHUB_COMMENT_AUTHOR`
122125

126+
## pullRequestReview
127+
128+
### Parameters
129+
130+
- `reviewStates` (__Optional__) - A Java array of the PR review states you wish to trigger the build with. If not specified it will trigger for any review state. Possible states are `pending, approved, changes_requested, commented, dismissed`
131+
132+
### Usage
133+
134+
#### Scripted Pipeline:
135+
```groovy
136+
properties([
137+
pipelineTriggers([
138+
pullRequestReview(reviewStates: ['approved'])
139+
])
140+
])
141+
```
142+
143+
#### Declarative Pipeline:
144+
```groovy
145+
pipeline {
146+
triggers {
147+
pullRequestReview(reviewStates: ['approved'])
148+
}
149+
}
150+
```
151+
152+
#### Detecting whether a build was started by the trigger in a script:
153+
154+
Note that the following uses `currentBuild.rawBuild` and should therefore only
155+
be done in a `@NonCPS` context. See [the workflow-cps-plugin Technical Design
156+
](https://github.com/jenkinsci/workflow-cps-plugin/blob/master/README.md#technical-design)
157+
for more information.
158+
159+
```groovy
160+
def triggerCause = currentBuild.rawBuild.getCause(org.jenkinsci.plugins.pipeline.github.trigger.PullRequestReviewCause)
161+
162+
if (triggerCause) {
163+
echo("Build was started by ${triggerCause.userLogin}, who reviewed the PR: " +
164+
"\"${triggerCause.state}\", which matches one of " +
165+
"\"${triggerCause.reviewStates}\" trigger pattern.")
166+
} else {
167+
echo('Build was not started by a trigger')
168+
}
169+
```
170+
171+
#### Environment variables
172+
173+
The GitHub review comment and author that triggered the build are exposed as environment variables (from version > 2.8).
174+
175+
* `GITHUB_REVIEW_COMMENT`
176+
* `GITHUB_REVIEW_AUTHOR`
177+
* `GITHUB_REVIEW_STATE`
178+
179+
123180
# Global Variables
124181

125182
## `repository`

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
<connection>scm:git:https://github.com/jenkinsci/${project.artifactId}-plugin.git</connection>
4040
<developerConnection>scm:git:ssh://[email protected]/jenkinsci/${project.artifactId}-plugin.git</developerConnection>
4141
<url>http://github.com/jenkinsci/${project.artifactId}-plugin</url>
42-
<tag>pipeline-github-2.6</tag>
42+
<tag>${scmTag}</tag>
4343
</scm>
4444

4545
<repositories>

src/main/java/org/jenkinsci/plugins/pipeline/github/trigger/GitHubEventSubscriber.java

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.kohsuke.github.GHEvent;
1717
import org.kohsuke.github.GHEventPayload;
1818
import org.kohsuke.github.GHIssueComment;
19+
import org.kohsuke.github.GHPullRequestReview;
1920
import org.kohsuke.github.GitHub;
2021
import org.slf4j.Logger;
2122
import org.slf4j.LoggerFactory;
@@ -73,6 +74,9 @@ protected void onEvent(final GHSubscriberEvent event) {
7374
case ISSUE_COMMENT:
7475
handleIssueComment(event);
7576
break;
77+
case PULL_REQUEST_REVIEW:
78+
handlePullRequestReview(event);
79+
break;
7680
default:
7781
// no-op
7882
}
@@ -189,12 +193,115 @@ private boolean triggerMatches(final IssueCommentTrigger trigger,
189193
return false;
190194
}
191195

196+
private void handlePullRequestReview(final GHSubscriberEvent event) {
197+
// we only care about created or updated events
198+
switch (event.getType()) {
199+
case CREATED:
200+
case UPDATED:
201+
// case: SUBMITTED:
202+
break;
203+
default:
204+
return;
205+
}
206+
// decode payload
207+
final GHEventPayload.PullRequestReview pullRequestReview;
208+
try {
209+
pullRequestReview = GitHub.offline()
210+
.parseEventPayload(new StringReader(event.getPayload()), GHEventPayload.PullRequestReview.class);
211+
} catch (final IOException e) {
212+
LOG.error("Unable to parse the payload of GHSubscriberEvent: {}", event, e);
213+
return;
214+
}
215+
216+
switch (pullRequestReview.getAction()) {
217+
case "submitted":
218+
break;
219+
default:
220+
LOG.debug("Ignoring pullRequestReview: {} with Action: {}",
221+
pullRequestReview.getReview(), pullRequestReview.getAction());
222+
return;
223+
}
224+
225+
final String key = String.format("%s/%s/%d",
226+
pullRequestReview.getRepository().getOwnerName(),
227+
pullRequestReview.getRepository().getName(),
228+
pullRequestReview.getPullRequest().getNumber());
229+
230+
// lookup trigger
231+
final PullRequestReviewTrigger.DescriptorImpl triggerDescriptor = (PullRequestReviewTrigger.DescriptorImpl) Jenkins.get()
232+
.getDescriptor(PullRequestReviewTrigger.class);
233+
234+
if (triggerDescriptor == null) {
235+
LOG.error("Unable to find the PullRequestReview Trigger, this shouldn't happen.");
236+
return;
237+
}
238+
String reviewer = pullRequestReview.getSender().getLogin();
239+
240+
// create values for the action if a new job is triggered afterward
241+
ArrayList<ParameterValue> reviewEnvVars = new ArrayList<ParameterValue>();
242+
reviewEnvVars.add(new StringParameterValue("GITHUB_REVIEW_COMMENT", String.valueOf(pullRequestReview.getReview().getBody())));
243+
reviewEnvVars.add(new StringParameterValue("GITHUB_REVIEW_AUTHOR", reviewer));
244+
reviewEnvVars.add(new StringParameterValue("GITHUB_REVIEW_STATE", pullRequestReview.getReview().getState().name()));
245+
246+
247+
for (final WorkflowJob job : triggerDescriptor.getJobs(key)) {
248+
// find triggers
249+
final List<PullRequestReviewTrigger> matchingTriggers = job.getTriggersJobProperty()
250+
.getTriggers()
251+
.stream()
252+
.filter(PullRequestReviewTrigger.class::isInstance)
253+
.map(PullRequestReviewTrigger.class::cast)
254+
.filter(t -> triggerMatches(t, pullRequestReview.getReview(), job))
255+
.collect(Collectors.toList());
256+
257+
// check if they have authorization
258+
for (final PullRequestReviewTrigger matchingTrigger : matchingTriggers) {
259+
boolean authorized = isAuthorized(job, reviewer);
260+
261+
if (authorized) {
262+
job.scheduleBuild2(
263+
Jenkins.get().getQuietPeriod(),
264+
new CauseAction(new PullRequestReviewCause(
265+
reviewer,
266+
pullRequestReview.getReview().getState().name().toLowerCase(),
267+
pullRequestReview.getReview().getBody(),
268+
matchingTrigger.getReviewStates())),
269+
new GitHubEnvironmentVariablesAction(reviewEnvVars));
270+
271+
LOG.info("Job: {} triggered by PullRequestReview: {}",
272+
job.getFullName(), pullRequestReview.getReview());
273+
} else {
274+
LOG.warn("Job: {}, PullRequestReview: {}, Reviewer: {} is not a collaborator, " +
275+
"and is therefore not authorized to trigger a build.",
276+
job.getFullName(),
277+
pullRequestReview.getReview(),
278+
reviewer);
279+
}
280+
}
281+
}
282+
}
283+
284+
private boolean triggerMatches(final PullRequestReviewTrigger trigger,
285+
final GHPullRequestReview review,
286+
final WorkflowJob job) {
287+
if (trigger.matches(review.getState().name().toLowerCase())) {
288+
LOG.debug("Job: {}, PullRequestReview: {} matched one of the states: {}",
289+
job.getFullName(), review, trigger.getReviewStates());
290+
return true;
291+
} else {
292+
LOG.debug("Job: {}, PullRequestReview: {}, state did not match the states: {}",
293+
job.getFullName(), review, trigger.getReviewStates());
294+
}
295+
return false;
296+
}
297+
192298
@Override
193299
protected Set<GHEvent> events() {
194300
final Set<GHEvent> events = new HashSet<>();
195301
// events.add(GHEvent.PULL_REQUEST_REVIEW_COMMENT);
196302
// events.add(GHEvent.COMMIT_COMMENT);
197303
events.add(GHEvent.ISSUE_COMMENT);
304+
events.add(GHEvent.PULL_REQUEST_REVIEW);
198305
return Collections.unmodifiableSet(events);
199306
}
200307
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package org.jenkinsci.plugins.pipeline.github.trigger;
2+
3+
import hudson.model.Cause;
4+
import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted;
5+
6+
/**
7+
* Represents the user who reviewed the PR that triggered the build.
8+
*
9+
* @author Aaron Walker
10+
*/
11+
public class PullRequestReviewCause extends Cause {
12+
13+
private final String userLogin;
14+
private final String comment;
15+
private final String state;
16+
private final String[] reviewStates;
17+
18+
public PullRequestReviewCause(final String userLogin, final String state, final String comment, final String[] reviewStates) {
19+
this.userLogin = userLogin;
20+
this.state = state;
21+
this.comment = comment;
22+
this.reviewStates = reviewStates;
23+
}
24+
25+
@Whitelisted
26+
public String getUserLogin() {
27+
return userLogin;
28+
}
29+
30+
@Whitelisted
31+
public String getComment() {
32+
return comment;
33+
}
34+
35+
@Whitelisted
36+
public String getState() {
37+
return state;
38+
}
39+
40+
@Whitelisted
41+
public String[] getReviewStates() {
42+
return reviewStates;
43+
}
44+
45+
@Override
46+
public String getShortDescription() {
47+
return String.format("%s reviewed: %s", userLogin, state);
48+
}
49+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package org.jenkinsci.plugins.pipeline.github.trigger;
2+
3+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
4+
import hudson.Extension;
5+
import hudson.model.Item;
6+
import hudson.triggers.Trigger;
7+
import hudson.triggers.TriggerDescriptor;
8+
import jenkins.scm.api.SCMHead;
9+
import jenkins.scm.api.SCMSource;
10+
import org.jenkinsci.Symbol;
11+
import org.jenkinsci.plugins.github_branch_source.GitHubSCMSource;
12+
import org.jenkinsci.plugins.github_branch_source.PullRequestSCMHead;
13+
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
14+
import org.kohsuke.stapler.DataBoundConstructor;
15+
import org.kohsuke.stapler.DataBoundSetter;
16+
import org.slf4j.Logger;
17+
import org.slf4j.LoggerFactory;
18+
19+
import javax.annotation.Nonnull;
20+
21+
import java.util.Arrays;
22+
import java.util.Collections;
23+
import java.util.HashSet;
24+
import java.util.List;
25+
import java.util.Map;
26+
import java.util.Set;
27+
import java.util.concurrent.ConcurrentHashMap;
28+
import java.util.regex.Pattern;
29+
/**
30+
* An PullRequestApprovalTrigger, to be used from pipeline scripts only.
31+
*
32+
* This trigger will not show up on a jobs configuration page.
33+
*
34+
* @author Aaron Walker
35+
* @see org.jenkinsci.plugins.workflow.job.properties.PipelineTriggersJobProperty
36+
*/
37+
public class PullRequestReviewTrigger extends Trigger<WorkflowJob> {
38+
private static final Logger LOG = LoggerFactory.getLogger(PullRequestReviewTrigger.class);
39+
40+
private String[] reviewStates = null;
41+
42+
@DataBoundConstructor
43+
public PullRequestReviewTrigger() {}
44+
45+
public String[] getReviewStates() {
46+
return reviewStates;
47+
}
48+
49+
@DataBoundSetter
50+
public void setReviewStates(@Nonnull final String [] reviewStates) {
51+
this.reviewStates = reviewStates;
52+
}
53+
54+
@Override
55+
public void start(final WorkflowJob project, final boolean newInstance) {
56+
super.start(project, newInstance);
57+
// we only care about pull requests
58+
if (SCMHead.HeadByItem.findHead(project) instanceof PullRequestSCMHead) {
59+
DescriptorImpl.jobs
60+
.computeIfAbsent(getKey(project), key -> new HashSet<>())
61+
.add(project);
62+
}
63+
}
64+
65+
@Override
66+
public void stop() {
67+
if (SCMHead.HeadByItem.findHead(job) instanceof PullRequestSCMHead) {
68+
DescriptorImpl.jobs.getOrDefault(getKey(job), Collections.emptySet())
69+
.remove(job);
70+
}
71+
}
72+
73+
@SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE")
74+
private String getKey(final WorkflowJob project) {
75+
final GitHubSCMSource scmSource = (GitHubSCMSource) SCMSource.SourceByItem.findSource(project);
76+
final PullRequestSCMHead scmHead = (PullRequestSCMHead) SCMHead.HeadByItem.findHead(project);
77+
78+
return String.format("%s/%s/%d",
79+
scmSource.getRepoOwner(),
80+
scmSource.getRepository(),
81+
scmHead.getNumber());
82+
}
83+
84+
boolean matches(final String reviewState) {
85+
if(reviewStates == null) {
86+
return true; //defaults to trigger on any review state
87+
} else {
88+
List<String> list = Arrays.asList(reviewStates);
89+
return list.contains(reviewState);
90+
}
91+
}
92+
93+
@Symbol("pullRequestReview")
94+
@Extension
95+
public static class DescriptorImpl extends TriggerDescriptor {
96+
private transient static final Map<String, Set<WorkflowJob>> jobs = new ConcurrentHashMap<>();
97+
98+
@Override
99+
public boolean isApplicable(final Item item) {
100+
return false; // this is not configurable from the ui.
101+
}
102+
103+
public Set<WorkflowJob> getJobs(final String key) {
104+
return jobs.getOrDefault(key, Collections.emptySet());
105+
}
106+
}
107+
}

0 commit comments

Comments
 (0)