Skip to content

Commit b0db868

Browse files
committed
New action that automatically edits PR bodies adding issue links
1 parent c68d58b commit b0db868

8 files changed

+1334
-23
lines changed

src/main/java/org/hibernate/infra/bot/CheckPullRequestContributionRules.java

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,10 @@ private List<Check> createChecks(RepositoryConfig repositoryConfig) {
135135
checks.add( new TitleCheck() );
136136

137137
if ( repositoryConfig != null && repositoryConfig.jira != null ) {
138+
final boolean checkMentions = repositoryConfig.enableEditing == null
139+
|| repositoryConfig.enableEditing.equals( Boolean.FALSE );
138140
repositoryConfig.jira.getIssueKeyPattern()
139-
.ifPresent( issueKeyPattern -> checks.add( new JiraIssuesCheck( issueKeyPattern ) ) );
141+
.ifPresent( issueKeyPattern -> checks.add( new JiraIssuesCheck( issueKeyPattern, checkMentions ) ) );
140142
}
141143

142144
return checks;
@@ -163,9 +165,12 @@ static class JiraIssuesCheck extends Check {
163165

164166
private final Pattern issueKeyPattern;
165167

166-
JiraIssuesCheck(Pattern issueKeyPattern) {
168+
private final boolean checkMentions;
169+
170+
JiraIssuesCheck(Pattern issueKeyPattern, boolean checkMentions) {
167171
super( "Contribution — JIRA issues" );
168172
this.issueKeyPattern = issueKeyPattern;
173+
this.checkMentions = checkMentions;
169174
}
170175

171176
@Override
@@ -196,17 +201,19 @@ public void perform(CheckRunContext context, CheckRunOutput output) throws IOExc
196201
commitRule.failed( "Offending commits: " + commitsWithMessageNotStartingWithIssueKey );
197202
}
198203

199-
CheckRunRule pullRequestRule = output.rule(
200-
"The PR title or body should list the keys of all JIRA issues mentioned in the commits" );
201-
List<String> issueKeysNotMentionedInPullRequest = issueKeys.stream()
202-
.filter( issueKey -> ( title == null || !title.contains( issueKey ) )
203-
&& ( body == null || !body.contains( issueKey ) ) )
204-
.toList();
205-
if ( issueKeysNotMentionedInPullRequest.isEmpty() ) {
206-
pullRequestRule.passed();
207-
}
208-
else {
209-
pullRequestRule.failed( "Issue keys mentioned in commits but missing from the PR title or body: " + issueKeysNotMentionedInPullRequest );
204+
if ( checkMentions ) {
205+
// We only need to check mentions if automatic body editing is disabled
206+
CheckRunRule pullRequestRule = output.rule(
207+
"The PR title or body should list the keys of all JIRA issues mentioned in the commits");
208+
List<String> issueKeysNotMentionedInPullRequest = issueKeys.stream()
209+
.filter(issueKey -> (title == null || !title.contains(issueKey))
210+
&& (body == null || !body.contains(issueKey)))
211+
.toList();
212+
if (issueKeysNotMentionedInPullRequest.isEmpty()) {
213+
pullRequestRule.passed();
214+
} else {
215+
pullRequestRule.failed("Issue keys mentioned in commits but missing from the PR title or body: " + issueKeysNotMentionedInPullRequest);
216+
}
210217
}
211218
}
212219
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
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.Set;
8+
9+
import org.hibernate.infra.bot.config.DeploymentConfig;
10+
import org.hibernate.infra.bot.config.RepositoryConfig;
11+
import org.hibernate.infra.bot.util.CommitMessages;
12+
13+
import org.jboss.logging.Logger;
14+
15+
import io.quarkiverse.githubapp.ConfigFile;
16+
import io.quarkiverse.githubapp.event.CheckRun;
17+
import io.quarkiverse.githubapp.event.CheckSuite;
18+
import io.quarkiverse.githubapp.event.PullRequest;
19+
import jakarta.inject.Inject;
20+
import org.kohsuke.github.GHEventPayload;
21+
import org.kohsuke.github.GHIssueState;
22+
import org.kohsuke.github.GHPullRequest;
23+
import org.kohsuke.github.GHPullRequestCommitDetail;
24+
import org.kohsuke.github.GHRepository;
25+
26+
/**
27+
* @author Marco Belladelli
28+
*/
29+
public class EditPullRequestBodyAddIssueLinks {
30+
private static final Logger LOG = Logger.getLogger( EditPullRequestBodyAddIssueLinks.class );
31+
32+
private static final String START_MARKER = "<!-- Hibernate GitHub Bot issue links start -->";
33+
34+
private static final String END_MARKER = "<!-- Hibernate GitHub Bot issue links end -->";
35+
36+
private static final String LINK_TEMPLATE = "https://hibernate.atlassian.net/browse/%s";
37+
38+
@Inject
39+
DeploymentConfig deploymentConfig;
40+
41+
void pullRequestChanged(
42+
@PullRequest.Opened @PullRequest.Reopened @PullRequest.Edited @PullRequest.Synchronize
43+
GHEventPayload.PullRequest payload,
44+
@ConfigFile( "hibernate-github-bot.yml" ) RepositoryConfig repositoryConfig) throws IOException {
45+
editPullRequestBodyAddIssueLinks( payload.getRepository(), repositoryConfig, payload.getPullRequest() );
46+
}
47+
48+
void checkRunRequested(
49+
@CheckRun.Rerequested GHEventPayload.CheckRun payload,
50+
@ConfigFile( "hibernate-github-bot.yml" ) RepositoryConfig repositoryConfig) throws IOException {
51+
for ( GHPullRequest pullRequest : payload.getCheckRun().getPullRequests() ) {
52+
editPullRequestBodyAddIssueLinks( payload.getRepository(), repositoryConfig, pullRequest );
53+
}
54+
}
55+
56+
void checkSuiteRequested(
57+
@CheckSuite.Requested @CheckSuite.Rerequested GHEventPayload.CheckSuite payload,
58+
@ConfigFile( "hibernate-github-bot.yml" ) RepositoryConfig repositoryConfig) throws IOException {
59+
for ( GHPullRequest pullRequest : payload.getCheckSuite().getPullRequests() ) {
60+
editPullRequestBodyAddIssueLinks( payload.getRepository(), repositoryConfig, pullRequest );
61+
}
62+
}
63+
64+
private void editPullRequestBodyAddIssueLinks(
65+
GHRepository repository,
66+
RepositoryConfig repositoryConfig,
67+
GHPullRequest pullRequest) throws IOException {
68+
if ( !shouldCheck( repository, pullRequest ) ) {
69+
return;
70+
}
71+
72+
if ( repositoryConfig == null || repositoryConfig.enableEditing == null || repositoryConfig.enableEditing.equals( Boolean.FALSE )
73+
|| repositoryConfig.jira == null || repositoryConfig.jira.getIssueKeyPattern().isEmpty() ) {
74+
return;
75+
}
76+
77+
final Set<String> issueKeys = new HashSet<>();
78+
// Collect all issue keys from commit messages
79+
repositoryConfig.jira.getIssueKeyPattern().ifPresent( issueKeyPattern -> {
80+
for ( GHPullRequestCommitDetail commitDetails : pullRequest.listCommits() ) {
81+
final GHPullRequestCommitDetail.Commit commit = commitDetails.getCommit();
82+
final List<String> commitIssueKeys = CommitMessages.extractIssueKeys(
83+
issueKeyPattern,
84+
commit.getMessage()
85+
);
86+
issueKeys.addAll( commitIssueKeys );
87+
}
88+
} );
89+
90+
if ( issueKeys.isEmpty() ) {
91+
LOG.info( "Found no issue keys in commits, terminating." );
92+
return;
93+
}
94+
95+
final String originalBody = pullRequest.getBody();
96+
final StringBuilder sb = new StringBuilder();
97+
if ( originalBody != null ) {
98+
// Check if the body already contains the link section
99+
final int startIndex = originalBody.indexOf( START_MARKER );
100+
final int endIndex = startIndex > -1 ? originalBody.indexOf( END_MARKER ) : -1;
101+
if ( startIndex > -1 && endIndex > -1 ) {
102+
// Remove the whole section, it will be re-appended at the end of the body
103+
sb.append( originalBody.substring( 0, startIndex ).trim() );
104+
final String following = originalBody.substring( endIndex + END_MARKER.length() ).trim();
105+
if ( following.length() > 0 ) {
106+
sb.append( "\n\n" );
107+
sb.append( following );
108+
}
109+
}
110+
else {
111+
sb.append( originalBody.trim() );
112+
}
113+
}
114+
115+
final String body = sb.toString();
116+
final String linksSection = constructLinksSection( issueKeys, body );
117+
if ( linksSection == null ) {
118+
// All issue links were already found in the request body, nothing to do
119+
return;
120+
}
121+
122+
final String newBody = body.length() == 0
123+
? linksSection
124+
: body + "\n\n" + linksSection;
125+
if ( !deploymentConfig.isDryRun() ) {
126+
pullRequest.setBody( newBody );
127+
}
128+
else {
129+
LOG.info( "Pull request #" + pullRequest.getNumber() + " - Updated PR body: " + newBody );
130+
}
131+
}
132+
133+
private String constructLinksSection(Set<String> issueKeys, String originalBody) {
134+
final String lowerCaseBody = originalBody.toLowerCase( Locale.ROOT );
135+
final StringBuilder sb = new StringBuilder();
136+
for ( String key : issueKeys ) {
137+
if ( !lowerCaseBody.contains( key.toLowerCase( Locale.ROOT ) ) ) {
138+
// Only add links for issue keys that are not already found
139+
// in the original PR body
140+
sb.append( String.format( LINK_TEMPLATE, key ) ).append( '\n' );
141+
}
142+
}
143+
144+
if ( sb.isEmpty() ) {
145+
return null;
146+
}
147+
148+
return START_MARKER + "\n" + sb + END_MARKER;
149+
}
150+
151+
private boolean shouldCheck(GHRepository repository, GHPullRequest pullRequest) {
152+
return !GHIssueState.CLOSED.equals( pullRequest.getState() )
153+
&& repository.getId() == pullRequest.getBase().getRepository().getId();
154+
}
155+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ public class RepositoryConfig {
99

1010
public JiraConfig jira;
1111

12+
public Boolean enableEditing;
13+
1214
public static class JiraConfig {
1315
private Optional<Pattern> issueKeyPattern = Optional.empty();
1416

src/test/java/org/hibernate/infra/bot/tests/ApplicationSanityTest.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static org.assertj.core.api.Assertions.assertThat;
44

55
import org.hibernate.infra.bot.CheckPullRequestContributionRules;
6+
import org.hibernate.infra.bot.EditPullRequestBodyAddIssueLinks;
67

78
import org.junit.jupiter.api.Test;
89

@@ -14,8 +15,8 @@ public class ApplicationSanityTest {
1415

1516
@Test
1617
void checkApplicationIncludesCheckPullRequestContributionRules() {
17-
assertThat( Arc.container().instance( CheckPullRequestContributionRules.class ) )
18-
.isNotNull();
18+
assertThat( Arc.container().instance( CheckPullRequestContributionRules.class ) ).isNotNull();
19+
assertThat( Arc.container().instance( EditPullRequestBodyAddIssueLinks.class ) ).isNotNull();
1920
}
2021

2122
}

0 commit comments

Comments
 (0)