diff --git a/release-tools/src/main/java/org/springframework/data/release/issues/IssueTracker.java b/release-tools/src/main/java/org/springframework/data/release/issues/IssueTracker.java index 4229332..86573ef 100644 --- a/release-tools/src/main/java/org/springframework/data/release/issues/IssueTracker.java +++ b/release-tools/src/main/java/org/springframework/data/release/issues/IssueTracker.java @@ -95,11 +95,19 @@ public interface IssueTracker extends Plugin { */ void createReleaseTicket(ModuleIteration module); + /** + * Creates a ticket for the given {@link ModuleIteration} and summary {@code text}. + * + * @param module must not be {@literal null}. + * @param text the text to use. + * @return the created ticket. + */ + Ticket createTicket(ModuleIteration module, String text); + /** * Assigns the ticket to the current user. * * @param ticket must not be {@literal null}. - * @param credentials must not be {@literal null}. */ void assignTicketToMe(Ticket ticket); diff --git a/release-tools/src/main/java/org/springframework/data/release/issues/IssueTrackerCommands.java b/release-tools/src/main/java/org/springframework/data/release/issues/IssueTrackerCommands.java index ed18854..2528c3d 100644 --- a/release-tools/src/main/java/org/springframework/data/release/issues/IssueTrackerCommands.java +++ b/release-tools/src/main/java/org/springframework/data/release/issues/IssueTrackerCommands.java @@ -126,6 +126,15 @@ class IssueTrackerCommands extends TimedCommand { return releaseTickets(iteration); } + @CliCommand(value = "tracker create tickets") + public String createTickets(@CliOption(key = "", mandatory = true) TrainIteration iteration, + @CliOption(key = "text", mandatory = true) String text) { + + return iteration.stream().// + map(module -> getTrackerFor(module).createTicket(module, text)).collect(Tickets.toTicketsCollector()) + .toString(); + } + @CliCommand("tracker changelog") public String changelog(@CliOption(key = "", mandatory = true) TrainIteration iteration, // @CliOption(key = "module") String moduleName) { diff --git a/release-tools/src/main/java/org/springframework/data/release/issues/Ticket.java b/release-tools/src/main/java/org/springframework/data/release/issues/Ticket.java index 10827c2..8e5acef 100644 --- a/release-tools/src/main/java/org/springframework/data/release/issues/Ticket.java +++ b/release-tools/src/main/java/org/springframework/data/release/issues/Ticket.java @@ -24,13 +24,15 @@ import org.springframework.util.Assert; /** * Value object to represent a {@link Ticket}. - * + * * @author Oliver Gierke + * @author Mark Paluch */ @Value public class Ticket { String id, summary; + String url; TicketStatus ticketStatus; /* @@ -39,7 +41,7 @@ public class Ticket { */ @Override public String toString() { - return String.format("%14s - %s", id, summary); + return String.format("%14s - %s (%s)", id, summary, url); } public boolean isResolved() { @@ -48,7 +50,7 @@ public class Ticket { /** * Returns whether the current {@link Ticket} is the release ticket for the given {@link ModuleIteration}. - * + * * @param module must not be {@literal null}. * @return */ @@ -60,7 +62,7 @@ public class Ticket { /** * Returns whether the current {@link Ticket} is a release ticket for the given {@link TrainIteration}. - * + * * @param train must not be {@literal null}. * @return */ diff --git a/release-tools/src/main/java/org/springframework/data/release/issues/github/GitHub.java b/release-tools/src/main/java/org/springframework/data/release/issues/github/GitHub.java index a107e66..120eda5 100644 --- a/release-tools/src/main/java/org/springframework/data/release/issues/github/GitHub.java +++ b/release-tools/src/main/java/org/springframework/data/release/issues/github/GitHub.java @@ -246,24 +246,38 @@ class GitHub implements IssueTracker { Assert.notNull(moduleIteration, "ModuleIteration must not be null."); - HttpHeaders httpHeaders = newUserScopedHttpHeaders(); Tickets tickets = getTicketsFor(moduleIteration); - if (tickets.hasReleaseTicket(moduleIteration)) { return; } + logger.log(moduleIteration, "Creating release ticket…"); + + doCreateTicket(moduleIteration, Tracker.releaseTicketSummary(moduleIteration)); + } + + @Override + public Ticket createTicket(ModuleIteration moduleIteration, String text) { + + logger.log(moduleIteration, "Creating ticket…"); + + return doCreateTicket(moduleIteration, text); + } + + private Ticket doCreateTicket(ModuleIteration moduleIteration, String text) { + HttpHeaders httpHeaders = newUserScopedHttpHeaders(); + String repositoryName = GitProject.of(moduleIteration.getProject()).getRepositoryName(); Milestone milestone = getMilestone(moduleIteration, repositoryName); - GitHubIssue gitHubIssue = GitHubIssue.of(Tracker.releaseTicketSummary(moduleIteration), milestone); + GitHubIssue gitHubIssue = GitHubIssue.of(text, milestone); Map parameters = newUrlTemplateVariables(); parameters.put("repoName", repositoryName); - logger.log(moduleIteration, "Creating release ticket…"); + GitHubIssue body = operations.exchange(ISSUES_URI_TEMPLATE, HttpMethod.POST, + new HttpEntity(gitHubIssue, httpHeaders), GitHubIssue.class, parameters).getBody(); - operations.exchange(ISSUES_URI_TEMPLATE, HttpMethod.POST, new HttpEntity(gitHubIssue, httpHeaders), - GitHubIssue.class, parameters).getBody(); + return toTicket(body); } @Cacheable("tickets") @@ -530,12 +544,12 @@ class GitHub implements IssueTracker { Optional milestone = findMilestone(moduleIteration, repositoryName); return milestone - .orElseThrow(() -> new IllegalArgumentException(String.format("No milestone for %s found containing %s!", // + .orElseThrow(() -> new IllegalStateException(String.format("No milestone for %s found containing %s!", // moduleIteration.getProject().getFullName(), // new GithubMilestone(moduleIteration)))); } private static Ticket toTicket(GitHubIssue issue) { - return new Ticket(issue.getId(), issue.getTitle(), new GithubTicketStatus(issue.getState())); + return new Ticket(issue.getId(), issue.getTitle(), issue.getHtmlUrl(), new GithubTicketStatus(issue.getState())); } } diff --git a/release-tools/src/main/java/org/springframework/data/release/issues/github/GitHubIssue.java b/release-tools/src/main/java/org/springframework/data/release/issues/github/GitHubIssue.java index 5881547..ffc5d5e 100644 --- a/release-tools/src/main/java/org/springframework/data/release/issues/github/GitHubIssue.java +++ b/release-tools/src/main/java/org/springframework/data/release/issues/github/GitHubIssue.java @@ -15,7 +15,6 @@ */ package org.springframework.data.release.issues.github; -import lombok.RequiredArgsConstructor; import lombok.Value; import java.util.Collections; @@ -25,31 +24,42 @@ import org.springframework.data.release.model.ModuleIteration; import org.springframework.util.Assert; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; /** * @author Oliver Gierke * @author Mark Paluch */ @Value -@RequiredArgsConstructor class GitHubIssue { String number, title, state; List assignees; + String htmlUrl; Object milestone; + public GitHubIssue(String number, String title, String state, List assignees, + @JsonProperty("html_url") String htmlUrl, Object milestone) { + this.number = number; + this.title = title; + this.state = state; + this.assignees = assignees; + this.htmlUrl = htmlUrl; + this.milestone = milestone; + } + public static GitHubIssue of(String title, Milestone milestone) { - return new GitHubIssue(null, title, null, null, milestone.number); + return new GitHubIssue(null, title, null, null, null, milestone.number); } public static GitHubIssue assignedTo(String username) { Assert.hasText(username, "Username must not be null or empty!"); - return new GitHubIssue(null, null, null, Collections.singletonList(username), null); + return new GitHubIssue(null, null, null, Collections.singletonList(username), null, null); } public GitHubIssue close() { - return new GitHubIssue(this.number, this.title, "closed", this.assignees, this.milestone); + return new GitHubIssue(this.number, this.title, "closed", this.assignees, null, this.milestone); } public String getId() { diff --git a/release-tools/src/main/java/org/springframework/data/release/issues/jira/Jira.java b/release-tools/src/main/java/org/springframework/data/release/issues/jira/Jira.java index d857ac2..53b69e0 100644 --- a/release-tools/src/main/java/org/springframework/data/release/issues/jira/Jira.java +++ b/release-tools/src/main/java/org/springframework/data/release/issues/jira/Jira.java @@ -39,6 +39,7 @@ import org.springframework.data.release.issues.jira.JiraIssue.StatusCategory; import org.springframework.data.release.model.ModuleIteration; import org.springframework.data.release.model.Project; import org.springframework.data.release.model.ProjectKey; +import org.springframework.data.release.model.Projects; import org.springframework.data.release.model.Tracker; import org.springframework.data.release.model.TrainIteration; import org.springframework.data.release.utils.Logger; @@ -230,7 +231,7 @@ class Jira implements JiraConnector { Optional versionsForModuleIteration = findJiraReleaseVersion(moduleIteration); - if (versionsForModuleIteration.isPresent()) { + if (versionsForModuleIteration.isPresent() || moduleIteration.getProject() == Projects.GEMFIRE) { return; } @@ -303,28 +304,45 @@ class Jira implements JiraConnector { Assert.notNull(moduleIteration, "ModuleIteration must not be null."); - HttpHeaders httpHeaders = newUserScopedHttpHeaders(); - - Map parameters = newUrlTemplateVariables(); - Tickets tickets = getTicketsFor(moduleIteration); - if (tickets.hasReleaseTicket(moduleIteration)) { + if (tickets.hasReleaseTicket(moduleIteration) || moduleIteration.getProject() == Projects.GEODE) { return; } - findJiraReleaseVersion(moduleIteration).orElseThrow( - () -> new IllegalStateException(String.format("No release version for %s found containing %s!", - moduleIteration.getProject().getFullName(), new JiraVersion(moduleIteration)))); - - JiraComponents jiraComponents = getJiraComponents(moduleIteration.getProjectKey()); - logger.log(moduleIteration, "Creating release ticket…"); - JiraIssue jiraIssue = prepareJiraIssueToCreate(moduleIteration, jiraComponents); + doCreateTicket(moduleIteration, Tracker.releaseTicketSummary(moduleIteration)); + } - operations.exchange(CREATE_ISSUES_TEMPLATE, HttpMethod.POST, new HttpEntity(jiraIssue, httpHeaders), - CreatedJiraIssue.class, parameters).getBody(); + @Override + public Ticket createTicket(ModuleIteration moduleIteration, String text) { + + Assert.notNull(moduleIteration, "ModuleIteration must not be null."); + + logger.log(moduleIteration, "Creating ticket…"); + + return doCreateTicket(moduleIteration, text); + } + + private Ticket doCreateTicket(ModuleIteration moduleIteration, String text) { + + HttpHeaders httpHeaders = newUserScopedHttpHeaders(); + Map parameters = newUrlTemplateVariables(); + + findJiraReleaseVersion(moduleIteration).orElseThrow( + () -> new IllegalStateException(String.format("No release version for %s found", moduleIteration))); + + JiraComponents jiraComponents = getJiraComponents(moduleIteration.getProjectKey()); + JiraIssue jiraIssue = prepareJiraIssueToCreate(text, moduleIteration, jiraComponents); + + CreatedJiraIssue created = operations.exchange(CREATE_ISSUES_TEMPLATE, HttpMethod.POST, + new HttpEntity(jiraIssue, httpHeaders), CreatedJiraIssue.class, parameters).getBody(); + + JiraIssue createdIssue = getJiraIssue(created.getKey()) + .orElseThrow(() -> new IllegalStateException(String.format("Cannot retrieve ticket %s", created.getKey()))); + + return toTicket(createdIssue); } /* @@ -357,7 +375,8 @@ class Jira implements JiraConnector { operations.exchange(ISSUE_TEMPLATE, HttpMethod.PUT, new HttpEntity(editMeta, httpHeaders), String.class, parameters).getBody(); } catch (HttpClientErrorException e) { - logger.warn("Ticket", "Self-assignment of %s failed with status ", ticket, e.getStatusCode()); + logger.warn("Ticket", "Self-assignment of %s failed with status %s (%s)", ticket, e.getStatusCode(), + e.getResponseBodyAsString()); } } @@ -409,7 +428,8 @@ class Jira implements JiraConnector { operations.exchange(TRANSITION_TEMPLATE, HttpMethod.POST, new HttpEntity(editMeta, httpHeaders), String.class, parameters).getBody(); } catch (HttpClientErrorException e) { - logger.warn("Ticket", "Start progress of %s failed with status ", ticket, e.getStatusCode()); + logger.warn("Ticket", "Start progress of %s failed with status %s (%s)", ticket, e.getStatusCode(), + e.getResponseBodyAsString()); } } @@ -436,7 +456,8 @@ class Jira implements JiraConnector { operations.exchange(TRANSITION_TEMPLATE, HttpMethod.POST, new HttpEntity(editMeta, httpHeaders), String.class, parameters).getBody(); } catch (HttpClientErrorException e) { - logger.warn("Ticket", "Resolution of %s failed with status ", ticket, e.getStatusCode()); + logger.warn("Ticket", "Resolution of %s failed with status %s (%s)", ticket, e.getStatusCode(), + e.getResponseBodyAsString()); } } } @@ -564,11 +585,11 @@ class Jira implements JiraConnector { return JiraComponents.of(components); } - private JiraIssue prepareJiraIssueToCreate(ModuleIteration moduleIteration, JiraComponents jiraComponents) { + private JiraIssue prepareJiraIssueToCreate(String text, ModuleIteration moduleIteration, + JiraComponents jiraComponents) { JiraIssue jiraIssue = JiraIssue.createTask(); - jiraIssue.project(moduleIteration.getProjectKey()).summary(Tracker.releaseTicketSummary(moduleIteration)) - .fixVersion(moduleIteration); + jiraIssue.project(moduleIteration.getProjectKey()).summary(text).fixVersion(moduleIteration); Fields fields = jiraIssue.getFields(); @@ -684,7 +705,8 @@ class Jira implements JiraConnector { jiraTicketStatus = JiraTicketStatus.UNKNOWN; } - return new Ticket(issue.getKey(), fields.getSummary(), jiraTicketStatus); + return new Ticket(issue.getKey(), fields.getSummary(), + String.format("%s/browse/%s", jiraProperties.getApiUrl(), issue.getKey()), jiraTicketStatus); } /** diff --git a/release-tools/src/main/java/org/springframework/data/release/model/ModuleIteration.java b/release-tools/src/main/java/org/springframework/data/release/model/ModuleIteration.java index 025d3c2..2616c57 100644 --- a/release-tools/src/main/java/org/springframework/data/release/model/ModuleIteration.java +++ b/release-tools/src/main/java/org/springframework/data/release/model/ModuleIteration.java @@ -158,6 +158,6 @@ public class ModuleIteration implements IterationVersion, ProjectAware { */ @Override public String toString() { - return String.format("%s %s", module.getProject().getFullName(), getShortVersionString()); + return String.format("%s %s", module.getProject().getFullName(), getMediumVersionString()); } } diff --git a/release-tools/src/test/java/org/springframework/data/release/git/CommitUnitTests.java b/release-tools/src/test/java/org/springframework/data/release/git/CommitUnitTests.java index 9a695b5..59f7a3b 100644 --- a/release-tools/src/test/java/org/springframework/data/release/git/CommitUnitTests.java +++ b/release-tools/src/test/java/org/springframework/data/release/git/CommitUnitTests.java @@ -33,7 +33,8 @@ class CommitUnitTests { @Test void shouldRenderCommitMessage() { - assertThat(new Commit(new Ticket("1234", "Hello", Mockito.mock(TicketStatus.class)), "Summary", Optional.empty())) + assertThat( + new Commit(new Ticket("1234", "Hello", null, Mockito.mock(TicketStatus.class)), "Summary", Optional.empty())) .hasToString("1234 - Summary."); } @@ -41,7 +42,8 @@ class CommitUnitTests { void shouldRenderCommitMessageWithDetail() { assertThat( - new Commit(new Ticket("1234", "Hello", Mockito.mock(TicketStatus.class)), "Summary", Optional.of("detail"))) + new Commit(new Ticket("1234", "Hello", null, Mockito.mock(TicketStatus.class)), "Summary", + Optional.of("detail"))) .hasToString("1234 - Summary.\n" + "\n" + "detail"); } } diff --git a/release-tools/src/test/java/org/springframework/data/release/issues/jira/JiraConnectorIntegrationTests.java b/release-tools/src/test/java/org/springframework/data/release/issues/jira/JiraConnectorIntegrationTests.java index 34181db..428967d 100644 --- a/release-tools/src/test/java/org/springframework/data/release/issues/jira/JiraConnectorIntegrationTests.java +++ b/release-tools/src/test/java/org/springframework/data/release/issues/jira/JiraConnectorIntegrationTests.java @@ -181,6 +181,8 @@ class JiraConnectorIntegrationTests extends AbstractIntegrationTests { mockGetProjectComponentsWith("projectComponents.json", moduleIteration.getProjectKey()); mockSearchWith("emptyTickets.json"); prepareCreateIssueAndReturn("issueCreated.json"); + mockService.stubFor(get(urlPathMatching("/rest/api/2/issue/DATAREDIS-42")).// + willReturn(json("existingTicket.json"))); jira.createReleaseTicket(moduleIteration); @@ -200,6 +202,8 @@ class JiraConnectorIntegrationTests extends AbstractIntegrationTests { mockGetProjectComponentsWith("emptyProjectComponents.json", moduleIteration.getProjectKey()); mockSearchWith("emptyTickets.json"); prepareCreateIssueAndReturn("issueCreated.json"); + mockService.stubFor(get(urlPathMatching("/rest/api/2/issue/DATAREDIS-42")).// + willReturn(json("existingTicket.json"))); jira.createReleaseTicket(moduleIteration); @@ -218,7 +222,7 @@ class JiraConnectorIntegrationTests extends AbstractIntegrationTests { mockGetProjectVersionsWith("emptyReleaseVersions.json", moduleIteration.getProjectKey()); assertThatIllegalStateException().isThrownBy(() -> jira.createReleaseTicket(moduleIteration)) - .withMessageContaining("No release version for Spring Data REST found containing 2.5 RC1 (Hopper)!"); + .withMessageContaining("No release version for Spring Data REST 2.5 RC1 (Hopper) found"); } @Test // #5 @@ -247,7 +251,7 @@ class JiraConnectorIntegrationTests extends AbstractIntegrationTests { mockService.stubFor(put(urlPathMatching("/rest/api/2/issue/DATAREDIS-302")).// willReturn(aResponse().withStatus(204))); - jira.assignTicketToMe(new Ticket("DATAREDIS-302", "", null)); + jira.assignTicketToMe(new Ticket("DATAREDIS-302", "", null, null)); verify(putRequestedFor(urlPathMatching("/rest/api/2/issue/DATAREDIS-302")).withRequestBody(equalToJson( "{\"update\":{\"assignee\":[ {\"set\":{\"name\":\"dummy\"}} ] }, \"transition\":{}, \"fields\":{}}"))); @@ -267,7 +271,7 @@ class JiraConnectorIntegrationTests extends AbstractIntegrationTests { mockService.stubFor(post(urlPathMatching("/rest/api/2/issue/DATACASS-302")).// willReturn(aResponse().withStatus(204))); - jira.assignTicketToMe(new Ticket("DATACASS-302", "", null)); + jira.assignTicketToMe(new Ticket("DATACASS-302", "", null, null)); verify(0, postRequestedFor(urlPathMatching("/rest/api/2/issue/DATACASS-302"))); } diff --git a/release-tools/src/test/java/org/springframework/data/release/issues/jira/TicketsUnitTests.java b/release-tools/src/test/java/org/springframework/data/release/issues/jira/TicketsUnitTests.java index 1a93c77..467932c 100644 --- a/release-tools/src/test/java/org/springframework/data/release/issues/jira/TicketsUnitTests.java +++ b/release-tools/src/test/java/org/springframework/data/release/issues/jira/TicketsUnitTests.java @@ -37,7 +37,7 @@ class TicketsUnitTests { @Test void hasReleaseTicketShouldReturnTrue() { - Ticket ticket = new Ticket("1234", "Release 1.10 GA (Hopper)", JiraTicketStatus.of(false, "", "")); + Ticket ticket = new Ticket("1234", "Release 1.10 GA (Hopper)", null, JiraTicketStatus.of(false, "", "")); Tickets tickets = new Tickets(Collections.singletonList(ticket)); boolean result = tickets.hasReleaseTicket(ReleaseTrains.HOPPER.getModuleIteration(Projects.JPA, Iteration.GA)); @@ -47,7 +47,7 @@ class TicketsUnitTests { @Test void hasReleaseTickeForTicketWithoutTrainNameShouldReturnFalse() { - Ticket ticket = new Ticket("1234", "Release 1.10 GA", JiraTicketStatus.of(false, "", "")); + Ticket ticket = new Ticket("1234", "Release 1.10 GA", null, JiraTicketStatus.of(false, "", "")); Tickets tickets = new Tickets(Collections.singletonList(ticket)); boolean result = tickets.hasReleaseTicket(ReleaseTrains.HOPPER.getModuleIteration(Projects.JPA, Iteration.GA)); @@ -57,7 +57,7 @@ class TicketsUnitTests { @Test void getReleaseTicketReturnsReleaseTicket() { - Ticket ticket = new Ticket("1234", "Release 1.10 GA (Hopper)", JiraTicketStatus.of(false, "", "")); + Ticket ticket = new Ticket("1234", "Release 1.10 GA (Hopper)", null, JiraTicketStatus.of(false, "", "")); Tickets tickets = new Tickets(Collections.singletonList(ticket)); Ticket releaseTicket = tickets @@ -69,20 +69,23 @@ class TicketsUnitTests { void getReleaseTicketReturnsCalverReleaseTicket() { Tickets tickets = new Tickets( - Collections.singletonList(new Ticket("1234", "Release 2.4 GA (2020.0.0)", JiraTicketStatus.of(false, "", "")))); + Collections + .singletonList(new Ticket("1234", "Release 2.4 GA (2020.0.0)", null, JiraTicketStatus.of(false, "", "")))); Ticket releaseTicket = tickets .getReleaseTicket(ReleaseTrains.OCKHAM.getModuleIteration(Projects.JPA, Iteration.GA)); assertThat(releaseTicket).isNotNull(); tickets = new Tickets( - Collections.singletonList(new Ticket("1234", "Release 2.4 M1 (2020.0.0)", JiraTicketStatus.of(false, "", "")))); + Collections + .singletonList(new Ticket("1234", "Release 2.4 M1 (2020.0.0)", null, JiraTicketStatus.of(false, "", "")))); releaseTicket = tickets.getReleaseTicket(ReleaseTrains.OCKHAM.getModuleIteration(Projects.JPA, Iteration.M1)); assertThat(releaseTicket).isNotNull(); tickets = new Tickets( - Collections.singletonList(new Ticket("1234", "Release 2.4.1 (2020.0.1)", JiraTicketStatus.of(false, "", "")))); + Collections + .singletonList(new Ticket("1234", "Release 2.4.1 (2020.0.1)", null, JiraTicketStatus.of(false, "", "")))); releaseTicket = tickets.getReleaseTicket(ReleaseTrains.OCKHAM.getModuleIteration(Projects.JPA, Iteration.SR1)); assertThat(releaseTicket).isNotNull(); @@ -91,7 +94,7 @@ class TicketsUnitTests { @Test void getReleaseTicketThrowsExceptionWithoutAReleaseTicket() { - Ticket ticket = new Ticket("1234", "Release 1.10 GA", JiraTicketStatus.of(false, "", "")); + Ticket ticket = new Ticket("1234", "Release 1.10 GA", null, JiraTicketStatus.of(false, "", "")); Tickets tickets = new Tickets(Collections.singletonList(ticket)); assertThatIllegalArgumentException().isThrownBy( @@ -101,7 +104,7 @@ class TicketsUnitTests { @Test void getResolvedReleaseTicket() { - Ticket ticket = new Ticket("1234", "Release 1.10 GA (Hopper)", JiraTicketStatus.of(true, "", "")); + Ticket ticket = new Ticket("1234", "Release 1.10 GA (Hopper)", null, JiraTicketStatus.of(true, "", "")); Tickets tickets = new Tickets(Collections.singletonList(ticket)); Ticket releaseTicket = tickets @@ -112,7 +115,7 @@ class TicketsUnitTests { @Test void getReleaseTicketsReturnsReleaseTickets() { - Ticket ticket = new Ticket("1234", "Release 1.10 GA (Hopper)", JiraTicketStatus.of(false, "", "")); + Ticket ticket = new Ticket("1234", "Release 1.10 GA (Hopper)", null, JiraTicketStatus.of(false, "", "")); Tickets tickets = new Tickets(Collections.singletonList(ticket)); Tickets result = tickets