Add support for monitoring issues that are waiting for feedback

This commit is contained in:
Andy Wilkinson
2015-12-08 17:00:13 +00:00
parent d19f043c41
commit 2275ca1daa
34 changed files with 1512 additions and 106 deletions

View File

@@ -24,6 +24,10 @@
</properties>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>

View File

@@ -16,6 +16,8 @@
package io.spring.issuebot;
import java.util.List;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -54,6 +56,11 @@ public class GitHubProperties {
*/
private String name;
/**
* The names of the repository's collaborators.
*/
private List<String> collaborators;
}
/**

View File

@@ -22,6 +22,7 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableScheduling;
import io.spring.issuebot.github.GitHubOperations;
import io.spring.issuebot.github.GitHubTemplate;
@@ -33,6 +34,7 @@ import io.spring.issuebot.github.RegexLinkParser;
* @author Andy Wilkinson
*/
@SpringBootApplication
@EnableScheduling
@EnableConfigurationProperties(GitHubProperties.class)
public class IssueBotApplication {

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.spring.issuebot.feedback;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.spring.issuebot.GitHubProperties;
import io.spring.issuebot.github.GitHubOperations;
/**
* Central configuration for the beans involved in managing issues that are waiting for
* feedback.
*
* @author Andy Wilkinson
*/
@Configuration
@EnableConfigurationProperties(FeedbackProperties.class)
class FeedbackConfiguration {
@Bean
FeedbackIssueListener feedbackIssueListener(GitHubOperations gitHub,
GitHubProperties githubProperties, FeedbackProperties feedbackProperties) {
return new FeedbackIssueListener(gitHub, feedbackProperties.getRequiredLabel(),
githubProperties.getRepository().getCollaborators(),
new StandardFeedbackListener(gitHub,
feedbackProperties.getProvidedLabel(),
feedbackProperties.getRequiredLabel(),
feedbackProperties.getReminderComment(),
feedbackProperties.getCloseComment()));
}
}

View File

@@ -0,0 +1,114 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.spring.issuebot.feedback;
import java.time.OffsetDateTime;
import java.util.List;
import io.spring.issuebot.IssueListener;
import io.spring.issuebot.github.Comment;
import io.spring.issuebot.github.Event;
import io.spring.issuebot.github.GitHubOperations;
import io.spring.issuebot.github.Issue;
import io.spring.issuebot.github.Label;
import io.spring.issuebot.github.Page;
/**
* An {@link IssueListener} that processes issues that are waiting for feedback.
*
* @author Andy Wilkinson
*/
final class FeedbackIssueListener implements IssueListener {
private final GitHubOperations gitHub;
private final String labelName;
private final List<String> collaborators;
private final FeedbackListener feedbackListener;
FeedbackIssueListener(GitHubOperations gitHub, String labelName,
List<String> collaborators, FeedbackListener feedbackListener) {
this.gitHub = gitHub;
this.labelName = labelName;
this.collaborators = collaborators;
this.feedbackListener = feedbackListener;
}
@Override
public void onOpenIssue(Issue issue) {
if (waitingForFeedback(issue)) {
OffsetDateTime waitingSince = getWaitingSince(issue);
if (waitingSince != null) {
processWaitingIssue(issue, waitingSince);
}
}
}
private void processWaitingIssue(Issue issue, OffsetDateTime waitingSince) {
if (commentedSince(waitingSince, issue)) {
this.feedbackListener.feedbackProvided(issue);
}
else {
this.feedbackListener.feedbackRequired(issue, waitingSince);
}
}
private boolean waitingForFeedback(Issue issue) {
return issue.getPullRequest() == null && labelledAsWaitingForFeedback(issue);
}
private boolean labelledAsWaitingForFeedback(Issue issue) {
for (Label label : issue.getLabels()) {
if (this.labelName.equals(label.getName())) {
return true;
}
}
return false;
}
private OffsetDateTime getWaitingSince(Issue issue) {
OffsetDateTime createdAt = null;
Page<Event> page = this.gitHub.getEvents(issue);
while (page != null) {
for (Event event : page.getContent()) {
if (Event.Type.LABELED.equals(event.getType())
&& this.labelName.equals(event.getLabel().getName())) {
createdAt = event.getCreationTime();
}
}
page = page.next();
}
return createdAt;
}
private boolean commentedSince(OffsetDateTime waitingForFeedbackSince, Issue issue) {
Page<Comment> page = this.gitHub.getComments(issue);
while (page != null) {
for (Comment comment : page.getContent()) {
if (!this.collaborators.contains(comment.getUser().getLogin())
&& comment.getCreationTime().isAfter(waitingForFeedbackSince)) {
return true;
}
}
page = page.next();
}
return false;
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.spring.issuebot.feedback;
import java.time.OffsetDateTime;
import io.spring.issuebot.github.Issue;
/**
* A {@code FeedbackListener} is notified when feedback has been provided or is still
* required for an {@link Issue}.
*
* @author Andy Wilkinson
*/
public interface FeedbackListener {
/**
* Notification that feedback has been provided for the given {@code issue}.
*
* @param issue the issue
*/
void feedbackProvided(Issue issue);
/**
* Notification that feedback is still required for the given {@code issue} having
* been requested at the given {@code requestTime}.
*
* @param issue the issue
* @param requestTime the time when feedback was requested
*/
void feedbackRequired(Issue issue, OffsetDateTime requestTime);
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.spring.issuebot.feedback;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
/**
* {@link EnableConfigurationProperties Configuration properties} for configuring the
* monitoring of issues that require user feedback.
*
* @author Andy Wilkinson
*/
@Getter
@Setter
@ConfigurationProperties(prefix = "issuebot.feedback")
final class FeedbackProperties {
/**
* Name of the label that is applied when feedback is required.
*/
private String requiredLabel;
/**
* Name of the label that is applied when feedback has been provided.
*/
private String providedLabel;
/**
* The text of the comment that is added as a reminder that feedback is required.
*/
private String reminderComment;
/**
* The text of the comment that is added when an issue is clsed as feedback has not
* been provided.
*/
private String closeComment;
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.spring.issuebot.feedback;
import java.time.OffsetDateTime;
import io.spring.issuebot.github.GitHubOperations;
import io.spring.issuebot.github.Issue;
/**
* Standard implementation of {@link FeedbackListener}.
*
* @author Andy Wilkinson
*/
final class StandardFeedbackListener implements FeedbackListener {
private final GitHubOperations gitHub;
private final String providedLabel;
private final String requiredLabel;
private final String reminderComment;
private final String closeComment;
StandardFeedbackListener(GitHubOperations gitHub, String providedLabel,
String requiredLabel, String reminderComment, String closeComment) {
this.gitHub = gitHub;
this.providedLabel = providedLabel;
this.requiredLabel = requiredLabel;
this.reminderComment = reminderComment;
this.closeComment = closeComment;
}
@Override
public void feedbackProvided(Issue issue) {
this.gitHub.addLabel(issue, this.providedLabel);
this.gitHub.removeLabel(issue, this.requiredLabel);
}
@Override
public void feedbackRequired(Issue issue, OffsetDateTime requestTime) {
OffsetDateTime now = OffsetDateTime.now();
if (requestTime.plusDays(14).isBefore(now)) {
close(issue);
}
else if (requestTime.plusDays(7).isBefore(now)) {
remind(issue);
}
}
private void close(Issue issue) {
this.gitHub.addComment(issue, this.closeComment);
this.gitHub.close(issue);
}
private void remind(Issue issue) {
this.gitHub.addComment(issue, this.reminderComment);
}
}

View File

@@ -16,6 +16,8 @@
package io.spring.issuebot.github;
import java.time.OffsetDateTime;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
@@ -30,14 +32,20 @@ public final class Comment {
private final User user;
private final OffsetDateTime creationTime;
/**
* Creates a new comment that was authored by the given {@code user}.
* Creates a new comment that was authored by the given {@code user} at the given
* {@code creationTime}.
*
* @param user the user
* @param creationTime the creation time
*/
@JsonCreator
public Comment(@JsonProperty("user") User user) {
public Comment(@JsonProperty("user") User user,
@JsonProperty("created_at") OffsetDateTime creationTime) {
this.user = user;
this.creationTime = creationTime;
}
}

View File

@@ -0,0 +1,164 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.spring.issuebot.github;
import java.time.OffsetDateTime;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
/**
* An event that has been performed on an {@link Issue}.
*
* @author Andy Wilkinson
*/
@Getter
public class Event {
private final Type type;
private final OffsetDateTime creationTime;
private final Label label;
/**
* Creates a new {@code Event}.
*
* @param type the type of the event
* @param creationTime the timestamp of when the event was created
* @param label the label associated with the event
*/
@JsonCreator
public Event(@JsonProperty("event") String type,
@JsonProperty("created_at") OffsetDateTime creationTime,
@JsonProperty("label") Label label) {
this.type = Type.valueFrom(type);
this.creationTime = creationTime;
this.label = label;
}
/**
* The type of an {@link Event}.
*
* @author Andy Wilkinson
*/
public enum Type {
/**
* The issue was closed by the actor.
*/
CLOSED("closed"),
/**
* The issue was reopened by the actor.
*/
REOPENED("reopened"),
/**
* The actor subscribed to receive notifications for an issue.
*/
SUBSCRIBED("subscribed"),
/**
* The issue was merged by the actor.
*/
MERGED("merged"),
/**
* The issue was referenced from a commit message.
*/
REFERENCED("referenced"),
/**
* The actor was {@code @mentioned} in an issue body.
*/
MENTIONED("mentioned"),
/**
* The issue was assigned to the actor.
*/
ASSIGNED("assigned"),
/**
* The actor was unassigned from the issue.
*/
UNASSIGNED("unassigned"),
/**
* A label was added to the issue.
*/
LABELED("labeled"),
/**
* A label was removed from the issue.
*/
UNLABELED("unlabeled"),
/**
* The issue was added to a milestone.
*/
MILESTONED("milestoned"),
/**
* The issue was removed from a milestone.
*/
DEMILESTONED("demilestoned"),
/**
* The issue title was changed.
*/
RENAMED("renamed"),
/**
* The issue was locked by the actor.
*/
LOCKED("locked"),
/**
* The issue was unlocked by the actor.
*/
UNLOCKED("unlocked"),
/**
* The pull request's branch was deleted.
*/
HEAD_REF_DELETED("head_ref_deleted"),
/**
* The pull request's branch was restored.
*/
HEAD_REF_RESTORED("head_ref_restored");
private String type;
Type(String type) {
this.type = type;
}
static Type valueFrom(String type) {
for (Type value : values()) {
if (type.equals(value.type)) {
return value;
}
}
throw new IllegalArgumentException(
"'" + type + "' is not a valid event type");
}
}
}

View File

@@ -45,9 +45,43 @@ public interface GitHubOperations {
* Adds the given {@code label} to the given {@code issue}.
*
* @param issue the issue
* @param label the label;
* @return the updated issue
* @param label the label
* @return the modified issue
*/
Issue addLabel(Issue issue, String label);
/**
* Removes the given {@code label} from the given {@code issue}.
*
* @param issue the issue
* @param label the label
* @return the modified issue
*/
Issue removeLabel(Issue issue, String label);
/**
* Adds the given {@code comment} to the given {@code issue}.
*
* @param issue the issue
* @param comment the comment
* @return the added comment
*/
Comment addComment(Issue issue, String comment);
/**
* Closes the given {@code issue}.
*
* @param issue the issue
* @return the modified issue
*/
Issue close(Issue issue);
/**
* Returns the events that have occurred on the given {@code issue}.
*
* @param issue the issue
* @return the events
*/
Page<Event> getEvents(Issue issue);
}

View File

@@ -73,10 +73,6 @@ public class GitHubTemplate implements GitHubOperations {
this.linkParser = linkParser;
}
RestOperations getRestOperations() {
return this.rest;
}
static RestTemplate createDefaultRestTemplate(String username, String password) {
RestTemplate rest = new RestTemplate();
rest.setErrorHandler(new DefaultResponseErrorHandler() {
@@ -103,48 +99,83 @@ public class GitHubTemplate implements GitHubOperations {
public Page<Issue> getIssues(String organization, String repository) {
String url = "https://api.github.com/repos/" + organization + "/" + repository
+ "/issues";
return getIssues(url);
return getPage(url, Issue[].class);
}
private Page<Issue> getIssues(String url) {
@Override
public Page<Comment> getComments(Issue issue) {
return getPage(issue.getCommentsUrl(), Comment[].class);
}
@Override
public Page<Event> getEvents(Issue issue) {
return getPage(issue.getEventsUrl(), Event[].class);
}
private <T> Page<T> getPage(String url, Class<T[]> type) {
if (!StringUtils.hasText(url)) {
return null;
}
ResponseEntity<Issue[]> issues = this.rest.getForEntity(url, Issue[].class);
return new StandardPage<Issue>(Arrays.asList(issues.getBody()),
() -> getIssues(getNextUrl(issues)));
ResponseEntity<T[]> contents = this.rest.getForEntity(url, type);
return new StandardPage<T>(Arrays.asList(contents.getBody()),
() -> getPage(getNextUrl(contents), type));
}
private String getNextUrl(ResponseEntity<?> response) {
return this.linkParser.parse(response.getHeaders().getFirst("Link")).get("next");
}
@Override
public Page<Comment> getComments(Issue issue) {
return getComments(issue.getCommentsUrl());
}
private Page<Comment> getComments(String url) {
if (!StringUtils.hasText(url)) {
return null;
}
ResponseEntity<Comment[]> comments = this.rest.getForEntity(url, Comment[].class);
return new StandardPage<Comment>(Arrays.asList(comments.getBody()),
() -> getComments(getNextUrl(comments)));
}
@Override
public Issue addLabel(Issue issue, String labelName) {
Map<String, Object> body = new HashMap<>();
body.put("labels", Arrays.asList(labelName));
ResponseEntity<Issue> exchange = this.rest.exchange(
ResponseEntity<Label[]> response = this.rest.exchange(
new RequestEntity<>(body, HttpMethod.POST, URI.create(issue.getUrl())),
Issue.class);
if (exchange.getStatusCode() != HttpStatus.OK) {
Label[].class);
if (response.getStatusCode() != HttpStatus.OK) {
log.warn("Failed to add label to issue. Response status: "
+ exchange.getStatusCode());
+ response.getStatusCode());
}
return exchange.getBody();
return new Issue(issue.getUrl(), issue.getCommentsUrl(), issue.getEventsUrl(),
issue.getLabelsUrl(), issue.getUser(), Arrays.asList(response.getBody()),
issue.getMilestone(), issue.getPullRequest());
}
@Override
public Issue removeLabel(Issue issue, String labelName) {
ResponseEntity<Label[]> response = this.rest.exchange(
new RequestEntity<Void>(HttpMethod.DELETE, URI.create(
issue.getLabelsUrl().replace("{/name}", "/" + labelName))),
Label[].class);
if (response.getStatusCode() != HttpStatus.OK) {
log.warn("Failed to remove label from issue. Response status: "
+ response.getStatusCode());
}
return new Issue(issue.getUrl(), issue.getCommentsUrl(), issue.getEventsUrl(),
issue.getLabelsUrl(), issue.getUser(), Arrays.asList(response.getBody()),
issue.getMilestone(), issue.getPullRequest());
}
@Override
public Comment addComment(Issue issue, String comment) {
Map<String, String> body = new HashMap<>();
body.put("body", comment);
return this.rest.postForEntity(issue.getCommentsUrl(), body, Comment.class)
.getBody();
}
@Override
public Issue close(Issue issue) {
Map<String, String> body = new HashMap<>();
body.put("state", "closed");
ResponseEntity<Issue> response = this.rest.exchange(
new RequestEntity<>(body, HttpMethod.PATCH, URI.create(issue.getUrl())),
Issue.class);
if (response.getStatusCode() != HttpStatus.OK) {
log.warn("Failed to close issue. Response status: "
+ response.getStatusCode());
}
return response.getBody();
}
private static class BasicAuthorizationInterceptor

View File

@@ -20,6 +20,7 @@ import java.util.List;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AccessLevel;
import lombok.Getter;
/**
@@ -27,38 +28,65 @@ import lombok.Getter;
*
* @author Andy Wilkinson
*/
@Getter
public class Issue {
@Getter(AccessLevel.PACKAGE)
private final String url;
@Getter(AccessLevel.PACKAGE)
private final String commentsUrl;
@Getter(AccessLevel.PACKAGE)
private final String eventsUrl;
@Getter(AccessLevel.PACKAGE)
private final String labelsUrl;
@Getter
private final User user;
private final List<Label> labels;
@Getter
private List<Label> labels;
@Getter
private final Milestone milestone;
@Getter
private final PullRequest pullRequest;
/**
* Creates a new {@code Issue}.
*
* @param url the url of the issue in the GitHub API
* @param commentsUrl the url of the comments on the issue in the GitHub API
* @param eventsUrl the url of the events on the issue in the GitHub API
* @param labelsUrl the url of the labels on the issue in the GitHub API
* @param user the user that created the issue
* @param labels the labels applied to the issue
* @param milestone the milestone applied to the issue
* @param pullRequest details of the pull request (if this issue is a pull request)
*/
@JsonCreator
public Issue(@JsonProperty("url") String url,
@JsonProperty("comments_url") String commentsUrl,
@JsonProperty("user") User user, @JsonProperty("labels") List<Label> labels,
@JsonProperty("milestone") Milestone milestone) {
@JsonProperty("events_url") String eventsUrl,
@JsonProperty("labels_url") String labelsUrl, @JsonProperty("user") User user,
@JsonProperty("labels") List<Label> labels,
@JsonProperty("milestone") Milestone milestone,
@JsonProperty("pull_request") PullRequest pullRequest) {
this.url = url;
this.commentsUrl = commentsUrl;
this.eventsUrl = eventsUrl;
this.labelsUrl = labelsUrl;
this.user = user;
this.labels = labels;
this.milestone = milestone;
this.pullRequest = pullRequest;
}
@Override
public String toString() {
return this.url;
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.spring.issuebot.github;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
/**
* Details of a GitHub pull request.
*
* @author Andy Wilkinson
*/
@Getter
public class PullRequest {
private final String url;
/**
* Creates a new {@code PullRequest} that has the given {@code url} in the GitHub API.
* @param url the url
*/
@JsonCreator
public PullRequest(@JsonProperty("url") String url) {
this.url = url;
}
}

View File

@@ -40,4 +40,9 @@ public class User {
this.login = login;
}
@Override
public String toString() {
return this.login;
}
}

View File

@@ -55,8 +55,8 @@ final class CommentedByCollaboratorTriageFilter implements TriageFilter {
while (page != null) {
for (Comment comment : page.getContent()) {
if (this.collaborators.contains(comment.getUser().getLogin())) {
log.debug("{} has been triaged. It was commented on by {}",
issue.getUrl(), comment.getUser().getLogin());
log.debug("{} has been triaged. It was commented on by {}", issue,
comment.getUser());
return true;
}
}

View File

@@ -34,7 +34,7 @@ final class LabelledTriageFilter implements TriageFilter {
@Override
public boolean triaged(Issue issue) {
if (issue.getLabels() != null && !issue.getLabels().isEmpty()) {
log.debug("{} has been triaged. It has been labelled.", issue.getUrl());
log.debug("{} has been triaged. It has been labelled.", issue);
return true;
}
return false;

View File

@@ -45,8 +45,7 @@ final class OpenedByCollaboratorTriageFilter implements TriageFilter {
@Override
public boolean triaged(Issue issue) {
if (this.collaborators.contains(issue.getUser().getLogin())) {
log.debug("{} has been triaged. It was opened by {}", issue.getUrl(),
issue.getUser().getLogin());
log.debug("{} has been triaged. It was opened by {}", issue, issue.getUser());
return true;
}
return false;

View File

@@ -21,8 +21,8 @@ import java.util.Arrays;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import io.spring.issuebot.GitHubProperties;
import io.spring.issuebot.github.GitHubOperations;
/**
@@ -31,20 +31,20 @@ import io.spring.issuebot.github.GitHubOperations;
* @author Andy Wilkinson
*/
@Configuration
@EnableScheduling
@EnableConfigurationProperties(TriageProperties.class)
class TriageConfiguration {
@Bean
TriageIssueListener triageIssueListener(GitHubOperations gitHubOperations,
TriageProperties triageProperties) {
TriageProperties triageProperties, GitHubProperties gitHubProperties) {
return new TriageIssueListener(
Arrays.asList(
new OpenedByCollaboratorTriageFilter(
triageProperties.getCollaborators()),
gitHubProperties.getRepository().getCollaborators()),
new LabelledTriageFilter(), new MilestoneAppliedTriageFilter(),
new CommentedByCollaboratorTriageFilter(
triageProperties.getCollaborators(), gitHubOperations)),
gitHubProperties.getRepository().getCollaborators(),
gitHubOperations)),
new LabelApplyingTriageListener(gitHubOperations,
triageProperties.getLabel()));
}

View File

@@ -16,8 +16,6 @@
package io.spring.issuebot.triage;
import java.util.List;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -34,11 +32,6 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
@ConfigurationProperties(prefix = "issuebot.triage")
class TriageProperties {
/**
* The names of the project collaborators whose issues do not require triage.
*/
private List<String> collaborators;
/**
* The name of the label that should be applied to issues that are waiting for triage.
*/

View File

@@ -3,10 +3,20 @@ issuebot:
repository:
organization: spring-projects
name: spring-boot
collaborators:
- dsyer
- philwebb
- snicoll
- wilkinsona
triage:
label: waiting-for-triage
collaborators:
- dsyer
- philwebb
- snicoll
- wilkinsona
feedback:
required_label: waiting-for-feedback
provided_label: feedback-provided
reminder_comment: >
If you would like us to look at this issue, please provide the requested
information. If the information is not provided within the next 7 days this issue
will be closed.
close_comment: >
Closing due to lack of requested feedback. If you would like us to look at this
issue, please provide the requested information and we will re-open the issue.

View File

@@ -57,8 +57,8 @@ public class RepositoryMonitorTests {
public void repositoryWithOpenIssues() {
@SuppressWarnings("unchecked")
Page<Issue> page = mock(Page.class);
Issue issueOne = new Issue(null, null, null, null, null);
Issue issueTwo = new Issue(null, null, null, null, null);
Issue issueOne = new Issue(null, null, null, null, null, null, null, null);
Issue issueTwo = new Issue(null, null, null, null, null, null, null, null);
given(page.getContent()).willReturn(Arrays.asList(issueOne, issueTwo));
given(this.gitHub.getIssues("test", "test")).willReturn(page);
this.repositoryMonitor.monitor();

View File

@@ -0,0 +1,150 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.spring.issuebot.feedback;
import java.time.OffsetDateTime;
import java.util.Arrays;
import org.junit.Test;
import io.spring.issuebot.IssueListener;
import io.spring.issuebot.github.Comment;
import io.spring.issuebot.github.Event;
import io.spring.issuebot.github.GitHubOperations;
import io.spring.issuebot.github.Issue;
import io.spring.issuebot.github.Label;
import io.spring.issuebot.github.PullRequest;
import io.spring.issuebot.github.StandardPage;
import io.spring.issuebot.github.User;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
/**
* Tests for {@link FeedbackIssueListener}.
*
* @author Andy Wilkinson
*/
public class FeedbackIssueListenerTests {
private final GitHubOperations gitHub = mock(GitHubOperations.class);
private final FeedbackListener feedbackListener = mock(FeedbackListener.class);
private final IssueListener listener = new FeedbackIssueListener(this.gitHub,
"required", Arrays.asList("Amy", "Brian"), this.feedbackListener);
@Test
public void pullRequestsAreIgnored() {
Issue issue = new Issue(null, null, null, null, null,
Arrays.asList(new Label("required")), null, new PullRequest("url"));
this.listener.onOpenIssue(issue);
verifyNoMoreInteractions(this.gitHub, this.feedbackListener);
}
@Test
public void issuesWithFeedbackRequiredLabelAreIgnored() {
Issue issue = new Issue(null, null, null, null, null,
Arrays.asList(new Label("something-else")), null, null);
this.listener.onOpenIssue(issue);
verifyNoMoreInteractions(this.gitHub, this.feedbackListener);
}
@Test
public void feedbackRequiredForLabeledIssueWithEvent() {
Issue issue = new Issue(null, null, null, null, null,
Arrays.asList(new Label("required")), null, null);
OffsetDateTime requestTime = OffsetDateTime.now();
given(this.gitHub.getEvents(issue)).willReturn(new StandardPage<>(
Arrays.asList(new Event("labeled", requestTime, new Label("required"))),
() -> null));
this.listener.onOpenIssue(issue);
verify(this.feedbackListener).feedbackRequired(issue, requestTime);
}
@Test
public void feedbackProvidedAfterCommentFromNonCollaborator() {
Issue issue = new Issue("issue_url", null, null, null, null,
Arrays.asList(new Label("required")), null, null);
OffsetDateTime requestTime = OffsetDateTime.now().minusDays(1);
given(this.gitHub.getEvents(issue)).willReturn(new StandardPage<>(
Arrays.asList(new Event("labeled", requestTime, new Label("required"))),
() -> null));
given(this.gitHub.getComments(issue)).willReturn(new StandardPage<>(
Arrays.asList(new Comment(new User("Charlie"), OffsetDateTime.now())),
() -> null));
this.listener.onOpenIssue(issue);
verify(this.feedbackListener).feedbackProvided(issue);
}
@Test
public void feedbackRequiredAfterCommentFromNonCollaboratorBeforeRequest() {
Issue issue = new Issue("issue_url", null, null, null, null,
Arrays.asList(new Label("required")), null, null);
OffsetDateTime requestTime = OffsetDateTime.now().minusDays(1);
given(this.gitHub.getEvents(issue)).willReturn(new StandardPage<>(
Arrays.asList(new Event("labeled", requestTime, new Label("required"))),
() -> null));
given(this.gitHub.getComments(issue)).willReturn(new StandardPage<>(Arrays.asList(
new Comment(new User("Charlie"), OffsetDateTime.now().minusDays(2))),
() -> null));
this.listener.onOpenIssue(issue);
verify(this.feedbackListener).feedbackRequired(issue, requestTime);
}
@Test
public void feedbackRequiredAfterCommentFromCollaborator() {
Issue issue = new Issue(null, null, null, null, null,
Arrays.asList(new Label("required")), null, null);
OffsetDateTime requestTime = OffsetDateTime.now().minusDays(1);
given(this.gitHub.getEvents(issue)).willReturn(new StandardPage<>(
Arrays.asList(new Event("labeled", requestTime, new Label("required"))),
() -> null));
given(this.gitHub.getComments(issue)).willReturn(new StandardPage<>(
Arrays.asList(new Comment(new User("Amy"), OffsetDateTime.now())),
() -> null));
this.listener.onOpenIssue(issue);
verify(this.feedbackListener).feedbackRequired(issue, requestTime);
}
@Test
public void issueWithNoMatchingLabeledEventIsIgnored() {
Issue issue = new Issue(null, null, null, null, null,
Arrays.asList(new Label("required")), null, null);
OffsetDateTime requestTime = OffsetDateTime.now();
given(this.gitHub.getEvents(issue)).willReturn(new StandardPage<>(
Arrays.asList(
new Event("labeled", requestTime, new Label("something-else"))),
() -> null));
this.listener.onOpenIssue(issue);
verifyNoMoreInteractions(this.feedbackListener);
}
@Test
public void eventsWithWrongTypeAreIgnored() {
Issue issue = new Issue(null, null, null, null, null,
Arrays.asList(new Label("required")), null, null);
OffsetDateTime requestTime = OffsetDateTime.now();
given(this.gitHub.getEvents(issue)).willReturn(new StandardPage<>(
Arrays.asList(new Event("milestoned", requestTime, null)), () -> null));
this.listener.onOpenIssue(issue);
verifyNoMoreInteractions(this.feedbackListener);
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.spring.issuebot.feedback;
import java.time.OffsetDateTime;
import org.junit.Test;
import io.spring.issuebot.github.GitHubOperations;
import io.spring.issuebot.github.Issue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
/**
* Tests for {@link StandardFeedbackListener}.
*
* @author Andy Wilkinson
*/
public class StandardFeedbackListenerTests {
private final GitHubOperations gitHub = mock(GitHubOperations.class);
private final FeedbackListener listener = new StandardFeedbackListener(this.gitHub,
"provided", "required", "reminder", "closing");
private final Issue issue = new Issue(null, null, null, null, null, null, null, null);
@Test
public void feedbackProvided() {
this.listener.feedbackProvided(this.issue);
verify(this.gitHub).addLabel(this.issue, "provided");
verify(this.gitHub).removeLabel(this.issue, "required");
}
@Test
public void feedbackRequiredButReminderNotYetDue() {
this.listener.feedbackRequired(this.issue, OffsetDateTime.now());
verifyNoMoreInteractions(this.gitHub);
}
@Test
public void feedbackRequiredAndReminderDue() {
this.listener.feedbackRequired(this.issue, OffsetDateTime.now().minusDays(8));
verify(this.gitHub).addComment(this.issue, "reminder");
}
@Test
public void feedbackRequiredAndOverdue() {
this.listener.feedbackRequired(this.issue, OffsetDateTime.now().minusDays(15));
verify(this.gitHub).addComment(this.issue, "closing");
}
}

View File

@@ -16,7 +16,6 @@
package io.spring.issuebot.github;
import java.util.Arrays;
import java.util.Date;
import org.junit.Rule;
@@ -33,6 +32,7 @@ import org.springframework.test.web.client.response.DefaultResponseCreator;
import org.springframework.util.Base64Utils;
import org.springframework.web.client.RestTemplate;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.equalToIgnoringCase;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
@@ -121,8 +121,8 @@ public class GitHubTemplateTests {
this.server.expect(requestTo("commentsUrl")).andExpect(method(HttpMethod.GET))
.andExpect(basicAuth())
.andRespond(withSuccess("[]", MediaType.APPLICATION_JSON));
Page<Comment> comments = this.gitHub
.getComments(new Issue(null, "commentsUrl", null, null, null));
Page<Comment> comments = this.gitHub.getComments(
new Issue(null, "commentsUrl", null, null, null, null, null, null));
assertThat(comments.getContent().size(), is(0));
assertThat(comments.next(), is(nullValue()));
}
@@ -132,8 +132,8 @@ public class GitHubTemplateTests {
this.server.expect(requestTo("commentsUrl")).andExpect(method(HttpMethod.GET))
.andExpect(basicAuth())
.andRespond(withResource("comments-page-one.json"));
Page<Comment> comments = this.gitHub
.getComments(new Issue(null, "commentsUrl", null, null, null));
Page<Comment> comments = this.gitHub.getComments(
new Issue(null, "commentsUrl", null, null, null, null, null, null));
assertThat(comments.getContent().size(), is(17));
assertThat(comments.next(), is(nullValue()));
}
@@ -148,8 +148,8 @@ public class GitHubTemplateTests {
this.server.expect(requestTo("page-two")).andExpect(method(HttpMethod.GET))
.andExpect(basicAuth())
.andRespond(withResource("comments-page-two.json"));
Page<Comment> pageOne = this.gitHub
.getComments(new Issue(null, "commentsUrl", null, null, null));
Page<Comment> pageOne = this.gitHub.getComments(
new Issue(null, "commentsUrl", null, null, null, null, null, null));
assertThat(pageOne.getContent().size(), is(17));
Page<Comment> pageTwo = pageOne.next();
assertThat(pageTwo, is(not(nullValue())));
@@ -157,25 +157,75 @@ public class GitHubTemplateTests {
}
@Test
public void addLabelToUnlabelledIssue() {
public void addLabelToIssue() {
this.server.expect(requestTo("issueUrl")).andExpect(method(HttpMethod.POST))
.andExpect(basicAuth())
.andExpect(content().string("{\"labels\":[\"test\"]}"))
.andRespond(withResource("issue-single-label.json"));
Issue issue = new Issue("issueUrl", null, null, Arrays.asList(), null);
Issue labelledIssue = this.gitHub.addLabel(issue, "test");
assertThat(labelledIssue.getLabels(), hasSize(1));
.andExpect(content().string("{\"labels\":[\"test\"]}")).andRespond(
withSuccess("[{\"name\":\"test\"}]", MediaType.APPLICATION_JSON));
Issue issue = new Issue("issueUrl", null, null, null, null, null, null, null);
Issue modifiedIssue = this.gitHub.addLabel(issue, "test");
assertThat(modifiedIssue.getLabels(), hasSize(1));
}
@Test
public void addAdditionalLabelToIssue() {
this.server.expect(requestTo("issueUrl")).andExpect(method(HttpMethod.POST))
public void removeLabelFromIssue() {
this.server.expect(requestTo("labels/test")).andExpect(method(HttpMethod.DELETE))
.andExpect(basicAuth())
.andExpect(content().string("{\"labels\":[\"test\"]}"))
.andRespond(withResource("issue-two-labels.json"));
Issue issue = new Issue("issueUrl", null, null, Arrays.asList(), null);
Issue labelledIssue = this.gitHub.addLabel(issue, "test");
assertThat(labelledIssue.getLabels(), hasSize(2));
.andRespond(withSuccess("[]", MediaType.APPLICATION_JSON));
Issue issue = new Issue(null, null, null, "labels{/name}", null, null, null,
null);
Issue modifiedIssue = this.gitHub.removeLabel(issue, "test");
assertThat(modifiedIssue.getLabels(), hasSize(0));
}
@Test
public void addCommentToIssue() {
this.server.expect(requestTo("commentsUrl")).andExpect(method(HttpMethod.POST))
.andExpect(basicAuth())
.andExpect(content().string("{\"body\":\"A test comment\"}"))
.andRespond(withResource("new-comment.json"));
Issue issue = new Issue(null, "commentsUrl", null, null, null, null, null, null);
Comment comment = this.gitHub.addComment(issue, "A test comment");
assertThat(comment, is(not(nullValue())));
}
@Test
public void singlePageOfEvents() {
this.server.expect(requestTo("eventsUrl")).andExpect(method(HttpMethod.GET))
.andExpect(basicAuth()).andRespond(withResource("events-page-one.json"));
Page<Event> events = this.gitHub.getEvents(
new Issue(null, null, "eventsUrl", null, null, null, null, null));
assertThat(events.getContent().size(), is(12));
assertThat(events.next(), is(nullValue()));
}
@Test
public void multiplePagesOfEvents() {
HttpHeaders headers = new HttpHeaders();
headers.set("Link", "<page-two>; rel=\"next\"");
this.server.expect(requestTo("eventsUrl")).andExpect(method(HttpMethod.GET))
.andExpect(basicAuth()).andRespond(withResource("events-page-one.json",
"Link:<page-two>; rel=\"next\""));
this.server.expect(requestTo("page-two")).andExpect(method(HttpMethod.GET))
.andExpect(basicAuth()).andRespond(withResource("events-page-two.json"));
Page<Event> pageOne = this.gitHub.getEvents(
new Issue(null, null, "eventsUrl", null, null, null, null, null));
assertThat(pageOne.getContent().size(), is(12));
Page<Event> pageTwo = pageOne.next();
assertThat(pageTwo, is(not(nullValue())));
assertThat(pageTwo.getContent().size(), is(3));
}
@Test
public void closeIssue() {
this.server.expect(requestTo("issueUrl")).andExpect(method(HttpMethod.PATCH))
.andExpect(basicAuth())
.andExpect(content().string("{\"state\":\"closed\"}"))
.andRespond(withSuccess("{\"url\":\"updatedIssueUrl\"}",
MediaType.APPLICATION_JSON));
Issue closedIssue = this.gitHub
.close(new Issue("issueUrl", null, null, null, null, null, null, null));
assertThat(closedIssue.getUrl(), is(equalTo("updatedIssueUrl")));
}
private DefaultResponseCreator withResource(String resource, String... headers) {

View File

@@ -45,7 +45,7 @@ public class CommentedByCollaboratorTriageFilterTests {
private final TriageFilter filter = new CommentedByCollaboratorTriageFilter(
Arrays.asList("Adam", "Brenda", "Charlie"), this.gitHub);
private final Issue issue = new Issue(null, null, null, null, null);
private final Issue issue = new Issue(null, null, null, null, null, null, null, null);
@Test
@SuppressWarnings("unchecked")
@@ -61,7 +61,7 @@ public class CommentedByCollaboratorTriageFilterTests {
public void noCommentsByCollaborators() {
Page<Comment> pageOne = mock(Page.class);
given(pageOne.getContent())
.willReturn(Arrays.asList(new Comment(new User("Debbie"))));
.willReturn(Arrays.asList(new Comment(new User("Debbie"), null)));
given(this.gitHub.getComments(this.issue)).willReturn(pageOne);
assertThat(this.filter.triaged(this.issue), is(false));
}
@@ -71,7 +71,7 @@ public class CommentedByCollaboratorTriageFilterTests {
public void commentByCollaboratorOnFirstPage() {
Page<Comment> pageOne = mock(Page.class);
given(pageOne.getContent())
.willReturn(Arrays.asList(new Comment(new User("Brenda"))));
.willReturn(Arrays.asList(new Comment(new User("Brenda"), null)));
given(this.gitHub.getComments(this.issue)).willReturn(pageOne);
assertThat(this.filter.triaged(this.issue), is(true));
}
@@ -81,10 +81,10 @@ public class CommentedByCollaboratorTriageFilterTests {
public void commentByCollaboratorOnLaterPage() {
Page<Comment> pageOne = mock(Page.class);
given(pageOne.getContent())
.willReturn(Arrays.asList(new Comment(new User("Debbie"))));
.willReturn(Arrays.asList(new Comment(new User("Debbie"), null)));
Page<Comment> pageTwo = mock(Page.class);
given(pageTwo.getContent())
.willReturn(Arrays.asList(new Comment(new User("Brenda"))));
.willReturn(Arrays.asList(new Comment(new User("Brenda"), null)));
given(pageOne.next()).willReturn(pageTwo);
given(this.gitHub.getComments(this.issue)).willReturn(pageOne);
assertThat(this.filter.triaged(this.issue), is(true));

View File

@@ -38,7 +38,7 @@ public class LabelApplyingTriageListenerTests {
@Test
public void requiresTriage() {
Issue issue = new Issue(null, null, null, null, null);
Issue issue = new Issue(null, null, null, null, null, null, null, null);
this.listener.requiresTriage(issue);
verify(this.gitHub).addLabel(issue, "test");
}

View File

@@ -38,23 +38,22 @@ public class LabelledTriageFilterTests {
@Test
public void issueWithLabels() {
assertThat(this.filter.triaged(
new Issue(null, null, null, Arrays.asList(new Label("test")), null)),
is(true));
assertThat(this.filter.triaged(new Issue(null, null, null, null, null,
Arrays.asList(new Label("test")), null, null)), is(true));
}
@Test
public void issueWithNullLabels() {
assertThat(this.filter.triaged(new Issue(null, null, null, null, null)),
assertThat(
this.filter.triaged(
new Issue(null, null, null, null, null, null, null, null)),
is(false));
}
@Test
public void issueWithNoLabels() {
assertThat(
this.filter.triaged(
new Issue(null, null, null, Collections.emptyList(), null)),
is(false));
assertThat(this.filter.triaged(new Issue(null, null, null, null, null,
Collections.emptyList(), null, null)), is(false));
}
}

View File

@@ -36,15 +36,15 @@ public class MilestoneAppliedTriageFilterTests {
@Test
public void issueWithMilestoneApplied() {
assertThat(
this.filter.triaged(
new Issue(null, null, null, null, new Milestone("test"))),
is(true));
assertThat(this.filter.triaged(new Issue(null, null, null, null, null, null,
new Milestone("test"), null)), is(true));
}
@Test
public void issueWithNoMilestoneApplied() {
assertThat(this.filter.triaged(new Issue(null, null, null, null, null)),
assertThat(
this.filter.triaged(
new Issue(null, null, null, null, null, null, null, null)),
is(false));
}

View File

@@ -38,16 +38,15 @@ public class OpenedByCollaboratorTriageFilterTests {
@Test
public void openedByCollaborator() {
assertThat(
this.filter.triaged(new Issue(null, null, new User("Adam"), null, null)),
assertThat(this.filter.triaged(
new Issue(null, null, null, null, new User("Adam"), null, null, null)),
is(true));
}
@Test
public void openedByAnotherUser() {
assertThat(
this.filter
.triaged(new Issue(null, null, new User("Debbie"), null, null)),
assertThat(this.filter.triaged(
new Issue(null, null, null, null, new User("Debbie"), null, null, null)),
is(false));
}

View File

@@ -46,7 +46,7 @@ public class TriageIssueListenerTests {
@Test
public void listenerIsCalledWhenIssueRequiresTriage() {
Issue issue = new Issue(null, null, null, null, null);
Issue issue = new Issue(null, null, null, null, null, null, null, null);
given(this.triageFilterOne.triaged(issue)).willReturn(false);
given(this.triageFilterTwo.triaged(issue)).willReturn(false);
this.issueListener.onOpenIssue(issue);
@@ -57,7 +57,7 @@ public class TriageIssueListenerTests {
@Test
public void listenerIsNotCalledWhenIssueHasAlreadyBeenTriaged() {
Issue issue = new Issue(null, null, null, null, null);
Issue issue = new Issue(null, null, null, null, null, null, null, null);
given(this.triageFilterOne.triaged(issue)).willReturn(true);
this.issueListener.onOpenIssue(issue);
verify(this.triageFilterOne).triaged(issue);

View File

@@ -0,0 +1,360 @@
[
{
"id": 465099546,
"url": "https://api.github.com/repos/spring-projects/spring-boot/issues/events/465099546",
"actor": {
"login": "rwinch",
"id": 362503,
"avatar_url": "https://avatars.githubusercontent.com/u/362503?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/rwinch",
"html_url": "https://github.com/rwinch",
"followers_url": "https://api.github.com/users/rwinch/followers",
"following_url": "https://api.github.com/users/rwinch/following{/other_user}",
"gists_url": "https://api.github.com/users/rwinch/gists{/gist_id}",
"starred_url": "https://api.github.com/users/rwinch/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/rwinch/subscriptions",
"organizations_url": "https://api.github.com/users/rwinch/orgs",
"repos_url": "https://api.github.com/users/rwinch/repos",
"events_url": "https://api.github.com/users/rwinch/events{/privacy}",
"received_events_url": "https://api.github.com/users/rwinch/received_events",
"type": "User",
"site_admin": false
},
"event": "mentioned",
"commit_id": null,
"commit_url": null,
"created_at": "2015-11-16T06:53:18Z"
},
{
"id": 465099547,
"url": "https://api.github.com/repos/spring-projects/spring-boot/issues/events/465099547",
"actor": {
"login": "rwinch",
"id": 362503,
"avatar_url": "https://avatars.githubusercontent.com/u/362503?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/rwinch",
"html_url": "https://github.com/rwinch",
"followers_url": "https://api.github.com/users/rwinch/followers",
"following_url": "https://api.github.com/users/rwinch/following{/other_user}",
"gists_url": "https://api.github.com/users/rwinch/gists{/gist_id}",
"starred_url": "https://api.github.com/users/rwinch/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/rwinch/subscriptions",
"organizations_url": "https://api.github.com/users/rwinch/orgs",
"repos_url": "https://api.github.com/users/rwinch/repos",
"events_url": "https://api.github.com/users/rwinch/events{/privacy}",
"received_events_url": "https://api.github.com/users/rwinch/received_events",
"type": "User",
"site_admin": false
},
"event": "subscribed",
"commit_id": null,
"commit_url": null,
"created_at": "2015-11-16T06:53:18Z"
},
{
"id": 465559518,
"url": "https://api.github.com/repos/spring-projects/spring-boot/issues/events/465559518",
"actor": {
"login": "cemo",
"id": 44156,
"avatar_url": "https://avatars.githubusercontent.com/u/44156?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/cemo",
"html_url": "https://github.com/cemo",
"followers_url": "https://api.github.com/users/cemo/followers",
"following_url": "https://api.github.com/users/cemo/following{/other_user}",
"gists_url": "https://api.github.com/users/cemo/gists{/gist_id}",
"starred_url": "https://api.github.com/users/cemo/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/cemo/subscriptions",
"organizations_url": "https://api.github.com/users/cemo/orgs",
"repos_url": "https://api.github.com/users/cemo/repos",
"events_url": "https://api.github.com/users/cemo/events{/privacy}",
"received_events_url": "https://api.github.com/users/cemo/received_events",
"type": "User",
"site_admin": false
},
"event": "mentioned",
"commit_id": null,
"commit_url": null,
"created_at": "2015-11-16T15:39:55Z"
},
{
"id": 465559520,
"url": "https://api.github.com/repos/spring-projects/spring-boot/issues/events/465559520",
"actor": {
"login": "cemo",
"id": 44156,
"avatar_url": "https://avatars.githubusercontent.com/u/44156?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/cemo",
"html_url": "https://github.com/cemo",
"followers_url": "https://api.github.com/users/cemo/followers",
"following_url": "https://api.github.com/users/cemo/following{/other_user}",
"gists_url": "https://api.github.com/users/cemo/gists{/gist_id}",
"starred_url": "https://api.github.com/users/cemo/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/cemo/subscriptions",
"organizations_url": "https://api.github.com/users/cemo/orgs",
"repos_url": "https://api.github.com/users/cemo/repos",
"events_url": "https://api.github.com/users/cemo/events{/privacy}",
"received_events_url": "https://api.github.com/users/cemo/received_events",
"type": "User",
"site_admin": false
},
"event": "subscribed",
"commit_id": null,
"commit_url": null,
"created_at": "2015-11-16T15:39:55Z"
},
{
"id": 466343988,
"url": "https://api.github.com/repos/spring-projects/spring-boot/issues/events/466343988",
"actor": {
"login": "rwinch",
"id": 362503,
"avatar_url": "https://avatars.githubusercontent.com/u/362503?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/rwinch",
"html_url": "https://github.com/rwinch",
"followers_url": "https://api.github.com/users/rwinch/followers",
"following_url": "https://api.github.com/users/rwinch/following{/other_user}",
"gists_url": "https://api.github.com/users/rwinch/gists{/gist_id}",
"starred_url": "https://api.github.com/users/rwinch/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/rwinch/subscriptions",
"organizations_url": "https://api.github.com/users/rwinch/orgs",
"repos_url": "https://api.github.com/users/rwinch/repos",
"events_url": "https://api.github.com/users/rwinch/events{/privacy}",
"received_events_url": "https://api.github.com/users/rwinch/received_events",
"type": "User",
"site_admin": false
},
"event": "mentioned",
"commit_id": null,
"commit_url": null,
"created_at": "2015-11-17T06:35:03Z"
},
{
"id": 466343989,
"url": "https://api.github.com/repos/spring-projects/spring-boot/issues/events/466343989",
"actor": {
"login": "rwinch",
"id": 362503,
"avatar_url": "https://avatars.githubusercontent.com/u/362503?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/rwinch",
"html_url": "https://github.com/rwinch",
"followers_url": "https://api.github.com/users/rwinch/followers",
"following_url": "https://api.github.com/users/rwinch/following{/other_user}",
"gists_url": "https://api.github.com/users/rwinch/gists{/gist_id}",
"starred_url": "https://api.github.com/users/rwinch/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/rwinch/subscriptions",
"organizations_url": "https://api.github.com/users/rwinch/orgs",
"repos_url": "https://api.github.com/users/rwinch/repos",
"events_url": "https://api.github.com/users/rwinch/events{/privacy}",
"received_events_url": "https://api.github.com/users/rwinch/received_events",
"type": "User",
"site_admin": false
},
"event": "subscribed",
"commit_id": null,
"commit_url": null,
"created_at": "2015-11-17T06:35:03Z"
},
{
"id": 473947325,
"url": "https://api.github.com/repos/spring-projects/spring-boot/issues/events/473947325",
"actor": {
"login": "philwebb",
"id": 519772,
"avatar_url": "https://avatars.githubusercontent.com/u/519772?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/philwebb",
"html_url": "https://github.com/philwebb",
"followers_url": "https://api.github.com/users/philwebb/followers",
"following_url": "https://api.github.com/users/philwebb/following{/other_user}",
"gists_url": "https://api.github.com/users/philwebb/gists{/gist_id}",
"starred_url": "https://api.github.com/users/philwebb/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/philwebb/subscriptions",
"organizations_url": "https://api.github.com/users/philwebb/orgs",
"repos_url": "https://api.github.com/users/philwebb/repos",
"events_url": "https://api.github.com/users/philwebb/events{/privacy}",
"received_events_url": "https://api.github.com/users/philwebb/received_events",
"type": "User",
"site_admin": false
},
"event": "milestoned",
"commit_id": null,
"commit_url": null,
"created_at": "2015-11-25T00:07:11Z",
"milestone": {
"title": "1.3.1"
}
},
{
"id": 473947414,
"url": "https://api.github.com/repos/spring-projects/spring-boot/issues/events/473947414",
"actor": {
"login": "philwebb",
"id": 519772,
"avatar_url": "https://avatars.githubusercontent.com/u/519772?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/philwebb",
"html_url": "https://github.com/philwebb",
"followers_url": "https://api.github.com/users/philwebb/followers",
"following_url": "https://api.github.com/users/philwebb/following{/other_user}",
"gists_url": "https://api.github.com/users/philwebb/gists{/gist_id}",
"starred_url": "https://api.github.com/users/philwebb/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/philwebb/subscriptions",
"organizations_url": "https://api.github.com/users/philwebb/orgs",
"repos_url": "https://api.github.com/users/philwebb/repos",
"events_url": "https://api.github.com/users/philwebb/events{/privacy}",
"received_events_url": "https://api.github.com/users/philwebb/received_events",
"type": "User",
"site_admin": false
},
"event": "labeled",
"commit_id": null,
"commit_url": null,
"created_at": "2015-11-25T00:07:21Z",
"label": {
"name": "for team discussion",
"color": "006b75"
}
},
{
"id": 474642336,
"url": "https://api.github.com/repos/spring-projects/spring-boot/issues/events/474642336",
"actor": {
"login": "philwebb",
"id": 519772,
"avatar_url": "https://avatars.githubusercontent.com/u/519772?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/philwebb",
"html_url": "https://github.com/philwebb",
"followers_url": "https://api.github.com/users/philwebb/followers",
"following_url": "https://api.github.com/users/philwebb/following{/other_user}",
"gists_url": "https://api.github.com/users/philwebb/gists{/gist_id}",
"starred_url": "https://api.github.com/users/philwebb/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/philwebb/subscriptions",
"organizations_url": "https://api.github.com/users/philwebb/orgs",
"repos_url": "https://api.github.com/users/philwebb/repos",
"events_url": "https://api.github.com/users/philwebb/events{/privacy}",
"received_events_url": "https://api.github.com/users/philwebb/received_events",
"type": "User",
"site_admin": false
},
"event": "unlabeled",
"commit_id": null,
"commit_url": null,
"created_at": "2015-11-25T15:41:39Z",
"label": {
"name": "for team discussion",
"color": "006b75"
}
},
{
"id": 474642710,
"url": "https://api.github.com/repos/spring-projects/spring-boot/issues/events/474642710",
"actor": {
"login": "wilkinsona",
"id": 914682,
"avatar_url": "https://avatars.githubusercontent.com/u/914682?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/wilkinsona",
"html_url": "https://github.com/wilkinsona",
"followers_url": "https://api.github.com/users/wilkinsona/followers",
"following_url": "https://api.github.com/users/wilkinsona/following{/other_user}",
"gists_url": "https://api.github.com/users/wilkinsona/gists{/gist_id}",
"starred_url": "https://api.github.com/users/wilkinsona/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/wilkinsona/subscriptions",
"organizations_url": "https://api.github.com/users/wilkinsona/orgs",
"repos_url": "https://api.github.com/users/wilkinsona/repos",
"events_url": "https://api.github.com/users/wilkinsona/events{/privacy}",
"received_events_url": "https://api.github.com/users/wilkinsona/received_events",
"type": "User",
"site_admin": false
},
"event": "labeled",
"commit_id": null,
"commit_url": null,
"created_at": "2015-11-25T15:41:57Z",
"label": {
"name": "enhancement",
"color": "84b6eb"
}
},
{
"id": 479204419,
"url": "https://api.github.com/repos/spring-projects/spring-boot/issues/events/479204419",
"actor": {
"login": "wilkinsona",
"id": 914682,
"avatar_url": "https://avatars.githubusercontent.com/u/914682?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/wilkinsona",
"html_url": "https://github.com/wilkinsona",
"followers_url": "https://api.github.com/users/wilkinsona/followers",
"following_url": "https://api.github.com/users/wilkinsona/following{/other_user}",
"gists_url": "https://api.github.com/users/wilkinsona/gists{/gist_id}",
"starred_url": "https://api.github.com/users/wilkinsona/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/wilkinsona/subscriptions",
"organizations_url": "https://api.github.com/users/wilkinsona/orgs",
"repos_url": "https://api.github.com/users/wilkinsona/repos",
"events_url": "https://api.github.com/users/wilkinsona/events{/privacy}",
"received_events_url": "https://api.github.com/users/wilkinsona/received_events",
"type": "User",
"site_admin": false
},
"event": "assigned",
"commit_id": null,
"commit_url": null,
"created_at": "2015-12-01T17:13:26Z",
"assignee": {
"login": "wilkinsona",
"id": 914682,
"avatar_url": "https://avatars.githubusercontent.com/u/914682?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/wilkinsona",
"html_url": "https://github.com/wilkinsona",
"followers_url": "https://api.github.com/users/wilkinsona/followers",
"following_url": "https://api.github.com/users/wilkinsona/following{/other_user}",
"gists_url": "https://api.github.com/users/wilkinsona/gists{/gist_id}",
"starred_url": "https://api.github.com/users/wilkinsona/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/wilkinsona/subscriptions",
"organizations_url": "https://api.github.com/users/wilkinsona/orgs",
"repos_url": "https://api.github.com/users/wilkinsona/repos",
"events_url": "https://api.github.com/users/wilkinsona/events{/privacy}",
"received_events_url": "https://api.github.com/users/wilkinsona/received_events",
"type": "User",
"site_admin": false
}
},
{
"id": 479248577,
"url": "https://api.github.com/repos/spring-projects/spring-boot/issues/events/479248577",
"actor": {
"login": "wilkinsona",
"id": 914682,
"avatar_url": "https://avatars.githubusercontent.com/u/914682?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/wilkinsona",
"html_url": "https://github.com/wilkinsona",
"followers_url": "https://api.github.com/users/wilkinsona/followers",
"following_url": "https://api.github.com/users/wilkinsona/following{/other_user}",
"gists_url": "https://api.github.com/users/wilkinsona/gists{/gist_id}",
"starred_url": "https://api.github.com/users/wilkinsona/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/wilkinsona/subscriptions",
"organizations_url": "https://api.github.com/users/wilkinsona/orgs",
"repos_url": "https://api.github.com/users/wilkinsona/repos",
"events_url": "https://api.github.com/users/wilkinsona/events{/privacy}",
"received_events_url": "https://api.github.com/users/wilkinsona/received_events",
"type": "User",
"site_admin": false
},
"event": "closed",
"commit_id": "524a32879fea9179e9ba51c2c3a4ce031cdf8a13",
"commit_url": "https://api.github.com/repos/spring-projects/spring-boot/commits/524a32879fea9179e9ba51c2c3a4ce031cdf8a13",
"created_at": "2015-12-01T17:53:40Z"
}
]

View File

@@ -0,0 +1,83 @@
[
{
"id": 480011364,
"url": "https://api.github.com/repos/spring-projects/spring-boot/issues/events/480011364",
"actor": {
"login": "wilkinsona",
"id": 914682,
"avatar_url": "https://avatars.githubusercontent.com/u/914682?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/wilkinsona",
"html_url": "https://github.com/wilkinsona",
"followers_url": "https://api.github.com/users/wilkinsona/followers",
"following_url": "https://api.github.com/users/wilkinsona/following{/other_user}",
"gists_url": "https://api.github.com/users/wilkinsona/gists{/gist_id}",
"starred_url": "https://api.github.com/users/wilkinsona/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/wilkinsona/subscriptions",
"organizations_url": "https://api.github.com/users/wilkinsona/orgs",
"repos_url": "https://api.github.com/users/wilkinsona/repos",
"events_url": "https://api.github.com/users/wilkinsona/events{/privacy}",
"received_events_url": "https://api.github.com/users/wilkinsona/received_events",
"type": "User",
"site_admin": false
},
"event": "reopened",
"commit_id": null,
"commit_url": null,
"created_at": "2015-12-02T10:27:17Z"
},
{
"id": 480034760,
"url": "https://api.github.com/repos/spring-projects/spring-boot/issues/events/480034760",
"actor": {
"login": "wilkinsona",
"id": 914682,
"avatar_url": "https://avatars.githubusercontent.com/u/914682?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/wilkinsona",
"html_url": "https://github.com/wilkinsona",
"followers_url": "https://api.github.com/users/wilkinsona/followers",
"following_url": "https://api.github.com/users/wilkinsona/following{/other_user}",
"gists_url": "https://api.github.com/users/wilkinsona/gists{/gist_id}",
"starred_url": "https://api.github.com/users/wilkinsona/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/wilkinsona/subscriptions",
"organizations_url": "https://api.github.com/users/wilkinsona/orgs",
"repos_url": "https://api.github.com/users/wilkinsona/repos",
"events_url": "https://api.github.com/users/wilkinsona/events{/privacy}",
"received_events_url": "https://api.github.com/users/wilkinsona/received_events",
"type": "User",
"site_admin": false
},
"event": "closed",
"commit_id": "f47449c800011fbd6844774be12d0a381d1bd020",
"commit_url": "https://api.github.com/repos/spring-projects/spring-boot/commits/f47449c800011fbd6844774be12d0a381d1bd020",
"created_at": "2015-12-02T10:52:07Z"
},
{
"id": 480202795,
"url": "https://api.github.com/repos/spring-projects/spring-boot/issues/events/480202795",
"actor": {
"login": "wilkinsona",
"id": 914682,
"avatar_url": "https://avatars.githubusercontent.com/u/914682?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/wilkinsona",
"html_url": "https://github.com/wilkinsona",
"followers_url": "https://api.github.com/users/wilkinsona/followers",
"following_url": "https://api.github.com/users/wilkinsona/following{/other_user}",
"gists_url": "https://api.github.com/users/wilkinsona/gists{/gist_id}",
"starred_url": "https://api.github.com/users/wilkinsona/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/wilkinsona/subscriptions",
"organizations_url": "https://api.github.com/users/wilkinsona/orgs",
"repos_url": "https://api.github.com/users/wilkinsona/repos",
"events_url": "https://api.github.com/users/wilkinsona/events{/privacy}",
"received_events_url": "https://api.github.com/users/wilkinsona/received_events",
"type": "User",
"site_admin": false
},
"event": "referenced",
"commit_id": "ffd6e8d7eb944d8a3613e40e6aa1f6aa57127046",
"commit_url": "https://api.github.com/repos/spring-projects/spring-boot/commits/ffd6e8d7eb944d8a3613e40e6aa1f6aa57127046",
"created_at": "2015-12-02T14:02:24Z"
}
]

View File

@@ -0,0 +1,28 @@
{
"url": "https://api.github.com/repos/wilkinsona/issue-bot-test/issues/comments/162885552",
"html_url": "https://github.com/wilkinsona/issue-bot-test/issues/1#issuecomment-162885552",
"issue_url": "https://api.github.com/repos/wilkinsona/issue-bot-test/issues/1",
"id": 162885552,
"user": {
"login": "wilkinsona",
"id": 914682,
"avatar_url": "https://avatars.githubusercontent.com/u/914682?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/wilkinsona",
"html_url": "https://github.com/wilkinsona",
"followers_url": "https://api.github.com/users/wilkinsona/followers",
"following_url": "https://api.github.com/users/wilkinsona/following{/other_user}",
"gists_url": "https://api.github.com/users/wilkinsona/gists{/gist_id}",
"starred_url": "https://api.github.com/users/wilkinsona/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/wilkinsona/subscriptions",
"organizations_url": "https://api.github.com/users/wilkinsona/orgs",
"repos_url": "https://api.github.com/users/wilkinsona/repos",
"events_url": "https://api.github.com/users/wilkinsona/events{/privacy}",
"received_events_url": "https://api.github.com/users/wilkinsona/received_events",
"type": "User",
"site_admin": false
},
"created_at": "2015-12-08T13:53:45Z",
"updated_at": "2015-12-08T13:53:45Z",
"body": "A test comment"
}