Add support for monitoring multiple repositories

This commit is contained in:
Andy Wilkinson
2018-09-06 16:19:19 +01:00
parent f0e89f901e
commit 54cf663e43
12 changed files with 331 additions and 165 deletions

View File

@@ -16,8 +16,6 @@
package io.spring.issuebot;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
@@ -29,20 +27,9 @@ import org.springframework.boot.context.properties.NestedConfigurationProperty;
@ConfigurationProperties(prefix = "issuebot.github")
public class GitHubProperties {
@NestedConfigurationProperty
private Repository repository = new Repository();
@NestedConfigurationProperty
private Credentials credentials = new Credentials();
public Repository getRepository() {
return this.repository;
}
public void setRepository(Repository repository) {
this.repository = repository;
}
public Credentials getCredentials() {
return this.credentials;
}
@@ -51,52 +38,6 @@ public class GitHubProperties {
this.credentials = credentials;
}
/**
* Configuration for a GitHub repository.
*/
public static class Repository {
/**
* The name of the organization that owns the repository.
*/
private String organization;
/**
* The name of the repository.
*/
private String name;
/**
* The names of the repository's collaborators.
*/
private List<String> collaborators;
public String getOrganization() {
return this.organization;
}
public void setOrganization(String organization) {
this.organization = organization;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public List<String> getCollaborators() {
return this.collaborators;
}
public void setCollaborators(List<String> collaborators) {
this.collaborators = collaborators;
}
}
/**
* Configuration for the credentials used to authenticate with GitHub.
*/

View File

@@ -35,7 +35,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
*/
@SpringBootApplication
@EnableScheduling
@EnableConfigurationProperties(GitHubProperties.class)
@EnableConfigurationProperties({ GitHubProperties.class, MonitoringProperties.class })
public class IssueBotApplication {
public static void main(String[] args) {
@@ -50,11 +50,9 @@ public class IssueBotApplication {
@Bean
RepositoryMonitor repositoryMonitor(GitHubOperations gitHub,
GitHubProperties gitHubProperties, List<IssueListener> issueListeners) {
return new RepositoryMonitor(gitHub,
new MonitoredRepository(
gitHubProperties.getRepository().getOrganization(),
gitHubProperties.getRepository().getName()),
MonitoringProperties monitoringProperties,
List<MultiRepositoryIssueListener> issueListeners) {
return new RepositoryMonitor(gitHub, monitoringProperties.getRepositories(),
issueListeners);
}

View File

@@ -19,7 +19,8 @@ package io.spring.issuebot;
import io.spring.issuebot.github.Issue;
/**
* An {@code IssueListener} is notified of issues found during repository monitoring.
* An {@code IssueListener} is notified of issues found during monitoring of a specific
* repository.
*
* @author Andy Wilkinson
*/

View File

@@ -1,49 +0,0 @@
/*
* Copyright 2015-2018 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;
/**
* A repository that should be monitored.
*
* @author Andy Wilkinson
*/
public class MonitoredRepository {
/**
* The name of the organization that owns the repository.
*/
private final String organization;
/**
* The name of the repository.
*/
private final String name;
public MonitoredRepository(String organization, String name) {
this.organization = organization;
this.name = name;
}
public String getOrganization() {
return this.organization;
}
public String getName() {
return this.name;
}
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2015-2018 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;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Properties for configuring repository monitoring.
*
* @author Andy Wilkinson
*/
@ConfigurationProperties(prefix = "issuebot.monitoring")
public class MonitoringProperties {
private List<Repository> repositories;
public List<Repository> getRepositories() {
return this.repositories;
}
public void setRepositories(List<Repository> repositories) {
this.repositories = repositories;
}
/**
* A repository that is monitored.
*/
public static class Repository {
/**
* The name of the organization that owns the repository.
*/
private String organization;
/**
* The name of the repository.
*/
private String name;
/**
* The names of the repository's collaborators.
*/
private List<String> collaborators;
public String getOrganization() {
return this.organization;
}
public void setOrganization(String organization) {
this.organization = organization;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public List<String> getCollaborators() {
return this.collaborators;
}
public void setCollaborators(List<String> collaborators) {
this.collaborators = collaborators;
}
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2015-2018 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;
import io.spring.issuebot.MonitoringProperties.Repository;
import io.spring.issuebot.github.Issue;
/**
* An {@code IssueListener} is notified of issues found during monitoring of all
* repositories.
*
* @author Andy Wilkinson
*/
public interface MultiRepositoryIssueListener {
/**
* Notification that, in the given {@code repository}, the given {@code issue} is
* open.
* @param repository the repository to which the issue belongs
* @param issue the open issue
*/
default void onOpenIssue(Repository repository, Issue issue) {
}
/**
* Notification that, in the give {@code repository}, the given {@code issue} is being
* closed.
* @param repository the repository to which the issue belongs
* @param issue the issue that is being closed
*/
default void onIssueClosure(Repository repository, Issue issue) {
}
}

View File

@@ -18,6 +18,7 @@ package io.spring.issuebot;
import java.util.List;
import io.spring.issuebot.MonitoringProperties.Repository;
import io.spring.issuebot.github.GitHubOperations;
import io.spring.issuebot.github.Issue;
import io.spring.issuebot.github.Page;
@@ -37,29 +38,34 @@ class RepositoryMonitor {
private final GitHubOperations gitHub;
private final MonitoredRepository repository;
private final List<Repository> repositories;
private final List<IssueListener> issueListeners;
private final List<MultiRepositoryIssueListener> issueListeners;
RepositoryMonitor(GitHubOperations gitHub, MonitoredRepository repository,
List<IssueListener> issueListeners) {
RepositoryMonitor(GitHubOperations gitHub, List<Repository> repositories,
List<MultiRepositoryIssueListener> issueListeners) {
this.gitHub = gitHub;
this.repository = repository;
this.repositories = repositories;
this.issueListeners = issueListeners;
}
@Scheduled(fixedRate = 5 * 60 * 1000)
void monitor() {
log.info("Monitoring {}/{}", this.repository.getOrganization(),
this.repository.getName());
for (Repository repository : this.repositories) {
monitor(repository);
}
}
private void monitor(Repository repository) {
log.info("Monitoring {}/{}", repository.getOrganization(), repository.getName());
try {
Page<Issue> page = this.gitHub.getIssues(this.repository.getOrganization(),
this.repository.getName());
Page<Issue> page = this.gitHub.getIssues(repository.getOrganization(),
repository.getName());
while (page != null) {
for (Issue issue : page.getContent()) {
for (IssueListener issueListener : this.issueListeners) {
for (MultiRepositoryIssueListener issueListener : this.issueListeners) {
try {
issueListener.onOpenIssue(issue);
issueListener.onOpenIssue(repository, issue);
}
catch (Exception ex) {
log.warn("Listener '{}' failed when handling issue '{}'",
@@ -71,10 +77,11 @@ class RepositoryMonitor {
}
}
catch (Exception ex) {
log.warn("A failure occurred during issue monitoring", ex);
log.warn("A failure occurred during monitoring of {}/{}",
repository.getOrganization(), repository.getName(), ex);
}
log.info("Monitoring of {}/{} completed", this.repository.getOrganization(),
this.repository.getName());
log.info("Monitoring of {}/{} completed", repository.getOrganization(),
repository.getName());
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2015-2018 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;
import java.util.Map;
import io.spring.issuebot.MonitoringProperties.Repository;
import io.spring.issuebot.github.Issue;
/**
* A {@code MultiRepositoryIssueListener} that performs repository-based routing to
* specific {@link IssueListener IssueListeners}.
*
* @author Andy Wilkinson
*/
public class RoutingMultiRepositoryIssueListener implements MultiRepositoryIssueListener {
private final Map<Repository, IssueListener> delegates;
public RoutingMultiRepositoryIssueListener(Map<Repository, IssueListener> delegates) {
this.delegates = delegates;
}
@Override
public void onOpenIssue(Repository repository, Issue issue) {
IssueListener listener = this.delegates.get(repository);
if (listener != null) {
listener.onOpenIssue(issue);
}
}
@Override
public void onIssueClosure(Repository repository, Issue issue) {
IssueListener listener = this.delegates.get(repository);
if (listener != null) {
listener.onIssueClosure(issue);
}
}
}

View File

@@ -16,10 +16,17 @@
package io.spring.issuebot.feedback;
import java.util.List;
import java.util.Collections;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import io.spring.issuebot.GitHubProperties;
import io.spring.issuebot.IssueListener;
import io.spring.issuebot.MonitoringProperties;
import io.spring.issuebot.MonitoringProperties.Repository;
import io.spring.issuebot.MultiRepositoryIssueListener;
import io.spring.issuebot.RoutingMultiRepositoryIssueListener;
import io.spring.issuebot.github.GitHubOperations;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -37,18 +44,29 @@ import org.springframework.context.annotation.Configuration;
class FeedbackConfiguration {
@Bean
FeedbackIssueListener feedbackIssueListener(GitHubOperations gitHub,
GitHubProperties githubProperties, FeedbackProperties feedbackProperties,
List<IssueListener> issueListener) {
MultiRepositoryIssueListener feedbackIssueListener(
MonitoringProperties monitoringProperties, GitHubOperations gitHub,
GitHubProperties githubProperties, FeedbackProperties feedbackProperties) {
Map<Repository, IssueListener> delegates = monitoringProperties.getRepositories()
.stream()
.collect(Collectors.toMap(Function.identity(),
(repository) -> createListener(repository, gitHub,
githubProperties, feedbackProperties)));
return new RoutingMultiRepositoryIssueListener(delegates);
}
private FeedbackIssueListener createListener(Repository repository,
GitHubOperations gitHub, GitHubProperties githubProperties,
FeedbackProperties feedbackProperties) {
return new FeedbackIssueListener(gitHub, feedbackProperties.getRequiredLabel(),
githubProperties.getRepository().getCollaborators(),
repository.getCollaborators(),
githubProperties.getCredentials().getUsername(),
new StandardFeedbackListener(gitHub,
feedbackProperties.getProvidedLabel(),
feedbackProperties.getRequiredLabel(),
feedbackProperties.getReminderLabel(),
feedbackProperties.getReminderComment(),
feedbackProperties.getCloseComment(), issueListener));
feedbackProperties.getCloseComment(), Collections.emptyList()));
}
}

View File

@@ -17,8 +17,16 @@
package io.spring.issuebot.triage;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import io.spring.issuebot.GitHubProperties;
import io.spring.issuebot.IssueListener;
import io.spring.issuebot.MonitoringProperties;
import io.spring.issuebot.MonitoringProperties.Repository;
import io.spring.issuebot.MultiRepositoryIssueListener;
import io.spring.issuebot.RoutingMultiRepositoryIssueListener;
import io.spring.issuebot.github.GitHubOperations;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -35,12 +43,23 @@ import org.springframework.context.annotation.Configuration;
class TriageConfiguration {
@Bean
TriageIssueListener triageIssueListener(GitHubOperations gitHubOperations,
TriageProperties triageProperties, GitHubProperties gitHubProperties) {
MultiRepositoryIssueListener triageIssueListener(GitHubOperations gitHubOperations,
TriageProperties triageProperties, MonitoringProperties monitoringProperties,
GitHubProperties gitHubProperties) {
Map<Repository, IssueListener> delegates = monitoringProperties.getRepositories()
.stream()
.collect(Collectors.toMap(Function.identity(),
(repository) -> createListener(repository, gitHubOperations,
triageProperties)));
return new RoutingMultiRepositoryIssueListener(delegates);
}
private TriageIssueListener createListener(Repository repository,
GitHubOperations gitHubOperations, TriageProperties triageProperties) {
return new TriageIssueListener(
Arrays.asList(
new OpenedByCollaboratorTriageFilter(
gitHubProperties.getRepository().getCollaborators()),
repository.getCollaborators()),
new LabelledTriageFilter(), new MilestoneAppliedTriageFilter()),
new LabelApplyingTriageListener(gitHubOperations,
triageProperties.getLabel()));

View File

@@ -1,14 +1,14 @@
issuebot:
github:
repository:
organization: spring-projects
name: spring-boot
collaborators:
- bclozel
- mbhave
- philwebb
- snicoll
- wilkinsona
monitoring:
repositories:
- organization: spring-projects
name: spring-boot
collaborators:
- bclozel
- mbhave
- philwebb
- snicoll
- wilkinsona
triage:
label: "status: waiting-for-triage"
feedback:
@@ -21,4 +21,4 @@ issuebot:
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.
issue, please provide the requested information and we will re-open the issue.

View File

@@ -18,9 +18,11 @@ package io.spring.issuebot;
import java.util.Arrays;
import io.spring.issuebot.MonitoringProperties.Repository;
import io.spring.issuebot.github.GitHubOperations;
import io.spring.issuebot.github.Issue;
import io.spring.issuebot.github.Page;
import org.junit.Before;
import org.junit.Test;
import static org.mockito.BDDMockito.given;
@@ -38,34 +40,70 @@ public class RepositoryMonitorTests {
private final GitHubOperations gitHub = mock(GitHubOperations.class);
private final IssueListener issueListenerOne = mock(IssueListener.class);
private final MultiRepositoryIssueListener issueListenerOne = mock(
MultiRepositoryIssueListener.class);
private final IssueListener issueListenerTwo = mock(IssueListener.class);
private final MultiRepositoryIssueListener issueListenerTwo = mock(
MultiRepositoryIssueListener.class);
private final Repository repositoryOne = new Repository();
private final Repository repositoryTwo = new Repository();
private final RepositoryMonitor repositoryMonitor = new RepositoryMonitor(this.gitHub,
new MonitoredRepository("test", "test"),
Arrays.asList(this.repositoryOne, this.repositoryTwo),
Arrays.asList(this.issueListenerOne, this.issueListenerTwo));
@Before
public void setUp() {
this.repositoryOne.setOrganization("test");
this.repositoryOne.setName("one");
this.repositoryTwo.setOrganization("test");
this.repositoryTwo.setName("two");
}
@Test
public void repositoryWithNoIssues() {
given(this.gitHub.getIssues("test", "test")).willReturn(null);
public void repositoriesWithNoIssues() {
given(this.gitHub.getIssues("test", "one")).willReturn(null);
given(this.gitHub.getIssues("test", "two")).willReturn(null);
this.repositoryMonitor.monitor();
verifyNoMoreInteractions(this.issueListenerOne, this.issueListenerTwo);
}
@Test
public void repositoryWithOpenIssues() {
public void oneRepositoryWithOpenIssues() {
@SuppressWarnings("unchecked")
Page<Issue> page = mock(Page.class);
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);
given(this.gitHub.getIssues("test", "one")).willReturn(page);
given(this.gitHub.getIssues("test", "two")).willReturn(null);
this.repositoryMonitor.monitor();
verify(this.issueListenerOne).onOpenIssue(issueOne);
verify(this.issueListenerOne).onOpenIssue(issueTwo);
verify(this.issueListenerTwo).onOpenIssue(issueOne);
verify(this.issueListenerTwo).onOpenIssue(issueTwo);
verify(this.issueListenerOne).onOpenIssue(this.repositoryOne, issueOne);
verify(this.issueListenerOne).onOpenIssue(this.repositoryOne, issueTwo);
verify(this.issueListenerTwo).onOpenIssue(this.repositoryOne, issueOne);
verify(this.issueListenerTwo).onOpenIssue(this.repositoryOne, issueTwo);
}
@Test
public void bothRepositoriesWithOpenIssues() {
@SuppressWarnings("unchecked")
Page<Issue> page = mock(Page.class);
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", "one")).willReturn(page);
given(this.gitHub.getIssues("test", "two")).willReturn(page);
this.repositoryMonitor.monitor();
verify(this.issueListenerOne).onOpenIssue(this.repositoryOne, issueOne);
verify(this.issueListenerOne).onOpenIssue(this.repositoryOne, issueTwo);
verify(this.issueListenerTwo).onOpenIssue(this.repositoryOne, issueOne);
verify(this.issueListenerTwo).onOpenIssue(this.repositoryOne, issueTwo);
verify(this.issueListenerOne).onOpenIssue(this.repositoryTwo, issueOne);
verify(this.issueListenerOne).onOpenIssue(this.repositoryTwo, issueTwo);
verify(this.issueListenerTwo).onOpenIssue(this.repositoryTwo, issueOne);
verify(this.issueListenerTwo).onOpenIssue(this.repositoryTwo, issueTwo);
}
@Test
@@ -74,17 +112,19 @@ public class RepositoryMonitorTests {
Page<Issue> page = mock(Page.class);
Issue issue = new Issue(null, null, null, null, null, null, null, null);
given(page.getContent()).willReturn(Arrays.asList(issue));
given(this.gitHub.getIssues("test", "test")).willReturn(page);
willThrow(new RuntimeException()).given(this.issueListenerOne).onOpenIssue(issue);
given(this.gitHub.getIssues("test", "one")).willReturn(page);
willThrow(new RuntimeException()).given(this.issueListenerOne)
.onOpenIssue(this.repositoryOne, issue);
this.repositoryMonitor.monitor();
verify(this.issueListenerOne).onOpenIssue(issue);
verify(this.issueListenerTwo).onOpenIssue(issue);
verify(this.issueListenerOne).onOpenIssue(this.repositoryOne, issue);
verify(this.issueListenerTwo).onOpenIssue(this.repositoryOne, issue);
}
@Test
public void exceptionFromGitHubIsHandledGracefully() {
given(this.gitHub.getIssues("test", "test")).willThrow(new RuntimeException());
given(this.gitHub.getIssues("test", "one")).willThrow(new RuntimeException());
this.repositoryMonitor.monitor();
verify(this.gitHub).getIssues("test", "one");
}
}