Skip to content

New action that automatically edits PR bodies adding issue links #174

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ This includes:
* Proper formatting of commits: every commit message must start with the key of a JIRA ticket.
* Etc.

The bot can also be configured to automatically add links to JIRA issues in PR descriptions. When this is enabled
links to JIRA tickets will be appended at the bottom of the PR body.

## Configuration

### Enabling the bot in a new repository
Expand All @@ -32,12 +35,13 @@ and add your repository under "Repository access".

If you wish to enable the JIRA-related features as well,
create the file `.github/hibernate-github-bot.yml` in default branch of your repository,
will the following content:
with the following content:

```yaml
---
jira:
projectKey: "HSEARCH" # Change to whatever your project key is
insertLinksInPullRequests: true # This is optional and enables automatically adding issue links to PR descriptions
```

### Altering the infrastructure
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,10 @@ private List<Check> createChecks(RepositoryConfig repositoryConfig) {
checks.add( new TitleCheck() );

if ( repositoryConfig != null && repositoryConfig.jira != null ) {
final boolean checkMentions = repositoryConfig.jira.getInsertLinksInPullRequests().isEmpty()
|| repositoryConfig.jira.getInsertLinksInPullRequests().get().equals( Boolean.FALSE );
repositoryConfig.jira.getIssueKeyPattern()
.ifPresent( issueKeyPattern -> checks.add( new JiraIssuesCheck( issueKeyPattern ) ) );
.ifPresent( issueKeyPattern -> checks.add( new JiraIssuesCheck( issueKeyPattern, checkMentions ) ) );
}

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

private final Pattern issueKeyPattern;

JiraIssuesCheck(Pattern issueKeyPattern) {
private final boolean checkMentions;

JiraIssuesCheck(Pattern issueKeyPattern, boolean checkMentions) {
super( "Contribution — JIRA issues" );
this.issueKeyPattern = issueKeyPattern;
this.checkMentions = checkMentions;
}

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

CheckRunRule pullRequestRule = output.rule(
"The PR title or body should list the keys of all JIRA issues mentioned in the commits" );
List<String> issueKeysNotMentionedInPullRequest = issueKeys.stream()
.filter( issueKey -> ( title == null || !title.contains( issueKey ) )
&& ( body == null || !body.contains( issueKey ) ) )
.toList();
if ( issueKeysNotMentionedInPullRequest.isEmpty() ) {
pullRequestRule.passed();
}
else {
pullRequestRule.failed( "Issue keys mentioned in commits but missing from the PR title or body: " + issueKeysNotMentionedInPullRequest );
if ( checkMentions ) {
// We only need to check mentions if automatic body editing is disabled
CheckRunRule pullRequestRule = output.rule(
"The PR title or body should list the keys of all JIRA issues mentioned in the commits");
List<String> issueKeysNotMentionedInPullRequest = issueKeys.stream()
.filter(issueKey -> (title == null || !title.contains(issueKey))
&& (body == null || !body.contains(issueKey)))
.toList();
if (issueKeysNotMentionedInPullRequest.isEmpty()) {
pullRequestRule.passed();
} else {
pullRequestRule.failed("Issue keys mentioned in commits but missing from the PR title or body: " + issueKeysNotMentionedInPullRequest);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package org.hibernate.infra.bot;

import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;

import org.hibernate.infra.bot.config.DeploymentConfig;
import org.hibernate.infra.bot.config.RepositoryConfig;
import org.hibernate.infra.bot.util.CommitMessages;

import org.jboss.logging.Logger;

import io.quarkiverse.githubapp.ConfigFile;
import io.quarkiverse.githubapp.event.CheckRun;
import io.quarkiverse.githubapp.event.CheckSuite;
import io.quarkiverse.githubapp.event.PullRequest;
import jakarta.inject.Inject;
import org.kohsuke.github.GHEventPayload;
import org.kohsuke.github.GHIssueState;
import org.kohsuke.github.GHPullRequest;
import org.kohsuke.github.GHPullRequestCommitDetail;
import org.kohsuke.github.GHRepository;

/**
* @author Marco Belladelli
*/
public class EditPullRequestBodyAddIssueLinks {
private static final Logger LOG = Logger.getLogger( EditPullRequestBodyAddIssueLinks.class );

private static final String START_MARKER = "<!-- Hibernate GitHub Bot issue links start -->";

private static final String END_MARKER = "<!-- Hibernate GitHub Bot issue links end -->";

private static final String EDITOR_WARNING = "<!-- THIS SECTION IS AUTOMATICALLY GENERATED, ANY MANUAL CHANGES WILL BE LOST -->\n";

private static final String LINK_TEMPLATE = "https://hibernate.atlassian.net/browse/%s";

@Inject
DeploymentConfig deploymentConfig;

void pullRequestChanged(
@PullRequest.Opened @PullRequest.Reopened @PullRequest.Edited @PullRequest.Synchronize
GHEventPayload.PullRequest payload,
@ConfigFile( "hibernate-github-bot.yml" ) RepositoryConfig repositoryConfig) throws IOException {
editPullRequestBodyAddIssueLinks( payload.getRepository(), repositoryConfig, payload.getPullRequest() );
}

private void editPullRequestBodyAddIssueLinks(
GHRepository repository,
RepositoryConfig repositoryConfig,
GHPullRequest pullRequest) throws IOException {
if ( repositoryConfig == null || repositoryConfig.jira == null
|| repositoryConfig.jira.getIssueKeyPattern().isEmpty()
|| repositoryConfig.jira.getInsertLinksInPullRequests().isEmpty()
|| repositoryConfig.jira.getInsertLinksInPullRequests().get().equals( Boolean.FALSE ) ) {
return;
}

if ( !shouldCheck( repository, pullRequest ) ) {
return;
}

final Set<String> issueKeys = new HashSet<>();
// Collect all issue keys from commit messages
repositoryConfig.jira.getIssueKeyPattern().ifPresent( issueKeyPattern -> {
for ( GHPullRequestCommitDetail commitDetails : pullRequest.listCommits() ) {
final GHPullRequestCommitDetail.Commit commit = commitDetails.getCommit();
final List<String> commitIssueKeys = CommitMessages.extractIssueKeys(
issueKeyPattern,
commit.getMessage()
);
issueKeys.addAll( commitIssueKeys );
}
} );

if ( issueKeys.isEmpty() ) {
LOG.debug( "Found no issue keys in commits, terminating." );
return;
}

final String originalBody = pullRequest.getBody();
final StringBuilder sb = new StringBuilder();
if ( originalBody != null ) {
// Check if the body already contains the link section
final int startIndex = originalBody.indexOf( START_MARKER );
final int endIndex = startIndex > -1 ? originalBody.indexOf( END_MARKER ) : -1;
if ( startIndex > -1 && endIndex > -1 ) {
// Remove the whole section, it will be re-appended at the end of the body
sb.append( originalBody.substring( 0, startIndex ).trim() );
final String following = originalBody.substring( endIndex + END_MARKER.length() ).trim();
if ( following.length() > 0 ) {
sb.append( "\n\n" );
sb.append( following );
}
}
else {
sb.append( originalBody.trim() );
}
}

final String body = sb.toString();
final String linksSection = constructLinksSection( issueKeys, body );
if ( linksSection == null ) {
// All issue links were already found in the request body, nothing to do
return;
}

final String newBody = body.length() == 0
? linksSection
: body + "\n\n" + linksSection;
if ( !deploymentConfig.isDryRun() ) {
pullRequest.setBody( newBody );
}
else {
LOG.info( "Pull request #" + pullRequest.getNumber() + " - Updated PR body: " + newBody );
}
}

private String constructLinksSection(Set<String> issueKeys, String originalBody) {
final String lowerCaseBody = originalBody.toLowerCase( Locale.ROOT );
final StringBuilder sb = new StringBuilder();
for ( String key : issueKeys ) {
if ( !lowerCaseBody.contains( key.toLowerCase( Locale.ROOT ) ) ) {
// Only add links for issue keys that are not already found
// in the original PR body
sb.append( String.format( LINK_TEMPLATE, key ) ).append( '\n' );
}
}

if ( sb.isEmpty() ) {
return null;
}

return START_MARKER + "\n" + EDITOR_WARNING + sb + END_MARKER;
}

private boolean shouldCheck(GHRepository repository, GHPullRequest pullRequest) {
return !GHIssueState.CLOSED.equals( pullRequest.getState() )
&& repository.getId() == pullRequest.getBase().getRepository().getId();
}
}
12 changes: 12 additions & 0 deletions src/main/java/org/hibernate/infra/bot/config/RepositoryConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import java.util.Optional;
import java.util.regex.Pattern;

import javax.swing.text.html.Option;

import org.hibernate.infra.bot.util.Patterns;

public class RepositoryConfig {
Expand All @@ -12,12 +14,22 @@ public class RepositoryConfig {
public static class JiraConfig {
private Optional<Pattern> issueKeyPattern = Optional.empty();

private Optional<Boolean> insertLinksInPullRequests = Optional.empty();

public void setProjectKey(String key) {
issueKeyPattern = Optional.of( Patterns.compile( key + "-\\d+" ) );
}

public Optional<Pattern> getIssueKeyPattern() {
return issueKeyPattern;
}

public void setInsertLinksInPullRequests(boolean insertLinksInPullRequests) {
this.insertLinksInPullRequests = Optional.of( insertLinksInPullRequests );
}

public Optional<Boolean> getInsertLinksInPullRequests() {
return insertLinksInPullRequests;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static org.assertj.core.api.Assertions.assertThat;

import org.hibernate.infra.bot.CheckPullRequestContributionRules;
import org.hibernate.infra.bot.EditPullRequestBodyAddIssueLinks;

import org.junit.jupiter.api.Test;

Expand All @@ -14,8 +15,8 @@ public class ApplicationSanityTest {

@Test
void checkApplicationIncludesCheckPullRequestContributionRules() {
assertThat( Arc.container().instance( CheckPullRequestContributionRules.class ) )
.isNotNull();
assertThat( Arc.container().instance( CheckPullRequestContributionRules.class ) ).isNotNull();
assertThat( Arc.container().instance( EditPullRequestBodyAddIssueLinks.class ) ).isNotNull();
}

}
Loading