Skip to content

Commit 400426b

Browse files
gsmetbitwiseman
andauthored
Implement milestone payload and query builder (hub4j#2284)
While a query builder is a bit heavy for what we actually need to do here, I favored a pattern we are using globally, and we never know what they might add to the API later. I wanted to be able to sort the milestones. Even that is extremely limited... Believe it or not, we can't sort by close dates. Co-authored-by: Liam Newman <bitwiseman@gmail.com>
1 parent a1d5213 commit 400426b

35 files changed

Lines changed: 2734 additions & 162 deletions

src/main/java/org/kohsuke/github/GHEventPayload.java

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -529,7 +529,7 @@ public GHRepository getForkee() {
529529
// ContentReferenceEvent
530530
// DeployKeyEvent DownloadEvent FollowEvent ForkApplyEvent GitHubAppAuthorizationEvent GistEvent GollumEvent
531531
// InstallationEvent InstallationRepositoriesEvent IssuesEvent LabelEvent MarketplacePurchaseEvent MemberEvent
532-
// MembershipEvent MetaEvent MilestoneEvent OrganizationEvent OrgBlockEvent PackageEvent PageBuildEvent
532+
// MembershipEvent MetaEvent OrganizationEvent OrgBlockEvent PackageEvent PageBuildEvent
533533
// ProjectCardEvent ProjectColumnEvent ProjectEvent RepositoryDispatchEvent RepositoryImportEvent
534534
// RepositoryVulnerabilityAlertEvent SecurityAdvisoryEvent StarEvent StatusEvent TeamEvent TeamAddEvent WatchEvent
535535

@@ -1005,6 +1005,41 @@ void lateBind() {
10051005
}
10061006
}
10071007

1008+
/**
1009+
* A milestone event has been triggered.
1010+
*
1011+
* @see <a href="https://docs.github.com/en/webhooks/webhook-events-and-payloads#milestone">milestone event</a>
1012+
*/
1013+
public static class Milestone extends GHEventPayload {
1014+
1015+
private GHMilestone milestone;
1016+
1017+
/**
1018+
* Create default Milestone instance
1019+
*/
1020+
public Milestone() {
1021+
}
1022+
1023+
/**
1024+
* Gets the milestone.
1025+
*
1026+
* @return the milestone
1027+
*/
1028+
@SuppressFBWarnings(value = { "EI_EXPOSE_REP" }, justification = "Expected behavior")
1029+
public GHMilestone getMilestone() {
1030+
return milestone;
1031+
}
1032+
1033+
@Override
1034+
void lateBind() {
1035+
super.lateBind();
1036+
GHRepository repository = getRepository();
1037+
if (repository != null && milestone != null) {
1038+
milestone.lateBind(repository);
1039+
}
1040+
}
1041+
}
1042+
10081043
/**
10091044
* A ping.
10101045
*
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package org.kohsuke.github;
2+
3+
/**
4+
* Lists up milestones with filtering and sorting.
5+
*
6+
* @see GHRepository#queryMilestones() GHRepository#queryMilestones()
7+
*/
8+
public class GHMilestoneQueryBuilder extends GHQueryBuilder<GHMilestone> {
9+
/**
10+
* The enum Sort.
11+
*/
12+
public enum Sort {
13+
14+
/** Sort by completeness (percentage of issues closed). */
15+
COMPLETENESS,
16+
/** Sort by due date. */
17+
DUE_ON
18+
}
19+
20+
private final GHRepository repo;
21+
22+
/**
23+
* Instantiates a new GH milestone query builder.
24+
*
25+
* @param repo
26+
* the repo
27+
*/
28+
GHMilestoneQueryBuilder(GHRepository repo) {
29+
super(repo.root());
30+
this.repo = repo;
31+
}
32+
33+
/**
34+
* Direction gh milestone query builder.
35+
*
36+
* @param d
37+
* the d
38+
* @return the gh milestone query builder
39+
*/
40+
public GHMilestoneQueryBuilder direction(GHDirection d) {
41+
req.with("direction", d);
42+
return this;
43+
}
44+
45+
/**
46+
* List.
47+
*
48+
* @return the paged iterable
49+
*/
50+
@Override
51+
public PagedIterable<GHMilestone> list() {
52+
return req.withUrlPath(repo.getApiTailUrl("milestones"))
53+
.toIterable(GHMilestone[].class, item -> item.lateBind(repo));
54+
}
55+
56+
/**
57+
* Sort gh milestone query builder.
58+
*
59+
* @param sort
60+
* the sort
61+
* @return the gh milestone query builder
62+
*/
63+
public GHMilestoneQueryBuilder sort(Sort sort) {
64+
req.with("sort", sort);
65+
return this;
66+
}
67+
68+
/**
69+
* State gh milestone query builder.
70+
*
71+
* @param state
72+
* the state
73+
* @return the gh milestone query builder
74+
*/
75+
public GHMilestoneQueryBuilder state(GHIssueState state) {
76+
req.with("state", state);
77+
return this;
78+
}
79+
}

src/main/java/org/kohsuke/github/GHRepository.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3007,6 +3007,15 @@ public GHIssueQueryBuilder.ForRepository queryIssues() {
30073007
return new GHIssueQueryBuilder.ForRepository(this);
30083008
}
30093009

3010+
/**
3011+
* Retrieves milestones.
3012+
*
3013+
* @return the gh milestone query builder
3014+
*/
3015+
public GHMilestoneQueryBuilder queryMilestones() {
3016+
return new GHMilestoneQueryBuilder(this);
3017+
}
3018+
30103019
/**
30113020
* Retrieves pull requests.
30123021
*

src/main/resources/META-INF/native-image/org.kohsuke/github-api/reflect-config.json

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2564,6 +2564,21 @@
25642564
"allPublicClasses": true,
25652565
"allDeclaredClasses": true
25662566
},
2567+
{
2568+
"name": "org.kohsuke.github.GHEventPayload$Milestone",
2569+
"allPublicFields": true,
2570+
"allDeclaredFields": true,
2571+
"queryAllPublicConstructors": true,
2572+
"queryAllDeclaredConstructors": true,
2573+
"allPublicConstructors": true,
2574+
"allDeclaredConstructors": true,
2575+
"queryAllPublicMethods": true,
2576+
"queryAllDeclaredMethods": true,
2577+
"allPublicMethods": true,
2578+
"allDeclaredMethods": true,
2579+
"allPublicClasses": true,
2580+
"allDeclaredClasses": true
2581+
},
25672582
{
25682583
"name": "org.kohsuke.github.GHEventPayload$Ping",
25692584
"allPublicFields": true,
@@ -3719,6 +3734,36 @@
37193734
"allPublicClasses": true,
37203735
"allDeclaredClasses": true
37213736
},
3737+
{
3738+
"name": "org.kohsuke.github.GHMilestoneQueryBuilder",
3739+
"allPublicFields": true,
3740+
"allDeclaredFields": true,
3741+
"queryAllPublicConstructors": true,
3742+
"queryAllDeclaredConstructors": true,
3743+
"allPublicConstructors": true,
3744+
"allDeclaredConstructors": true,
3745+
"queryAllPublicMethods": true,
3746+
"queryAllDeclaredMethods": true,
3747+
"allPublicMethods": true,
3748+
"allDeclaredMethods": true,
3749+
"allPublicClasses": true,
3750+
"allDeclaredClasses": true
3751+
},
3752+
{
3753+
"name": "org.kohsuke.github.GHMilestoneQueryBuilder$Sort",
3754+
"allPublicFields": true,
3755+
"allDeclaredFields": true,
3756+
"queryAllPublicConstructors": true,
3757+
"queryAllDeclaredConstructors": true,
3758+
"allPublicConstructors": true,
3759+
"allDeclaredConstructors": true,
3760+
"queryAllPublicMethods": true,
3761+
"queryAllDeclaredMethods": true,
3762+
"allPublicMethods": true,
3763+
"allDeclaredMethods": true,
3764+
"allPublicClasses": true,
3765+
"allDeclaredClasses": true
3766+
},
37223767
{
37233768
"name": "org.kohsuke.github.GHMilestoneState",
37243769
"allPublicFields": true,

src/test/java/org/kohsuke/github/GHEventPayloadTest.java

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -491,10 +491,6 @@ public void discussion_created() throws Exception {
491491
// @Test
492492
// public void membership() throws Exception {}
493493

494-
// TODO implement support classes and write test
495-
// @Test
496-
// public void milestone() throws Exception {}
497-
498494
// TODO implement support classes and write test
499495
// @Test
500496
// public void page_build() throws Exception {}
@@ -901,6 +897,38 @@ public void membership_added() throws Exception {
901897
assertThat(team.getOrganization().getLogin(), is("gsmet-bot-playground"));
902898
}
903899

900+
/**
901+
* Milestone closed.
902+
*
903+
* @throws Exception
904+
* the exception
905+
*/
906+
@Test
907+
@Payload("milestone")
908+
public void milestone() throws Exception {
909+
final GHEventPayload.Milestone event = GitHub.offline()
910+
.parseEventPayload(payload.asReader(), GHEventPayload.Milestone.class);
911+
assertThat(event.getAction(), is("closed"));
912+
913+
final GHMilestone milestone = event.getMilestone();
914+
assertThat(milestone.getId(), is(16523020L));
915+
assertThat(milestone.getNumber(), is(2));
916+
assertThat(milestone.getTitle(), is("Test milestone"));
917+
assertThat(milestone.getDescription(), is(""));
918+
assertThat(milestone.getState(), is(GHMilestoneState.CLOSED));
919+
assertThat(milestone.getOpenIssues(), is(0));
920+
assertThat(milestone.getClosedIssues(), is(0));
921+
assertThat(milestone.getCreatedAt().toEpochMilli(), is(1782373293000L));
922+
assertThat(milestone.getUpdatedAt().toEpochMilli(), is(1782373452000L));
923+
assertThat(milestone.getClosedAt().toEpochMilli(), is(1782373452000L));
924+
assertThat(milestone.getDueOn().toEpochMilli(), is(1782432000000L));
925+
assertThat(milestone.getCreator().getLogin(), is("gsmet"));
926+
927+
assertThat(event.getSender().getLogin(), is("gsmet"));
928+
assertThat(event.getRepository().getFullName(), is("gsmet/quarkus-bot-java-playground"));
929+
assertThat(event.getInstallation().getId(), is(90470530L));
930+
}
931+
904932
/**
905933
* Ping.
906934
*

src/test/java/org/kohsuke/github/GHMilestoneTest.java

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import java.io.IOException;
88
import java.time.Instant;
99
import java.util.Date;
10+
import java.util.List;
11+
import java.util.stream.Collectors;
1012

1113
import static org.hamcrest.Matchers.*;
1214

@@ -18,6 +20,13 @@
1820
*/
1921
public class GHMilestoneTest extends AbstractGitHubWireMockTest {
2022

23+
private static List<String> filterTestMilestones(List<GHMilestone> milestones) {
24+
return milestones.stream()
25+
.map(GHMilestone::getTitle)
26+
.filter(t -> t.startsWith("Milestone Sort "))
27+
.collect(Collectors.toList());
28+
}
29+
2130
/**
2231
* Create default GHMilestoneTest instance
2332
*/
@@ -38,14 +47,84 @@ public void cleanUp() throws Exception {
3847
return;
3948
}
4049

41-
for (GHMilestone milestone : getRepository(getNonRecordingGitHub()).listMilestones(GHIssueState.ALL)) {
50+
GHRepository repo = getRepository(getNonRecordingGitHub());
51+
52+
for (GHIssue issue : repo.queryIssues().state(GHIssueState.ALL).list()) {
53+
if (issue.getTitle().endsWith("for sort test") || issue.getTitle().equals("Issue for testUnsetMilestone")) {
54+
issue.close();
55+
}
56+
}
57+
58+
for (GHMilestone milestone : repo.listMilestones(GHIssueState.ALL)) {
4259
if ("Original Title".equals(milestone.getTitle()) || "Updated Title".equals(milestone.getTitle())
43-
|| "Unset Test Milestone".equals(milestone.getTitle())) {
60+
|| "Unset Test Milestone".equals(milestone.getTitle())
61+
|| "Milestone Sort A".equals(milestone.getTitle())
62+
|| "Milestone Sort B".equals(milestone.getTitle())) {
4463
milestone.delete();
4564
}
4665
}
4766
}
4867

68+
/**
69+
* Test list milestones with sort and direction.
70+
*
71+
* @throws IOException
72+
* Signals that an I/O exception has occurred.
73+
*/
74+
@Test
75+
public void testListMilestonesWithSort() throws IOException {
76+
GHRepository repo = getRepository();
77+
GHMilestone milestoneA = repo.createMilestone("Milestone Sort A", "First milestone");
78+
milestoneA.setDueOn(GitHubClient.parseInstant("2025-06-01T00:00:00Z"));
79+
GHMilestone milestoneB = repo.createMilestone("Milestone Sort B", "Second milestone");
80+
milestoneB.setDueOn(GitHubClient.parseInstant("2025-12-01T00:00:00Z"));
81+
82+
// List with due_on sort ascending (default)
83+
List<String> ascending = filterTestMilestones(repo.queryMilestones()
84+
.state(GHIssueState.OPEN)
85+
.sort(GHMilestoneQueryBuilder.Sort.DUE_ON)
86+
.direction(GHDirection.ASC)
87+
.list()
88+
.toList());
89+
assertThat(ascending, contains("Milestone Sort A", "Milestone Sort B"));
90+
91+
// List with due_on sort descending
92+
List<String> descending = filterTestMilestones(repo.queryMilestones()
93+
.state(GHIssueState.OPEN)
94+
.sort(GHMilestoneQueryBuilder.Sort.DUE_ON)
95+
.direction(GHDirection.DESC)
96+
.list()
97+
.toList());
98+
assertThat(descending, contains("Milestone Sort B", "Milestone Sort A"));
99+
100+
// Create issues to test completeness sort
101+
// Milestone A: 1 open, 1 closed = 50% complete
102+
GHIssue issueA1 = repo.createIssue("Issue A1 for sort test").milestone(milestoneA).create();
103+
GHIssue issueA2 = repo.createIssue("Issue A2 for sort test").milestone(milestoneA).create();
104+
issueA2.close();
105+
106+
// Milestone B: 1 open, 0 closed = 0% complete
107+
GHIssue issueB1 = repo.createIssue("Issue B1 for sort test").milestone(milestoneB).create();
108+
109+
// List with completeness sort ascending (least complete first)
110+
List<String> byCompleteness = filterTestMilestones(repo.queryMilestones()
111+
.state(GHIssueState.OPEN)
112+
.sort(GHMilestoneQueryBuilder.Sort.COMPLETENESS)
113+
.direction(GHDirection.ASC)
114+
.list()
115+
.toList());
116+
assertThat(byCompleteness, contains("Milestone Sort B", "Milestone Sort A"));
117+
118+
// List with completeness sort descending (most complete first)
119+
List<String> byCompletenessDesc = filterTestMilestones(repo.queryMilestones()
120+
.state(GHIssueState.OPEN)
121+
.sort(GHMilestoneQueryBuilder.Sort.COMPLETENESS)
122+
.direction(GHDirection.DESC)
123+
.list()
124+
.toList());
125+
assertThat(byCompletenessDesc, contains("Milestone Sort A", "Milestone Sort B"));
126+
}
127+
49128
/**
50129
* Test unset milestone.
51130
*

0 commit comments

Comments
 (0)