├── gradle.properties ├── .github ├── dco.yml └── workflows │ ├── run-codeql-analysis.yml │ └── build-and-deploy.yml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .sdkmanrc ├── .gitignore ├── settings.gradle ├── config └── checkstyle │ ├── checkstyle-suppressions.xml │ └── checkstyle.xml ├── src ├── main │ ├── java │ │ └── io │ │ │ └── spring │ │ │ └── issuebot │ │ │ ├── package-info.java │ │ │ ├── triage │ │ │ ├── package-info.java │ │ │ ├── TriageListener.java │ │ │ ├── TriageFilter.java │ │ │ ├── TriageProperties.java │ │ │ ├── LabelledTriageFilter.java │ │ │ ├── MilestoneAppliedTriageFilter.java │ │ │ ├── LabelApplyingTriageListener.java │ │ │ ├── TriageConfiguration.java │ │ │ ├── OpenedByCollaboratorTriageFilter.java │ │ │ └── TriageIssueListener.java │ │ │ ├── github │ │ │ ├── package-info.java │ │ │ ├── Page.java │ │ │ ├── LinkParser.java │ │ │ ├── Label.java │ │ │ ├── PullRequest.java │ │ │ ├── Milestone.java │ │ │ ├── User.java │ │ │ ├── RegexLinkParser.java │ │ │ ├── Comment.java │ │ │ ├── StandardPage.java │ │ │ ├── RateLimit.java │ │ │ ├── GitHubOperations.java │ │ │ ├── Issue.java │ │ │ ├── Event.java │ │ │ └── GitHubTemplate.java │ │ │ ├── feedback │ │ │ ├── package-info.java │ │ │ ├── FeedbackListener.java │ │ │ ├── FeedbackConfiguration.java │ │ │ ├── FeedbackProperties.java │ │ │ ├── StandardFeedbackListener.java │ │ │ └── FeedbackIssueListener.java │ │ │ ├── IssueListener.java │ │ │ ├── MonitoringProperties.java │ │ │ ├── GitHubProperties.java │ │ │ ├── IssueBotApplication.java │ │ │ ├── Repository.java │ │ │ └── RepositoryMonitor.java │ └── resources │ │ └── application.yml └── test │ ├── java │ └── io │ │ └── spring │ │ └── issuebot │ │ ├── IssueBotApplicationTests.java │ │ ├── triage │ │ ├── LabelApplyingTriageListenerTests.java │ │ ├── MilestoneAppliedTriageFilterTests.java │ │ ├── LabelledTriageFilterTests.java │ │ ├── OpenedByCollaboratorTriageFilterTests.java │ │ └── TriageIssueListenerTests.java │ │ ├── github │ │ ├── RegexLinkParserTests.java │ │ └── GitHubTemplateTests.java │ │ ├── RepositoryMonitorTests.java │ │ └── feedback │ │ ├── StandardFeedbackListenerTests.java │ │ └── FeedbackIssueListenerTests.java │ └── resources │ └── io │ └── spring │ └── issuebot │ └── github │ ├── new-comment.json │ ├── issue-single-label.json │ ├── issue-two-labels.json │ ├── comments-page-two.json │ ├── events-page-two.json │ └── events-page-one.json ├── README.md ├── gradlew.bat └── gradlew /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.caching=true 2 | -------------------------------------------------------------------------------- /.github/dco.yml: -------------------------------------------------------------------------------- 1 | require: 2 | members: false 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-io/issue-bot/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.sdkmanrc: -------------------------------------------------------------------------------- 1 | # Enable auto-env through the sdkman_auto_env config 2 | # Add key=value pairs of SDKs to use below 3 | java=17.0.14-librca 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | 4 | bin/ 5 | .classpath 6 | .project 7 | .settings 8 | .factorypath 9 | 10 | .idea 11 | *.iml 12 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenCentral() 4 | gradlePluginPortal() 5 | } 6 | } 7 | 8 | plugins { 9 | id "io.spring.develocity.conventions" version "0.0.22" 10 | } 11 | 12 | rootProject.name="issue-bot" 13 | 14 | enableFeaturePreview("STABLE_CONFIGURATION_CACHE") 15 | -------------------------------------------------------------------------------- /config/checkstyle/checkstyle-suppressions.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.github/workflows/run-codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "Run CodeQL Analysis" 2 | on: 3 | push: 4 | pull_request: 5 | workflow_dispatch: 6 | schedule: 7 | - cron: '45 0 * * 1' 8 | permissions: read-all 9 | jobs: 10 | run-analysis: 11 | permissions: 12 | actions: read 13 | contents: read 14 | security-events: write 15 | uses: spring-io/github-actions/.github/workflows/codeql-analysis.yml@6e66995f7d29de1e4ff76e4f0def7a10163fe910 16 | -------------------------------------------------------------------------------- /config/checkstyle/checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Central classes for the Issue Bot. 19 | */ 20 | package io.spring.issuebot; 21 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/triage/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Classes for triaging issues. 19 | */ 20 | package io.spring.issuebot.triage; 21 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/github/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Classes for interacting with GitHub's API. 19 | */ 20 | package io.spring.issuebot.github; 21 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/feedback/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Classes for managing user-provided feedback on issues. 19 | */ 20 | package io.spring.issuebot.feedback; 21 | -------------------------------------------------------------------------------- /src/test/java/io/spring/issuebot/IssueBotApplicationTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot; 18 | 19 | import org.junit.jupiter.api.Test; 20 | 21 | import org.springframework.boot.test.context.SpringBootTest; 22 | 23 | /** 24 | * Tests for {@link IssueBotApplication}. 25 | * 26 | * @author Andy Wilkinson 27 | */ 28 | @SpringBootTest(properties = "issuebot.monitoring.enabled=false") 29 | class IssueBotApplicationTests { 30 | 31 | @Test 32 | void contextLoads() { 33 | 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/github/Page.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot.github; 18 | 19 | import java.util.List; 20 | 21 | /** 22 | * A page of results. 23 | * 24 | * @param the type of the contents of the page 25 | * @author Andy Wilkinson 26 | */ 27 | public interface Page { 28 | 29 | /** 30 | * Returns the next page, if any. 31 | * @return the next page or {@code null} 32 | */ 33 | Page next(); 34 | 35 | /** 36 | * Returns the contents of the page. 37 | * @return the contents 38 | */ 39 | List getContent(); 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/github/LinkParser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot.github; 18 | 19 | import java.util.Map; 20 | 21 | /** 22 | * A {@code LinkParser} can be used to parse the 23 | * {@code Link} header that is 24 | * returned by the GitHub API. 25 | * 26 | * @author Andy Wilkinson 27 | */ 28 | interface LinkParser { 29 | 30 | /** 31 | * Parse the given {@code header} into a map of rel:url pairs. 32 | * @param header the header to parse 33 | * @return the map of links 34 | */ 35 | Map parse(String header); 36 | 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/build-and-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build and deploy 2 | on: 3 | push: 4 | branches: 5 | - main 6 | permissions: 7 | contents: read 8 | jobs: 9 | build: 10 | name: Build and deploy 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check out 14 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 15 | 16 | - name: Set up JDK 17 17 | uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 18 | with: 19 | java-version: '17' 20 | distribution: 'liberica' 21 | 22 | - name: Set up Gradle 23 | uses: gradle/actions/setup-gradle@94baf225fe0a508e581a564467443d0e2379123b # v4.3.0 24 | 25 | - name: Build 26 | run: ./gradlew build 27 | 28 | - name: Set up Azure 29 | uses: azure/login@a65d910e8af852a8061c627c456678983e180302 # v2.2.0 30 | with: 31 | creds: ${{ secrets.AZURE_CREDENTIALS_SPRING_ASA }} 32 | 33 | - name: Set up Azure Spring Extension 34 | run: az extension add --name spring 35 | 36 | - name: Deploy 37 | run: | 38 | az spring app deploy \ 39 | --name issue-bot \ 40 | --service spring-asa \ 41 | --resource-group spring-asa \ 42 | --artifact-path build/libs/issue-bot.jar 43 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/triage/TriageListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot.triage; 18 | 19 | import io.spring.issuebot.github.Issue; 20 | 21 | /** 22 | * A {@code TriageListener} is notified of issues' triage requirements. 23 | * 24 | * @author Andy Wilkinson 25 | */ 26 | interface TriageListener { 27 | 28 | /** 29 | * Notification that the given {@code issue} requires triage. 30 | * @param issue the issue 31 | */ 32 | void requiresTriage(Issue issue); 33 | 34 | /** 35 | * Notification that the given {@code issue} does not require triage. 36 | * @param issue the issue 37 | */ 38 | void doesNotRequireTriage(Issue issue); 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/github/Label.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot.github; 18 | 19 | import com.fasterxml.jackson.annotation.JsonCreator; 20 | import com.fasterxml.jackson.annotation.JsonProperty; 21 | 22 | /** 23 | * A label that can be applied to a GitHub issue. 24 | * 25 | * @author Andy Wilkinson 26 | */ 27 | public class Label { 28 | 29 | private final String name; 30 | 31 | /** 32 | * Creates a new label with the given {@code name}. 33 | * @param name the name of the label 34 | */ 35 | @JsonCreator 36 | public Label(@JsonProperty("name") String name) { 37 | this.name = name; 38 | } 39 | 40 | public String getName() { 41 | return this.name; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/github/PullRequest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot.github; 18 | 19 | import com.fasterxml.jackson.annotation.JsonCreator; 20 | import com.fasterxml.jackson.annotation.JsonProperty; 21 | 22 | /** 23 | * Details of a GitHub pull request. 24 | * 25 | * @author Andy Wilkinson 26 | */ 27 | public class PullRequest { 28 | 29 | private final String url; 30 | 31 | /** 32 | * Creates a new {@code PullRequest} that has the given {@code url} in the GitHub API. 33 | * @param url the url 34 | */ 35 | @JsonCreator 36 | public PullRequest(@JsonProperty("url") String url) { 37 | this.url = url; 38 | } 39 | 40 | public String getUrl() { 41 | return this.url; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/github/Milestone.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot.github; 18 | 19 | import com.fasterxml.jackson.annotation.JsonCreator; 20 | import com.fasterxml.jackson.annotation.JsonProperty; 21 | 22 | /** 23 | * A milestone to which a GitHub Issue can be added. 24 | * 25 | * @author Andy Wilkinson 26 | */ 27 | public class Milestone { 28 | 29 | private final String title; 30 | 31 | /** 32 | * Creates a new {@code Milestone} with the given {@code title}. 33 | * @param title the title 34 | */ 35 | @JsonCreator 36 | public Milestone(@JsonProperty("title") String title) { 37 | this.title = title; 38 | } 39 | 40 | public String getTitle() { 41 | return this.title; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/github/User.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot.github; 18 | 19 | import com.fasterxml.jackson.annotation.JsonCreator; 20 | import com.fasterxml.jackson.annotation.JsonProperty; 21 | 22 | /** 23 | * A GitHub user. 24 | * 25 | * @author Andy Wilkinson 26 | */ 27 | public class User { 28 | 29 | private final String login; 30 | 31 | /** 32 | * Creates a new {@code User} with the given login. 33 | * @param login the login 34 | */ 35 | @JsonCreator 36 | public User(@JsonProperty("login") String login) { 37 | this.login = login; 38 | } 39 | 40 | public String getLogin() { 41 | return this.login; 42 | } 43 | 44 | @Override 45 | public String toString() { 46 | return this.login; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/triage/TriageFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot.triage; 18 | 19 | import io.spring.issuebot.Repository; 20 | import io.spring.issuebot.github.Issue; 21 | 22 | /** 23 | * A {@code TriageFilter} is used to identify issues which are waiting for triage. 24 | * 25 | * @author Andy Wilkinson 26 | */ 27 | interface TriageFilter { 28 | 29 | /** 30 | * Returns {@code true} if the given issue has already been triaged, otherwise 31 | * {@code false}. 32 | * @param repository the repository to which the issue belongs 33 | * @param issue the issue 34 | * @return {@code true} if the issue has been triaged, {@code false} otherwise 35 | */ 36 | boolean triaged(Repository repository, Issue issue); 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/triage/TriageProperties.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot.triage; 18 | 19 | import org.springframework.boot.context.properties.ConfigurationProperties; 20 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 21 | 22 | /** 23 | * {@link EnableConfigurationProperties Configuration properties} for triaging GitHub 24 | * issues. 25 | * 26 | * @author Andy Wilkinson 27 | */ 28 | @ConfigurationProperties(prefix = "issuebot.triage") 29 | class TriageProperties { 30 | 31 | /** 32 | * The name of the label that should be applied to issues that are waiting for triage. 33 | */ 34 | private String label; 35 | 36 | public String getLabel() { 37 | return this.label; 38 | } 39 | 40 | public void setLabel(String label) { 41 | this.label = label; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/test/resources/io/spring/issuebot/github/new-comment.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://api.github.com/repos/wilkinsona/issue-bot-test/issues/comments/162885552", 3 | "html_url": "https://github.com/wilkinsona/issue-bot-test/issues/1#issuecomment-162885552", 4 | "issue_url": "https://api.github.com/repos/wilkinsona/issue-bot-test/issues/1", 5 | "id": 162885552, 6 | "user": { 7 | "login": "wilkinsona", 8 | "id": 914682, 9 | "avatar_url": "https://avatars.githubusercontent.com/u/914682?v=3", 10 | "gravatar_id": "", 11 | "url": "https://api.github.com/users/wilkinsona", 12 | "html_url": "https://github.com/wilkinsona", 13 | "followers_url": "https://api.github.com/users/wilkinsona/followers", 14 | "following_url": "https://api.github.com/users/wilkinsona/following{/other_user}", 15 | "gists_url": "https://api.github.com/users/wilkinsona/gists{/gist_id}", 16 | "starred_url": "https://api.github.com/users/wilkinsona/starred{/owner}{/repo}", 17 | "subscriptions_url": "https://api.github.com/users/wilkinsona/subscriptions", 18 | "organizations_url": "https://api.github.com/users/wilkinsona/orgs", 19 | "repos_url": "https://api.github.com/users/wilkinsona/repos", 20 | "events_url": "https://api.github.com/users/wilkinsona/events{/privacy}", 21 | "received_events_url": "https://api.github.com/users/wilkinsona/received_events", 22 | "type": "User", 23 | "site_admin": false 24 | }, 25 | "created_at": "2015-12-08T13:53:45Z", 26 | "updated_at": "2015-12-08T13:53:45Z", 27 | "body": "A test comment" 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/IssueListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot; 18 | 19 | import io.spring.issuebot.github.Issue; 20 | 21 | /** 22 | * An {@code IssueListener} is notified of issues found during monitoring of a specific 23 | * repository. 24 | * 25 | * @author Andy Wilkinson 26 | */ 27 | public interface IssueListener { 28 | 29 | /** 30 | * Notification that the given {@code issue} is open. 31 | * @param repository the repository to which the issue belongs 32 | * @param issue the open issue 33 | */ 34 | default void onOpenIssue(Repository repository, Issue issue) { 35 | 36 | } 37 | 38 | /** 39 | * Notification that the given {@code issue} is being closed. 40 | * @param repository the repository to which the issue belongs 41 | * @param issue the open issue 42 | */ 43 | default void onIssueClosure(Repository repository, Issue issue) { 44 | 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/triage/LabelledTriageFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot.triage; 18 | 19 | import io.spring.issuebot.Repository; 20 | import io.spring.issuebot.github.Issue; 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | 24 | /** 25 | * A {@link TriageFilter} that considers an issue as having been triaged if any labels 26 | * have been applied to it. 27 | * 28 | * @author Andy Wilkinson 29 | */ 30 | final class LabelledTriageFilter implements TriageFilter { 31 | 32 | private static final Logger log = LoggerFactory.getLogger(LabelledTriageFilter.class); 33 | 34 | @Override 35 | public boolean triaged(Repository repository, Issue issue) { 36 | if (issue.getLabels() != null && !issue.getLabels().isEmpty()) { 37 | log.debug("{} has been triaged. It has been labelled.", issue); 38 | return true; 39 | } 40 | return false; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/MonitoringProperties.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot; 18 | 19 | import java.util.List; 20 | 21 | import org.springframework.boot.context.properties.ConfigurationProperties; 22 | 23 | /** 24 | * Properties for configuring repository monitoring. 25 | * 26 | * @author Andy Wilkinson 27 | */ 28 | @ConfigurationProperties(prefix = "issuebot.monitoring") 29 | public class MonitoringProperties { 30 | 31 | private List repositories; 32 | 33 | private boolean enabled = true; 34 | 35 | public List getRepositories() { 36 | return this.repositories; 37 | } 38 | 39 | public void setRepositories(List repositories) { 40 | this.repositories = repositories; 41 | } 42 | 43 | public boolean isEnabled() { 44 | return this.enabled; 45 | } 46 | 47 | public void setEnabled(boolean enabled) { 48 | this.enabled = enabled; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/triage/MilestoneAppliedTriageFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot.triage; 18 | 19 | import io.spring.issuebot.Repository; 20 | import io.spring.issuebot.github.Issue; 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | 24 | /** 25 | * A {@link TriageFilter} that considers an issue as having been triaged if a milestone 26 | * has been applied to it. 27 | * 28 | * @author Andy Wilkinson 29 | */ 30 | final class MilestoneAppliedTriageFilter implements TriageFilter { 31 | 32 | private static final Logger log = LoggerFactory.getLogger(LabelledTriageFilter.class); 33 | 34 | @Override 35 | public boolean triaged(Repository repository, Issue issue) { 36 | if (issue.getMilestone() != null) { 37 | log.debug("Issue has been triaged. It has been added to milestone {}", issue.getMilestone().getTitle()); 38 | return true; 39 | } 40 | return false; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/io/spring/issuebot/triage/LabelApplyingTriageListenerTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot.triage; 18 | 19 | import io.spring.issuebot.github.GitHubOperations; 20 | import io.spring.issuebot.github.Issue; 21 | import org.junit.jupiter.api.Test; 22 | 23 | import static org.mockito.Mockito.mock; 24 | import static org.mockito.Mockito.verify; 25 | 26 | /** 27 | * Tests for {@link LabelApplyingTriageListener}. 28 | * 29 | * @author Andy Wilkinson 30 | */ 31 | class LabelApplyingTriageListenerTests { 32 | 33 | private final GitHubOperations gitHub = mock(GitHubOperations.class); 34 | 35 | private final LabelApplyingTriageListener listener = new LabelApplyingTriageListener(this.gitHub, "test"); 36 | 37 | @Test 38 | void requiresTriage() { 39 | Issue issue = new Issue(null, null, null, null, null, null, null, null); 40 | this.listener.requiresTriage(issue); 41 | verify(this.gitHub).addLabel(issue, "test"); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/github/RegexLinkParser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot.github; 18 | 19 | import java.util.HashMap; 20 | import java.util.Map; 21 | import java.util.regex.Matcher; 22 | import java.util.regex.Pattern; 23 | 24 | import org.springframework.util.StringUtils; 25 | 26 | /** 27 | * A {@code LinkParser} that uses a regular expression to parse the header. 28 | * 29 | * @author Andy Wilkinson 30 | */ 31 | public class RegexLinkParser implements LinkParser { 32 | 33 | private static final Pattern LINK_PATTERN = Pattern.compile("<(.+)>; rel=\"(.+)\""); 34 | 35 | @Override 36 | public Map parse(String input) { 37 | Map links = new HashMap<>(); 38 | for (String link : StringUtils.commaDelimitedListToStringArray(input)) { 39 | Matcher matcher = LINK_PATTERN.matcher(link.trim()); 40 | if (matcher.matches()) { 41 | links.put(matcher.group(2), matcher.group(1)); 42 | } 43 | } 44 | return links; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/github/Comment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot.github; 18 | 19 | import java.time.OffsetDateTime; 20 | 21 | import com.fasterxml.jackson.annotation.JsonCreator; 22 | import com.fasterxml.jackson.annotation.JsonProperty; 23 | 24 | /** 25 | * A comment that has been made on a GitHub issue. 26 | * 27 | * @author Andy Wilkinson 28 | */ 29 | public final class Comment { 30 | 31 | private final User user; 32 | 33 | private final OffsetDateTime creationTime; 34 | 35 | /** 36 | * Creates a new comment that was authored by the given {@code user} at the given 37 | * {@code creationTime}. 38 | * @param user the user 39 | * @param creationTime the creation time 40 | */ 41 | @JsonCreator 42 | public Comment(@JsonProperty("user") User user, @JsonProperty("created_at") OffsetDateTime creationTime) { 43 | this.user = user; 44 | this.creationTime = creationTime; 45 | } 46 | 47 | public User getUser() { 48 | return this.user; 49 | } 50 | 51 | public OffsetDateTime getCreationTime() { 52 | return this.creationTime; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/io/spring/issuebot/triage/MilestoneAppliedTriageFilterTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot.triage; 18 | 19 | import io.spring.issuebot.Repository; 20 | import io.spring.issuebot.github.Issue; 21 | import io.spring.issuebot.github.Milestone; 22 | import org.junit.jupiter.api.Test; 23 | 24 | import static org.assertj.core.api.Assertions.assertThat; 25 | 26 | /** 27 | * Tests for {@link MilestoneAppliedTriageFilter}. 28 | * 29 | * @author Andy Wilkinson 30 | */ 31 | class MilestoneAppliedTriageFilterTests { 32 | 33 | private final TriageFilter filter = new MilestoneAppliedTriageFilter(); 34 | 35 | private final Repository repository = new Repository(); 36 | 37 | @Test 38 | void issueWithMilestoneApplied() { 39 | assertThat(this.filter.triaged(this.repository, 40 | new Issue(null, null, null, null, null, null, new Milestone("test"), null))) 41 | .isTrue(); 42 | } 43 | 44 | @Test 45 | void issueWithNoMilestoneApplied() { 46 | assertThat(this.filter.triaged(this.repository, new Issue(null, null, null, null, null, null, null, null))) 47 | .isFalse(); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/github/StandardPage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot.github; 18 | 19 | import java.util.List; 20 | import java.util.function.Supplier; 21 | 22 | /** 23 | * Standard implementation of {@link Page}. 24 | * 25 | * @param the type of the contents of the page 26 | * @author Andy Wilkinson 27 | */ 28 | public class StandardPage implements Page { 29 | 30 | private final List content; 31 | 32 | private final Supplier> nextSupplier; 33 | 34 | /** 35 | * Creates a new {@code StandardPage} that has the given {@code content}. The given 36 | * {@code nextSupplier} will be used to obtain the next page {@link #next when 37 | * requested}. 38 | * @param content the content 39 | * @param nextSupplier the supplier of the next page 40 | */ 41 | public StandardPage(List content, Supplier> nextSupplier) { 42 | this.content = content; 43 | this.nextSupplier = nextSupplier; 44 | } 45 | 46 | @Override 47 | public Page next() { 48 | return this.nextSupplier.get(); 49 | } 50 | 51 | @Override 52 | public List getContent() { 53 | return this.content; 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Issue Bot for GitHub issues 2 | 3 | The issue bot helps manage open GitHub issues. 4 | 5 | ## Features 6 | 7 | The issue bot helps by managing [Waiting for Triage](#waiting-for-triage) and monitoring [Waiting for Feedback](#waiting-for-feedback). 8 | 9 | ### Waiting for Triage 10 | 11 | The issue bot labels issues and pull requests as waiting for triage. 12 | An issue or pull request is deemed as waiting for triage when all of the following is true: 13 | * It is open 14 | * It was not opened by a collaborator 15 | * It is not assigned to a milestone 16 | * It has no labels. 17 | 18 | When the issue is no longer waiting for triage, the label should be manually removed. 19 | 20 | ### Waiting for Feedback 21 | 22 | The issue bot monitors open issues and pull requests that are labeled as waiting for 23 | feedback. Monitoring begins when the waiting for feedback label is applied. Any comment 24 | that is not from a collaborator is considered to be feedback. 25 | 26 | For issues, the bot will: 27 | 28 | * Comment with a reminder of feedback is not provided within 7 days of the label being 29 | applied. 30 | * Close the issue if feedback is not provided within a further 7 days. 31 | * Add the feedback provided label and remove the waiting for feedback label when feedback 32 | is provided. 33 | 34 | For pull requests, the bot will: 35 | 36 | * Add the feedback provided label and remove the waiting for feedback label when feedback 37 | is provided. 38 | 39 | ## Monitoring a Repository 40 | 41 | Configured repositories are scanned periodically. 42 | To add a repository: 43 | 44 | * First ensure the repository has the labels: 45 | * `status: waiting-for-feedback` 46 | * `status: feedback-provided` 47 | * `status: feedback-reminder` 48 | * Then send a pull request similar to https://github.com/spring-io/issue-bot/pull/12 49 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/feedback/FeedbackListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot.feedback; 18 | 19 | import java.time.OffsetDateTime; 20 | 21 | import io.spring.issuebot.Repository; 22 | import io.spring.issuebot.github.Issue; 23 | 24 | /** 25 | * A {@code FeedbackListener} is notified when feedback has been provided or is still 26 | * required for an {@link Issue}. 27 | * 28 | * @author Andy Wilkinson 29 | */ 30 | public interface FeedbackListener { 31 | 32 | /** 33 | * Notification that feedback has been provided for the given {@code issue}. 34 | * @param repository the repository to which the issue belongs 35 | * @param issue the issue 36 | */ 37 | void feedbackProvided(Repository repository, Issue issue); 38 | 39 | /** 40 | * Notification that feedback is still required for the given {@code issue} having 41 | * been requested at the given {@code requestTime}. 42 | * @param repository the repository to which the issue belongs 43 | * @param issue the issue 44 | * @param requestTime the time when feedback was requested 45 | */ 46 | void feedbackRequired(Repository repository, Issue issue, OffsetDateTime requestTime); 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/triage/LabelApplyingTriageListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot.triage; 18 | 19 | import io.spring.issuebot.github.GitHubOperations; 20 | import io.spring.issuebot.github.Issue; 21 | 22 | /** 23 | * A {@link TriageListener} that applies a label to any issues that require triage. 24 | * 25 | * @author Andy Wilkinson 26 | */ 27 | final class LabelApplyingTriageListener implements TriageListener { 28 | 29 | private final GitHubOperations gitHub; 30 | 31 | private final String label; 32 | 33 | /** 34 | * Creates a new {@code LabelApplyingTriageListener} that will use the given 35 | * {@code gitHubOperations} to apply the given {@code label} to any issues that 36 | * require triage. 37 | * @param gitHubOperations the GitHubOperations 38 | * @param label the label 39 | */ 40 | LabelApplyingTriageListener(GitHubOperations gitHubOperations, String label) { 41 | this.gitHub = gitHubOperations; 42 | this.label = label; 43 | } 44 | 45 | @Override 46 | public void requiresTriage(Issue issue) { 47 | this.gitHub.addLabel(issue, this.label); 48 | } 49 | 50 | @Override 51 | public void doesNotRequireTriage(Issue issue) { 52 | this.gitHub.removeLabel(issue, this.label); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/triage/TriageConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot.triage; 18 | 19 | import java.util.Arrays; 20 | 21 | import io.spring.issuebot.GitHubProperties; 22 | import io.spring.issuebot.MonitoringProperties; 23 | import io.spring.issuebot.github.GitHubOperations; 24 | 25 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 26 | import org.springframework.context.annotation.Bean; 27 | import org.springframework.context.annotation.Configuration; 28 | 29 | /** 30 | * Central configuration for the beans involved in identifying issues that require triage. 31 | * 32 | * @author Andy Wilkinson 33 | */ 34 | @Configuration 35 | @EnableConfigurationProperties(TriageProperties.class) 36 | class TriageConfiguration { 37 | 38 | @Bean 39 | TriageIssueListener triageIssueListener(GitHubOperations gitHubOperations, TriageProperties triageProperties, 40 | MonitoringProperties monitoringProperties, GitHubProperties gitHubProperties) { 41 | return new TriageIssueListener( 42 | Arrays.asList(new OpenedByCollaboratorTriageFilter(monitoringProperties.getRepositories()), 43 | new LabelledTriageFilter(), new MilestoneAppliedTriageFilter()), 44 | new LabelApplyingTriageListener(gitHubOperations, triageProperties.getLabel())); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/io/spring/issuebot/triage/LabelledTriageFilterTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot.triage; 18 | 19 | import java.util.Collections; 20 | 21 | import io.spring.issuebot.Repository; 22 | import io.spring.issuebot.github.Issue; 23 | import io.spring.issuebot.github.Label; 24 | import org.junit.jupiter.api.Test; 25 | 26 | import static org.assertj.core.api.Assertions.assertThat; 27 | 28 | /** 29 | * Tests for {@link LabelledTriageFilter}. 30 | * 31 | * @author Andy Wilkinson 32 | */ 33 | class LabelledTriageFilterTests { 34 | 35 | private final TriageFilter filter = new LabelledTriageFilter(); 36 | 37 | private final Repository repository = new Repository(); 38 | 39 | @Test 40 | void issueWithLabels() { 41 | assertThat(this.filter.triaged(this.repository, 42 | new Issue(null, null, null, null, null, Collections.singletonList(new Label("test")), null, null))) 43 | .isTrue(); 44 | } 45 | 46 | @Test 47 | void issueWithNullLabels() { 48 | assertThat(this.filter.triaged(this.repository, new Issue(null, null, null, null, null, null, null, null))) 49 | .isFalse(); 50 | } 51 | 52 | @Test 53 | void issueWithNoLabels() { 54 | assertThat(this.filter.triaged(this.repository, 55 | new Issue(null, null, null, null, null, Collections.emptyList(), null, null))) 56 | .isFalse(); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/test/java/io/spring/issuebot/github/RegexLinkParserTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot.github; 18 | 19 | import java.util.Map; 20 | 21 | import org.junit.jupiter.api.Test; 22 | 23 | import static org.assertj.core.api.Assertions.assertThat; 24 | 25 | /** 26 | * Tests for {@link RegexLinkParser}. 27 | * 28 | * @author Andy Wilkinson 29 | */ 30 | class RegexLinkParserTests { 31 | 32 | private final LinkParser linkParser = new RegexLinkParser(); 33 | 34 | @Test 35 | void emptyInput() { 36 | assertThat(this.linkParser.parse("")).isEmpty(); 37 | } 38 | 39 | @Test 40 | void nullInput() { 41 | assertThat(this.linkParser.parse(null)).isEmpty(); 42 | } 43 | 44 | @Test 45 | void singleLink() { 46 | Map links = this.linkParser.parse("; rel=\"foo\""); 47 | assertThat(links).hasSize(1); 48 | assertThat(links).containsEntry("foo", "url"); 49 | } 50 | 51 | @Test 52 | void notALink() { 53 | Map links = this.linkParser.parse("; foo bar"); 54 | assertThat(links).isEmpty(); 55 | } 56 | 57 | @Test 58 | void multipleLinks() { 59 | Map links = this.linkParser.parse("; rel=\"foo\", ; rel=\"bar\""); 60 | assertThat(links).hasSize(2); 61 | assertThat(links).containsEntry("foo", "url-one"); 62 | assertThat(links).containsEntry("bar", "url-two"); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/triage/OpenedByCollaboratorTriageFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot.triage; 18 | 19 | import java.util.List; 20 | import java.util.Map; 21 | import java.util.function.Function; 22 | import java.util.stream.Collectors; 23 | 24 | import io.spring.issuebot.Repository; 25 | import io.spring.issuebot.github.Issue; 26 | import org.slf4j.Logger; 27 | import org.slf4j.LoggerFactory; 28 | 29 | /** 30 | * A {@link TriageFilter} that considers an issue as having been triaged if it was opened 31 | * by a collaborator. 32 | * 33 | * @author Andy Wilkinson 34 | */ 35 | final class OpenedByCollaboratorTriageFilter implements TriageFilter { 36 | 37 | private static final Logger log = LoggerFactory.getLogger(OpenedByCollaboratorTriageFilter.class); 38 | 39 | private final Map> repositoryCollaborators; 40 | 41 | OpenedByCollaboratorTriageFilter(List repositories) { 42 | this.repositoryCollaborators = repositories.stream() 43 | .collect(Collectors.toMap(Function.identity(), Repository::getCollaborators)); 44 | } 45 | 46 | @Override 47 | public boolean triaged(Repository repository, Issue issue) { 48 | if (this.repositoryCollaborators.get(repository).contains(issue.getUser().getLogin())) { 49 | log.debug("{} has been triaged. It was opened by {}", issue, issue.getUser()); 50 | return true; 51 | } 52 | return false; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/io/spring/issuebot/triage/OpenedByCollaboratorTriageFilterTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot.triage; 18 | 19 | import java.util.Arrays; 20 | import java.util.Collections; 21 | 22 | import io.spring.issuebot.Repository; 23 | import io.spring.issuebot.github.Issue; 24 | import io.spring.issuebot.github.User; 25 | import org.junit.jupiter.api.BeforeEach; 26 | import org.junit.jupiter.api.Test; 27 | 28 | import static org.assertj.core.api.Assertions.assertThat; 29 | 30 | /** 31 | * Tests for {@link OpenedByCollaboratorTriageFilter}. 32 | * 33 | * @author Andy Wilkinson 34 | */ 35 | class OpenedByCollaboratorTriageFilterTests { 36 | 37 | private final Repository repository = new Repository(); 38 | 39 | private TriageFilter filter; 40 | 41 | @BeforeEach 42 | void setUp() { 43 | this.repository.setCollaborators(Arrays.asList("Alice", "Brenda", "Charlie")); 44 | this.filter = new OpenedByCollaboratorTriageFilter(Collections.singletonList(this.repository)); 45 | } 46 | 47 | @Test 48 | void openedByCollaborator() { 49 | assertThat(this.filter.triaged(this.repository, 50 | new Issue(null, null, null, null, new User("Alice"), null, null, null))) 51 | .isTrue(); 52 | } 53 | 54 | @Test 55 | void openedByAnotherUser() { 56 | assertThat(this.filter.triaged(this.repository, 57 | new Issue(null, null, null, null, new User("Debbie"), null, null, null))) 58 | .isFalse(); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/GitHubProperties.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot; 18 | 19 | import org.springframework.boot.context.properties.ConfigurationProperties; 20 | import org.springframework.boot.context.properties.NestedConfigurationProperty; 21 | 22 | /** 23 | * {@link ConfigurationProperties} for connecting to GitHub. 24 | * 25 | * @author Andy Wilkinson 26 | */ 27 | @ConfigurationProperties(prefix = "issuebot.github") 28 | public class GitHubProperties { 29 | 30 | @NestedConfigurationProperty 31 | private Credentials credentials = new Credentials(); 32 | 33 | public Credentials getCredentials() { 34 | return this.credentials; 35 | } 36 | 37 | public void setCredentials(Credentials credentials) { 38 | this.credentials = credentials; 39 | } 40 | 41 | /** 42 | * Configuration for the credentials used to authenticate with GitHub. 43 | */ 44 | public static class Credentials { 45 | 46 | /** 47 | * The username used for authentication with GitHub. 48 | */ 49 | private String username; 50 | 51 | /** 52 | * The password used for authentication with GitHub. 53 | */ 54 | private String password; 55 | 56 | public String getUsername() { 57 | return this.username; 58 | } 59 | 60 | public void setUsername(String username) { 61 | this.username = username; 62 | } 63 | 64 | public String getPassword() { 65 | return this.password; 66 | } 67 | 68 | public void setPassword(String password) { 69 | this.password = password; 70 | } 71 | 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/feedback/FeedbackConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot.feedback; 18 | 19 | import java.util.List; 20 | 21 | import io.spring.issuebot.GitHubProperties; 22 | import io.spring.issuebot.IssueListener; 23 | import io.spring.issuebot.MonitoringProperties; 24 | import io.spring.issuebot.github.GitHubOperations; 25 | 26 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 27 | import org.springframework.context.annotation.Bean; 28 | import org.springframework.context.annotation.Configuration; 29 | 30 | /** 31 | * Central configuration for the beans involved in managing issues that are waiting for 32 | * feedback. 33 | * 34 | * @author Andy Wilkinson 35 | */ 36 | @Configuration 37 | @EnableConfigurationProperties(FeedbackProperties.class) 38 | class FeedbackConfiguration { 39 | 40 | @Bean 41 | FeedbackIssueListener feedbackIssueListener(GitHubOperations gitHub, GitHubProperties gitHubProperties, 42 | MonitoringProperties monitoringProperties, FeedbackProperties feedbackProperties, 43 | List issueListener) { 44 | return new FeedbackIssueListener(gitHub, feedbackProperties.getRequiredLabel(), 45 | monitoringProperties.getRepositories(), gitHubProperties.getCredentials().getUsername(), 46 | new StandardFeedbackListener(gitHub, feedbackProperties.getProvidedLabel(), 47 | feedbackProperties.getRequiredLabel(), feedbackProperties.getReminderLabel(), 48 | feedbackProperties.getReminderComment(), feedbackProperties.getCloseComment(), issueListener)); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/IssueBotApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot; 18 | 19 | import java.util.List; 20 | 21 | import io.spring.issuebot.github.GitHubOperations; 22 | import io.spring.issuebot.github.GitHubTemplate; 23 | import io.spring.issuebot.github.RegexLinkParser; 24 | 25 | import org.springframework.boot.SpringApplication; 26 | import org.springframework.boot.autoconfigure.SpringBootApplication; 27 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 28 | import org.springframework.context.annotation.Bean; 29 | import org.springframework.scheduling.annotation.EnableScheduling; 30 | 31 | /** 32 | * Main class for launching Issue Bot. 33 | * 34 | * @author Andy Wilkinson 35 | */ 36 | @SpringBootApplication 37 | @EnableScheduling 38 | @EnableConfigurationProperties({ GitHubProperties.class, MonitoringProperties.class }) 39 | public class IssueBotApplication { 40 | 41 | public static void main(String[] args) { 42 | SpringApplication.run(IssueBotApplication.class, args); 43 | } 44 | 45 | @Bean 46 | GitHubTemplate gitHubTemplate(GitHubProperties gitHubProperties) { 47 | return new GitHubTemplate(gitHubProperties.getCredentials().getUsername(), 48 | gitHubProperties.getCredentials().getPassword(), new RegexLinkParser()); 49 | } 50 | 51 | @Bean 52 | RepositoryMonitor repositoryMonitor(GitHubOperations gitHub, MonitoringProperties monitoringProperties, 53 | List issueListeners) { 54 | return new RepositoryMonitor(gitHub, monitoringProperties.getRepositories(), monitoringProperties.isEnabled(), 55 | issueListeners); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/triage/TriageIssueListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot.triage; 18 | 19 | import java.util.List; 20 | 21 | import io.spring.issuebot.IssueListener; 22 | import io.spring.issuebot.Repository; 23 | import io.spring.issuebot.github.Issue; 24 | 25 | /** 26 | * {@link IssueListener} that identifies open issues that require triage. 27 | * 28 | * @author Andy Wilkinson 29 | */ 30 | final class TriageIssueListener implements IssueListener { 31 | 32 | private final List triageFilters; 33 | 34 | private final TriageListener triageListener; 35 | 36 | /** 37 | * Creates a new {@code TriageIssueListener} that will use the given 38 | * {@code triageFilters} to identify issues that require triage and notify the given 39 | * {@code triageListener} of those that do. 40 | * @param triageFilters the triage filters 41 | * @param triageListener the triage listener 42 | */ 43 | TriageIssueListener(List triageFilters, TriageListener triageListener) { 44 | this.triageFilters = triageFilters; 45 | this.triageListener = triageListener; 46 | } 47 | 48 | @Override 49 | public void onOpenIssue(Repository repository, Issue issue) { 50 | if (requiresTriage(repository, issue)) { 51 | this.triageListener.requiresTriage(issue); 52 | } 53 | } 54 | 55 | private boolean requiresTriage(Repository repository, Issue issue) { 56 | for (TriageFilter filter : this.triageFilters) { 57 | if (filter.triaged(repository, issue)) { 58 | return false; 59 | } 60 | } 61 | return true; 62 | } 63 | 64 | @Override 65 | public void onIssueClosure(Repository repository, Issue issue) { 66 | this.triageListener.doesNotRequireTriage(issue); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/github/RateLimit.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot.github; 18 | 19 | import org.springframework.http.HttpHeaders; 20 | import org.springframework.http.client.ClientHttpResponse; 21 | 22 | /** 23 | * The current rate limit for interacting with GitHub's REST API. 24 | * 25 | * @author Andy Wilkinson 26 | */ 27 | public final class RateLimit { 28 | 29 | private static final String HEADER_REMAINING = "X-RateLimit-Remaining"; 30 | 31 | private static final String HEADER_LIMIT = "X-RateLimit-Limit"; 32 | 33 | private static final String HEADER_RESET = "X-RateLimit-Reset"; 34 | 35 | private final int limit; 36 | 37 | private final int remaining; 38 | 39 | private final long reset; 40 | 41 | private RateLimit(int limit, int remaining, long reset) { 42 | this.limit = limit; 43 | this.remaining = remaining; 44 | this.reset = reset; 45 | } 46 | 47 | static RateLimit from(ClientHttpResponse response) { 48 | HttpHeaders headers = response.getHeaders(); 49 | int remaining = Integer.parseInt(headers.getFirst(HEADER_REMAINING)); 50 | int limit = Integer.parseInt(headers.getFirst(HEADER_LIMIT)); 51 | long reset = Long.parseLong(headers.getFirst(HEADER_RESET)) * 1000; 52 | return new RateLimit(limit, remaining, reset); 53 | } 54 | 55 | /** 56 | * Returns the maximum number of requests permitted per rate limit window. 57 | * @return the requests limit 58 | * 59 | */ 60 | public int getLimit() { 61 | return this.limit; 62 | } 63 | 64 | /** 65 | * Returns the number of remaining number of requests that will be permitted in the 66 | * current rate limit window. 67 | * @return the remaining number of requests 68 | */ 69 | public int getRemaining() { 70 | return this.remaining; 71 | } 72 | 73 | /** 74 | * The time, in milliseconds since the epoch, at which the rate limit window will 75 | * reset. 76 | * @return the window reset time 77 | */ 78 | public long getReset() { 79 | return this.reset; 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/test/java/io/spring/issuebot/triage/TriageIssueListenerTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2020 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot.triage; 18 | 19 | import java.util.Arrays; 20 | 21 | import io.spring.issuebot.IssueListener; 22 | import io.spring.issuebot.Repository; 23 | import io.spring.issuebot.github.Issue; 24 | import org.junit.jupiter.api.Test; 25 | 26 | import static org.mockito.BDDMockito.given; 27 | import static org.mockito.Mockito.mock; 28 | import static org.mockito.Mockito.verify; 29 | import static org.mockito.Mockito.verifyNoMoreInteractions; 30 | 31 | /** 32 | * Tests for {@link TriageIssueListener}. 33 | * 34 | * @author Andy Wilkinson 35 | */ 36 | class TriageIssueListenerTests { 37 | 38 | private final Repository repository = new Repository(); 39 | 40 | private final TriageFilter triageFilterOne = mock(TriageFilter.class); 41 | 42 | private final TriageFilter triageFilterTwo = mock(TriageFilter.class); 43 | 44 | private final TriageListener listener = mock(TriageListener.class); 45 | 46 | private final IssueListener issueListener = new TriageIssueListener( 47 | Arrays.asList(this.triageFilterOne, this.triageFilterTwo), this.listener); 48 | 49 | @Test 50 | void listenerIsCalledWhenIssueRequiresTriage() { 51 | Issue issue = new Issue(null, null, null, null, null, null, null, null); 52 | given(this.triageFilterOne.triaged(this.repository, issue)).willReturn(false); 53 | given(this.triageFilterTwo.triaged(this.repository, issue)).willReturn(false); 54 | this.issueListener.onOpenIssue(this.repository, issue); 55 | verify(this.triageFilterOne).triaged(this.repository, issue); 56 | verify(this.triageFilterTwo).triaged(this.repository, issue); 57 | verify(this.listener).requiresTriage(issue); 58 | } 59 | 60 | @Test 61 | void listenerIsNotCalledWhenIssueHasAlreadyBeenTriaged() { 62 | Issue issue = new Issue(null, null, null, null, null, null, null, null); 63 | given(this.triageFilterOne.triaged(this.repository, issue)).willReturn(true); 64 | this.issueListener.onOpenIssue(this.repository, issue); 65 | verify(this.triageFilterOne).triaged(this.repository, issue); 66 | verifyNoMoreInteractions(this.triageFilterTwo, this.listener); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/github/GitHubOperations.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot.github; 18 | 19 | import io.spring.issuebot.github.Issue.ClosureReason; 20 | 21 | /** 22 | * Operations that can be performed against the GitHub API. 23 | * 24 | * @author Andy Wilkinson 25 | */ 26 | public interface GitHubOperations { 27 | 28 | /** 29 | * Returns the open issues in the {@code repository} owned by the given 30 | * {@code organization}. 31 | * @param organization the name of the organization 32 | * @param repository the name of the repository 33 | * @return the issues 34 | */ 35 | Page getIssues(String organization, String repository); 36 | 37 | /** 38 | * Returns the comments that have been made on the given {@code issue}. 39 | * @param issue the issue 40 | * @return the comments 41 | */ 42 | Page getComments(Issue issue); 43 | 44 | /** 45 | * Adds the given {@code label} to the given {@code issue}. 46 | * @param issue the issue 47 | * @param label the label 48 | * @return the modified issue 49 | */ 50 | Issue addLabel(Issue issue, String label); 51 | 52 | /** 53 | * Removes the given {@code label} from the given {@code issue}. 54 | * @param issue the issue 55 | * @param label the label 56 | * @return the modified issue 57 | */ 58 | Issue removeLabel(Issue issue, String label); 59 | 60 | /** 61 | * Adds the given {@code comment} to the given {@code issue}. 62 | * @param issue the issue 63 | * @param comment the comment 64 | * @return the added comment 65 | */ 66 | Comment addComment(Issue issue, String comment); 67 | 68 | /** 69 | * Closes the given {@code issue}. 70 | * @param issue the issue 71 | * @param reason the reason for closing the issue 72 | * @return the modified issue 73 | */ 74 | Issue close(Issue issue, ClosureReason reason); 75 | 76 | /** 77 | * Returns the events that have occurred on the given {@code issue}. 78 | * @param issue the issue 79 | * @return the events 80 | */ 81 | Page getEvents(Issue issue); 82 | 83 | /** 84 | * Returns the current rate limit information or {@code null} if the limit is 85 | * currently unknown. 86 | * @return the rate limit or {@code null} 87 | */ 88 | RateLimit getRateLimit(); 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/Repository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot; 18 | 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | import java.util.Objects; 22 | 23 | /** 24 | * A repository that is monitored. 25 | * 26 | * @author Andy Wilkinson 27 | */ 28 | public class Repository { 29 | 30 | /** 31 | * The name of the organization that owns the repository. 32 | */ 33 | private String organization; 34 | 35 | /** 36 | * The name of the repository. 37 | */ 38 | private String name; 39 | 40 | /** 41 | * The names of the repository's collaborators. 42 | */ 43 | private List collaborators = new ArrayList<>(); 44 | 45 | public String getOrganization() { 46 | return this.organization; 47 | } 48 | 49 | public void setOrganization(String organization) { 50 | this.organization = organization; 51 | } 52 | 53 | public String getName() { 54 | return this.name; 55 | } 56 | 57 | public void setName(String name) { 58 | this.name = name; 59 | } 60 | 61 | public List getCollaborators() { 62 | return this.collaborators; 63 | } 64 | 65 | public void setCollaborators(List collaborators) { 66 | this.collaborators = collaborators; 67 | } 68 | 69 | @Override 70 | public boolean equals(Object obj) { 71 | if (this == obj) { 72 | return true; 73 | } 74 | if (obj == null) { 75 | return false; 76 | } 77 | if (getClass() != obj.getClass()) { 78 | return false; 79 | } 80 | Repository other = (Repository) obj; 81 | if (this.name == null) { 82 | if (other.name != null) { 83 | return false; 84 | } 85 | } 86 | else if (!this.name.equals(other.name)) { 87 | return false; 88 | } 89 | if (this.organization == null) { 90 | return other.organization == null; 91 | } 92 | return Objects.equals(this.organization, other.organization); 93 | } 94 | 95 | @Override 96 | public int hashCode() { 97 | final int prime = 31; 98 | int result = 1; 99 | result = prime * result + ((this.name == null) ? 0 : this.name.hashCode()); 100 | result = prime * result + ((this.organization == null) ? 0 : this.organization.hashCode()); 101 | return result; 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/feedback/FeedbackProperties.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot.feedback; 18 | 19 | import org.springframework.boot.context.properties.ConfigurationProperties; 20 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 21 | 22 | /** 23 | * {@link EnableConfigurationProperties Configuration properties} for configuring the 24 | * monitoring of issues that require user feedback. 25 | * 26 | * @author Andy Wilkinson 27 | */ 28 | @ConfigurationProperties(prefix = "issuebot.feedback") 29 | final class FeedbackProperties { 30 | 31 | /** 32 | * Name of the label that is applied when feedback is required. 33 | */ 34 | private String requiredLabel; 35 | 36 | /** 37 | * Name of the label that is applied when feedback has been provided. 38 | */ 39 | private String providedLabel; 40 | 41 | /** 42 | * Name of the label that is applied when the feedback reminder comment has been made. 43 | */ 44 | private String reminderLabel; 45 | 46 | /** 47 | * The text of the comment that is added as a reminder that feedback is required. 48 | */ 49 | private String reminderComment; 50 | 51 | /** 52 | * The text of the comment that is added when an issue is closed as feedback has not 53 | * been provided. 54 | */ 55 | private String closeComment; 56 | 57 | public String getRequiredLabel() { 58 | return this.requiredLabel; 59 | } 60 | 61 | public void setRequiredLabel(String requiredLabel) { 62 | this.requiredLabel = requiredLabel; 63 | } 64 | 65 | public String getProvidedLabel() { 66 | return this.providedLabel; 67 | } 68 | 69 | public void setProvidedLabel(String providedLabel) { 70 | this.providedLabel = providedLabel; 71 | } 72 | 73 | public String getReminderLabel() { 74 | return this.reminderLabel; 75 | } 76 | 77 | public void setReminderLabel(String reminderLabel) { 78 | this.reminderLabel = reminderLabel; 79 | } 80 | 81 | public String getReminderComment() { 82 | return this.reminderComment; 83 | } 84 | 85 | public void setReminderComment(String reminderComment) { 86 | this.reminderComment = reminderComment; 87 | } 88 | 89 | public String getCloseComment() { 90 | return this.closeComment; 91 | } 92 | 93 | public void setCloseComment(String closeComment) { 94 | this.closeComment = closeComment; 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/RepositoryMonitor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot; 18 | 19 | import java.util.Date; 20 | import java.util.List; 21 | 22 | import io.spring.issuebot.github.GitHubOperations; 23 | import io.spring.issuebot.github.Issue; 24 | import io.spring.issuebot.github.Page; 25 | import io.spring.issuebot.github.RateLimit; 26 | import org.slf4j.Logger; 27 | import org.slf4j.LoggerFactory; 28 | 29 | import org.springframework.scheduling.annotation.Scheduled; 30 | 31 | /** 32 | * Central class for monitoring the configured repository. 33 | * 34 | * @author Andy Wilkinson 35 | */ 36 | class RepositoryMonitor { 37 | 38 | private static final Logger log = LoggerFactory.getLogger(RepositoryMonitor.class); 39 | 40 | private final GitHubOperations gitHub; 41 | 42 | private final List repositories; 43 | 44 | private final boolean enabled; 45 | 46 | private final List issueListeners; 47 | 48 | RepositoryMonitor(GitHubOperations gitHub, List repositories, boolean enabled, 49 | List issueListeners) { 50 | this.gitHub = gitHub; 51 | this.repositories = repositories; 52 | this.enabled = enabled; 53 | this.issueListeners = issueListeners; 54 | } 55 | 56 | @Scheduled(fixedRate = 5 * 60 * 1000) 57 | void monitor() { 58 | if (this.enabled) { 59 | for (Repository repository : this.repositories) { 60 | monitor(repository); 61 | } 62 | } 63 | } 64 | 65 | private void monitor(Repository repository) { 66 | log.info("Monitoring {}/{}", repository.getOrganization(), repository.getName()); 67 | try { 68 | Page page = this.gitHub.getIssues(repository.getOrganization(), repository.getName()); 69 | while (page != null) { 70 | for (Issue issue : page.getContent()) { 71 | for (IssueListener issueListener : this.issueListeners) { 72 | try { 73 | issueListener.onOpenIssue(repository, issue); 74 | } 75 | catch (Exception ex) { 76 | log.warn("Listener '{}' failed when handling issue '{}'", issueListener, issue, ex); 77 | } 78 | } 79 | } 80 | page = page.next(); 81 | } 82 | } 83 | catch (Exception ex) { 84 | log.warn("A failure occurred during monitoring of {}/{}", repository.getOrganization(), 85 | repository.getName(), ex); 86 | } 87 | RateLimit rateLimit = this.gitHub.getRateLimit(); 88 | if (rateLimit == null) { 89 | log.info("Monitoring of {}/{} completed. Remaining rate limit unknown", repository.getOrganization(), 90 | repository.getName()); 91 | } 92 | else { 93 | log.info("Monitoring of {}/{} completed. Remaining rate limit {} of {} resets at {}", 94 | repository.getOrganization(), repository.getName(), rateLimit.getRemaining(), rateLimit.getLimit(), 95 | new Date(rateLimit.getReset())); 96 | } 97 | 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/test/resources/io/spring/issuebot/github/issue-single-label.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://api.github.com/repos/spring-projects/spring-boot/issues/4644", 3 | "labels_url": "https://api.github.com/repos/spring-projects/spring-boot/issues/4644/labels{/name}", 4 | "comments_url": "https://api.github.com/repos/spring-projects/spring-boot/issues/4644/comments", 5 | "events_url": "https://api.github.com/repos/spring-projects/spring-boot/issues/4644/events", 6 | "html_url": "https://github.com/spring-projects/spring-boot/issues/4644", 7 | "id": 119467644, 8 | "number": 4644, 9 | "title": "Upgrade to Spring Framework 4.2.4.RELEASE", 10 | "user": { 11 | "login": "wilkinsona", 12 | "id": 914682, 13 | "avatar_url": "https://avatars.githubusercontent.com/u/914682?v=3", 14 | "gravatar_id": "", 15 | "url": "https://api.github.com/users/wilkinsona", 16 | "html_url": "https://github.com/wilkinsona", 17 | "followers_url": "https://api.github.com/users/wilkinsona/followers", 18 | "following_url": "https://api.github.com/users/wilkinsona/following{/other_user}", 19 | "gists_url": "https://api.github.com/users/wilkinsona/gists{/gist_id}", 20 | "starred_url": "https://api.github.com/users/wilkinsona/starred{/owner}{/repo}", 21 | "subscriptions_url": "https://api.github.com/users/wilkinsona/subscriptions", 22 | "organizations_url": "https://api.github.com/users/wilkinsona/orgs", 23 | "repos_url": "https://api.github.com/users/wilkinsona/repos", 24 | "events_url": "https://api.github.com/users/wilkinsona/events{/privacy}", 25 | "received_events_url": "https://api.github.com/users/wilkinsona/received_events", 26 | "type": "User", 27 | "site_admin": false 28 | }, 29 | "labels": [ 30 | { 31 | "url": "https://api.github.com/repos/spring-projects/spring-boot/labels/test", 32 | "name": "test", 33 | "color": "e11d21" 34 | } 35 | ], 36 | "state": "open", 37 | "locked": false, 38 | "assignee": null, 39 | "milestone": { 40 | "url": "https://api.github.com/repos/spring-projects/spring-boot/milestones/49", 41 | "html_url": "https://github.com/spring-projects/spring-boot/milestones/1.3.1", 42 | "labels_url": "https://api.github.com/repos/spring-projects/spring-boot/milestones/49/labels", 43 | "id": 1396509, 44 | "number": 49, 45 | "title": "1.3.1", 46 | "description": "", 47 | "creator": { 48 | "login": "philwebb", 49 | "id": 519772, 50 | "avatar_url": "https://avatars.githubusercontent.com/u/519772?v=3", 51 | "gravatar_id": "", 52 | "url": "https://api.github.com/users/philwebb", 53 | "html_url": "https://github.com/philwebb", 54 | "followers_url": "https://api.github.com/users/philwebb/followers", 55 | "following_url": "https://api.github.com/users/philwebb/following{/other_user}", 56 | "gists_url": "https://api.github.com/users/philwebb/gists{/gist_id}", 57 | "starred_url": "https://api.github.com/users/philwebb/starred{/owner}{/repo}", 58 | "subscriptions_url": "https://api.github.com/users/philwebb/subscriptions", 59 | "organizations_url": "https://api.github.com/users/philwebb/orgs", 60 | "repos_url": "https://api.github.com/users/philwebb/repos", 61 | "events_url": "https://api.github.com/users/philwebb/events{/privacy}", 62 | "received_events_url": "https://api.github.com/users/philwebb/received_events", 63 | "type": "User", 64 | "site_admin": false 65 | }, 66 | "open_issues": 41, 67 | "closed_issues": 31, 68 | "state": "open", 69 | "created_at": "2015-11-05T22:07:18Z", 70 | "updated_at": "2015-11-30T10:51:50Z", 71 | "due_on": "2015-12-16T00:00:00Z", 72 | "closed_at": null 73 | }, 74 | "comments": 0, 75 | "created_at": "2015-11-30T10:51:50Z", 76 | "updated_at": "2015-11-30T10:52:18Z", 77 | "closed_at": null, 78 | "body": "" 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/feedback/StandardFeedbackListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot.feedback; 18 | 19 | import java.time.OffsetDateTime; 20 | import java.util.List; 21 | 22 | import io.spring.issuebot.IssueListener; 23 | import io.spring.issuebot.Repository; 24 | import io.spring.issuebot.github.GitHubOperations; 25 | import io.spring.issuebot.github.Issue; 26 | import io.spring.issuebot.github.Issue.ClosureReason; 27 | import io.spring.issuebot.github.Label; 28 | 29 | /** 30 | * Standard implementation of {@link FeedbackListener}. 31 | * 32 | * @author Andy Wilkinson 33 | */ 34 | final class StandardFeedbackListener implements FeedbackListener { 35 | 36 | private final GitHubOperations gitHub; 37 | 38 | private final String providedLabel; 39 | 40 | private final String requiredLabel; 41 | 42 | private final String reminderLabel; 43 | 44 | private final String reminderComment; 45 | 46 | private final String closeComment; 47 | 48 | private final List issueListeners; 49 | 50 | StandardFeedbackListener(GitHubOperations gitHub, String providedLabel, String requiredLabel, String reminderLabel, 51 | String reminderComment, String closeComment, List issueListeners) { 52 | this.gitHub = gitHub; 53 | this.providedLabel = providedLabel; 54 | this.requiredLabel = requiredLabel; 55 | this.reminderLabel = reminderLabel; 56 | this.reminderComment = reminderComment; 57 | this.closeComment = closeComment; 58 | this.issueListeners = issueListeners; 59 | } 60 | 61 | @Override 62 | public void feedbackProvided(Repository repository, Issue issue) { 63 | this.gitHub.addLabel(issue, this.providedLabel); 64 | this.gitHub.removeLabel(issue, this.requiredLabel); 65 | if (hasReminderLabel(issue)) { 66 | this.gitHub.removeLabel(issue, this.reminderLabel); 67 | } 68 | } 69 | 70 | @Override 71 | public void feedbackRequired(Repository repository, Issue issue, OffsetDateTime requestTime) { 72 | if (issue.getPullRequest() != null) { 73 | return; 74 | } 75 | OffsetDateTime now = OffsetDateTime.now(); 76 | if (requestTime.plusDays(14).isBefore(now)) { 77 | close(repository, issue); 78 | } 79 | else if (requestTime.plusDays(7).isBefore(now) && !hasReminderLabel(issue)) { 80 | remind(issue); 81 | } 82 | } 83 | 84 | private void close(Repository repository, Issue issue) { 85 | this.gitHub.addComment(issue, this.closeComment); 86 | this.gitHub.close(issue, ClosureReason.NOT_PLANNED); 87 | this.gitHub.removeLabel(issue, this.requiredLabel); 88 | this.gitHub.removeLabel(issue, this.reminderLabel); 89 | this.issueListeners.forEach((listener) -> listener.onIssueClosure(repository, issue)); 90 | } 91 | 92 | private boolean hasReminderLabel(Issue issue) { 93 | if (issue.getLabels() != null) { 94 | for (Label label : issue.getLabels()) { 95 | if (this.reminderLabel.equals(label.getName())) { 96 | return true; 97 | } 98 | } 99 | } 100 | return false; 101 | } 102 | 103 | private void remind(Issue issue) { 104 | this.gitHub.addComment(issue, this.reminderComment); 105 | this.gitHub.addLabel(issue, this.reminderLabel); 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/io/spring/issuebot/github/Issue.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.spring.issuebot.github; 18 | 19 | import java.util.List; 20 | 21 | import com.fasterxml.jackson.annotation.JsonCreator; 22 | import com.fasterxml.jackson.annotation.JsonProperty; 23 | 24 | /** 25 | * A GitHub issue. 26 | * 27 | * @author Andy Wilkinson 28 | */ 29 | public class Issue { 30 | 31 | private final String url; 32 | 33 | private final String commentsUrl; 34 | 35 | private final String eventsUrl; 36 | 37 | private final String labelsUrl; 38 | 39 | private final User user; 40 | 41 | private final List