Skip to content
Open
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
37 changes: 36 additions & 1 deletion src/main/java/org/kohsuke/github/GHEventPayload.java
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,7 @@ public GHRepository getForkee() {
// ContentReferenceEvent
// DeployKeyEvent DownloadEvent FollowEvent ForkApplyEvent GitHubAppAuthorizationEvent GistEvent GollumEvent
// InstallationEvent InstallationRepositoriesEvent IssuesEvent LabelEvent MarketplacePurchaseEvent MemberEvent
// MembershipEvent MetaEvent MilestoneEvent OrganizationEvent OrgBlockEvent PackageEvent PageBuildEvent
// MembershipEvent MetaEvent OrganizationEvent OrgBlockEvent PackageEvent PageBuildEvent
// ProjectCardEvent ProjectColumnEvent ProjectEvent RepositoryDispatchEvent RepositoryImportEvent
// RepositoryVulnerabilityAlertEvent SecurityAdvisoryEvent StarEvent StatusEvent TeamEvent TeamAddEvent WatchEvent

Expand Down Expand Up @@ -1005,6 +1005,41 @@ void lateBind() {
}
}

/**
* A milestone event has been triggered.
*
* @see <a href="https://docs.github.com/en/webhooks/webhook-events-and-payloads#milestone">milestone event</a>
*/
public static class Milestone extends GHEventPayload {

private GHMilestone milestone;

/**
* Create default Milestone instance
*/
public Milestone() {
}

/**
* Gets the milestone.
*
* @return the milestone
*/
@SuppressFBWarnings(value = { "EI_EXPOSE_REP" }, justification = "Expected behavior")
public GHMilestone getMilestone() {
return milestone;
}

@Override
void lateBind() {
super.lateBind();
GHRepository repository = getRepository();
if (repository != null && milestone != null) {
milestone.lateBind(repository);
}
}
}

/**
* A ping.
*
Expand Down
79 changes: 79 additions & 0 deletions src/main/java/org/kohsuke/github/GHMilestoneQueryBuilder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package org.kohsuke.github;

/**
* Lists up milestones with filtering and sorting.
*
* @see GHRepository#queryMilestones() GHRepository#queryMilestones()
*/
public class GHMilestoneQueryBuilder extends GHQueryBuilder<GHMilestone> {
/**
* The enum Sort.
*/
public enum Sort {

/** Sort by completeness (percentage of issues closed). */
COMPLETENESS,
/** Sort by due date. */
DUE_ON
}

private final GHRepository repo;

/**
* Instantiates a new GH milestone query builder.
*
* @param repo
* the repo
*/
GHMilestoneQueryBuilder(GHRepository repo) {
super(repo.root());
this.repo = repo;
}

/**
* Direction gh milestone query builder.
*
* @param d
* the d
* @return the gh milestone query builder
*/
public GHMilestoneQueryBuilder direction(GHDirection d) {
req.with("direction", d);
return this;
}

/**
* List.
*
* @return the paged iterable
*/
@Override
public PagedIterable<GHMilestone> list() {
return req.withUrlPath(repo.getApiTailUrl("milestones"))
.toIterable(GHMilestone[].class, item -> item.lateBind(repo));
}

/**
* Sort gh milestone query builder.
*
* @param sort
* the sort
* @return the gh milestone query builder
*/
public GHMilestoneQueryBuilder sort(Sort sort) {
req.with("sort", sort);
return this;
}

/**
* State gh milestone query builder.
*
* @param state
* the state
* @return the gh milestone query builder
*/
public GHMilestoneQueryBuilder state(GHIssueState state) {
req.with("state", state);
return this;
}
}
9 changes: 9 additions & 0 deletions src/main/java/org/kohsuke/github/GHRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -3007,6 +3007,15 @@ public GHIssueQueryBuilder.ForRepository queryIssues() {
return new GHIssueQueryBuilder.ForRepository(this);
}

/**
* Retrieves milestones.
*
* @return the gh milestone query builder
*/
public GHMilestoneQueryBuilder queryMilestones() {
return new GHMilestoneQueryBuilder(this);
}

/**
* Retrieves pull requests.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2564,6 +2564,21 @@
"allPublicClasses": true,
"allDeclaredClasses": true
},
{
"name": "org.kohsuke.github.GHEventPayload$Milestone",
"allPublicFields": true,
"allDeclaredFields": true,
"queryAllPublicConstructors": true,
"queryAllDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredConstructors": true,
"queryAllPublicMethods": true,
"queryAllDeclaredMethods": true,
"allPublicMethods": true,
"allDeclaredMethods": true,
"allPublicClasses": true,
"allDeclaredClasses": true
},
{
"name": "org.kohsuke.github.GHEventPayload$Ping",
"allPublicFields": true,
Expand Down Expand Up @@ -3719,6 +3734,36 @@
"allPublicClasses": true,
"allDeclaredClasses": true
},
{
"name": "org.kohsuke.github.GHMilestoneQueryBuilder",
"allPublicFields": true,
"allDeclaredFields": true,
"queryAllPublicConstructors": true,
"queryAllDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredConstructors": true,
"queryAllPublicMethods": true,
"queryAllDeclaredMethods": true,
"allPublicMethods": true,
"allDeclaredMethods": true,
"allPublicClasses": true,
"allDeclaredClasses": true
},
{
"name": "org.kohsuke.github.GHMilestoneQueryBuilder$Sort",
"allPublicFields": true,
"allDeclaredFields": true,
"queryAllPublicConstructors": true,
"queryAllDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredConstructors": true,
"queryAllPublicMethods": true,
"queryAllDeclaredMethods": true,
"allPublicMethods": true,
"allDeclaredMethods": true,
"allPublicClasses": true,
"allDeclaredClasses": true
},
{
"name": "org.kohsuke.github.GHMilestoneState",
"allPublicFields": true,
Expand Down
36 changes: 32 additions & 4 deletions src/test/java/org/kohsuke/github/GHEventPayloadTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -491,10 +491,6 @@ public void discussion_created() throws Exception {
// @Test
// public void membership() throws Exception {}

// TODO implement support classes and write test
// @Test
// public void milestone() throws Exception {}

// TODO implement support classes and write test
// @Test
// public void page_build() throws Exception {}
Expand Down Expand Up @@ -901,6 +897,38 @@ public void membership_added() throws Exception {
assertThat(team.getOrganization().getLogin(), is("gsmet-bot-playground"));
}

/**
* Milestone closed.
*
* @throws Exception
* the exception
*/
@Test
@Payload("milestone")
public void milestone() throws Exception {
final GHEventPayload.Milestone event = GitHub.offline()
.parseEventPayload(payload.asReader(), GHEventPayload.Milestone.class);
assertThat(event.getAction(), is("closed"));

final GHMilestone milestone = event.getMilestone();
assertThat(milestone.getId(), is(16523020L));
assertThat(milestone.getNumber(), is(2));
assertThat(milestone.getTitle(), is("Test milestone"));
assertThat(milestone.getDescription(), is(""));
assertThat(milestone.getState(), is(GHMilestoneState.CLOSED));
assertThat(milestone.getOpenIssues(), is(0));
assertThat(milestone.getClosedIssues(), is(0));
assertThat(milestone.getCreatedAt().toEpochMilli(), is(1782373293000L));
assertThat(milestone.getUpdatedAt().toEpochMilli(), is(1782373452000L));
assertThat(milestone.getClosedAt().toEpochMilli(), is(1782373452000L));
assertThat(milestone.getDueOn().toEpochMilli(), is(1782432000000L));
assertThat(milestone.getCreator().getLogin(), is("gsmet"));

assertThat(event.getSender().getLogin(), is("gsmet"));
assertThat(event.getRepository().getFullName(), is("gsmet/quarkus-bot-java-playground"));
assertThat(event.getInstallation().getId(), is(90470530L));
}

/**
* Ping.
*
Expand Down
83 changes: 81 additions & 2 deletions src/test/java/org/kohsuke/github/GHMilestoneTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import java.io.IOException;
import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

import static org.hamcrest.Matchers.*;

Expand All @@ -18,6 +20,13 @@
*/
public class GHMilestoneTest extends AbstractGitHubWireMockTest {

private static List<String> filterTestMilestones(List<GHMilestone> milestones) {
return milestones.stream()
.map(GHMilestone::getTitle)
.filter(t -> t.startsWith("Milestone Sort "))
.collect(Collectors.toList());
}

/**
* Create default GHMilestoneTest instance
*/
Expand All @@ -38,14 +47,84 @@ public void cleanUp() throws Exception {
return;
}

for (GHMilestone milestone : getRepository(getNonRecordingGitHub()).listMilestones(GHIssueState.ALL)) {
GHRepository repo = getRepository(getNonRecordingGitHub());

for (GHIssue issue : repo.queryIssues().state(GHIssueState.ALL).list()) {
if (issue.getTitle().endsWith("for sort test") || issue.getTitle().equals("Issue for testUnsetMilestone")) {
issue.close();
}
}

for (GHMilestone milestone : repo.listMilestones(GHIssueState.ALL)) {
if ("Original Title".equals(milestone.getTitle()) || "Updated Title".equals(milestone.getTitle())
|| "Unset Test Milestone".equals(milestone.getTitle())) {
|| "Unset Test Milestone".equals(milestone.getTitle())
|| "Milestone Sort A".equals(milestone.getTitle())
|| "Milestone Sort B".equals(milestone.getTitle())) {
milestone.delete();
}
}
}

/**
* Test list milestones with sort and direction.
*
* @throws IOException
* Signals that an I/O exception has occurred.
*/
@Test
public void testListMilestonesWithSort() throws IOException {
GHRepository repo = getRepository();
GHMilestone milestoneA = repo.createMilestone("Milestone Sort A", "First milestone");
milestoneA.setDueOn(GitHubClient.parseInstant("2025-06-01T00:00:00Z"));
GHMilestone milestoneB = repo.createMilestone("Milestone Sort B", "Second milestone");
milestoneB.setDueOn(GitHubClient.parseInstant("2025-12-01T00:00:00Z"));

// List with due_on sort ascending (default)
List<String> ascending = filterTestMilestones(repo.queryMilestones()
.state(GHIssueState.OPEN)
.sort(GHMilestoneQueryBuilder.Sort.DUE_ON)
.direction(GHDirection.ASC)
.list()
.toList());
assertThat(ascending, contains("Milestone Sort A", "Milestone Sort B"));

// List with due_on sort descending
List<String> descending = filterTestMilestones(repo.queryMilestones()
.state(GHIssueState.OPEN)
.sort(GHMilestoneQueryBuilder.Sort.DUE_ON)
.direction(GHDirection.DESC)
.list()
.toList());
assertThat(descending, contains("Milestone Sort B", "Milestone Sort A"));

// Create issues to test completeness sort
// Milestone A: 1 open, 1 closed = 50% complete
GHIssue issueA1 = repo.createIssue("Issue A1 for sort test").milestone(milestoneA).create();
GHIssue issueA2 = repo.createIssue("Issue A2 for sort test").milestone(milestoneA).create();
issueA2.close();

// Milestone B: 1 open, 0 closed = 0% complete
GHIssue issueB1 = repo.createIssue("Issue B1 for sort test").milestone(milestoneB).create();

// List with completeness sort ascending (least complete first)
List<String> byCompleteness = filterTestMilestones(repo.queryMilestones()
.state(GHIssueState.OPEN)
.sort(GHMilestoneQueryBuilder.Sort.COMPLETENESS)
.direction(GHDirection.ASC)
.list()
.toList());
assertThat(byCompleteness, contains("Milestone Sort B", "Milestone Sort A"));

// List with completeness sort descending (most complete first)
List<String> byCompletenessDesc = filterTestMilestones(repo.queryMilestones()
.state(GHIssueState.OPEN)
.sort(GHMilestoneQueryBuilder.Sort.COMPLETENESS)
.direction(GHDirection.DESC)
.list()
.toList());
assertThat(byCompletenessDesc, contains("Milestone Sort A", "Milestone Sort B"));
}

/**
* Test unset milestone.
*
Expand Down
Loading
Loading