Skip to content

Commit 10de044

Browse files
committed
Make the bot add branch labels/title-prefixes to the pull requests targeting non-main branch
1 parent 56deb36 commit 10de044

File tree

5 files changed

+796
-0
lines changed

5 files changed

+796
-0
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,11 @@ pullRequestTasks:
140140
# Tasks specific to the improvement issue type:
141141
improvement:
142142
- improvement task1
143+
branchLabel:
144+
# Make the bot add a label with the name of base ref when a pull requests target a non-main branch.
145+
enabled: true
146+
# Additionally add the title prefix in a format "[base-ref]"
147+
titlePrefix: true
143148
```
144149
145150
### Altering the infrastructure
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package org.hibernate.infra.bot;
2+
3+
import java.io.IOException;
4+
import java.util.List;
5+
import java.util.Locale;
6+
import java.util.Random;
7+
8+
import org.hibernate.infra.bot.config.DeploymentConfig;
9+
import org.hibernate.infra.bot.config.RepositoryConfig;
10+
11+
import org.jboss.logging.Logger;
12+
13+
import io.quarkiverse.githubapp.ConfigFile;
14+
import io.quarkiverse.githubapp.event.PullRequest;
15+
import jakarta.inject.Inject;
16+
import org.kohsuke.github.GHEventPayload;
17+
import org.kohsuke.github.GHFileNotFoundException;
18+
import org.kohsuke.github.GHIssueState;
19+
import org.kohsuke.github.GHLabel;
20+
import org.kohsuke.github.GHPullRequest;
21+
import org.kohsuke.github.GHRepository;
22+
import org.kohsuke.github.GHUser;
23+
24+
public class EditPullRequestAddBranchTagLabel {
25+
private static final Logger LOG = Logger.getLogger( EditPullRequestAddBranchTagLabel.class );
26+
27+
@Inject
28+
DeploymentConfig deploymentConfig;
29+
30+
void pullRequestChanged(
31+
@PullRequest.Opened @PullRequest.Reopened @PullRequest.Edited @PullRequest.Synchronize
32+
GHEventPayload.PullRequest payload,
33+
@ConfigFile("hibernate-github-bot.yml") RepositoryConfig repositoryConfig
34+
) throws IOException {
35+
addTagOrLabel( payload.getRepository(), repositoryConfig, payload.getPullRequest() );
36+
}
37+
38+
private void addTagOrLabel(
39+
GHRepository repository,
40+
RepositoryConfig repositoryConfig,
41+
GHPullRequest pullRequest
42+
) throws IOException {
43+
if ( repositoryConfig == null || repositoryConfig.branchLabel == null
44+
|| !repositoryConfig.branchLabel.getEnabled().orElse( Boolean.FALSE ) ) {
45+
return;
46+
}
47+
48+
if ( !shouldCheck( repository, pullRequest, repositoryConfig.branchLabel.getIgnore() ) ) {
49+
return;
50+
}
51+
52+
String base = pullRequest.getBase().getRef();
53+
if ( "main".equalsIgnoreCase( base ) ) {
54+
return;
55+
}
56+
57+
if ( repositoryConfig.branchLabel.isTitlePrefix() ) {
58+
String prefix = "[" + base + "]";
59+
String title = pullRequest.getTitle();
60+
if ( !title.startsWith( prefix ) ) {
61+
pullRequest.setTitle( prefix + " " + title );
62+
}
63+
}
64+
65+
for ( GHLabel ghLabel : pullRequest.getLabels() ) {
66+
if ( ghLabel.getName().equals( base ) ) {
67+
return;
68+
}
69+
}
70+
71+
GHLabel label;
72+
try {
73+
label = repository.getLabel( base );
74+
}
75+
catch (GHFileNotFoundException e) {
76+
label = repository.createLabel(
77+
base,
78+
String.format( Locale.ROOT, "%06x", new Random().nextInt( 0xffffff + 1 ) ),
79+
"Label for pull requests targeting [" + base + "] branch."
80+
);
81+
}
82+
83+
if ( !deploymentConfig.isDryRun() ) {
84+
pullRequest.addLabels( label );
85+
}
86+
else {
87+
LOG.info( "Pull request #" + pullRequest.getNumber() + " - Adding label: " + label.getName() );
88+
}
89+
}
90+
91+
private boolean shouldCheck(GHRepository repository, GHPullRequest pullRequest, List<RepositoryConfig.IgnoreConfiguration> ignoredPRConfigurations) throws IOException {
92+
GHUser author = pullRequest.getUser();
93+
String title = pullRequest.getTitle();
94+
for ( RepositoryConfig.IgnoreConfiguration ignore : ignoredPRConfigurations ) {
95+
if ( ignore.getUser().equals( author.getLogin() )
96+
&& ignore.getTitlePattern().matcher( title ).matches() ) {
97+
return false;
98+
}
99+
}
100+
return !GHIssueState.CLOSED.equals( pullRequest.getState() )
101+
&& repository.getId() == pullRequest.getBase().getRepository().getId();
102+
}
103+
}

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ public class RepositoryConfig {
2020

2121
public TaskList pullRequestTasks;
2222

23+
public BranchLabel branchLabel;
24+
2325
public static class JiraConfig {
2426
private Optional<Pattern> issueKeyPattern = Optional.empty();
2527

@@ -229,4 +231,34 @@ public void setIgnore(List<IgnoreConfiguration> ignore) {
229231
this.ignore = ignore;
230232
}
231233
}
234+
235+
public static class BranchLabel {
236+
private Optional<Boolean> enabled = Optional.empty();
237+
private List<IgnoreConfiguration> ignore = Collections.emptyList();
238+
private boolean titlePrefix = false;
239+
240+
public Optional<Boolean> getEnabled() {
241+
return enabled;
242+
}
243+
244+
public void setEnabled(Boolean enabled) {
245+
this.enabled = Optional.of( enabled );
246+
}
247+
248+
public List<IgnoreConfiguration> getIgnore() {
249+
return ignore;
250+
}
251+
252+
public void setIgnore(List<IgnoreConfiguration> ignore) {
253+
this.ignore = ignore;
254+
}
255+
256+
public boolean isTitlePrefix() {
257+
return titlePrefix;
258+
}
259+
260+
public void setTitlePrefix(boolean titlePrefix) {
261+
this.titlePrefix = titlePrefix;
262+
}
263+
}
232264
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package org.hibernate.infra.bot.tests;
2+
3+
import static io.quarkiverse.githubapp.testing.GitHubAppTesting.given;
4+
import static org.assertj.core.api.Assertions.assertThat;
5+
import static org.mockito.ArgumentMatchers.any;
6+
import static org.mockito.ArgumentMatchers.anyString;
7+
import static org.mockito.Mockito.mock;
8+
import static org.mockito.Mockito.verify;
9+
import static org.mockito.Mockito.verifyNoMoreInteractions;
10+
import static org.mockito.Mockito.when;
11+
12+
import java.io.IOException;
13+
14+
import org.junit.jupiter.api.Test;
15+
import org.junit.jupiter.api.extension.ExtendWith;
16+
17+
import io.quarkiverse.githubapp.testing.GitHubAppTest;
18+
import io.quarkus.test.junit.QuarkusTest;
19+
import org.kohsuke.github.GHEvent;
20+
import org.kohsuke.github.GHFileNotFoundException;
21+
import org.kohsuke.github.GHLabel;
22+
import org.kohsuke.github.GHPullRequest;
23+
import org.kohsuke.github.GHRepository;
24+
import org.kohsuke.github.PagedIterator;
25+
import org.mockito.ArgumentCaptor;
26+
import org.mockito.junit.jupiter.MockitoExtension;
27+
28+
@QuarkusTest
29+
@GitHubAppTest
30+
@ExtendWith(MockitoExtension.class)
31+
public class EditPullRequestAddBranchTagLabelTest extends AbstractPullRequestTest {
32+
@Test
33+
void simple() throws IOException {
34+
long repoId = 344815557L;
35+
long prId = 585627026L;
36+
given()
37+
.github( mocks -> {
38+
mocks.configFile("hibernate-github-bot.yml")
39+
.fromString( """
40+
jira:
41+
projectKey: "HSEARCH"
42+
branchLabel:
43+
enabled: true
44+
""" );
45+
46+
GHRepository repoMock = mocks.repository( "yrodiere/hibernate-github-bot-playground" );
47+
when( repoMock.getId() ).thenReturn( repoId );
48+
when( repoMock.getLabel( any() ) ).thenAnswer( invocation -> {
49+
GHLabel label = mock( GHLabel.class );
50+
when( label.getName() ).thenReturn( "base-ref" );
51+
return label;
52+
} );
53+
54+
PullRequestMockHelper.start( mocks, prId, repoMock )
55+
.commit( "HSEARCH-1111 Correct message" )
56+
.comment( "Some comment" )
57+
.comment( "Some other comment" );
58+
59+
mockCheckRuns( repoMock, "6e9f11a1e2946b207c6eb245ec942f2b5a3ea156" );
60+
} )
61+
.when()
62+
.payloadFromClasspath( "/pullrequest-opened-hsearch-1111-non-main-branch.json" )
63+
.event( GHEvent.PULL_REQUEST )
64+
.then()
65+
.github( mocks -> {
66+
GHPullRequest prMock = mocks.pullRequest( prId );
67+
ArgumentCaptor<GHLabel> labelArgumentCaptor = ArgumentCaptor.forClass( GHLabel.class );
68+
verify( prMock ).addLabels( labelArgumentCaptor.capture() );
69+
assertThat( labelArgumentCaptor.getValue().getName() ).isEqualTo( "base-ref" );
70+
71+
verifyNoMoreInteractions( mocks.ghObjects() );
72+
} );
73+
}
74+
75+
@Test
76+
void labelDoesNotExist() throws IOException {
77+
long repoId = 344815557L;
78+
long prId = 585627026L;
79+
given()
80+
.github( mocks -> {
81+
mocks.configFile("hibernate-github-bot.yml")
82+
.fromString( """
83+
jira:
84+
projectKey: "HSEARCH"
85+
branchLabel:
86+
enabled: true
87+
""" );
88+
89+
GHRepository repoMock = mocks.repository( "yrodiere/hibernate-github-bot-playground" );
90+
when( repoMock.getId() ).thenReturn( repoId );
91+
when( repoMock.getLabel( any() ) ).thenThrow( GHFileNotFoundException.class );
92+
93+
when( repoMock.createLabel( any(), any(), any() ) ).thenAnswer( invocation -> {
94+
GHLabel label = mock( GHLabel.class );
95+
when( label.getName() ).thenReturn( "base-ref" );
96+
when( label.getColor() ).thenReturn( invocation.getArgument( 1 ) );
97+
when( label.getDescription() ).thenReturn( invocation.getArgument( 1 ) );
98+
return label;
99+
} );
100+
101+
PullRequestMockHelper.start( mocks, prId, repoMock )
102+
.commit( "HSEARCH-1111 Correct message" )
103+
.comment( "Some comment" )
104+
.comment( "Some other comment" );
105+
106+
mockCheckRuns( repoMock, "6e9f11a1e2946b207c6eb245ec942f2b5a3ea156" );
107+
} )
108+
.when()
109+
.payloadFromClasspath( "/pullrequest-opened-hsearch-1111-non-main-branch.json" )
110+
.event( GHEvent.PULL_REQUEST )
111+
.then()
112+
.github( mocks -> {
113+
GHRepository repoMock = mocks.repository( "yrodiere/hibernate-github-bot-playground" );
114+
GHPullRequest prMock = mocks.pullRequest( prId );
115+
116+
ArgumentCaptor<String> name = ArgumentCaptor.forClass( String.class );
117+
ArgumentCaptor<String> color = ArgumentCaptor.forClass( String.class );
118+
ArgumentCaptor<String> description = ArgumentCaptor.forClass( String.class );
119+
verify( repoMock ).createLabel( name.capture(), color.capture(), description.capture() );
120+
assertThat( color.getValue() ).hasSize( 6 );
121+
assertThat( description.getValue() ).contains( "Label for pull requests targeting" );
122+
123+
ArgumentCaptor<GHLabel> labelArgumentCaptor = ArgumentCaptor.forClass( GHLabel.class );
124+
verify( prMock ).addLabels( labelArgumentCaptor.capture() );
125+
assertThat( labelArgumentCaptor.getValue().getName() ).isEqualTo( "base-ref" );
126+
127+
verifyNoMoreInteractions( mocks.ghObjects() );
128+
} );
129+
}
130+
131+
@Test
132+
void updateTitle() throws IOException {
133+
long repoId = 344815557L;
134+
long prId = 585627026L;
135+
given()
136+
.github( mocks -> {
137+
mocks.configFile("hibernate-github-bot.yml")
138+
.fromString( """
139+
jira:
140+
projectKey: "HSEARCH"
141+
branchLabel:
142+
enabled: true
143+
titlePrefix: true
144+
""" );
145+
146+
GHRepository repoMock = mocks.repository( "yrodiere/hibernate-github-bot-playground" );
147+
when( repoMock.getId() ).thenReturn( repoId );
148+
when( repoMock.getLabel( any() ) ).thenAnswer( invocation -> {
149+
GHLabel label = mock( GHLabel.class );
150+
when( label.getName() ).thenReturn( "base-ref" );
151+
return label;
152+
} );
153+
154+
PullRequestMockHelper.start( mocks, prId, repoMock )
155+
.commit( "HSEARCH-1111 Correct message" )
156+
.comment( "Some comment" )
157+
.comment( "Some other comment" );
158+
159+
mockCheckRuns( repoMock, "6e9f11a1e2946b207c6eb245ec942f2b5a3ea156" );
160+
} )
161+
.when()
162+
.payloadFromClasspath( "/pullrequest-opened-hsearch-1111-non-main-branch.json" )
163+
.event( GHEvent.PULL_REQUEST )
164+
.then()
165+
.github( mocks -> {
166+
GHPullRequest prMock = mocks.pullRequest( prId );
167+
ArgumentCaptor<GHLabel> labelArgumentCaptor = ArgumentCaptor.forClass( GHLabel.class );
168+
verify( prMock ).addLabels( labelArgumentCaptor.capture() );
169+
assertThat( labelArgumentCaptor.getValue().getName() ).isEqualTo( "base-ref" );
170+
171+
ArgumentCaptor<String> title = ArgumentCaptor.forClass( String.class );
172+
verify( prMock ).setTitle( title.capture() );
173+
assertThat( title.getValue() ).startsWith( "[" );
174+
175+
verifyNoMoreInteractions( mocks.ghObjects() );
176+
} );
177+
}
178+
}

0 commit comments

Comments
 (0)