8000 Automatically add PR tasks depending on the jira issue type · hibernate/hibernate-github-bot@74701f7 · GitHub
[go: up one dir, main page]

Skip to content

Commit 74701f7

Browse files
committed
Automatically add PR tasks depending on the jira issue type
1 parent e402989 commit 74701f7

10 files changed

+278
-0
lines changed
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package org.hibernate.infra.bot;
2+
3+
import java.io.IOException;
4+
import java.util.HashSet;
5+
import java.util.List;
6+
import java.util.Locale;
7+
import java.util.Objects;
8+
import java.util.Set;
9+
import java.util.regex.Pattern;
10+
11+
import org.hibernate.infra.bot.config.DeploymentConfig;
12+
import org.hibernate.infra.bot.config.RepositoryConfig;
13+
import org.hibernate.infra.bot.jira.JiraIssue;
14+
import org.hibernate.infra.bot.jira.JiraIssues;
15+
import org.hibernate.infra.bot.jira.JiraRestClient;
16+
import org.hibernate.infra.bot.util.CommitMessages;
17+
import org.hibernate.infra.bot.util.Patterns;
18+
19+
import org.jboss.logging.Logger;
20+
21+
import io.quarkiverse.githubapp.ConfigFile;
22+
import io.quarkiverse.githubapp.event.PullRequest;
23+
import jakarta.inject.Inject;
24+
import org.eclipse.microprofile.rest.client.inject.RestClient;
25+
import org.kohsuke.github.GHEventPayload;
26+
import org.kohsuke.github.GHIssueState;
27+
import org.kohsuke.github.GHPullRequest;
28+
import org.kohsuke.github.GHPullRequestCommitDetail;
29+
import org.kohsuke.github.GHRepository;
30+
31+
public class EditPullRequestBodyAddTaskList {
32+
private static final Logger LOG = Logger.getLogger( EditPullRequestBodyAddTaskList.class );
33+
34+
private static final String START_MARKER = "<!-- Hibernate GitHub Bot task list start -->";
35+
36+
private static final String END_MARKER = "<!-- Hibernate GitHub Bot task list end -->";
37+
38+
@Inject
39+
DeploymentConfig deploymentConfig;
40+
@RestClient
41+
JiraRestClient jiraRestClient;
42+
43+
void pullRequestChanged(
44+
@PullRequest.Opened @PullRequest.Reopened @PullRequest.Edited @PullRequest.Synchronize
45+
GHEventPayload.PullRequest payload,
46+
@ConfigFile("hibernate-github-bot.yml") RepositoryConfig repositoryConfig
47+
) throws IOException {
48+
addUpdateTaskList( payload.getRepository(), repositoryConfig, payload.getPullRequest() );
49+
}
50+
51+
private void addUpdateTaskList(
52+
GHRepository repository,
53+
RepositoryConfig repositoryConfig,
54+
GHPullRequest pullRequest
55+
) throws IOException {
56+
if ( repositoryConfig == null || repositoryConfig.pullRequestTasks == null
57+
|| !repositoryConfig.pullRequestTasks.getEnabled().orElse( Boolean.FALSE ) ) {
58+
return;
59+
}
60+
61+
if ( !shouldCheck( repository, pullRequest ) ) {
62+
return;
63+
}
64+
65+
final Set<String> issueKeys = new HashSet<>();
66+
boolean genericTasksRequired = false;
67+
if ( repositoryConfig.jira.getIssueKeyPattern().isPresent() ) {
68+
Pattern issueKeyPattern = repositoryConfig.jira.getIssueKeyPattern().get();
69+
70+
for ( GHPullRequestCommitDetail commitDetails : pullRequest.listCommits() ) {
71+
final GHPullRequestCommitDetail.Commit commit = commitDetails.getCommit();
72+
final List<String> commitIssueKeys = CommitMessages.extractIssueKeys(
73+
issueKeyPattern,
74+
commit.getMessage()
75+
);
76+
issueKeys.addAll( commitIssueKeys );
77+
genericTasksRequired = genericTasksRequired || commitIssueKeys.isEmpty();
78+
}
79+
}
80+
else {
81+
genericTasksRequired = true;
82+
}
83+
84+
final String originalBody = Objects.toString( pullRequest.getBody(), "" );
85+
final String currentTasks = currentTaskBody( originalBody );
86+
final String tasks = generateTaskList( repositoryConfig.pullRequestTasks, genericTasksRequired, issueKeys );
87+
88+
String body;
89+
90+
if ( currentTasks == null && tasks == null ) {
91+
return;
92+
}
93+
94+
if ( tasks == null ) {
95+
body = originalBody.replace( currentTasks, "" );
96+
}
97+
else if ( currentTasks != null ) {
98+
if (tasksAreTheSame( currentTasks, tasks ) ) {
99+
return;
100+
}
101+
body = originalBody.replace( currentTasks, tasks );
102+
}
103+
else {
104+
body = "%s\n\n---\n%s\n%s\n%s".formatted( originalBody, START_MARKER, tasks, END_MARKER );
105+
}
106+
107+
if ( !deploymentConfig.isDryRun() ) {
108+
pullRequest.setBody( body );
109+
}
110+
else {
111+
LOG.info( "Pull request #" + pullRequest.getNumber() + " - Updated PR body: " + body );
112+
}
113+
}
114+
115+
private boolean tasksAreTheSame(String currentTasks, String tasks) {
116+
return Patterns.compile( tasks.replace( "- [ ]", "- \\[.\\]" ).replace( "\n", "\\n" ) )
117+
.matcher( currentTasks )
118+
.matches();
119+
}
120+
121+
private String generateTaskList(RepositoryConfig.TaskList taskListConfiguration, boolean genericTasksRequired, Set<String> issueKeys) {
122+
if ( !genericTasksRequired && issueKeys.isEmpty() ) {
123+
return null;
124+
}
125+
StringBuilder taskList = new StringBuilder();
126+
taskList.append( "Please make sure that the following tasks are completed:\n" );
127+
if ( genericTasksRequired ) {
128+
addTasks( taskList, taskListConfiguration.defaultTasks() );
129+
}
130+
if ( !issueKeys.isEmpty() ) {
131+
JiraIssues issues = jiraRestClient.find( "key IN (" + String.join( ",", issueKeys ) + ") ORDER BY KEY DESC", "issuetype,key" );
132+
for ( JiraIssue issue : issues.issues ) {
133+
taskList.append( "Tasks specific to " )
134+
.append( issue.key )
135+
.append( " (" )
136+
.append( issue.fields.issuetype.name )
137+
.append( "):\n" );
138+
addTasks( taskList, taskListConfiguration.getTasks().getOrDefault( issue.fields.issuetype.name.toLowerCase( Locale.ROOT ), taskListConfiguration.defaultTasks() ) );
139+
}
140+
}
141+
142+
return taskList.toString();
143+
}
144+
145+
private void addTasks(StringBuilder sb, List<String> tasks) {
146+
for ( String task : tasks ) {
147+
sb.append( "- [ ] " )
148+
.append( task )
149+
.append( "\n" );
150+
}
151+
sb.append( "\n" );
152+
}
153+
154+
private static String currentTaskBody(String originalBody) {
155+
// Check if the body already contains the tasks
156+
final int startIndex = originalBody.indexOf( START_MARKER );
157+
final int endIndex = startIndex > -1 ? originalBody.indexOf( END_MARKER ) : -1;
158+
if ( startIndex > -1 && endIndex > -1 ) {
159+
return originalBody.substring( startIndex + START_MARKER.length() + 1, endIndex - 1 );
160+
}
161+
else {
162+
return null;
163+
}
164+
}
165+
166+
private boolean shouldCheck(GHRepository repository, GHPullRequest pullRequest) {
167+
return !GHIssueState.CLOSED.equals( pullRequest.getState() )
168+
&& repository.getId() == pullRequest.getBase().getRepository().getId();
169+
}
170+
}

src/main/java/org/hibernate/infra/bot/config/DeploymentConfig.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ public interface DeploymentConfig {
1414

1515
Jenkins jenkins();
1616

17+
Jira jira();
18+
1719
default boolean isDryRun() {
1820
Optional<Boolean> dryRun = dryRun();
1921
return dryRun.isPresent() && dryRun.get();
@@ -27,4 +29,10 @@ interface Develocity {
2729
interface Jenkins {
2830
long githubAppId();
2931
}
32+
33+
interface Jira {
34+
URI uri();
35+
String username();
36+
String token();
37+
}
3038
}

src/main/java/org/hibernate/infra/bot/config/RepositoryConfig.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import java.util.ArrayList;
44
import java.util.Collections;
5+
import java.util.HashMap;
56
import java.util.List;
7+
import java.util.Map;
68
import java.util.Optional;
79
import java.util.regex.Pattern;
810

@@ -16,6 +18,8 @@ public class RepositoryConfig {
1618

1719
public LicenseAgreement licenseAgreement;
1820

21+
public TaskList pullRequestTasks;
22+
1923
public static class JiraConfig {
2024
private Optional<Pattern> issueKeyPattern = Optional.empty();
2125

@@ -151,4 +155,30 @@ public void setPullRequestTemplatePattern(String pullRequestTemplatePattern) {
151155
this.pullRequestTemplatePattern = Patterns.compile( pullRequestTemplatePattern );
152156
}
153157
}
158+
159+
public static class TaskList {
160+
private static final String DEFAULT_TASKS_CATEGORY = "default";
161+
private Optional<Boolean> enabled = Optional.empty();
162+
private Map<String, List<String>> tasks = new HashMap<>();
163+
164+
public Optional<Boolean> getEnabled() {
165+
return enabled;
166+
}
167+
168+
public void setEnabled(Boolean enabled) {
169+
this.enabled = Optional.of( enabled );
170+
}
171+
172+
public Map<String, List<String>> getTasks() {
173+
return tasks;
174+
}
175+
176+
public void setTasks(Map<String, List<String>> tasks) {
177+
this.tasks = tasks;
178+
}
179+
180+
public List<String> defaultTasks() {
181+
return tasks.getOrDefault( DEFAULT_TASKS_CATEGORY, List.of() );
182+
}
183+
}
154184
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.hibernate.infra.bot.jira;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
6+
import com.fasterxml.jackson.annotation.JsonAnyGetter;
7+
import com.fasterxml.jackson.annotation.JsonAnySetter;
8+
9+
public class JiraBaseObject {
10+
@JsonAnyGetter
11+
@JsonAnySetter
12+
private Map<String, Object> properties = new HashMap<>();
13+
14+
public Map<String, Object> properties() {
15+
return properties;
16+
}
17+
18+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package org.hibernate.infra.bot.jira;
2+
3+
public class JiraFields extends JiraBaseObject {
4+
public JiraSimpleObject issuetype;
5+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package org.hibernate.infra.bot.jira;
2+
3+
import java.net.URI;
4+
5+
public class JiraIssue extends JiraBaseObject {
6+
public Long id;
7+
public String key;
8+
public URI self;
9+
public JiraFields fields;
10+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.hibernate.infra.bot.jira;
2+
3+
import java.util.List;
4+
5+
public class JiraIssues extends JiraBaseObject {
6+
public List<JiraIssue> issues;
7+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package org.hibernate.infra.bot.jira;
2+
3+
import io.quarkus.rest.client.reactive.ClientBasicAuth;
4+
import jakarta.ws.rs.GET;
5+
import jakarta.ws.rs.Path;
6+
import jakarta.ws.rs.QueryParam;
7+
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
8+
9+
@Path("/rest/api/3")
10+
@RegisterRestClient(configKey = "jira")
11+
@ClientBasicAuth(username = "${hibernate-github-bot.jira.username}", password = "${hibernate-github-bot.jira.token}")
12+
public interface JiraRestClient {
13+
@GET
14+
@Path("/search/jql")
15+
JiraIssues find(@QueryParam("jql") String jql, @QueryParam("fields") String fields);
16+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.hibernate.infra.bot.jira;
2+
3+
import java.net.URI;
4+
5+
public class JiraSimpleObject extends JiraBaseObject {
6+
public String id;
7+
public String name;
8+
public URI self;
9+
}

src/main/resources/application.properties

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ quarkus.openapi-generator.gradle_enterprise_2023_4_api_yaml.auth.DevelocityAcces
2323
%dev,test.hibernate-github-bot.dry-run=false
2424
%dev,test.hibernate-github-bot.develocity.access-key=foo
2525

26+
hibernate-github-bot.jira.uri=https://hibernate.atlassian.net/
27+
hibernate-github-bot.jira.username=
28+
hibernate-github-bot.jira.token=
29+
quarkus.rest-client.jira.url=${hibernate-github-bot.jira.uri}
30+
2631
##############
2732
# Deployment configuration:
2833
#

0 commit comments

Comments
 (0)
0