diff --git a/application-local.template b/application-local.template index 33be287..13c1367 100644 --- a/application-local.template +++ b/application-local.template @@ -3,6 +3,7 @@ git.username= git.author= git.email= git.password= +github.api.url=https://api.github.com # Maven maven.mavenHome= @@ -12,6 +13,11 @@ deployment.repository-prefix= deployment.password= deployment.api-key= +# Jira +jira.username= +jira.password= +jira.url=https://jira.spring.io + # GPG deployment.gpg.keyname= deployment.gpg.password= diff --git a/pom.xml b/pom.xml index bcf50df..65ea289 100644 --- a/pom.xml +++ b/pom.xml @@ -103,6 +103,19 @@ 0.9.1 + + com.github.tomakehurst + wiremock + 1.58 + test + + + + com.google.guava + guava + 19.0 + + diff --git a/src/main/java/org/springframework/data/release/cli/IssueTrackerCommands.java b/src/main/java/org/springframework/data/release/cli/IssueTrackerCommands.java index 6d4742a..6e19e5b 100644 --- a/src/main/java/org/springframework/data/release/cli/IssueTrackerCommands.java +++ b/src/main/java/org/springframework/data/release/cli/IssueTrackerCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,14 @@ package org.springframework.data.release.cli; import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.env.Environment; import org.springframework.data.release.CliComponent; import org.springframework.data.release.jira.Changelog; -import org.springframework.data.release.jira.Credentials; import org.springframework.data.release.jira.IssueTracker; import org.springframework.data.release.jira.JiraConnector; +import org.springframework.data.release.jira.Tickets; import org.springframework.data.release.model.ModuleIteration; import org.springframework.data.release.model.Project; import org.springframework.data.release.model.TrainIteration; @@ -36,13 +36,13 @@ import org.springframework.util.StringUtils; /** * @author Oliver Gierke + * @author Mark Paluch */ @CliComponent public class IssueTrackerCommands implements CommandMarker { private final PluginRegistry tracker; private final JiraConnector jira; - private final Credentials credentials; /** * @param tracker must not be {@literal null}. @@ -50,20 +50,15 @@ public class IssueTrackerCommands implements CommandMarker { * @param environment must not be {@literal null}. */ @Autowired - public IssueTrackerCommands(PluginRegistry tracker, JiraConnector jira, - Environment environment) { - - String username = environment.getProperty("jira.username", (String) null); - String password = environment.getProperty("jira.password", (String) null); + public IssueTrackerCommands(PluginRegistry tracker, JiraConnector jira) { this.tracker = tracker; this.jira = jira; - this.credentials = StringUtils.hasText(username) ? new Credentials(username, password) : null; } - @CliCommand("jira evict") - public void jiraEvict() { - jira.reset(); + @CliCommand("tracker evict") + public void evict() { + StreamSupport.stream(tracker.spliterator(), false).forEach(IssueTracker::reset); } @CliCommand(value = "jira tickets") @@ -71,11 +66,52 @@ public class IssueTrackerCommands implements CommandMarker { @CliOption(key = "for-current-user", specifiedDefaultValue = "true", unspecifiedDefaultValue = "false") boolean forCurrentUser) { - if (forCurrentUser && credentials == null) { - return "No authentication specified! Use 'jira authenticate' first!"; - } + Tickets tickets = StreamSupport.stream(tracker.spliterator(), false). // + flatMap(issueTracker -> issueTracker. // + getTicketsFor(iteration, forCurrentUser).stream()) + . // + collect(Tickets.toTicketsCollector()); + return tickets.toString(); + } - return jira.getTicketsFor(iteration, forCurrentUser ? credentials : null).toString(); + @CliCommand(value = "tracker releasetickets") + public String releaseTickets(@CliOption(key = "", mandatory = true) TrainIteration iteration) { + + Tickets tickets = StreamSupport.stream(tracker.spliterator(), false). // + flatMap(issueTracker -> issueTracker.getTicketsFor(iteration).stream()). // + collect(Tickets.toTicketsCollector()); + return tickets.getReleaseTickets(iteration).toString(); + } + + @CliCommand(value = "jira self-assign releasetickets") + public String jiraSelfAssignReleaseTickets(@CliOption(key = "", mandatory = true) TrainIteration iteration) { + + Tickets releaseTickets = jira.getTicketsFor(iteration).getReleaseTickets(iteration); + releaseTickets.forEach(ticket -> jira.assignTicketToMe(ticket)); + return releaseTickets(iteration); + } + + @CliCommand(value = "tracker create releaseversions") + public void jiraCreateReleaseVersions(@CliOption(key = "", mandatory = true) TrainIteration iteration) { + iteration.forEach(this::createReleaseVersion); + } + + @CliCommand(value = "tracker create releasetickets") + public String createReleaseTickets(@CliOption(key = "", mandatory = true) TrainIteration iteration) { + + iteration.forEach(this::createReleaseVersion); + + evict(); + + return releaseTickets(iteration); + } + + private void createReleaseVersion(ModuleIteration moduleIteration) { + getPluginFor(moduleIteration).createReleaseVersion(moduleIteration); + } + + private IssueTracker getPluginFor(ModuleIteration moduleIteration) { + return tracker.getPluginFor(moduleIteration.getProject()); } @CliCommand("tracker changelog") diff --git a/src/main/java/org/springframework/data/release/git/GitProperties.java b/src/main/java/org/springframework/data/release/git/GitProperties.java index 0b2244d..3a96fa2 100644 --- a/src/main/java/org/springframework/data/release/git/GitProperties.java +++ b/src/main/java/org/springframework/data/release/git/GitProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import javax.annotation.PostConstruct; import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.data.release.utils.HttpBasicCredentials; import org.springframework.stereotype.Component; @@ -32,22 +33,26 @@ import org.springframework.util.Assert; * Configurable properties for Git. * * @author Oliver Gierke + * @author Mark Paluch */ @Data @Component @ConfigurationProperties(prefix = "git") public class GitProperties { - private @Getter(AccessLevel.PRIVATE) String username, password; - private String author, email; + private @Getter(AccessLevel.PRIVATE) String password; + private String username, author, email; + + @Value("${github.api.url}") private String githubApiBaseUrl; @PostConstruct public void init() { Assert.hasText(username, "No GitHub username (git.username) configured!"); - Assert.hasText(username, "No GitHub password (git.password) configured!"); - Assert.hasText(username, "No Git author (git.author) configured!"); - Assert.hasText(username, "No Git email (git.email) configured!"); + Assert.hasText(password, "No GitHub password (git.password) configured!"); + Assert.hasText(author, "No Git author (git.author) configured!"); + Assert.hasText(email, "No Git email (git.email) configured!"); + Assert.hasText(githubApiBaseUrl, "No GitHub API base url configured!"); } /** diff --git a/src/main/java/org/springframework/data/release/jira/CreatedJiraIssue.java b/src/main/java/org/springframework/data/release/jira/CreatedJiraIssue.java new file mode 100644 index 0000000..85521a3 --- /dev/null +++ b/src/main/java/org/springframework/data/release/jira/CreatedJiraIssue.java @@ -0,0 +1,32 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.release.jira; + +import lombok.Data; + +/** + * Value object for a created Jira issue. + * + * @author Mark Paluch + */ +@Data +public class CreatedJiraIssue { + + private String id; + private String key; + +} diff --git a/src/main/java/org/springframework/data/release/jira/GitHubIssue.java b/src/main/java/org/springframework/data/release/jira/GitHubIssue.java index 8afb420..6a9c211 100644 --- a/src/main/java/org/springframework/data/release/jira/GitHubIssue.java +++ b/src/main/java/org/springframework/data/release/jira/GitHubIssue.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,25 +15,77 @@ */ package org.springframework.data.release.jira; -import lombok.Data; - import org.springframework.data.release.model.ModuleIteration; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.Value; + /** * @author Oliver Gierke + * @author Mark Paluch */ -@Data +@Value class GitHubIssue { private String number; private String title; private String state; + private Object milestone; + + @JsonCreator + public GitHubIssue(@JsonProperty("number") String number, @JsonProperty("title") String title, + @JsonProperty("state") String state, @JsonProperty("milestone") Object milestone) { + this.number = number; + this.title = title; + this.state = state; + this.milestone = milestone; + } + + public GitHubIssue(String title, Milestone milestone) { + + this.title = title; + this.milestone = milestone.getNumber(); + this.number = null; + this.state = null; + } public String getId() { + + if (number == null) { + return null; + } return "#".concat(number); } - public boolean isReleaseTicket(ModuleIteration module) { - return title.contains("Release") && title.contains(module.getShortVersionString()); + /** + * @author Oliver Gierke + * @author Mark Paluch + */ + @Value + static class Milestone { + + private final String title; + private final Long number; + private final String description; + + @JsonCreator + public Milestone(@JsonProperty("title") String title, @JsonProperty("number") Long number, + @JsonProperty("description") String description) { + this.title = title; + this.number = number; + this.description = description; + } + + public Milestone(String title, String description) { + this.number = null; + this.title = title; + this.description = description; + } + + public boolean matchesIteration(ModuleIteration moduleIteration) { + return title.contains(moduleIteration.getShortVersionString()); + } } } diff --git a/src/main/java/org/springframework/data/release/jira/GitHubIssueTracker.java b/src/main/java/org/springframework/data/release/jira/GitHubIssueTracker.java index 340fc59..2c085db 100644 --- a/src/main/java/org/springframework/data/release/jira/GitHubIssueTracker.java +++ b/src/main/java/org/springframework/data/release/jira/GitHubIssueTracker.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,6 @@ */ package org.springframework.data.release.jira; -import lombok.RequiredArgsConstructor; - import java.net.URI; import java.util.ArrayList; import java.util.Arrays; @@ -24,7 +22,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; +import java.util.Optional; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; @@ -45,21 +43,28 @@ import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; +import org.springframework.util.Assert; import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.RestOperations; import org.springframework.web.util.UriTemplate; +import lombok.RequiredArgsConstructor; + /** * @author Oliver Gierke + * @author Mark Paluch */ @RequiredArgsConstructor class GitHubIssueTracker implements IssueTracker { - private static final String MILESTONE_URI = "https://api.github.com/repos/spring-projects/{repoName}/milestones?state={state}"; - private static final String ISSUES_BY_MILESTONE_URI_TEMPLATE = "https://api.github.com/repos/spring-projects/{repoName}/issues?milestone={id}&state=all"; - private static final String ISSUE_BY_ID_URI_TEMPLATE = "https://api.github.com/repos/spring-projects/{repoName}/issues/{id}"; + private static final String MILESTONE_URI = "{githubBaseUrl}/repos/spring-projects/{repoName}/milestones?state={state}"; + private static final String ISSUES_BY_MILESTONE_AND_ASSIGNEE_URI_TEMPLATE = "{githubBaseUrl}/repos/spring-projects/{repoName}/issues?milestone={id}&state=all&assignee={assignee}"; + private static final String ISSUES_BY_MILESTONE_URI_TEMPLATE = "{githubBaseUrl}/repos/spring-projects/{repoName}/issues?milestone={id}&state=all"; + private static final String MILESTONES_URI_TEMPLATE = "{githubBaseUrl}/repos/spring-projects/{repoName}/milestones"; + private static final String ISSUE_BY_ID_URI_TEMPLATE = "{githubBaseUrl}/repos/spring-projects/{repoName}/issues/{id}"; + private static final String ISSUES_URI_TEMPLATE = "{githubBaseUrl}/repos/spring-projects/{repoName}/issues"; - private static final ParameterizedTypeReference> MILESTONES_TYPE = new ParameterizedTypeReference>() {}; + private static final ParameterizedTypeReference> MILESTONES_TYPE = new ParameterizedTypeReference>() {}; private static final ParameterizedTypeReference> ISSUES_TYPE = new ParameterizedTypeReference>() {}; private static final ParameterizedTypeReference ISSUE_TYPE = new ParameterizedTypeReference() {}; @@ -72,7 +77,7 @@ class GitHubIssueTracker implements IssueTracker { * @see org.springframework.data.release.jira.JiraConnector#flushTickets() */ @Override - @CacheEvict(value = "tickets", allEntries = true) + @CacheEvict(value = { "tickets", "release-tickets", "milestone" }, allEntries = true) public void reset() { } @@ -84,20 +89,10 @@ class GitHubIssueTracker implements IssueTracker { @Override @Cacheable("release-tickets") public Ticket getReleaseTicketFor(ModuleIteration module) { - - return getIssuesFor(module).stream().// - filter(issue -> issue.isReleaseTicket(module)).// - findFirst().// - map(issue -> toTicket(issue)).// - orElseThrow( - () -> new IllegalArgumentException(String.format("Could not find a release ticket for %s!", module))); + return getTicketsFor(module).getReleaseTicket(module); } - private Ticket toTicket(GitHubIssue issue) { - return new Ticket(issue.getId(), issue.getTitle(), new GithubTicketStatus(issue.getState())); - } - - /** + /* * (non-Javadoc) * * @see IssueTracker#findTickets(Project, Collection) @@ -110,13 +105,13 @@ class GitHubIssueTracker implements IssueTracker { List tickets = new ArrayList<>(); for (String ticketId : ticketIds) { - Map parameters = new HashMap<>(); + Map parameters = newUrlTemplateVariables(); parameters.put("repoName", repositoryName); parameters.put("id", ticketId); try { GitHubIssue gitHubIssue = operations.exchange(ISSUE_BY_ID_URI_TEMPLATE, HttpMethod.GET, - new HttpEntity<>(getAuthenticationHeaders()), ISSUE_TYPE, parameters).getBody(); + new HttpEntity<>(newUserScopedHttpHeaders()), ISSUE_TYPE, parameters).getBody(); tickets.add(toTicket(gitHubIssue)); } catch (HttpStatusCodeException e) { @@ -136,15 +131,15 @@ class GitHubIssueTracker implements IssueTracker { */ @Override @Cacheable("changelogs") - public Changelog getChangelogFor(ModuleIteration module) { + public Changelog getChangelogFor(ModuleIteration moduleIteration) { - List tickets = getIssuesFor(module).stream().// + Tickets tickets = getIssuesFor(moduleIteration, false).stream().// map(issue -> toTicket(issue)).// - collect(Collectors.toList()); + collect(Tickets.toTicketsCollector()); - logger.log(module, "Created changelog with %s entries.", tickets.size()); + logger.log(moduleIteration, "Created changelog with %s entries.", tickets.getOverallTotal()); - return new Changelog(module, new Tickets(tickets)); + return new Changelog(moduleIteration, tickets); } /* @@ -156,53 +151,159 @@ class GitHubIssueTracker implements IssueTracker { return project.uses(Tracker.GITHUB); } - private List getIssuesFor(ModuleIteration module) { + @Cacheable("tickets") + public Tickets getTicketsFor(ModuleIteration iteration) { + return getTicketsFor(iteration, false); + } - String repositoryName = GitProject.of(module.getProject()).getRepositoryName(); + @Override + public Tickets getTicketsFor(TrainIteration iteration) { + return getTicketsFor(iteration, false); + } - GitHubMilestone milestone = findMilestone(module, repositoryName); + /* + * (non-Javadoc) + * @see org.springframework.data.release.jira.GitHubIssueConnector#getTicketsFor(org.springframework.data.release.model.TrainIteration, boolean) + */ + @Override + public Tickets getTicketsFor(TrainIteration trainIteration, boolean forCurrentUser) { - Map parameters = new HashMap<>(); + if (forCurrentUser) { + logger.log(trainIteration, "Retrieving tickets (for user %s)…", properties.getUsername()); + } else { + logger.log(trainIteration, "Retrieving tickets…"); + } + + Tickets tickets = trainIteration.stream(). // + filter(moduleIteration -> supports(moduleIteration.getProject())). // + flatMap(moduleIteration -> getTicketsFor(moduleIteration, forCurrentUser).stream()). // + collect(Tickets.toTicketsCollector()); + + return tickets; + } + + @Override + public void createReleaseVersion(ModuleIteration moduleIteration) { + + Assert.notNull(moduleIteration, "ModuleIteration must not be null."); + + String repositoryName = GitProject.of(moduleIteration.getProject()).getRepositoryName(); + + Optional milestone = findMilestone(moduleIteration, repositoryName); + + if (milestone.isPresent()) { + return; + } + + GithubMilestone githubMilestone = new GithubMilestone(moduleIteration); + logger.log(moduleIteration, "Creating GitHub milestone %s", githubMilestone); + + HttpHeaders httpHeaders = newUserScopedHttpHeaders(); + Map parameters = newUrlTemplateVariables(); + parameters.put("repoName", repositoryName); + + operations.exchange(MILESTONES_URI_TEMPLATE, HttpMethod.POST, + new HttpEntity(githubMilestone.toMilestone(), httpHeaders), GitHubIssue.Milestone.class, parameters); + } + + @Override + public void createReleaseTicket(ModuleIteration moduleIteration) { + + Assert.notNull(moduleIteration, "ModuleIteration must not be null."); + + HttpHeaders httpHeaders = newUserScopedHttpHeaders(); + + Tickets tickets = getTicketsFor(moduleIteration); + + if (tickets.hasReleaseTicket(moduleIteration)) { + return; + } + + String repositoryName = GitProject.of(moduleIteration.getProject()).getRepositoryName(); + + Map parameters = newUrlTemplateVariables(); + parameters.put("repoName", repositoryName); + + GitHubIssue.Milestone milestone = getMilestone(moduleIteration, repositoryName); + + logger.log(moduleIteration, "Creating release ticket…"); + + GitHubIssue gitHubIssue = new GitHubIssue(Tracker.releaseTicketSummary(moduleIteration), milestone); + + operations.exchange(ISSUES_URI_TEMPLATE, HttpMethod.POST, new HttpEntity(gitHubIssue, httpHeaders), + GitHubIssue.class, parameters).getBody(); + + } + + private Tickets getTicketsFor(ModuleIteration moduleIteration, boolean forCurrentUser) { + + List issues = getIssuesFor(moduleIteration, forCurrentUser); + return issues.stream().map(this::toTicket).collect(Tickets.toTicketsCollector()); + } + + private List getIssuesFor(ModuleIteration moduleIteration, boolean forCurrentUser) { + + String repositoryName = GitProject.of(moduleIteration.getProject()).getRepositoryName(); + + GitHubIssue.Milestone milestone = getMilestone(moduleIteration, repositoryName); + + Map parameters = newUrlTemplateVariables(); parameters.put("repoName", repositoryName); parameters.put("id", milestone.getNumber()); + if (forCurrentUser) { + parameters.put("assignee", properties.getUsername()); + + return operations.exchange(ISSUES_BY_MILESTONE_AND_ASSIGNEE_URI_TEMPLATE, HttpMethod.GET, + new HttpEntity<>(newUserScopedHttpHeaders()), ISSUES_TYPE, parameters).getBody(); + } + return operations.exchange(ISSUES_BY_MILESTONE_URI_TEMPLATE, HttpMethod.GET, - new HttpEntity<>(getAuthenticationHeaders()), ISSUES_TYPE, parameters).getBody(); + new HttpEntity<>(newUserScopedHttpHeaders()), ISSUES_TYPE, parameters).getBody(); } - private GitHubMilestone findMilestone(ModuleIteration module, String repositoryName) { + private GitHubIssue.Milestone getMilestone(ModuleIteration moduleIteration, String repositoryName) { + + Optional milestone = findMilestone(moduleIteration, repositoryName); + return milestone + .orElseThrow(() -> new IllegalStateException(String.format("No milestone for %s found containing %s!", // + moduleIteration.getProject().getFullName(), // + moduleIteration.getShortVersionString()))); + } + + @Cacheable("milestone") + protected Optional findMilestone(ModuleIteration moduleIteration, String repositoryName) { for (String state : Arrays.asList("close", "open")) { - Map parameters = new HashMap<>(); + Map parameters = newUrlTemplateVariables(); parameters.put("repoName", repositoryName); parameters.put("state", state); URI milestoneUri = new UriTemplate(MILESTONE_URI).expand(parameters); - logger.log(module, "Looking up milestone from %s…", milestoneUri); + logger.log(moduleIteration, "Looking up milestone from %s…", milestoneUri); - List exchange = operations.exchange(MILESTONE_URI, HttpMethod.GET, - new HttpEntity<>(getAuthenticationHeaders()), MILESTONES_TYPE, parameters).getBody(); + List milestones = operations.exchange(MILESTONE_URI, HttpMethod.GET, + new HttpEntity<>(newUserScopedHttpHeaders()), MILESTONES_TYPE, parameters).getBody(); - GitHubMilestone milestone = null; + Optional milestone = milestones.stream(). // + filter(m -> m.matchesIteration(moduleIteration)). // + findFirst(). // + map(m -> { + logger.log(moduleIteration, "Found milestone %s.", m); + return m; + }); - for (GitHubMilestone candidate : exchange) { - if (candidate.getTitle().contains(module.getShortVersionString())) { - milestone = candidate; - } - } - - if (milestone != null) { - logger.log(module, "Found milestone %s.", milestone); + if (milestone.isPresent()) { return milestone; } } - throw new IllegalStateException(String.format("No milestone found containing %s!", module.getShortVersionString())); + return Optional.empty(); } - private HttpHeaders getAuthenticationHeaders() { + private HttpHeaders newUserScopedHttpHeaders() { HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", properties.getHttpCredentials().toString()); @@ -210,6 +311,16 @@ class GitHubIssueTracker implements IssueTracker { return headers; } + private Map newUrlTemplateVariables() { + Map parameters = new HashMap<>(); + parameters.put("githubBaseUrl", properties.getGithubApiBaseUrl()); + return parameters; + } + + private Ticket toTicket(GitHubIssue issue) { + return new Ticket(issue.getId(), issue.getTitle(), new GithubTicketStatus(issue.getState())); + } + public static void main(String[] args) { try (ConfigurableApplicationContext context = new ClassPathXmlApplicationContext( diff --git a/src/main/java/org/springframework/data/release/jira/GitHubMilestone.java b/src/main/java/org/springframework/data/release/jira/GitHubMilestone.java index 7cee9a4..166b816 100644 --- a/src/main/java/org/springframework/data/release/jira/GitHubMilestone.java +++ b/src/main/java/org/springframework/data/release/jira/GitHubMilestone.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,14 +15,32 @@ */ package org.springframework.data.release.jira; -import lombok.Data; +import org.springframework.data.release.model.ModuleIteration; + +import lombok.Value; /** - * @author Oliver Gierke + * @author Mark Paluch */ -@Data -class GitHubMilestone { +@Value +class GithubMilestone { - private String title; - private Long number; + private ModuleIteration module; + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return module.getMediumVersionString(); + } + + public String getDescription() { + return module.getTrain().getName() + " " + module.getIteration().getName(); + } + + public GitHubIssue.Milestone toMilestone() { + return new GitHubIssue.Milestone(toString(), getDescription()); + } } diff --git a/src/main/java/org/springframework/data/release/jira/IssueTracker.java b/src/main/java/org/springframework/data/release/jira/IssueTracker.java index 38ffe16..58b311a 100644 --- a/src/main/java/org/springframework/data/release/jira/IssueTracker.java +++ b/src/main/java/org/springframework/data/release/jira/IssueTracker.java @@ -17,12 +17,16 @@ package org.springframework.data.release.jira; import java.util.Collection; +import org.springframework.data.release.model.Iteration; import org.springframework.data.release.model.ModuleIteration; import org.springframework.data.release.model.Project; +import org.springframework.data.release.model.Train; +import org.springframework.data.release.model.TrainIteration; import org.springframework.plugin.core.Plugin; /** * @author Oliver Gierke + * @author Mark Paluch */ public interface IssueTracker extends Plugin { @@ -31,6 +35,23 @@ public interface IssueTracker extends Plugin { */ void reset(); + /** + * Returns all {@link Tickets} for the given {@link Train} and {@link Iteration}. + * + * @param iteration must not be {@literal null}. + * @return + */ + Tickets getTicketsFor(TrainIteration iteration); + + /** + * Returns all {@link Tickets} for the given {@link Train} and {@link Iteration}. + * + * @param iteration must not be {@literal null}. + * @param forCurrentUser + * @return + */ + Tickets getTicketsFor(TrainIteration iteration, boolean forCurrentUser); + /** * Returns the {@link Ticket} that tracks modifications in the context of a release. * @@ -49,5 +70,21 @@ public interface IssueTracker extends Plugin { */ Collection findTickets(Project project, Collection ticketIds); + /** + * Creates a release version if release version is missing. + * + * @param moduleIteration must not be {@literal null}. + * @param credentials must not be {@literal null}. + */ + void createReleaseVersion(ModuleIteration moduleIteration); + + /** + * Create release ticket if release ticket is missing. + * + * @param iteration must not be {@literal null}. + * @param credentials must not be {@literal null}. + */ + void createReleaseTicket(ModuleIteration moduleIteration); + Changelog getChangelogFor(ModuleIteration iteration); } diff --git a/src/main/java/org/springframework/data/release/jira/IssueTrackerConfiguration.java b/src/main/java/org/springframework/data/release/jira/IssueTrackerConfiguration.java index 5b92b03..78f69b7 100644 --- a/src/main/java/org/springframework/data/release/jira/IssueTrackerConfiguration.java +++ b/src/main/java/org/springframework/data/release/jira/IssueTrackerConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ package org.springframework.data.release.jira; import java.util.ArrayList; import java.util.List; +import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.concurrent.ConcurrentMapCacheManager; @@ -32,11 +33,13 @@ import org.springframework.plugin.core.OrderAwarePluginRegistry; import org.springframework.plugin.core.PluginRegistry; import org.springframework.web.client.RestTemplate; +import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; /** * @author Oliver Gierke + * @author Mark Paluch */ @Configuration @EnableCaching @@ -57,6 +60,7 @@ class IssueTrackerConfiguration { ObjectMapper mapper = new ObjectMapper(); mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.setSerializationInclusion(Include.NON_NULL); return mapper; } @@ -77,8 +81,8 @@ class IssueTrackerConfiguration { } @Bean - public Jira jira(Logger logger) { - return new Jira(restTemplate(), logger); + public Jira jira(Logger logger, JiraProperties jiraProperties) { + return new Jira(restTemplate(), logger, jiraProperties); } @Bean diff --git a/src/main/java/org/springframework/data/release/jira/Jira.java b/src/main/java/org/springframework/data/release/jira/Jira.java index 13b93d4..4130ece 100644 --- a/src/main/java/org/springframework/data/release/jira/Jira.java +++ b/src/main/java/org/springframework/data/release/jira/Jira.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * Copyright 2013-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,18 +22,21 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; -import lombok.RequiredArgsConstructor; - import org.springframework.boot.SpringApplication; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.data.release.Application; +import org.springframework.data.release.jira.JiraIssue.Component; +import org.springframework.data.release.jira.JiraIssue.Fields; import org.springframework.data.release.model.Iteration; 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.ReleaseTrains; import org.springframework.data.release.model.Tracker; import org.springframework.data.release.model.TrainIteration; @@ -41,46 +44,57 @@ import org.springframework.data.release.utils.Logger; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; +import org.springframework.util.Assert; import org.springframework.web.client.RestOperations; import org.springframework.web.util.UriTemplate; +import lombok.RequiredArgsConstructor; + /** * @author Oliver Gierke + * @author Mark Paluch */ @RequiredArgsConstructor class Jira implements JiraConnector { - private static final String JIRA_HOST = "https://jira.spring.io"; - private static final String BASE_URI = "/rest/api/2"; - private static final String SEARCH_TEMPLATE = JIRA_HOST + BASE_URI - + "/search?jql={jql}&fields={fields}&startAt={startAt}"; + private static final String BASE_URI = "{jiraBaseUrl}/rest/api/2"; + private static final String CREATE_ISSUES_TEMPLATE = BASE_URI + "/issue"; + private static final String UPDATE_ISSUE_TEMPLATE = BASE_URI + "/issue/{ticketId}"; + private static final String PROJECT_VERSIONS_TEMPLATE = BASE_URI + "/project/{project}/version?startAt={startAt}"; + private static final String PROJECT_COMPONENTS_TEMPLATE = BASE_URI + "/project/{project}/components"; + private static final String VERSIONS_TEMPLATE = BASE_URI + "/version"; + private static final String SEARCH_TEMPLATE = BASE_URI + "/search?jql={jql}&fields={fields}&startAt={startAt}"; + + public static final String INFRASTRUCTURE_COMPONENT_NAME = "Infrastructure"; private final RestOperations operations; private final Logger logger; - /* + private final JiraProperties jiraProperties; + + /* * (non-Javadoc) - * @see org.springframework.data.release.jira.JiraConnector#flushTickets() + * @see org.springframework.data.release.jira.JiraConnector#reset() */ @Override - @CacheEvict(value = "tickets", allEntries = true) + @CacheEvict(value = { "release-ticket", "tickets", "changelogs", "release-version" }, allEntries = true) public void reset() {} - @Cacheable("release-tickets") - public Ticket getReleaseTicketFor(ModuleIteration iteration) { + /* + * (non-Javadoc) + * @see org.springframework.data.release.jira.IssueTracker#getReleaseTicketFor(org.springframework.data.release.model.ModuleIteration) + */ + @Override + @Cacheable("release-ticket") + public Ticket getReleaseTicketFor(ModuleIteration moduleIteration) { - JqlQuery query = JqlQuery.from(iteration).and("summary ~ \"Release\""); + JqlQuery query = JqlQuery.from(moduleIteration) + .and(String.format("summary ~ \"%s\"", Tracker.releaseTicketSummary(moduleIteration))); - Map parameters = new HashMap<>(); - parameters.put("jql", query); - parameters.put("fields", "summary,status,resolution"); - parameters.put("startAt", 0); - - JiraIssues issues = operations.exchange(SEARCH_TEMPLATE, HttpMethod.GET, null, JiraIssues.class, parameters) - .getBody(); + JiraIssues issues = getJiraIssues(query, new HttpHeaders(), 0); if (issues.getIssues().isEmpty()) { - throw new IllegalStateException(String.format("Did not find a release ticket for %s!", iteration)); + throw new IllegalArgumentException(String.format("Did not find a release ticket for %s!", moduleIteration)); } JiraIssue issue = issues.getIssues().get(0); @@ -88,9 +102,9 @@ class Jira implements JiraConnector { return toTicket(issue); } - /** + /* * (non-Javadoc) - * + * * @see org.springframework.data.release.jira.IssueTracker#findTickets(Project, Collection) */ @Override @@ -103,7 +117,7 @@ class Jira implements JiraConnector { JqlQuery query = JqlQuery.from(ticketIds).and(" resolution is not EMPTY"); - Map parameters = new HashMap<>(); + Map parameters = newUrlTemplateVariables(); parameters.put("jql", query); parameters.put("fields", "summary,status,resolution"); parameters.put("startAt", 0); @@ -116,57 +130,330 @@ class Jira implements JiraConnector { collect(Collectors.toList()); } - /* - * (non-Javadoc) - * @see org.springframework.data.release.jira.JiraConnector#getTicketsFor(org.springframework.data.release.model.Train, org.springframework.data.release.model.Iteration, org.springframework.data.release.jira.Credentials) - */ - @Override @Cacheable("tickets") - public Tickets getTicketsFor(TrainIteration iteration, Credentials credentials) { + @Override + public Tickets getTicketsFor(TrainIteration iteration) { + return getTicketsFor(iteration, false); + } - JqlQuery query = JqlQuery.from(iteration); + @Cacheable("tickets") + @Override + public Tickets getTicketsFor(TrainIteration trainIteration, boolean forCurrentUser) { + + JqlQuery query = JqlQuery + .from(trainIteration.stream().filter(moduleIteration -> supports(moduleIteration.getProject()))); + + HttpHeaders headers = newUserScopedHttpHeaders(); - HttpHeaders headers = new HttpHeaders(); - int startAt = 0; List tickets = new ArrayList<>(); - JiraIssues issues = null; - - if (credentials != null) { + if (forCurrentUser) { query = query.and("assignee = currentUser()"); - - headers.set("Authorization", String.format("Basic %s", credentials.asBase64())); - - logger.log(iteration, "Retrieving tickets (for user %s)…", credentials.getUsername()); + logger.log(trainIteration, "Retrieving tickets (for user %s)…", jiraProperties.getUsername()); } else { - logger.log(iteration, "Retrieving tickets…"); + logger.log(trainIteration, "Retrieving tickets…"); } query = query.orderBy("updatedDate DESC"); - do { + JiraIssues issues = execute(trainIteration.toString(), query, headers, jiraIssues -> { + jiraIssues.stream().// + filter(jiraIssue -> !jiraIssue.wasBackportedFrom(trainIteration.getTrain())). // + forEach(jiraIssue -> tickets.add(toTicket(jiraIssue))); + }); - Map parameters = new HashMap<>(); - parameters.put("jql", query); - parameters.put("fields", "summary,status,resolution,fixVersions"); - parameters.put("startAt", startAt); + return new Tickets(tickets, issues.getTotal()); + } - issues = operations - .exchange(SEARCH_TEMPLATE, HttpMethod.GET, new HttpEntity<>(headers), JiraIssues.class, parameters).getBody(); + @Cacheable("tickets") + public Tickets getTicketsFor(ModuleIteration moduleIteration) { - logger.log(iteration, "Got tickets %s to %s of %s.", startAt, issues.getNextStartAt(), issues.getTotal()); + JqlQuery query = JqlQuery.from(moduleIteration); - for (JiraIssue issue : issues) { - if (!issue.wasBackportedFrom(iteration.getTrain())) { - tickets.add(toTicket(issue)); - } + HttpHeaders headers = new HttpHeaders(); + + List tickets = new ArrayList<>(); + + logger.log(moduleIteration, "Retrieving tickets…"); + + query = query.orderBy("updatedDate DESC"); + + JiraIssues issues = execute(moduleIteration.toString(), query, headers, jiraIssues -> { + jiraIssues.stream().// + filter(jiraIssue -> !jiraIssue.wasBackportedFrom(moduleIteration.getTrain())). // + forEach(jiraIssue -> tickets.add(toTicket(jiraIssue))); + }); + + return new Tickets(tickets, issues.getTotal()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.release.jira.JiraConnector#createReleaseVersion(org.springframework.data.release.model.ModuleIteration, org.springframework.data.release.jira.Credentials) + */ + @Override + public void createReleaseVersion(ModuleIteration moduleIteration) { + + Assert.notNull(moduleIteration, "ModuleIteration must not be null."); + + Map parameters = newUrlTemplateVariables(); + HttpHeaders httpHeaders = newUserScopedHttpHeaders(); + + Optional versionsForModuleIteration = findJiraReleaseVersion(moduleIteration); + + if (versionsForModuleIteration.isPresent()) { + return; + } + + JiraVersion jiraVersion = new JiraVersion(moduleIteration); + logger.log(moduleIteration, "Creating Jira release version %s", jiraVersion); + + JiraReleaseVersion jiraReleaseVersion = new JiraReleaseVersion(moduleIteration, jiraVersion); + + operations.exchange(VERSIONS_TEMPLATE, HttpMethod.POST, new HttpEntity(jiraReleaseVersion, httpHeaders), + JiraReleaseVersion.class, parameters).getBody(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.release.jira.JiraConnector#findJiraReleaseVersion(org.springframework.data.release.model.ModuleIteration) + */ + @Cacheable("release-version") + public Optional findJiraReleaseVersion(ModuleIteration moduleIteration) { + + JiraVersion jiraVersion = new JiraVersion(moduleIteration); + HttpHeaders httpHeaders = new HttpHeaders(); + + List versionsForModuleIteration = new ArrayList<>(); + getReleaseVersions(moduleIteration.toString(), moduleIteration.getProjectKey(), httpHeaders, releaseVersions -> { + releaseVersions.stream(). // + filter(jiraReleaseVersion -> jiraReleaseVersion.hasSameNameAs(jiraVersion)). // + findFirst(). // + ifPresent(jiraReleaseVersion -> versionsForModuleIteration.add(jiraReleaseVersion)); + }); + + return versionsForModuleIteration.stream().findFirst(); + } + + @Override + public void createReleaseTicket(ModuleIteration moduleIteration) { + + Assert.notNull(moduleIteration, "ModuleIteration must not be null."); + + HttpHeaders httpHeaders = newUserScopedHttpHeaders(); + + Map parameters = newUrlTemplateVariables(); + + Tickets tickets = getTicketsFor(moduleIteration); + + if (tickets.hasReleaseTicket(moduleIteration)) { + return; + } + + Optional jiraReleaseVersion = findJiraReleaseVersion(moduleIteration); + jiraReleaseVersion.orElseThrow( + () -> new IllegalStateException(String.format("Did not find a release version for %s", moduleIteration))); + + JiraComponents jiraComponents = getJiraComponents(moduleIteration.getProjectKey()); + + logger.log(moduleIteration, "Creating release ticket…"); + + JiraIssue jiraIssue = prepareJiraIssueToCreate(moduleIteration, jiraComponents); + + operations.exchange(CREATE_ISSUES_TEMPLATE, HttpMethod.POST, new HttpEntity(jiraIssue, httpHeaders), + CreatedJiraIssue.class, parameters).getBody(); + } + + @Override + public void assignTicketToMe(Ticket ticket) { + + Assert.notNull(ticket, "Ticket must not be null."); + + HttpHeaders httpHeaders = newUserScopedHttpHeaders(); + + Map parameters = newUrlTemplateVariables(); + parameters.put("ticketId", ticket.getId()); + + JiraIssue jiraIssue = JiraIssue.create().assignTo(jiraProperties.getCredentials()); + + operations.exchange(UPDATE_ISSUE_TEMPLATE, HttpMethod.POST, new HttpEntity(jiraIssue, httpHeaders), + String.class, parameters).getBody(); + } + + private JiraIssue prepareJiraIssueToCreate(ModuleIteration moduleIteration, JiraComponents jiraComponents) { + + JiraIssue jiraIssue = JiraIssue.createTask(); + jiraIssue.project(moduleIteration.getProjectKey()).summary(Tracker.releaseTicketSummary(moduleIteration)) + .fixVersion(moduleIteration); + + Fields fields = jiraIssue.getFields(); + + Optional component = jiraComponents.findComponent(INFRASTRUCTURE_COMPONENT_NAME); + component.ifPresent( + jiraComponent -> fields.setComponents(Collections.singletonList(Component.from(jiraComponent.getName())))); + + return jiraIssue; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.release.jira.JiraConnector#verifyBeforeRelease(org.springframework.data.release.model.Train, org.springframework.data.release.model.Iteration) + */ + @Override + public void verifyBeforeRelease(TrainIteration trainIteration) { + + // for each module + for (ModuleIteration moduleIteration : trainIteration) { + Tickets tickets = getTicketsFor(moduleIteration); + + Ticket releaseTicket = tickets.getReleaseTicket(moduleIteration); + + if (releaseTicket.isResolved()) { + throw new IllegalStateException( + String.format("Release ticket %s for %s is resolved", releaseTicket, moduleIteration)); } + Tickets issueTickets = tickets.getIssueTickets(moduleIteration); + + List unresolvedTickets = issueTickets.stream().// + filter(ticket -> !ticket.isResolved()).// + collect(Collectors.toList()); + + if (!unresolvedTickets.isEmpty()) { + throw new IllegalStateException( + String.format("Unresolved tickets for %s: %s", moduleIteration, unresolvedTickets)); + } + } + } + + /* + * + * (non-Javadoc) + * @see org.springframework.data.release.jira.JiraConnector#closeIteration(org.springframework.data.release.model.Train, org.springframework.data.release.model.Iteration) + */ + @Override + public void closeIteration(TrainIteration iteration) { + + // for each module + + // - close all tickets + // -- make sure only one ticket is open + // -- resolve open ticket + // -- close tickets + + // - mark version as releases + // - if no next version exists, create + } + + /* + * (non-Javadoc) + * @see org.springframework.data.release.jira.JiraConnector#getChangelogFor(org.springframework.data.release.model.Module, org.springframework.data.release.model.Iteration) + */ + @Override + @Cacheable("changelogs") + public Changelog getChangelogFor(ModuleIteration moduleIteration) { + + Map parameters = newUrlTemplateVariables(); + parameters.put("jql", JqlQuery.from(moduleIteration)); + parameters.put("fields", "summary,status,resolution,fixVersions"); + parameters.put("startAt", 0); + + URI searchUri = new UriTemplate(SEARCH_TEMPLATE).expand(parameters); + + logger.log(moduleIteration, "Looking up JIRA issues from %s…", searchUri); + + JiraIssues issues = operations.getForObject(searchUri, JiraIssues.class); + Tickets tickets = issues.stream().map(this::toTicket).collect(Tickets.toTicketsCollector()); + logger.log(moduleIteration, "Created changelog with %s entries.", tickets.getOverallTotal()); + + return new Changelog(moduleIteration, tickets); + } + + /* + * (non-Javadoc) + * @see org.springframework.plugin.core.Plugin#supports(java.lang.Object) + */ + @Override + public boolean supports(Project project) { + return project.uses(Tracker.JIRA); + } + + @Cacheable("jira-components") + protected JiraComponents getJiraComponents(ProjectKey projectKey) { + + HttpHeaders headers = new HttpHeaders(); + Map parameters = newUrlTemplateVariables(); + parameters.put("project", projectKey.getKey()); + + List components = operations.exchange(PROJECT_COMPONENTS_TEMPLATE, HttpMethod.GET, + new HttpEntity<>(headers), new ParameterizedTypeReference>() {}, parameters).getBody(); + + return JiraComponents.of(components); + } + + private JiraIssues execute(String context, JqlQuery query, HttpHeaders headers, JiraIssuesCallback callback) { + + JiraIssues issues; + int startAt = 0; + + do { + issues = getJiraIssues(query, headers, startAt); + + logger.log(context, "Got tickets %s to %s of %s.", startAt, issues.getNextStartAt(), issues.getTotal()); + + callback.doWithJiraIssues(issues); + startAt = issues.getNextStartAt(); } while (issues.hasMoreResults()); - return new Tickets(Collections.unmodifiableList(tickets), issues.getTotal()); + return issues; + } + + private JiraIssues getJiraIssues(JqlQuery query, HttpHeaders headers, int startAt) { + + JiraIssues issues; + Map parameters = newUrlTemplateVariables(); + parameters.put("jql", query); + parameters.put("fields", "summary,status,resolution,fixVersions"); + parameters.put("startAt", startAt); + + issues = operations + .exchange(SEARCH_TEMPLATE, HttpMethod.GET, new HttpEntity<>(headers), JiraIssues.class, parameters).getBody(); + return issues; + } + + private JiraReleaseVersions getReleaseVersions(String context, ProjectKey projectKey, HttpHeaders headers, + JiraReleaseVersionsCallback callback) { + + JiraReleaseVersions releaseVersions; + int startAt = 0; + + do { + + releaseVersions = getJiraReleaseVersions(projectKey, headers, startAt); + + logger.log(context, "Got release versions %s to %s of %s.", startAt, releaseVersions.getNextStartAt(), + releaseVersions.getTotal()); + + callback.doWithJiraReleaseVersions(releaseVersions); + + startAt = releaseVersions.getNextStartAt(); + + } while (releaseVersions.hasMoreResults()); + + return releaseVersions; + } + + private JiraReleaseVersions getJiraReleaseVersions(ProjectKey projectKey, HttpHeaders headers, int startAt) { + + Map parameters = newUrlTemplateVariables(); + parameters.put("project", projectKey.getKey()); + parameters.put("fields", "summary,status,resolution,fixVersions"); + parameters.put("startAt", startAt); + + return operations.exchange(PROJECT_VERSIONS_TEMPLATE, HttpMethod.GET, new HttpEntity<>(headers), + JiraReleaseVersions.class, parameters).getBody(); } private Ticket toTicket(JiraIssue issue) { @@ -187,73 +474,16 @@ class Jira implements JiraConnector { return new Ticket(issue.getKey(), fields.getSummary(), jiraTicketStatus); } - /* - * (non-Javadoc) - * @see org.springframework.data.release.jira.JiraConnector#verifyBeforeRelease(org.springframework.data.release.model.Train, org.springframework.data.release.model.Iteration) - */ - @Override - public void verifyBeforeRelease(TrainIteration iteration) { - - // for each module - - // - make sure only one ticket is open - } - - /* + /** + * Returns new {@link HttpHeaders} with authentication headers. * - * (non-Javadoc) - * @see org.springframework.data.release.jira.JiraConnector#closeIteration(org.springframework.data.release.model.Train, org.springframework.data.release.model.Iteration, org.springframework.data.release.jira.Credentials) + * @return */ - @Override - public void closeIteration(TrainIteration iteration, Credentials credentials) { + private HttpHeaders newUserScopedHttpHeaders() { - // for each module - - // - close all tickets - // -- make sure only one ticket is open - // -- resolve open ticket - // -- close tickets - - // - mark version as releases - // - if no next version exists, create - } - - /* - * (non-Javadoc) - * @see org.springframework.data.release.jira.JiraConnector#getChangelogFor(org.springframework.data.release.model.Module, org.springframework.data.release.model.Iteration) - */ - @Override - @Cacheable("changelogs") - public Changelog getChangelogFor(ModuleIteration module) { - - Map parameters = new HashMap<>(); - parameters.put("jql", JqlQuery.from(module)); - parameters.put("fields", "summary,status,resolution,fixVersions"); - parameters.put("startAt", 0); - - URI searchUri = new UriTemplate(SEARCH_TEMPLATE).expand(parameters); - - logger.log(module, "Looking up JIRA issues from %s…", searchUri); - - JiraIssues issues = operations.getForObject(searchUri, JiraIssues.class); - List tickets = new ArrayList<>(); - - for (JiraIssue issue : issues) { - tickets.add(toTicket(issue)); - } - - logger.log(module, "Created changelog with %s entries.", tickets.size()); - - return new Changelog(module, new Tickets(tickets, tickets.size())); - } - - /* - * (non-Javadoc) - * @see org.springframework.plugin.core.Plugin#supports(java.lang.Object) - */ - @Override - public boolean supports(Project project) { - return project.uses(Tracker.JIRA); + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", String.format("Basic %s", jiraProperties.getCredentials().asBase64())); + return headers; } public static void main(String[] args) { @@ -270,4 +500,26 @@ class Jira implements JiraConnector { System.out.println(tracker.getReleaseTicketFor(module)); } } + + private Map newUrlTemplateVariables() { + Map parameters = new HashMap<>(); + parameters.put("jiraBaseUrl", jiraProperties.getUrl()); + return parameters; + } + + /** + * Callback for {@link JiraIssues}. + */ + interface JiraIssuesCallback { + + void doWithJiraIssues(JiraIssues jiraIssues); + } + + /** + * Callback for {@link JiraReleaseVersions}. + */ + interface JiraReleaseVersionsCallback { + + void doWithJiraReleaseVersions(JiraReleaseVersions releaseVersions); + } } diff --git a/src/main/java/org/springframework/data/release/jira/JiraComponent.java b/src/main/java/org/springframework/data/release/jira/JiraComponent.java new file mode 100644 index 0000000..d41fdf0 --- /dev/null +++ b/src/main/java/org/springframework/data/release/jira/JiraComponent.java @@ -0,0 +1,44 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.release.jira; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.Value; + +/* + * Value object to bind REST responses to. + * + * @author Mark Paluch + */ +@Value +class JiraComponent { + + private String id; + private String name; + + @JsonCreator + public JiraComponent(@JsonProperty("id") String id, @JsonProperty("name") String name) { + this.id = id; + this.name = name; + } + + public boolean hasName(String name) { + return this.name.equals(name); + } +} diff --git a/src/main/java/org/springframework/data/release/jira/JiraComponents.java b/src/main/java/org/springframework/data/release/jira/JiraComponents.java new file mode 100644 index 0000000..c3285b7 --- /dev/null +++ b/src/main/java/org/springframework/data/release/jira/JiraComponents.java @@ -0,0 +1,60 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.release.jira; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Optional; + +import org.springframework.util.Assert; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NonNull; + +/* + * Value object to represent a list of {@link JiraComponent}s. + * @author Mark Paluch + */ +@Data +@AllArgsConstructor(access = AccessLevel.PRIVATE) +class JiraComponents implements Iterable { + + @NonNull private Collection jiraComponents; + + /** + * Try to find a component by its {@code name}. + * + * @param name + * @return + */ + public Optional findComponent(String name) { + return jiraComponents.stream().filter(jiraComponent -> jiraComponent.hasName(name)).findFirst(); + } + + public static JiraComponents of(Collection jiraComponents) { + + Assert.notNull(jiraComponents, "JiraComponents must not be null!"); + return new JiraComponents(jiraComponents); + } + + @Override + public Iterator iterator() { + return jiraComponents.iterator(); + } +} diff --git a/src/main/java/org/springframework/data/release/jira/JiraConnector.java b/src/main/java/org/springframework/data/release/jira/JiraConnector.java index b24ef73..7137581 100644 --- a/src/main/java/org/springframework/data/release/jira/JiraConnector.java +++ b/src/main/java/org/springframework/data/release/jira/JiraConnector.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * Copyright 2013-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,27 +15,41 @@ */ package org.springframework.data.release.jira; -import org.springframework.data.release.model.Iteration; -import org.springframework.data.release.model.Train; +import java.util.Optional; + +import org.springframework.data.release.model.ModuleIteration; import org.springframework.data.release.model.TrainIteration; /** * @author Oliver Gierke + * @author Mark Paluch */ public interface JiraConnector extends IssueTracker { - void reset(); - /** - * Returns all {@link Tickets} for the given {@link Train} and {@link Iteration}. + * Verifies the state of all {@link Tickets} before releasing. In particular: Check whether the release ticket exists, + * check whether all other issue tickets are in a resolved state. * - * @param iteration must not be {@literal null}. - * @param credentials may be {@literal null}. - * @return + * @param iteration */ - Tickets getTicketsFor(TrainIteration iteration, Credentials credentials); - void verifyBeforeRelease(TrainIteration iteration); - void closeIteration(TrainIteration iteration, Credentials credentials); + void closeIteration(TrainIteration iteration); + + /** + * Lookup a Jira release version. + * + * @param moduleIteration must not be {@literal null}. + * @return + */ + Optional findJiraReleaseVersion(ModuleIteration moduleIteration); + + /** + * 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/src/main/java/org/springframework/data/release/jira/JiraIssue.java b/src/main/java/org/springframework/data/release/jira/JiraIssue.java index 7779932..9adfc4f 100644 --- a/src/main/java/org/springframework/data/release/jira/JiraIssue.java +++ b/src/main/java/org/springframework/data/release/jira/JiraIssue.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,18 +15,24 @@ */ package org.springframework.data.release.jira; +import java.util.Collections; import java.util.List; +import org.springframework.data.release.model.ModuleIteration; +import org.springframework.data.release.model.ProjectKey; +import org.springframework.data.release.model.Tracker; +import org.springframework.data.release.model.Train; +import org.springframework.util.Assert; + import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import org.springframework.data.release.model.Train; - /** * Value object to bind REST responses to. - * + * * @author Oliver Gierke + * @author Mark Paluch */ @Data class JiraIssue { @@ -37,23 +43,154 @@ class JiraIssue { /** * Returns whether the ticket is only fixed in the given {@link Train}. {@literal false} indicates the {@link Ticket} * has been resolved for - * + * * @param train * @return */ public boolean wasBackportedFrom(Train train) { List fixVersions = fields.getFixVersions(); - return !(fixVersions.size() == 1 && fixVersions.get(0).name.contains(train.getName())); + return fixVersions != null && wasBackportedFrom(train, fixVersions); + } + + private boolean wasBackportedFrom(Train train, List fixVersions) { + return fixVersions.size() > 1 && isPresent(train, fixVersions); + } + + private boolean isPresent(Train train, List fixVersions) { + return fixVersions.stream().// + filter(fixVersion -> fixVersion.isTrain(train)).// + findFirst().// + isPresent(); + } + + /** + * Returns whether the ticket is a release ticket. + * + * @param moduleIteration + * @return + */ + public boolean isReleaseTicket(ModuleIteration moduleIteration) { + + List fixVersions = fields.getFixVersions(); + return fixVersions.size() == 1 && isPresent(moduleIteration.getTrain(), fixVersions) + && fields.hasSummary(Tracker.releaseTicketSummary(moduleIteration)); + } + + /** + * Creates a new Jira issue value object. + * + * @return + */ + public static JiraIssue create() { + + JiraIssue jiraIssue = new JiraIssue(); + jiraIssue.setFields(new Fields()); + return jiraIssue; + } + + /** + * Creates a new Jira issue value object with a issue type of {@link IssueType#TASK}. + * + * @return + */ + public static JiraIssue createTask() { + JiraIssue jiraIssue = create(); + jiraIssue.getFields().setIssuetype(IssueType.TASK); + return jiraIssue; + } + + /** + * Assign to the user specified in {@link Credentials}. + * + * @param credentials must not be {@literal null}. + * @return + */ + public JiraIssue assignTo(Credentials credentials) { + return assignTo(credentials.getUsername()); + } + + /** + * Assign the ticket to {@code username}. + * + * @param username must not be empty and not {@literal null}. + * @return + */ + public JiraIssue assignTo(String username) { + + Assert.hasText(username, "Username must not be empty!"); + getFields().setAssignee(JiraUser.from(username)); + return this; + } + + /** + * Set the project to {@code projectKey}. + * + * @param projectKey must not be {@literal null}. + * @return + */ + public JiraIssue project(ProjectKey projectKey) { + + getFields().setProject(new Project(projectKey)); + return this; + } + + /** + * Set the issue summary. + * + * @param summary must not be empty and not {@literal null}. + */ + public JiraIssue summary(String summary) { + + Assert.hasText(summary, "Summary must not be empty!"); + getFields().setSummary(summary); + return this; + } + + /** + * Set the fix version. + * + * @param moduleIteration must not be {@literal null}. + * @return + */ + public JiraIssue fixVersion(ModuleIteration moduleIteration) { + + Assert.notNull(moduleIteration, "ModuleIteration must not be null!"); + getFields().setFixVersions(Collections.singletonList(new JiraVersion(moduleIteration).toFixVersions())); + + return this; } @Data static class Fields { + Project project; + IssueType issuetype; String summary; List fixVersions; + List components; Status status; Resolution resolution; + JiraUser assignee; + + public boolean hasSummary(String other) { + return summary.equals(other); + } + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + static class Component { + + String id; + String name; + + public static Component from(String name) { + + Assert.hasText(name, "Name must not be empty!"); + return new Component(null, name); + } } @Data @@ -61,6 +198,27 @@ class JiraIssue { @AllArgsConstructor static class FixVersions { + String id; + String name; + + /** + * Returns whether this {@link FixVersions} matches the given {@link Train}. + * + * @param train + * @return + */ + public boolean isTrain(Train train) { + return name.contains(train.getName()); + } + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + static class IssueType { + + public final static IssueType TASK = new IssueType("Task"); + String name; } @@ -73,6 +231,18 @@ class JiraIssue { StatusCategory statusCategory; } + @Data + @NoArgsConstructor + @AllArgsConstructor + static class Project { + + String key; + + public Project(ProjectKey projectKey) { + this.key = projectKey.getKey(); + } + } + @Data @NoArgsConstructor @AllArgsConstructor @@ -88,4 +258,17 @@ class JiraIssue { String key; } + + @Data + @NoArgsConstructor + @AllArgsConstructor + static class JiraUser { + + String name; + String displayName; + + static JiraUser from(String username) { + return new JiraUser(username, null); + } + } } diff --git a/src/main/java/org/springframework/data/release/jira/JiraIssues.java b/src/main/java/org/springframework/data/release/jira/JiraIssues.java index d8daf87..3413527 100644 --- a/src/main/java/org/springframework/data/release/jira/JiraIssues.java +++ b/src/main/java/org/springframework/data/release/jira/JiraIssues.java @@ -18,13 +18,15 @@ package org.springframework.data.release.jira; import java.util.Iterator; import java.util.List; +import org.springframework.data.release.Streamable; + import lombok.Data; /** * @author Oliver Gierke */ @Data -class JiraIssues implements Iterable { +class JiraIssues implements Streamable { private int startAt; private int maxResults; diff --git a/src/main/java/org/springframework/data/release/jira/JiraProperties.java b/src/main/java/org/springframework/data/release/jira/JiraProperties.java new file mode 100644 index 0000000..c4ac05c --- /dev/null +++ b/src/main/java/org/springframework/data/release/jira/JiraProperties.java @@ -0,0 +1,59 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.release.jira; + +import javax.annotation.PostConstruct; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +import lombok.AccessLevel; +import lombok.Data; +import lombok.Getter; + +/** + * Configurable properties for Git. + * + * @author Mark Paluch + */ +@Data +@Component +@ConfigurationProperties(prefix = "jira") +class JiraProperties { + + private @Getter(AccessLevel.PRIVATE) String password; + private String username, url; + + @PostConstruct + public void init() { + + Assert.hasText(username, "No Jira username (jira.username) configured!"); + Assert.hasText(password, "No Jira password (jira.password) configured!"); + Assert.hasText(url, "No Jira url (jira.url) configured!"); + } + + /** + * Returns the {@link Credentials} to be used. + * + * @return + */ + public Credentials getCredentials() { + return new Credentials(username, password); + } + +} diff --git a/src/main/java/org/springframework/data/release/jira/JiraReleaseVersion.java b/src/main/java/org/springframework/data/release/jira/JiraReleaseVersion.java new file mode 100644 index 0000000..7ce3942 --- /dev/null +++ b/src/main/java/org/springframework/data/release/jira/JiraReleaseVersion.java @@ -0,0 +1,62 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.release.jira; + +import lombok.Data; + +import org.springframework.data.release.model.ModuleIteration; +import org.springframework.util.Assert; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Value object to bind REST responses to. + * + * @author Mark Paluch + */ +@Data +public class JiraReleaseVersion { + + private String id; + private String name; + private String project; + private String description; + + @JsonCreator + public JiraReleaseVersion(@JsonProperty("id") String id, @JsonProperty("name") String name, + @JsonProperty("string") String project, @JsonProperty("description") String description) { + this.id = id; + this.name = name; + this.project = project; + this.description = description; + } + + public JiraReleaseVersion(ModuleIteration moduleIteration, JiraVersion jiraVersion) { + + Assert.notNull(moduleIteration, "ModuleIteration must not be null."); + Assert.notNull(jiraVersion, "JiraVersion must not be null."); + + project = moduleIteration.getProjectKey().getKey(); + name = jiraVersion.toString(); + description = jiraVersion.getDescription(); + } + + public boolean hasSameNameAs(JiraVersion jiraVersion) { + return getName().equals(jiraVersion.toString()); + } +} diff --git a/src/main/java/org/springframework/data/release/jira/JiraReleaseVersions.java b/src/main/java/org/springframework/data/release/jira/JiraReleaseVersions.java new file mode 100644 index 0000000..75b1c60 --- /dev/null +++ b/src/main/java/org/springframework/data/release/jira/JiraReleaseVersions.java @@ -0,0 +1,55 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.release.jira; + +import java.util.Iterator; +import java.util.List; + +import org.springframework.data.release.Streamable; + +import lombok.Data; + +/** + * Value object to bind REST responses to. + * + * @author Mark Paluch + */ +@Data +class JiraReleaseVersions implements Streamable { + + private int startAt; + private int maxResults; + private int total; + + private List values; + + public int getNextStartAt() { + return startAt + values.size(); + } + + public boolean hasMoreResults() { + return startAt + values.size() < total; + } + + /* + * (non-Javadoc) + * @see java.lang.Iterable#iterator() + */ + @Override + public Iterator iterator() { + return values.iterator(); + } +} diff --git a/src/main/java/org/springframework/data/release/jira/JiraVersion.java b/src/main/java/org/springframework/data/release/jira/JiraVersion.java index 3e08b41..99a761d 100644 --- a/src/main/java/org/springframework/data/release/jira/JiraVersion.java +++ b/src/main/java/org/springframework/data/release/jira/JiraVersion.java @@ -15,10 +15,11 @@ */ package org.springframework.data.release.jira; -import lombok.Value; - +import org.springframework.data.release.jira.JiraIssue.FixVersions; import org.springframework.data.release.model.ModuleIteration; +import lombok.Value; + /** * @author Oliver Gierke */ @@ -35,4 +36,12 @@ class JiraVersion { public String toString() { return module.getMediumVersionString(); } + + public String getDescription() { + return module.getTrain().getName() + " " + module.getIteration().getName(); + } + + public FixVersions toFixVersions() { + return new FixVersions(null, toString()); + } } diff --git a/src/main/java/org/springframework/data/release/jira/JqlQuery.java b/src/main/java/org/springframework/data/release/jira/JqlQuery.java index bef4f93..40d0b26 100644 --- a/src/main/java/org/springframework/data/release/jira/JqlQuery.java +++ b/src/main/java/org/springframework/data/release/jira/JqlQuery.java @@ -15,27 +15,25 @@ */ package org.springframework.data.release.jira; -import static org.springframework.data.release.model.Projects.*; - -import lombok.Value; - import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.StringJoiner; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.springframework.data.release.model.ModuleIteration; -import org.springframework.data.release.model.TrainIteration; import org.springframework.util.StringUtils; +import lombok.Value; + /** * @author Oliver Gierke + * @author Mark Paluch */ @Value class JqlQuery { - private static final String PROJECT_VERSION_TEMPLATE = "project = %s AND fixVersion = \"%s\""; + private static final String PROJECT_VERSION_TEMPLATE = "(project = %s AND fixVersion = \"%s\" )"; private static final String ISSUE_KEY_IN_TEMPLATE = "issueKey in (%s)"; private final String query; @@ -62,15 +60,14 @@ class JqlQuery { return new JqlQuery(String.format(ISSUE_KEY_IN_TEMPLATE, joinedTicketIds)); } - public static JqlQuery from(TrainIteration iteration) { + public static JqlQuery from(Stream stream) { List parts = new ArrayList<>(); - for (ModuleIteration module : iteration.getModulesExcept(BUILD)) { - - JiraVersion version = new JiraVersion(module); - parts.add(String.format(PROJECT_VERSION_TEMPLATE, module.getProjectKey(), version)); - } + stream.forEach(moduleIteration -> { + JiraVersion version = new JiraVersion(moduleIteration); + parts.add(String.format(PROJECT_VERSION_TEMPLATE, moduleIteration.getProjectKey(), version)); + }); return new JqlQuery(StringUtils.collectionToDelimitedString(parts, " OR ")); } diff --git a/src/main/java/org/springframework/data/release/jira/Ticket.java b/src/main/java/org/springframework/data/release/jira/Ticket.java index ec8a99e..ed15a72 100644 --- a/src/main/java/org/springframework/data/release/jira/Ticket.java +++ b/src/main/java/org/springframework/data/release/jira/Ticket.java @@ -15,6 +15,10 @@ */ package org.springframework.data.release.jira; +import org.springframework.data.release.model.ModuleIteration; +import org.springframework.data.release.model.Tracker; +import org.springframework.data.release.model.TrainIteration; + import lombok.Value; /** @@ -37,4 +41,16 @@ public class Ticket { public String toString() { return String.format("%13s - %s", id, summary); } + + public boolean isResolved() { + return ticketStatus.isResolved(); + } + + public boolean isReleaseTicketFor(ModuleIteration moduleIteration) { + return summary.equals(Tracker.releaseTicketSummary(moduleIteration)); + } + + public boolean isReleaseTicketFor(TrainIteration iteration) { + return iteration.stream().filter(this::isReleaseTicketFor).findFirst().isPresent(); + } } diff --git a/src/main/java/org/springframework/data/release/jira/TicketBranches.java b/src/main/java/org/springframework/data/release/jira/TicketBranches.java index eb51fa9..36c8fe0 100644 --- a/src/main/java/org/springframework/data/release/jira/TicketBranches.java +++ b/src/main/java/org/springframework/data/release/jira/TicketBranches.java @@ -15,10 +15,6 @@ */ package org.springframework.data.release.jira; -import lombok.EqualsAndHashCode; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; - import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; @@ -29,6 +25,10 @@ import org.springframework.data.release.Streamable; import org.springframework.data.release.git.Branch; import org.springframework.util.Assert; +import lombok.EqualsAndHashCode; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + /** * Value object to represent a collection of {@link Branch}es with assigned tickets. * diff --git a/src/main/java/org/springframework/data/release/jira/Tickets.java b/src/main/java/org/springframework/data/release/jira/Tickets.java index c7d6160..8c1d746 100644 --- a/src/main/java/org/springframework/data/release/jira/Tickets.java +++ b/src/main/java/org/springframework/data/release/jira/Tickets.java @@ -15,31 +15,45 @@ */ package org.springframework.data.release.jira; +import java.util.ArrayList; +import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collector; +import java.util.stream.Stream; + +import org.springframework.data.release.Streamable; +import org.springframework.data.release.model.ModuleIteration; +import org.springframework.data.release.model.TrainIteration; +import org.springframework.util.StringUtils; import lombok.RequiredArgsConstructor; import lombok.Value; -import org.springframework.util.StringUtils; - /** * Value object to represent a list of {@link Ticket}s. - * + * * @author Oliver Gierke + * @author Mark Paluch */ @Value @RequiredArgsConstructor -public class Tickets implements Iterable { +public class Tickets implements Streamable { private final List tickets; private final int overallTotal; public Tickets(List tickets) { - this(tickets, tickets.size()); + this(Collections.unmodifiableList(tickets), tickets.size()); } - /* + /* * (non-Javadoc) * @see java.lang.Iterable#iterator() */ @@ -48,6 +62,37 @@ public class Tickets implements Iterable { return tickets.iterator(); } + public boolean hasReleaseTicket(ModuleIteration moduleIteration) { + return findReleaseTicket(moduleIteration).isPresent(); + } + + public Ticket getReleaseTicket(ModuleIteration moduleIteration) { + + Optional releaseTicket = findReleaseTicket(moduleIteration); + + return releaseTicket.orElseThrow( + () -> new IllegalStateException(String.format("Did not find a release ticket for %s!", moduleIteration))); + } + + public Tickets getIssueTickets(ModuleIteration moduleIteration) { + return tickets.stream(). // + filter(ticket -> !ticket.isReleaseTicketFor(moduleIteration)).// + collect(toTicketsCollector()); + } + + public Tickets getReleaseTickets(TrainIteration iteration) { + + return stream().// + filter(ticket -> ticket.isReleaseTicketFor(iteration)).// + collect(toTicketsCollector()); + } + + private Optional findReleaseTicket(ModuleIteration moduleIteration) { + return stream().// + filter(ticket -> ticket.isReleaseTicketFor(moduleIteration)).// + findFirst(); + } + @Override public String toString() { @@ -58,4 +103,43 @@ public class Tickets implements Iterable { return builder.toString(); } + + /** + * Returns a new collector to toTicketsCollector {@link Ticket} as {@link Tickets} using the {@link Stream} API. + * + * @return + */ + public static Collector toTicketsCollector() { + + return new Collector, Tickets>() { + @Override + public Supplier> supplier() { + return ArrayList::new; + } + + @Override + public BiConsumer, Ticket> accumulator() { + return List::add; + } + + @Override + public BinaryOperator> combiner() { + return (left, right) -> { + left.addAll(right); + return left; + }; + } + + @Override + public Function, Tickets> finisher() { + return tickets -> new Tickets(tickets); + } + + @Override + public Set characteristics() { + return Collections.emptySet(); + } + + }; + } } diff --git a/src/main/java/org/springframework/data/release/model/Tracker.java b/src/main/java/org/springframework/data/release/model/Tracker.java index afc779d..bf26dbd 100644 --- a/src/main/java/org/springframework/data/release/model/Tracker.java +++ b/src/main/java/org/springframework/data/release/model/Tracker.java @@ -21,6 +21,7 @@ import lombok.RequiredArgsConstructor; /** * @author Oliver Gierke + * @author Mark Paluch */ @Getter @RequiredArgsConstructor(access = AccessLevel.PACKAGE) @@ -30,4 +31,8 @@ public enum Tracker { GITHUB("((#)?\\d+)"); private final String ticketPattern; + + public static final String releaseTicketSummary(ModuleIteration moduleIteration) { + return "Release " + moduleIteration.getMediumVersionString(); + } } diff --git a/src/test/java/org/springframework/data/release/jira/GitHubIssueTrackerIntegrationTests.java b/src/test/java/org/springframework/data/release/jira/GitHubIssueTrackerIntegrationTests.java index c1a88ec..f4167e7 100644 --- a/src/test/java/org/springframework/data/release/jira/GitHubIssueTrackerIntegrationTests.java +++ b/src/test/java/org/springframework/data/release/jira/GitHubIssueTrackerIntegrationTests.java @@ -16,28 +16,223 @@ package org.springframework.data.release.jira; -import static org.hamcrest.collection.IsCollectionWithSize.hasSize; -import static org.junit.Assert.assertThat; +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.*; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; import java.util.Arrays; import java.util.Collection; +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.release.AbstractIntegrationTests; +import org.springframework.data.release.model.Iteration; +import org.springframework.data.release.model.ModuleIteration; import org.springframework.data.release.model.Projects; +import org.springframework.data.release.model.ReleaseTrains; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder; +import com.github.tomakehurst.wiremock.common.ClasspathFileSource; +import com.github.tomakehurst.wiremock.junit.WireMockRule; /** + * Integration Tests for {@link GitHubIssueTracker} using a local {@link WireMockRule} server. + * * @author Mark Paluch */ public class GitHubIssueTrackerIntegrationTests extends AbstractIntegrationTests { - @Autowired GitHubConnector gitHubIssueTracker; + public static final String ISSUES_URI = "/repos/spring-projects/spring-data-build/issues"; + public static final String MILESTONES_URI = "/repos/spring-projects/spring-data-build/milestones"; + public static final ModuleIteration BUILD_HOPPER_RC1 = ReleaseTrains.HOPPER.getModuleIteration(Iteration.RC1, + "Build"); + @Rule public WireMockRule mockService = new WireMockRule( + wireMockConfig().port(8888).fileSource(new ClasspathFileSource("integration/github"))); + + @Rule public ExpectedException expectedException = ExpectedException.none(); + + @Autowired IssueTracker github; + + @Before + public void before() throws Exception { + github.reset(); + } + + /** + * @see #5 + */ @Test - public void getTickets() throws Exception { + public void findTicketsByTicketIds() throws Exception { + + mockGetIssueWith("issue.json", 233); + + Collection tickets = github.findTickets(Projects.BUILD, Arrays.asList("233")); + assertThat(tickets, hasSize(1)); + } + + /** + * @see #5 + */ + @Test + public void ignoresUnknownTicketsByTicketId() throws Exception { + + Collection tickets = github.findTickets(Projects.BUILD, Arrays.asList("123")); + assertThat(tickets, hasSize(0)); + } + + /** + * @see #5 + */ + @Test + public void emptyResultWithEmptyTicketIds() throws Exception { + + Collection tickets = github.findTickets(Projects.COMMONS, Arrays.asList()); + assertThat(tickets, hasSize(0)); + } + + /** + * @see #5 + */ + @Test + public void getReleaseTicketForReturnsTheReleaseTicket() throws Exception { + + mockGetMilestonesWith("milestones.json"); + mockGetIssuesWith("issues.json"); + + Ticket releaseTicket = github.getReleaseTicketFor(BUILD_HOPPER_RC1); + assertThat(releaseTicket.getId(), is(Matchers.equalTo("#233"))); + } + + /** + * @see #5 + */ + @Test + public void noReleaseTicketFound() throws Exception { + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("No milestone for Spring Data Build found containing 1.8 RC1!"); + + mockGetMilestonesWith("emptyMilestones.json"); + + github.getReleaseTicketFor(BUILD_HOPPER_RC1); + + fail("Missing IllegalStateException"); + } + + /** + * @see #5 + */ + @Test + public void createReleaseVersionShouldCreateAVersion() throws Exception { + + mockGetMilestonesWith("emptyMilestones.json"); + mockCreateMilestoneWith("milestone.json"); + + github.createReleaseVersion(BUILD_HOPPER_RC1); + + verify(postRequestedFor(urlPathMatching(MILESTONES_URI)) + .withRequestBody(equalToJson("{\"title\":\"1.8 RC1 (Hopper)\", \"description\":\"Hopper RC1\"}"))); + } + + /** + * @see #5 + */ + @Test + public void createReleaseVersionShouldFindExistingReleaseVersion() throws Exception { + + mockGetMilestonesWith("milestones.json"); + + github.createReleaseVersion(BUILD_HOPPER_RC1); + + verify(0, postRequestedFor(urlPathMatching(MILESTONES_URI))); + } + + /** + * @see #5 + */ + @Test + public void createReleaseTicketShouldCreateReleaseTicket() throws Exception { + + mockGetMilestonesWith("milestones.json"); + mockGetIssuesWith("emptyIssues.json"); + mockCreateIssueWith("issue.json"); + + github.createReleaseTicket(BUILD_HOPPER_RC1); + + verify(postRequestedFor(urlPathMatching(ISSUES_URI)) + .withRequestBody(equalToJson("{\"title\":\"Release 1.8 RC1 (Hopper)\",\"milestone\":45}"))); + } + + /** + * @see #5 + */ + @Test + public void createReleaseTicketShouldFailWithNoReleaseVersion() throws Exception { + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("No milestone for Spring Data Build found containing 1.8 RC1!"); + + ModuleIteration moduleIteration = ReleaseTrains.HOPPER.getModuleIteration(Iteration.RC1, "Build"); + + mockGetIssuesWith("emptyIssues.json"); + mockGetMilestonesWith("emptyMilestones.json"); + + github.createReleaseTicket(moduleIteration); + + fail("Missing IllegalStateException"); + } + + /** + * @see #5 + */ + @Test + public void createReleaseTicketShouldFindExistingTicket() throws Exception { + + mockGetMilestonesWith("milestones.json"); + mockGetIssuesWith("issues.json"); + + github.createReleaseTicket(BUILD_HOPPER_RC1); + + verify(0, postRequestedFor(urlPathMatching(MILESTONES_URI))); + } + + private void mockGetIssueWith(String fromClassPath, int issueId) { + mockService.stubFor(get(urlPathMatching(ISSUES_URI + "/" + issueId)).// + willReturn(json(fromClassPath))); + } + + private void mockGetIssuesWith(String fromClassPath) { + mockService.stubFor(get(urlPathMatching(ISSUES_URI)).// + willReturn(json(fromClassPath))); + } + + private void mockCreateIssueWith(String fromClassPath) { + mockService.stubFor(post(urlPathMatching(ISSUES_URI)).// + willReturn(json(fromClassPath))); + } + + private void mockGetMilestonesWith(String fromClassPath) { + mockService.stubFor(get(urlPathMatching(MILESTONES_URI)).// + willReturn(json(fromClassPath))); + } + + private void mockCreateMilestoneWith(String fromClassPath) { + mockService.stubFor(post(urlPathMatching(MILESTONES_URI)).// + willReturn(json(fromClassPath))); + } + + private ResponseDefinitionBuilder json(String fromClassPathFile) { + return aResponse().// + withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).// + withBodyFile(fromClassPathFile); - Collection tickets = gitHubIssueTracker.findTickets(Projects.BUILD, Arrays.asList("1", "2", "-1")); - assertThat(tickets, hasSize(2)); } } diff --git a/src/test/java/org/springframework/data/release/jira/GithubMilestoneUnitTests.java b/src/test/java/org/springframework/data/release/jira/GithubMilestoneUnitTests.java new file mode 100644 index 0000000..4260219 --- /dev/null +++ b/src/test/java/org/springframework/data/release/jira/GithubMilestoneUnitTests.java @@ -0,0 +1,80 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.release.jira; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.data.release.model.Iteration; +import org.springframework.data.release.model.ModuleIteration; +import org.springframework.data.release.model.ReleaseTrains; + +/** + * Unit tests for {@link GithubMilestone}. + * + * @author Mark Paluch + */ +public class GithubMilestoneUnitTests { + + @Test + public void rendersGithubGaVersionCorrectly() { + + assertIterationVersion(Iteration.M1, "1.8 M1 (Dijkstra)"); + assertIterationVersion(Iteration.RC1, "1.8 RC1 (Dijkstra)"); + assertIterationVersion(Iteration.GA, "1.8 GA (Dijkstra)"); + + assertIterationVersion(Iteration.SR1, "1.8.1 (Dijkstra SR1)"); + assertIterationVersion(Iteration.SR2, "1.8.2 (Dijkstra SR2)"); + assertIterationVersion(Iteration.SR3, "1.8.3 (Dijkstra SR3)"); + assertIterationVersion(Iteration.SR4, "1.8.4 (Dijkstra SR4)"); + } + + @Test + public void usesCustomModuleIterationStartVersion() { + + ModuleIteration module = ReleaseTrains.DIJKSTRA.getModuleIteration(Iteration.M1, "Elasticsearch"); + + GithubMilestone version = new GithubMilestone(module); + assertThat(version.toString(), is("1.0 M1 (Dijkstra)")); + } + + @Test + public void doesNotUseCustomIterationOnNonFirstiterations() { + + ModuleIteration module = ReleaseTrains.DIJKSTRA.getModuleIteration(Iteration.RC1, "Elasticsearch"); + + GithubMilestone version = new GithubMilestone(module); + assertThat(version.toString(), is("1.0 RC1 (Dijkstra)")); + } + + @Test + public void rendersDescriptionCorrectly() { + + ModuleIteration module = ReleaseTrains.DIJKSTRA.getModuleIteration(Iteration.M1, "Elasticsearch"); + + GithubMilestone version = new GithubMilestone(module); + assertThat(version.getDescription(), is("Dijkstra M2")); + } + + private void assertIterationVersion(Iteration iteration, String expected) { + + ModuleIteration module = ReleaseTrains.DIJKSTRA.getModuleIteration(iteration, "Commons"); + + GithubMilestone version = new GithubMilestone(module); + assertThat(version.toString(), is(expected)); + } +} diff --git a/src/test/java/org/springframework/data/release/jira/JiraComponentsUnitTests.java b/src/test/java/org/springframework/data/release/jira/JiraComponentsUnitTests.java new file mode 100644 index 0000000..edf5092 --- /dev/null +++ b/src/test/java/org/springframework/data/release/jira/JiraComponentsUnitTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.release.jira; + +import static org.hamcrest.core.Is.*; +import static org.junit.Assert.*; + +import java.util.Collections; + +import org.junit.Test; + +/** + * Unit tests for {@link JiraComponents}. + * + * @author Mark Paluch + */ +public class JiraComponentsUnitTests { + + /** + * @see #5 + */ + @Test + public void returnsComponentByName() throws Exception { + + JiraComponent fooComponent = new JiraComponent("123", "foo"); + JiraComponents jiraComponents = JiraComponents.of(Collections.singleton(fooComponent)); + + assertThat(jiraComponents.findComponent("foo").isPresent(), is(true)); + } + + /** + * @see #5 + */ + @Test + public void returnsEmptyIfComponentMissing() throws Exception { + + JiraComponent fooComponent = new JiraComponent("123", "foo"); + JiraComponents jiraComponents = JiraComponents.of(Collections.singleton(fooComponent)); + + assertThat(jiraComponents.findComponent("baz").isPresent(), is(false)); + } + + /** + * @see #5 + */ + @Test(expected = IllegalArgumentException.class) + public void failsOnNullArgumentConstruction() throws Exception { + + JiraComponents.of(null); + + fail("Missing IllegalArgumentException"); + } +} diff --git a/src/test/java/org/springframework/data/release/jira/JiraIntegrationTests.java b/src/test/java/org/springframework/data/release/jira/JiraIntegrationTests.java index 9a83915..6b55437 100644 --- a/src/test/java/org/springframework/data/release/jira/JiraIntegrationTests.java +++ b/src/test/java/org/springframework/data/release/jira/JiraIntegrationTests.java @@ -16,42 +16,292 @@ package org.springframework.data.release.jira; -import static org.hamcrest.Matchers.hasSize; -import static org.junit.Assert.assertThat; +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.*; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; import java.util.Arrays; import java.util.Collection; +import java.util.Optional; +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.release.AbstractIntegrationTests; +import org.springframework.data.release.model.Iteration; +import org.springframework.data.release.model.ModuleIteration; +import org.springframework.data.release.model.ProjectKey; import org.springframework.data.release.model.Projects; +import org.springframework.data.release.model.ReleaseTrains; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder; +import com.github.tomakehurst.wiremock.common.ClasspathFileSource; +import com.github.tomakehurst.wiremock.junit.WireMockRule; /** + * Integration Tests for {@link Jira} using a local {@link WireMockRule} server. + * * @author Mark Paluch */ public class JiraIntegrationTests extends AbstractIntegrationTests { + public static final String CREATE_ISSUE_URI = "/rest/api/2/issue"; + public static final String CREATE_VERSION_URI = "/rest/api/2/version"; + public static final String SEARCH_URI = "/rest/api/2/search"; + public static final String PROJECT_VERSION_URI = "/rest/api/2/project/%s/version"; + public static final String PROJECT_COMPONENTS_URI = "/rest/api/2/project/%s/components"; + public static final ModuleIteration REST_HOPPER_RC1 = ReleaseTrains.HOPPER.getModuleIteration(Iteration.RC1, "REST"); + + @Rule public WireMockRule mockService = new WireMockRule( + wireMockConfig().port(8888).fileSource(new ClasspathFileSource("integration/jira"))); + + @Rule public ExpectedException expectedException = ExpectedException.none(); + @Autowired JiraConnector jira; + @Before + public void before() throws Exception { + jira.reset(); + } + + /** + * @see #5 + */ @Test public void findResolvedTicketsByTicketIds() throws Exception { + mockSearchWith("DATAREDIS-1andDATAJPA-1.json"); + Collection tickets = jira.findTickets(Projects.COMMONS, Arrays.asList("DATAREDIS-1", "DATAJPA-1")); assertThat(tickets, hasSize(2)); } + /** + * @see #5 + */ @Test public void ignoresUnknownTicketsByTicketId() throws Exception { + mockSearchWith("emptyTickets.json"); + Collection tickets = jira.findTickets(Projects.COMMONS, Arrays.asList("XYZ-1", "UNKOWN-1")); assertThat(tickets, hasSize(0)); } + /** + * @see #5 + */ @Test public void emptyResultWithEmptyTicketIds() throws Exception { Collection tickets = jira.findTickets(Projects.COMMONS, Arrays.asList()); assertThat(tickets, hasSize(0)); } + + /** + * @see #5 + */ + @Test + public void getReleaseTicketForReturnsTheReleaseTicket() throws Exception { + + mockSearchWith("releaseTickets.json"); + + Ticket releaseTicket = jira.getReleaseTicketFor(REST_HOPPER_RC1); + assertThat(releaseTicket.getId(), is(Matchers.equalTo("DATAREST-782"))); + } + + /** + * @see #5 + */ + @Test + public void noReleaseTicketFound() throws Exception { + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Did not find a release ticket for Spring Data REST 2.5 RC1"); + + mockSearchWith("emptyTickets.json"); + + jira.getReleaseTicketFor(REST_HOPPER_RC1); + + fail("Missing IllegalStateException"); + } + + /** + * @see #5 + */ + @Test + public void getReleaseVersion() throws Exception { + + mockGetProjectVersionsWith("releaseVersions.json", REST_HOPPER_RC1.getProjectKey()); + + Optional optional = jira.findJiraReleaseVersion(REST_HOPPER_RC1); + + assertThat(optional.isPresent(), is(true)); + assertThat(optional.get().getName(), is(Matchers.equalTo("2.5 RC1 (Hopper)"))); + } + + /** + * @see #5 + */ + @Test + public void createReleaseVersionShouldCreateAVersion() throws Exception { + + mockGetProjectVersionsWith("emptyReleaseVersions.json", REST_HOPPER_RC1.getProjectKey()); + mockCreateVersionWith("versionCreated.json"); + + jira.createReleaseVersion(REST_HOPPER_RC1); + + verify(postRequestedFor(urlPathMatching(CREATE_VERSION_URI)).withRequestBody( + equalToJson("{\"name\":\"2.5 RC1 (Hopper)\",\"project\":\"DATAREST\",\"description\":\"Hopper RC1\"}"))); + } + + /** + * @see #5 + */ + @Test + public void createReleaseVersionShouldFindExistingReleaseVersion() throws Exception { + + ModuleIteration moduleIteration = ReleaseTrains.HOPPER.getModuleIteration(Iteration.RC1, "REST"); + + mockGetProjectVersionsWith("releaseVersions.json", moduleIteration.getProjectKey()); + + jira.createReleaseVersion(moduleIteration); + + verify(0, postRequestedFor(urlPathMatching(CREATE_VERSION_URI))); + } + + /** + * @see #5 + */ + @Test + public void createReleaseTicketShouldCreateReleaseTicket() throws Exception { + + ModuleIteration moduleIteration = ReleaseTrains.HOPPER.getModuleIteration(Iteration.RC1, "REST"); + + mockGetProjectVersionsWith("releaseVersions.json", moduleIteration.getProjectKey()); + mockGetProjectComponentsWith("projectComponents.json", moduleIteration.getProjectKey()); + mockSearchWith("emptyTickets.json"); + prepareCreateIssueAndReturn("issueCreated.json"); + + jira.createReleaseTicket(moduleIteration); + + verify(postRequestedFor(urlPathMatching(CREATE_ISSUE_URI)) + .withRequestBody(equalToJson("{\"fields\":{\"project\":{\"key\":\"DATAREST\"}," + + "\"issuetype\":{\"name\":\"Task\"},\"summary\":\"Release 2.5 RC1 (Hopper)\"," + + "\"fixVersions\":[{\"name\":\"2.5 RC1 (Hopper)\"}]," + + "\"components\":[{\"name\":\"Infrastructure\"}]}}"))); + + } + + /** + * @see #5 + */ + @Test + public void createReleaseTicketShouldCreateReleaseTicketWithoutComponent() throws Exception { + + ModuleIteration moduleIteration = ReleaseTrains.HOPPER.getModuleIteration(Iteration.RC1, "REST"); + + mockGetProjectVersionsWith("releaseVersions.json", moduleIteration.getProjectKey()); + mockGetProjectComponentsWith("emptyProjectComponents.json", moduleIteration.getProjectKey()); + mockSearchWith("emptyTickets.json"); + prepareCreateIssueAndReturn("issueCreated.json"); + + jira.createReleaseTicket(moduleIteration); + + verify(postRequestedFor(urlPathMatching(CREATE_ISSUE_URI)) + .withRequestBody(equalToJson("{\"fields\":{\"project\":{\"key\":\"DATAREST\"}," + + "\"issuetype\":{\"name\":\"Task\"},\"summary\":\"Release 2.5 RC1 (Hopper)\"," + + "\"fixVersions\":[{\"name\":\"2.5 RC1 (Hopper)\"}]}}"))); + + } + + /** + * @see #5 + */ + @Test + public void createReleaseTicketShouldFailWithNoReleaseVersion() throws Exception { + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("Did not find a release version for Spring Data REST 2.5 RC1"); + + ModuleIteration moduleIteration = ReleaseTrains.HOPPER.getModuleIteration(Iteration.RC1, "REST"); + + mockSearchWith("emptyTickets.json"); + mockGetProjectVersionsWith("emptyReleaseVersions.json", moduleIteration.getProjectKey()); + + jira.createReleaseTicket(moduleIteration); + + fail("Missing IllegalStateException"); + } + + /** + * @see #5 + */ + @Test + public void createReleaseTicketShouldFindExistingTicket() throws Exception { + + ModuleIteration moduleIteration = ReleaseTrains.HOPPER.getModuleIteration(Iteration.RC1, "REST"); + + mockGetProjectVersionsWith("releaseVersions.json", moduleIteration.getProjectKey()); + mockGetProjectComponentsWith("projectComponents.json", moduleIteration.getProjectKey()); + mockSearchWith("releaseTickets.json"); + + jira.createReleaseTicket(moduleIteration); + + verify(0, postRequestedFor(urlPathMatching(CREATE_ISSUE_URI))); + } + + /** + * @see #5 + */ + @Test + public void assignTicketToMe() throws Exception { + + mockService.stubFor(post(urlPathMatching("/rest/api/2/issue/DATAREDIS-99999")).// + willReturn(aResponse().withStatus(204))); + + jira.assignTicketToMe(new Ticket("DATAREDIS-99999", "", null)); + + verify(postRequestedFor(urlPathMatching("/rest/api/2/issue/DATAREDIS-99999")) + .withRequestBody(equalToJson("{\"fields\":{\"assignee\":{\"name\":\"dummy\"}}}"))); + } + + private void mockSearchWith(String fromClassPath) { + mockService.stubFor(get(urlPathMatching(SEARCH_URI)).// + willReturn(json(fromClassPath))); + } + + private void mockGetProjectVersionsWith(String fromClassPath, ProjectKey projectKey) { + mockService.stubFor(get(urlPathMatching(String.format(PROJECT_VERSION_URI, projectKey))).// + willReturn(json(fromClassPath))); + } + + private void mockGetProjectComponentsWith(String fromClassPath, ProjectKey projectKey) { + mockService.stubFor(get(urlPathMatching(String.format(PROJECT_COMPONENTS_URI, projectKey))).// + willReturn(json(fromClassPath))); + } + + private void prepareCreateIssueAndReturn(String fromClassPath) { + mockService.stubFor(post(urlPathMatching(CREATE_ISSUE_URI)).// + willReturn(json(fromClassPath))); + } + + private void mockCreateVersionWith(String fromClassPath) { + mockService.stubFor(post(urlPathMatching(CREATE_VERSION_URI)).// + willReturn(json(fromClassPath))); + } + + private ResponseDefinitionBuilder json(String fromClassPathFile) { + return aResponse().// + withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).// + withBodyFile(fromClassPathFile); + + } + } diff --git a/src/test/java/org/springframework/data/release/jira/JiraVersionUnitTests.java b/src/test/java/org/springframework/data/release/jira/JiraVersionUnitTests.java index 71d236b..2c02e09 100644 --- a/src/test/java/org/springframework/data/release/jira/JiraVersionUnitTests.java +++ b/src/test/java/org/springframework/data/release/jira/JiraVersionUnitTests.java @@ -25,7 +25,7 @@ import org.springframework.data.release.model.ReleaseTrains; /** * Unit tests for {@link JiraVersion}. - * + * * @author Oliver Gierke */ public class JiraVersionUnitTests { @@ -49,7 +49,7 @@ public class JiraVersionUnitTests { ModuleIteration module = ReleaseTrains.DIJKSTRA.getModuleIteration(Iteration.M1, "Elasticsearch"); JiraVersion version = new JiraVersion(module); - assertThat(version.toString(), is("1.0 M2 (Dijkstra)")); + assertThat(version.toString(), is("1.0 M1 (Dijkstra)")); } @Test @@ -61,6 +61,15 @@ public class JiraVersionUnitTests { assertThat(version.toString(), is("1.0 RC1 (Dijkstra)")); } + @Test + public void rendersDescriptionCorrectly() { + + ModuleIteration module = ReleaseTrains.DIJKSTRA.getModuleIteration(Iteration.M1, "Elasticsearch"); + + JiraVersion version = new JiraVersion(module); + assertThat(version.getDescription(), is("Dijkstra M2")); + } + private void assertIterationVersion(Iteration iteration, String expected) { ModuleIteration module = ReleaseTrains.DIJKSTRA.getModuleIteration(iteration, "Commons"); diff --git a/src/test/java/org/springframework/data/release/jira/TicketsUnitTests.java b/src/test/java/org/springframework/data/release/jira/TicketsUnitTests.java new file mode 100644 index 0000000..424eb0e --- /dev/null +++ b/src/test/java/org/springframework/data/release/jira/TicketsUnitTests.java @@ -0,0 +1,97 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.release.jira; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.springframework.test.util.AssertionErrors.fail; + +import java.util.Collections; + +import org.junit.Test; +import org.springframework.data.release.model.Iteration; +import org.springframework.data.release.model.ReleaseTrains; + +/** + * Unit tests for {@link Tickets}. + * + * @author Mark Paluch + */ +public class TicketsUnitTests { + + @Test + public void hasReleaseTicketShouldReturnTrue() throws Exception { + + Ticket ticket = new Ticket("1234", "Release 1.10 GA (Hopper)", new JiraTicketStatus(false, "", "")); + Tickets tickets = new Tickets(Collections.singletonList(ticket)); + + boolean result = tickets.hasReleaseTicket(ReleaseTrains.HOPPER.getModuleIteration(Iteration.GA, "JPA")); + assertThat(result, is(true)); + } + + @Test + public void hasReleaseTickeForTicketWithoutTrainNameShouldReturnFalse() throws Exception { + + Ticket ticket = new Ticket("1234", "Release 1.10 GA", new JiraTicketStatus(false, "", "")); + Tickets tickets = new Tickets(Collections.singletonList(ticket)); + + boolean result = tickets.hasReleaseTicket(ReleaseTrains.HOPPER.getModuleIteration(Iteration.GA, "JPA")); + assertThat(result, is(false)); + } + + @Test + public void getReleaseTicketReturnsReleaseTicket() throws Exception { + + Ticket ticket = new Ticket("1234", "Release 1.10 GA (Hopper)", new JiraTicketStatus(false, "", "")); + Tickets tickets = new Tickets(Collections.singletonList(ticket)); + + Ticket releaseTicket = tickets.getReleaseTicket(ReleaseTrains.HOPPER.getModuleIteration(Iteration.GA, "JPA")); + assertThat(releaseTicket, is(ticket)); + } + + @Test(expected = IllegalStateException.class) + public void getReleaseTicketThrowsExceptionWithoutAReleaseTicket() throws Exception { + + Ticket ticket = new Ticket("1234", "Release 1.10 GA", new JiraTicketStatus(false, "", "")); + Tickets tickets = new Tickets(Collections.singletonList(ticket)); + + tickets.getReleaseTicket(ReleaseTrains.HOPPER.getModuleIteration(Iteration.GA, "JPA")); + fail("Missing IllegalStateException"); + } + + @Test + public void getResolvedReleaseTicket() throws Exception { + + Ticket ticket = new Ticket("1234", "Release 1.10 GA (Hopper)", new JiraTicketStatus(true, "", "")); + Tickets tickets = new Tickets(Collections.singletonList(ticket)); + + Ticket releaseTicket = tickets.getReleaseTicket(ReleaseTrains.HOPPER.getModuleIteration(Iteration.GA, "JPA")); + assertThat(releaseTicket, is(ticket)); + } + + @Test + public void getReleaseTicketsReturnsReleaseTickets() throws Exception { + + Ticket ticket = new Ticket("1234", "Release 1.10 GA (Hopper)", new JiraTicketStatus(false, "", "")); + Tickets tickets = new Tickets(Collections.singletonList(ticket)); + + Tickets result = tickets + .getReleaseTickets(ReleaseTrains.HOPPER.getModuleIteration(Iteration.GA, "JPA").getTrainIteration()); + assertThat(result.getTickets().contains(ticket), is(true)); + } + +} diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties index 8e2583c..4dfd272 100644 --- a/src/test/resources/application-test.properties +++ b/src/test/resources/application-test.properties @@ -5,3 +5,8 @@ logging.level.org.springframework.web.client=TRACE # Deployment deployment.repository-prefix=test- + +jira.username=dummy +jira.password=dummy +jira.url=http://localhost:8888 +github.api.url=http://localhost:8888 \ No newline at end of file diff --git a/src/test/resources/integration/github/__files/emptyIssues.json b/src/test/resources/integration/github/__files/emptyIssues.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/src/test/resources/integration/github/__files/emptyIssues.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/src/test/resources/integration/github/__files/emptyMilestones.json b/src/test/resources/integration/github/__files/emptyMilestones.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/src/test/resources/integration/github/__files/emptyMilestones.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/src/test/resources/integration/github/__files/issue.json b/src/test/resources/integration/github/__files/issue.json new file mode 100644 index 0000000..c5e97b5 --- /dev/null +++ b/src/test/resources/integration/github/__files/issue.json @@ -0,0 +1,75 @@ +{ + "url": "https://api.github.com/repos/spring-projects/spring-data-build/issues/233", + "repository_url": "https://api.github.com/repos/spring-projects/spring-data-build", + "labels_url": "https://api.github.com/repos/spring-projects/spring-data-build/issues/233/labels{/name}", + "comments_url": "https://api.github.com/repos/spring-projects/spring-data-build/issues/233/comments", + "events_url": "https://api.github.com/repos/spring-projects/spring-data-build/issues/233/events", + "html_url": "https://github.com/spring-projects/spring-data-build/issues/233", + "id": 141655399, + "number": 233, + "title": "Release 1.8 RC1 (Hopper)", + "user": { + "login": "olivergierke", + "id": 128577, + "avatar_url": "https://avatars.githubusercontent.com/u/128577?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/olivergierke", + "html_url": "https://github.com/olivergierke", + "followers_url": "https://api.github.com/users/olivergierke/followers", + "following_url": "https://api.github.com/users/olivergierke/following{/other_user}", + "gists_url": "https://api.github.com/users/olivergierke/gists{/gist_id}", + "starred_url": "https://api.github.com/users/olivergierke/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/olivergierke/subscriptions", + "organizations_url": "https://api.github.com/users/olivergierke/orgs", + "repos_url": "https://api.github.com/users/olivergierke/repos", + "events_url": "https://api.github.com/users/olivergierke/events{/privacy}", + "received_events_url": "https://api.github.com/users/olivergierke/received_events", + "type": "User", + "site_admin": false + }, + "labels": [ + ], + "state": "open", + "locked": false, + "assignee": null, + "milestone": { + "url": "https://api.github.com/repos/spring-projects/spring-data-build/milestones/45", + "html_url": "https://github.com/spring-projects/spring-data-build/milestones/1.8%20RC1%20(Hopper)", + "labels_url": "https://api.github.com/repos/spring-projects/spring-data-build/milestones/45/labels", + "id": 1579988, + "number": 45, + "title": "1.8 RC1 (Hopper)", + "description": "", + "creator": { + "login": "olivergierke", + "id": 128577, + "avatar_url": "https://avatars.githubusercontent.com/u/128577?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/olivergierke", + "html_url": "https://github.com/olivergierke", + "followers_url": "https://api.github.com/users/olivergierke/followers", + "following_url": "https://api.github.com/users/olivergierke/following{/other_user}", + "gists_url": "https://api.github.com/users/olivergierke/gists{/gist_id}", + "starred_url": "https://api.github.com/users/olivergierke/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/olivergierke/subscriptions", + "organizations_url": "https://api.github.com/users/olivergierke/orgs", + "repos_url": "https://api.github.com/users/olivergierke/repos", + "events_url": "https://api.github.com/users/olivergierke/events{/privacy}", + "received_events_url": "https://api.github.com/users/olivergierke/received_events", + "type": "User", + "site_admin": false + }, + "open_issues": 1, + "closed_issues": 4, + "state": "open", + "created_at": "2016-02-12T16:05:35Z", + "updated_at": "2016-03-17T17:46:03Z", + "due_on": null, + "closed_at": null + }, + "comments": 0, + "created_at": "2016-03-17T17:43:09Z", + "updated_at": "2016-03-17T17:43:09Z", + "closed_at": null, + "body": "" +} \ No newline at end of file diff --git a/src/test/resources/integration/github/__files/issues.json b/src/test/resources/integration/github/__files/issues.json new file mode 100644 index 0000000..e04d930 --- /dev/null +++ b/src/test/resources/integration/github/__files/issues.json @@ -0,0 +1,436 @@ +[ + { + "url": "https://api.github.com/repos/spring-projects/spring-data-build/issues/233", + "repository_url": "https://api.github.com/repos/spring-projects/spring-data-build", + "labels_url": "https://api.github.com/repos/spring-projects/spring-data-build/issues/233/labels{/name}", + "comments_url": "https://api.github.com/repos/spring-projects/spring-data-build/issues/233/comments", + "events_url": "https://api.github.com/repos/spring-projects/spring-data-build/issues/233/events", + "html_url": "https://github.com/spring-projects/spring-data-build/issues/233", + "id": 141655399, + "number": 233, + "title": "Release 1.8 RC1 (Hopper)", + "user": { + "login": "olivergierke", + "id": 128577, + "avatar_url": "https://avatars.githubusercontent.com/u/128577?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/olivergierke", + "html_url": "https://github.com/olivergierke", + "followers_url": "https://api.github.com/users/olivergierke/followers", + "following_url": "https://api.github.com/users/olivergierke/following{/other_user}", + "gists_url": "https://api.github.com/users/olivergierke/gists{/gist_id}", + "starred_url": "https://api.github.com/users/olivergierke/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/olivergierke/subscriptions", + "organizations_url": "https://api.github.com/users/olivergierke/orgs", + "repos_url": "https://api.github.com/users/olivergierke/repos", + "events_url": "https://api.github.com/users/olivergierke/events{/privacy}", + "received_events_url": "https://api.github.com/users/olivergierke/received_events", + "type": "User", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignee": null, + "milestone": { + "url": "https://api.github.com/repos/spring-projects/spring-data-build/milestones/45", + "html_url": "https://github.com/spring-projects/spring-data-build/milestones/1.8%20RC1%20(Hopper)", + "labels_url": "https://api.github.com/repos/spring-projects/spring-data-build/milestones/45/labels", + "id": 1579988, + "number": 45, + "title": "1.8 RC1 (Hopper)", + "description": "", + "creator": { + "login": "olivergierke", + "id": 128577, + "avatar_url": "https://avatars.githubusercontent.com/u/128577?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/olivergierke", + "html_url": "https://github.com/olivergierke", + "followers_url": "https://api.github.com/users/olivergierke/followers", + "following_url": "https://api.github.com/users/olivergierke/following{/other_user}", + "gists_url": "https://api.github.com/users/olivergierke/gists{/gist_id}", + "starred_url": "https://api.github.com/users/olivergierke/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/olivergierke/subscriptions", + "organizations_url": "https://api.github.com/users/olivergierke/orgs", + "repos_url": "https://api.github.com/users/olivergierke/repos", + "events_url": "https://api.github.com/users/olivergierke/events{/privacy}", + "received_events_url": "https://api.github.com/users/olivergierke/received_events", + "type": "User", + "site_admin": false + }, + "open_issues": 1, + "closed_issues": 4, + "state": "open", + "created_at": "2016-02-12T16:05:35Z", + "updated_at": "2016-03-17T17:46:03Z", + "due_on": null, + "closed_at": null + }, + "comments": 0, + "created_at": "2016-03-17T17:43:09Z", + "updated_at": "2016-03-17T17:43:09Z", + "closed_at": null, + "body": "" + }, + { + "url": "https://api.github.com/repos/spring-projects/spring-data-build/issues/232", + "repository_url": "https://api.github.com/repos/spring-projects/spring-data-build", + "labels_url": "https://api.github.com/repos/spring-projects/spring-data-build/issues/232/labels{/name}", + "comments_url": "https://api.github.com/repos/spring-projects/spring-data-build/issues/232/comments", + "events_url": "https://api.github.com/repos/spring-projects/spring-data-build/issues/232/events", + "html_url": "https://github.com/spring-projects/spring-data-build/issues/232", + "id": 139529287, + "number": 232, + "title": "Remove spring41-next build profile.", + "user": { + "login": "christophstrobl", + "id": 2317257, + "avatar_url": "https://avatars.githubusercontent.com/u/2317257?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/christophstrobl", + "html_url": "https://github.com/christophstrobl", + "followers_url": "https://api.github.com/users/christophstrobl/followers", + "following_url": "https://api.github.com/users/christophstrobl/following{/other_user}", + "gists_url": "https://api.github.com/users/christophstrobl/gists{/gist_id}", + "starred_url": "https://api.github.com/users/christophstrobl/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/christophstrobl/subscriptions", + "organizations_url": "https://api.github.com/users/christophstrobl/orgs", + "repos_url": "https://api.github.com/users/christophstrobl/repos", + "events_url": "https://api.github.com/users/christophstrobl/events{/privacy}", + "received_events_url": "https://api.github.com/users/christophstrobl/received_events", + "type": "User", + "site_admin": false + }, + "labels": [ + + ], + "state": "closed", + "locked": false, + "assignee": null, + "milestone": { + "url": "https://api.github.com/repos/spring-projects/spring-data-build/milestones/45", + "html_url": "https://github.com/spring-projects/spring-data-build/milestones/1.8%20RC1%20(Hopper)", + "labels_url": "https://api.github.com/repos/spring-projects/spring-data-build/milestones/45/labels", + "id": 1579988, + "number": 45, + "title": "1.8 RC1 (Hopper)", + "description": "", + "creator": { + "login": "olivergierke", + "id": 128577, + "avatar_url": "https://avatars.githubusercontent.com/u/128577?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/olivergierke", + "html_url": "https://github.com/olivergierke", + "followers_url": "https://api.github.com/users/olivergierke/followers", + "following_url": "https://api.github.com/users/olivergierke/following{/other_user}", + "gists_url": "https://api.github.com/users/olivergierke/gists{/gist_id}", + "starred_url": "https://api.github.com/users/olivergierke/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/olivergierke/subscriptions", + "organizations_url": "https://api.github.com/users/olivergierke/orgs", + "repos_url": "https://api.github.com/users/olivergierke/repos", + "events_url": "https://api.github.com/users/olivergierke/events{/privacy}", + "received_events_url": "https://api.github.com/users/olivergierke/received_events", + "type": "User", + "site_admin": false + }, + "open_issues": 1, + "closed_issues": 4, + "state": "open", + "created_at": "2016-02-12T16:05:35Z", + "updated_at": "2016-03-17T17:46:03Z", + "due_on": null, + "closed_at": null + }, + "comments": 0, + "created_at": "2016-03-09T09:56:42Z", + "updated_at": "2016-03-17T17:46:03Z", + "closed_at": "2016-03-17T17:46:03Z", + "body": "With the baseline upgrade to Spring 4.2 the `spring41-next` profile has become obsolete." + }, + { + "url": "https://api.github.com/repos/spring-projects/spring-data-build/issues/231", + "repository_url": "https://api.github.com/repos/spring-projects/spring-data-build", + "labels_url": "https://api.github.com/repos/spring-projects/spring-data-build/issues/231/labels{/name}", + "comments_url": "https://api.github.com/repos/spring-projects/spring-data-build/issues/231/comments", + "events_url": "https://api.github.com/repos/spring-projects/spring-data-build/issues/231/events", + "html_url": "https://github.com/spring-projects/spring-data-build/issues/231", + "id": 137830920, + "number": 231, + "title": "Upgrade to latest Querydsl", + "user": { + "login": "olivergierke", + "id": 128577, + "avatar_url": "https://avatars.githubusercontent.com/u/128577?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/olivergierke", + "html_url": "https://github.com/olivergierke", + "followers_url": "https://api.github.com/users/olivergierke/followers", + "following_url": "https://api.github.com/users/olivergierke/following{/other_user}", + "gists_url": "https://api.github.com/users/olivergierke/gists{/gist_id}", + "starred_url": "https://api.github.com/users/olivergierke/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/olivergierke/subscriptions", + "organizations_url": "https://api.github.com/users/olivergierke/orgs", + "repos_url": "https://api.github.com/users/olivergierke/repos", + "events_url": "https://api.github.com/users/olivergierke/events{/privacy}", + "received_events_url": "https://api.github.com/users/olivergierke/received_events", + "type": "User", + "site_admin": false + }, + "labels": [ + + ], + "state": "closed", + "locked": false, + "assignee": { + "login": "olivergierke", + "id": 128577, + "avatar_url": "https://avatars.githubusercontent.com/u/128577?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/olivergierke", + "html_url": "https://github.com/olivergierke", + "followers_url": "https://api.github.com/users/olivergierke/followers", + "following_url": "https://api.github.com/users/olivergierke/following{/other_user}", + "gists_url": "https://api.github.com/users/olivergierke/gists{/gist_id}", + "starred_url": "https://api.github.com/users/olivergierke/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/olivergierke/subscriptions", + "organizations_url": "https://api.github.com/users/olivergierke/orgs", + "repos_url": "https://api.github.com/users/olivergierke/repos", + "events_url": "https://api.github.com/users/olivergierke/events{/privacy}", + "received_events_url": "https://api.github.com/users/olivergierke/received_events", + "type": "User", + "site_admin": false + }, + "milestone": { + "url": "https://api.github.com/repos/spring-projects/spring-data-build/milestones/45", + "html_url": "https://github.com/spring-projects/spring-data-build/milestones/1.8%20RC1%20(Hopper)", + "labels_url": "https://api.github.com/repos/spring-projects/spring-data-build/milestones/45/labels", + "id": 1579988, + "number": 45, + "title": "1.8 RC1 (Hopper)", + "description": "", + "creator": { + "login": "olivergierke", + "id": 128577, + "avatar_url": "https://avatars.githubusercontent.com/u/128577?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/olivergierke", + "html_url": "https://github.com/olivergierke", + "followers_url": "https://api.github.com/users/olivergierke/followers", + "following_url": "https://api.github.com/users/olivergierke/following{/other_user}", + "gists_url": "https://api.github.com/users/olivergierke/gists{/gist_id}", + "starred_url": "https://api.github.com/users/olivergierke/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/olivergierke/subscriptions", + "organizations_url": "https://api.github.com/users/olivergierke/orgs", + "repos_url": "https://api.github.com/users/olivergierke/repos", + "events_url": "https://api.github.com/users/olivergierke/events{/privacy}", + "received_events_url": "https://api.github.com/users/olivergierke/received_events", + "type": "User", + "site_admin": false + }, + "open_issues": 1, + "closed_issues": 4, + "state": "open", + "created_at": "2016-02-12T16:05:35Z", + "updated_at": "2016-03-17T17:46:03Z", + "due_on": null, + "closed_at": null + }, + "comments": 0, + "created_at": "2016-03-02T10:11:20Z", + "updated_at": "2016-03-02T10:14:24Z", + "closed_at": "2016-03-02T10:14:24Z", + "body": "" + }, + { + "url": "https://api.github.com/repos/spring-projects/spring-data-build/issues/230", + "repository_url": "https://api.github.com/repos/spring-projects/spring-data-build", + "labels_url": "https://api.github.com/repos/spring-projects/spring-data-build/issues/230/labels{/name}", + "comments_url": "https://api.github.com/repos/spring-projects/spring-data-build/issues/230/comments", + "events_url": "https://api.github.com/repos/spring-projects/spring-data-build/issues/230/events", + "html_url": "https://github.com/spring-projects/spring-data-build/issues/230", + "id": 137829896, + "number": 230, + "title": "Upgrade to Spring 4.2", + "user": { + "login": "olivergierke", + "id": 128577, + "avatar_url": "https://avatars.githubusercontent.com/u/128577?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/olivergierke", + "html_url": "https://github.com/olivergierke", + "followers_url": "https://api.github.com/users/olivergierke/followers", + "following_url": "https://api.github.com/users/olivergierke/following{/other_user}", + "gists_url": "https://api.github.com/users/olivergierke/gists{/gist_id}", + "starred_url": "https://api.github.com/users/olivergierke/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/olivergierke/subscriptions", + "organizations_url": "https://api.github.com/users/olivergierke/orgs", + "repos_url": "https://api.github.com/users/olivergierke/repos", + "events_url": "https://api.github.com/users/olivergierke/events{/privacy}", + "received_events_url": "https://api.github.com/users/olivergierke/received_events", + "type": "User", + "site_admin": false + }, + "labels": [ + + ], + "state": "closed", + "locked": false, + "assignee": { + "login": "olivergierke", + "id": 128577, + "avatar_url": "https://avatars.githubusercontent.com/u/128577?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/olivergierke", + "html_url": "https://github.com/olivergierke", + "followers_url": "https://api.github.com/users/olivergierke/followers", + "following_url": "https://api.github.com/users/olivergierke/following{/other_user}", + "gists_url": "https://api.github.com/users/olivergierke/gists{/gist_id}", + "starred_url": "https://api.github.com/users/olivergierke/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/olivergierke/subscriptions", + "organizations_url": "https://api.github.com/users/olivergierke/orgs", + "repos_url": "https://api.github.com/users/olivergierke/repos", + "events_url": "https://api.github.com/users/olivergierke/events{/privacy}", + "received_events_url": "https://api.github.com/users/olivergierke/received_events", + "type": "User", + "site_admin": false + }, + "milestone": { + "url": "https://api.github.com/repos/spring-projects/spring-data-build/milestones/45", + "html_url": "https://github.com/spring-projects/spring-data-build/milestones/1.8%20RC1%20(Hopper)", + "labels_url": "https://api.github.com/repos/spring-projects/spring-data-build/milestones/45/labels", + "id": 1579988, + "number": 45, + "title": "1.8 RC1 (Hopper)", + "description": "", + "creator": { + "login": "olivergierke", + "id": 128577, + "avatar_url": "https://avatars.githubusercontent.com/u/128577?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/olivergierke", + "html_url": "https://github.com/olivergierke", + "followers_url": "https://api.github.com/users/olivergierke/followers", + "following_url": "https://api.github.com/users/olivergierke/following{/other_user}", + "gists_url": "https://api.github.com/users/olivergierke/gists{/gist_id}", + "starred_url": "https://api.github.com/users/olivergierke/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/olivergierke/subscriptions", + "organizations_url": "https://api.github.com/users/olivergierke/orgs", + "repos_url": "https://api.github.com/users/olivergierke/repos", + "events_url": "https://api.github.com/users/olivergierke/events{/privacy}", + "received_events_url": "https://api.github.com/users/olivergierke/received_events", + "type": "User", + "site_admin": false + }, + "open_issues": 1, + "closed_issues": 4, + "state": "open", + "created_at": "2016-02-12T16:05:35Z", + "updated_at": "2016-03-17T17:46:03Z", + "due_on": null, + "closed_at": null + }, + "comments": 0, + "created_at": "2016-03-02T10:05:48Z", + "updated_at": "2016-03-02T10:14:29Z", + "closed_at": "2016-03-02T10:14:29Z", + "body": "" + }, + { + "url": "https://api.github.com/repos/spring-projects/spring-data-build/issues/227", + "repository_url": "https://api.github.com/repos/spring-projects/spring-data-build", + "labels_url": "https://api.github.com/repos/spring-projects/spring-data-build/issues/227/labels{/name}", + "comments_url": "https://api.github.com/repos/spring-projects/spring-data-build/issues/227/comments", + "events_url": "https://api.github.com/repos/spring-projects/spring-data-build/issues/227/events", + "html_url": "https://github.com/spring-projects/spring-data-build/issues/227", + "id": 134905897, + "number": 227, + "title": "Downgrade Jackson baseline to 2.6", + "user": { + "login": "olivergierke", + "id": 128577, + "avatar_url": "https://avatars.githubusercontent.com/u/128577?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/olivergierke", + "html_url": "https://github.com/olivergierke", + "followers_url": "https://api.github.com/users/olivergierke/followers", + "following_url": "https://api.github.com/users/olivergierke/following{/other_user}", + "gists_url": "https://api.github.com/users/olivergierke/gists{/gist_id}", + "starred_url": "https://api.github.com/users/olivergierke/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/olivergierke/subscriptions", + "organizations_url": "https://api.github.com/users/olivergierke/orgs", + "repos_url": "https://api.github.com/users/olivergierke/repos", + "events_url": "https://api.github.com/users/olivergierke/events{/privacy}", + "received_events_url": "https://api.github.com/users/olivergierke/received_events", + "type": "User", + "site_admin": false + }, + "labels": [ + + ], + "state": "closed", + "locked": false, + "assignee": { + "login": "olivergierke", + "id": 128577, + "avatar_url": "https://avatars.githubusercontent.com/u/128577?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/olivergierke", + "html_url": "https://github.com/olivergierke", + "followers_url": "https://api.github.com/users/olivergierke/followers", + "following_url": "https://api.github.com/users/olivergierke/following{/other_user}", + "gists_url": "https://api.github.com/users/olivergierke/gists{/gist_id}", + "starred_url": "https://api.github.com/users/olivergierke/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/olivergierke/subscriptions", + "organizations_url": "https://api.github.com/users/olivergierke/orgs", + "repos_url": "https://api.github.com/users/olivergierke/repos", + "events_url": "https://api.github.com/users/olivergierke/events{/privacy}", + "received_events_url": "https://api.github.com/users/olivergierke/received_events", + "type": "User", + "site_admin": false + }, + "milestone": { + "url": "https://api.github.com/repos/spring-projects/spring-data-build/milestones/45", + "html_url": "https://github.com/spring-projects/spring-data-build/milestones/1.8%20RC1%20(Hopper)", + "labels_url": "https://api.github.com/repos/spring-projects/spring-data-build/milestones/45/labels", + "id": 1579988, + "number": 45, + "title": "1.8 RC1 (Hopper)", + "description": "", + "creator": { + "login": "olivergierke", + "id": 128577, + "avatar_url": "https://avatars.githubusercontent.com/u/128577?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/olivergierke", + "html_url": "https://github.com/olivergierke", + "followers_url": "https://api.github.com/users/olivergierke/followers", + "following_url": "https://api.github.com/users/olivergierke/following{/other_user}", + "gists_url": "https://api.github.com/users/olivergierke/gists{/gist_id}", + "starred_url": "https://api.github.com/users/olivergierke/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/olivergierke/subscriptions", + "organizations_url": "https://api.github.com/users/olivergierke/orgs", + "repos_url": "https://api.github.com/users/olivergierke/repos", + "events_url": "https://api.github.com/users/olivergierke/events{/privacy}", + "received_events_url": "https://api.github.com/users/olivergierke/received_events", + "type": "User", + "site_admin": false + }, + "open_issues": 1, + "closed_issues": 4, + "state": "open", + "created_at": "2016-02-12T16:05:35Z", + "updated_at": "2016-03-17T17:46:03Z", + "due_on": null, + "closed_at": null + }, + "comments": 0, + "created_at": "2016-02-19T16:21:22Z", + "updated_at": "2016-02-19T16:37:54Z", + "closed_at": "2016-02-19T16:37:54Z", + "body": "The Jackson 2.7 release causes significant regressions in a variety of places and it seems proper fixes in Spring Framework will only be available in the 4.2 line. That's good reason to stay on Jackson 2.6 for Hopper. Users requiring Jackson 2.7 features would have to use at least Spring 4.2, 4.3 even preferred.\r\n\r\nhttps://jira.spring.io/browse/SPR-13853" + } +] \ No newline at end of file diff --git a/src/test/resources/integration/github/__files/milestone.json b/src/test/resources/integration/github/__files/milestone.json new file mode 100644 index 0000000..c47fa6d --- /dev/null +++ b/src/test/resources/integration/github/__files/milestone.json @@ -0,0 +1,35 @@ +{ + "url": "https://api.github.com/repos/spring-projects/spring-data-build/milestones/45", + "html_url": "https://github.com/spring-projects/spring-data-build/milestones/1.8%20RC1%20(Hopper)", + "labels_url": "https://api.github.com/repos/spring-projects/spring-data-build/milestones/45/labels", + "id": 1579988, + "number": 45, + "title": "1.8 RC1 (Hopper)", + "description": "", + "creator": { + "login": "olivergierke", + "id": 128577, + "avatar_url": "https://avatars.githubusercontent.com/u/128577?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/olivergierke", + "html_url": "https://github.com/olivergierke", + "followers_url": "https://api.github.com/users/olivergierke/followers", + "following_url": "https://api.github.com/users/olivergierke/following{/other_user}", + "gists_url": "https://api.github.com/users/olivergierke/gists{/gist_id}", + "starred_url": "https://api.github.com/users/olivergierke/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/olivergierke/subscriptions", + "organizations_url": "https://api.github.com/users/olivergierke/orgs", + "repos_url": "https://api.github.com/users/olivergierke/repos", + "events_url": "https://api.github.com/users/olivergierke/events{/privacy}", + "received_events_url": "https://api.github.com/users/olivergierke/received_events", + "type": "User", + "site_admin": false + }, + "open_issues": 1, + "closed_issues": 4, + "state": "open", + "created_at": "2016-02-12T16:05:35Z", + "updated_at": "2016-03-17T17:46:03Z", + "due_on": null, + "closed_at": null +} \ No newline at end of file diff --git a/src/test/resources/integration/github/__files/milestones.json b/src/test/resources/integration/github/__files/milestones.json new file mode 100644 index 0000000..593eff6 --- /dev/null +++ b/src/test/resources/integration/github/__files/milestones.json @@ -0,0 +1,72 @@ +[ + { + "url": "https://api.github.com/repos/spring-projects/spring-data-build/milestones/44", + "html_url": "https://github.com/spring-projects/spring-data-build/milestones/1.7.4%20(Gosling%20SR4)", + "labels_url": "https://api.github.com/repos/spring-projects/spring-data-build/milestones/44/labels", + "id": 1488963, + "number": 44, + "title": "1.7.4 (Gosling SR4)", + "description": "", + "creator": { + "login": "olivergierke", + "id": 128577, + "avatar_url": "https://avatars.githubusercontent.com/u/128577?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/olivergierke", + "html_url": "https://github.com/olivergierke", + "followers_url": "https://api.github.com/users/olivergierke/followers", + "following_url": "https://api.github.com/users/olivergierke/following{/other_user}", + "gists_url": "https://api.github.com/users/olivergierke/gists{/gist_id}", + "starred_url": "https://api.github.com/users/olivergierke/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/olivergierke/subscriptions", + "organizations_url": "https://api.github.com/users/olivergierke/orgs", + "repos_url": "https://api.github.com/users/olivergierke/repos", + "events_url": "https://api.github.com/users/olivergierke/events{/privacy}", + "received_events_url": "https://api.github.com/users/olivergierke/received_events", + "type": "User", + "site_admin": false + }, + "open_issues": 1, + "closed_issues": 2, + "state": "open", + "created_at": "2016-01-07T07:19:58Z", + "updated_at": "2016-02-11T13:51:38Z", + "due_on": null, + "closed_at": null + }, + { + "url": "https://api.github.com/repos/spring-projects/spring-data-build/milestones/45", + "html_url": "https://github.com/spring-projects/spring-data-build/milestones/1.8%20RC1%20(Hopper)", + "labels_url": "https://api.github.com/repos/spring-projects/spring-data-build/milestones/45/labels", + "id": 1579988, + "number": 45, + "title": "1.8 RC1 (Hopper)", + "description": "", + "creator": { + "login": "olivergierke", + "id": 128577, + "avatar_url": "https://avatars.githubusercontent.com/u/128577?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/olivergierke", + "html_url": "https://github.com/olivergierke", + "followers_url": "https://api.github.com/users/olivergierke/followers", + "following_url": "https://api.github.com/users/olivergierke/following{/other_user}", + "gists_url": "https://api.github.com/users/olivergierke/gists{/gist_id}", + "starred_url": "https://api.github.com/users/olivergierke/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/olivergierke/subscriptions", + "organizations_url": "https://api.github.com/users/olivergierke/orgs", + "repos_url": "https://api.github.com/users/olivergierke/repos", + "events_url": "https://api.github.com/users/olivergierke/events{/privacy}", + "received_events_url": "https://api.github.com/users/olivergierke/received_events", + "type": "User", + "site_admin": false + }, + "open_issues": 1, + "closed_issues": 4, + "state": "open", + "created_at": "2016-02-12T16:05:35Z", + "updated_at": "2016-03-17T17:46:03Z", + "due_on": null, + "closed_at": null + } +] \ No newline at end of file diff --git a/src/test/resources/integration/jira/__files/DATAREDIS-1andDATAJPA-1.json b/src/test/resources/integration/jira/__files/DATAREDIS-1andDATAJPA-1.json new file mode 100644 index 0000000..f765b31 --- /dev/null +++ b/src/test/resources/integration/jira/__files/DATAREDIS-1andDATAJPA-1.json @@ -0,0 +1,66 @@ +{ + "expand": "schema,names", + "startAt": 0, + "maxResults": 50, + "total": 2, + "issues": [ + { + "expand": "operations,editmeta,changelog,transitions,renderedFields", + "id": "35482", + "self": "https://jira.spring.io/rest/api/2/issue/35482", + "key": "DATAREDIS-1", + "fields": { + "summary": "Connections obtained from pool are never returned", + "resolution": { + "self": "https://jira.spring.io/rest/api/2/resolution/8", + "id": "8", + "description": "", + "name": "Complete" + }, + "status": { + "self": "https://jira.spring.io/rest/api/2/status/5", + "description": "A resolution has been taken, and it is awaiting verification by reporter. From here issues are either reopened, or are closed.", + "iconUrl": "https://jira.spring.io/images/icons/statuses/resolved.png", + "name": "Resolved", + "id": "5", + "statusCategory": { + "self": "https://jira.spring.io/rest/api/2/statuscategory/3", + "id": 3, + "key": "done", + "colorName": "green", + "name": "Done" + } + } + } + }, + { + "expand": "operations,editmeta,changelog,transitions,renderedFields", + "id": "35060", + "self": "https://jira.spring.io/rest/api/2/issue/35060", + "key": "DATAJPA-1", + "fields": { + "summary": "Move JPA parts of Hades library into Spring JPA", + "resolution": { + "self": "https://jira.spring.io/rest/api/2/resolution/1", + "id": "1", + "description": "A fix for this issue is checked into the tree and tested.", + "name": "Fixed" + }, + "status": { + "self": "https://jira.spring.io/rest/api/2/status/6", + "description": "The issue is considered finished, the resolution is correct. Issues which are not closed can be reopened.", + "iconUrl": "https://jira.spring.io/images/icons/statuses/closed.png", + "name": "Closed", + "id": "6", + "statusCategory": { + "self": "https://jira.spring.io/rest/api/2/statuscategory/3", + "id": 3, + "key": "done", + "colorName": "green", + "name": "Done" + } + } + } + } + ] +} \ No newline at end of file diff --git a/src/test/resources/integration/jira/__files/emptyProjectComponents.json b/src/test/resources/integration/jira/__files/emptyProjectComponents.json new file mode 100644 index 0000000..32960f8 --- /dev/null +++ b/src/test/resources/integration/jira/__files/emptyProjectComponents.json @@ -0,0 +1,2 @@ +[ +] \ No newline at end of file diff --git a/src/test/resources/integration/jira/__files/emptyReleaseVersions.json b/src/test/resources/integration/jira/__files/emptyReleaseVersions.json new file mode 100644 index 0000000..a1b5d85 --- /dev/null +++ b/src/test/resources/integration/jira/__files/emptyReleaseVersions.json @@ -0,0 +1,8 @@ +{ + "self": "https://jira.spring.io/rest/api/2/project/DATAREST/version?maxResults=50&startAt=0", + "maxResults": 50, + "startAt": 0, + "total": 0, + "isLast": true, + "values": [] +} \ No newline at end of file diff --git a/src/test/resources/integration/jira/__files/emptyTickets.json b/src/test/resources/integration/jira/__files/emptyTickets.json new file mode 100644 index 0000000..e095eab --- /dev/null +++ b/src/test/resources/integration/jira/__files/emptyTickets.json @@ -0,0 +1,6 @@ +{ + "startAt": 0, + "maxResults": 50, + "total": 0, + "issues": [] +} \ No newline at end of file diff --git a/src/test/resources/integration/jira/__files/issueCreated.json b/src/test/resources/integration/jira/__files/issueCreated.json new file mode 100644 index 0000000..23d4f76 --- /dev/null +++ b/src/test/resources/integration/jira/__files/issueCreated.json @@ -0,0 +1,5 @@ +{ + "id": "10000", + "key": "DATAREDIS-42", + "self": "http://www.example.com/jira/rest/api/2/issue/10000" +} \ No newline at end of file diff --git a/src/test/resources/integration/jira/__files/projectComponents.json b/src/test/resources/integration/jira/__files/projectComponents.json new file mode 100644 index 0000000..0c21251 --- /dev/null +++ b/src/test/resources/integration/jira/__files/projectComponents.json @@ -0,0 +1,146 @@ +[ + { + "self": "https://jira.spring.io/rest/api/2/component/12775", + "id": "12775", + "name": "Infrastructure", + "assigneeType": "PROJECT_DEFAULT", + "assignee": { + "self": "https://jira.spring.io/rest/api/2/user?username=olivergierke", + "key": "olivergierke", + "name": "olivergierke", + "avatarUrls": { + "16x16": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=16", + "24x24": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=24", + "32x32": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=32", + "48x48": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=48" + }, + "displayName": "Oliver Gierke", + "active": true + }, + "realAssigneeType": "PROJECT_DEFAULT", + "realAssignee": { + "self": "https://jira.spring.io/rest/api/2/user?username=olivergierke", + "key": "olivergierke", + "name": "olivergierke", + "avatarUrls": { + "16x16": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=16", + "24x24": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=24", + "32x32": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=32", + "48x48": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=48" + }, + "displayName": "Oliver Gierke", + "active": true + }, + "isAssigneeTypeValid": true, + "project": "DATAREST", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/component/12748", + "id": "12748", + "name": "Content negotiation", + "assigneeType": "PROJECT_DEFAULT", + "assignee": { + "self": "https://jira.spring.io/rest/api/2/user?username=olivergierke", + "key": "olivergierke", + "name": "olivergierke", + "avatarUrls": { + "16x16": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=16", + "24x24": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=24", + "32x32": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=32", + "48x48": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=48" + }, + "displayName": "Oliver Gierke", + "active": true + }, + "realAssigneeType": "PROJECT_DEFAULT", + "realAssignee": { + "self": "https://jira.spring.io/rest/api/2/user?username=olivergierke", + "key": "olivergierke", + "name": "olivergierke", + "avatarUrls": { + "16x16": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=16", + "24x24": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=24", + "32x32": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=32", + "48x48": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=48" + }, + "displayName": "Oliver Gierke", + "active": true + }, + "isAssigneeTypeValid": true, + "project": "DATAREST", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/component/12747", + "id": "12747", + "name": "Documentation", + "assigneeType": "PROJECT_DEFAULT", + "assignee": { + "self": "https://jira.spring.io/rest/api/2/user?username=olivergierke", + "key": "olivergierke", + "name": "olivergierke", + "avatarUrls": { + "16x16": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=16", + "24x24": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=24", + "32x32": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=32", + "48x48": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=48" + }, + "displayName": "Oliver Gierke", + "active": true + }, + "realAssigneeType": "PROJECT_DEFAULT", + "realAssignee": { + "self": "https://jira.spring.io/rest/api/2/user?username=olivergierke", + "key": "olivergierke", + "name": "olivergierke", + "avatarUrls": { + "16x16": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=16", + "24x24": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=24", + "32x32": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=32", + "48x48": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=48" + }, + "displayName": "Oliver Gierke", + "active": true + }, + "isAssigneeTypeValid": true, + "project": "DATAREST", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/component/12746", + "id": "12746", + "name": "Repositories", + "assigneeType": "PROJECT_DEFAULT", + "assignee": { + "self": "https://jira.spring.io/rest/api/2/user?username=olivergierke", + "key": "olivergierke", + "name": "olivergierke", + "avatarUrls": { + "16x16": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=16", + "24x24": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=24", + "32x32": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=32", + "48x48": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=48" + }, + "displayName": "Oliver Gierke", + "active": true + }, + "realAssigneeType": "PROJECT_DEFAULT", + "realAssignee": { + "self": "https://jira.spring.io/rest/api/2/user?username=olivergierke", + "key": "olivergierke", + "name": "olivergierke", + "avatarUrls": { + "16x16": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=16", + "24x24": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=24", + "32x32": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=32", + "48x48": "https://secure.gravatar.com/avatar/27320c872264d5db7a38f65137d423db?d=mm&s=48" + }, + "displayName": "Oliver Gierke", + "active": true + }, + "isAssigneeTypeValid": true, + "project": "DATAREST", + "projectId": 10901 + } +] \ No newline at end of file diff --git a/src/test/resources/integration/jira/__files/releaseTickets.json b/src/test/resources/integration/jira/__files/releaseTickets.json new file mode 100644 index 0000000..dfd5564 --- /dev/null +++ b/src/test/resources/integration/jira/__files/releaseTickets.json @@ -0,0 +1,37 @@ +{ + "expand": "names,schema", + "startAt": 0, + "maxResults": 50, + "total": 1, + "issues": [ + { + "expand": "operations,editmeta,changelog,transitions,renderedFields", + "id": "67908", + "self": "https://jira.spring.io/rest/api/2/issue/67908", + "key": "DATAREST-782", + "fields": { + "summary": "Release 2.5 RC1 (Hopper)", + "resolution": { + "self": "https://jira.spring.io/rest/api/2/resolution/1", + "id": "1", + "description": "A fix for this issue is checked into the tree and tested.", + "name": "Fixed" + }, + "status": { + "self": "https://jira.spring.io/rest/api/2/status/6", + "description": "The issue is considered finished, the resolution is correct. Issues which are not closed can be reopened.", + "iconUrl": "https://jira.spring.io/images/icons/statuses/closed.png", + "name": "Closed", + "id": "6", + "statusCategory": { + "self": "https://jira.spring.io/rest/api/2/statuscategory/3", + "id": 3, + "key": "done", + "colorName": "green", + "name": "Done" + } + } + } + } + ] +} \ No newline at end of file diff --git a/src/test/resources/integration/jira/__files/releaseVersions.json b/src/test/resources/integration/jira/__files/releaseVersions.json new file mode 100644 index 0000000..b2311ad --- /dev/null +++ b/src/test/resources/integration/jira/__files/releaseVersions.json @@ -0,0 +1,446 @@ +{ + "self": "https://jira.spring.io/rest/api/2/project/DATAREST/version?maxResults=50&startAt=0", + "maxResults": 50, + "startAt": 0, + "total": 44, + "isLast": true, + "values": [ + { + "self": "https://jira.spring.io/rest/api/2/version/12843", + "id": "12843", + "name": "1.0 - M1", + "archived": true, + "released": true, + "releaseDate": "2012-04-24", + "userReleaseDate": "24/Apr/12", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/12844", + "id": "12844", + "name": "1.0 - M2", + "archived": true, + "released": true, + "releaseDate": "2012-05-16", + "userReleaseDate": "16/May/12", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/12845", + "id": "12845", + "name": "1.0.0.RC1", + "archived": true, + "released": true, + "releaseDate": "2012-06-26", + "userReleaseDate": "26/Jun/12", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/13203", + "id": "13203", + "name": "1.0.0.RC2", + "archived": true, + "released": true, + "releaseDate": "2012-07-31", + "userReleaseDate": "31/Jul/12", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/13598", + "id": "13598", + "name": "1.0.0.RC3", + "archived": true, + "released": true, + "releaseDate": "2012-09-14", + "userReleaseDate": "14/Sep/12", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/12846", + "id": "12846", + "name": "1.0.0 GA", + "archived": true, + "released": true, + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/14009", + "id": "14009", + "name": "1.1.0.M1", + "archived": true, + "released": true, + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/13003", + "id": "13003", + "name": "2.0 M1 (Codd)", + "archived": true, + "released": true, + "releaseDate": "2013-11-21", + "userReleaseDate": "21/Nov/13", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/14259", + "id": "14259", + "name": "2.0 RC1 (Codd)", + "archived": true, + "released": true, + "releaseDate": "2014-01-29", + "userReleaseDate": "29/Jan/14", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/14260", + "id": "14260", + "name": "2.0 GA (Codd)", + "archived": true, + "released": true, + "releaseDate": "2014-02-24", + "userReleaseDate": "24/Feb/14", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/14384", + "id": "14384", + "name": "2.0.1 (Codd SR1)", + "archived": true, + "released": true, + "releaseDate": "2014-03-13", + "userReleaseDate": "13/Mar/14", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/14385", + "id": "14385", + "name": "2.1 M1 (Dijkstra)", + "archived": true, + "released": true, + "releaseDate": "2014-04-01", + "userReleaseDate": "01/Apr/14", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/14412", + "id": "14412", + "name": "2.0.2 (Codd SR2)", + "archived": true, + "released": true, + "releaseDate": "2014-04-15", + "userReleaseDate": "15/Apr/14", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/14386", + "id": "14386", + "name": "2.1 RC1 (Dijkstra)", + "archived": true, + "released": true, + "releaseDate": "2014-05-02", + "userReleaseDate": "02/May/14", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/14520", + "id": "14520", + "name": "2.0.3 (Codd SR3)", + "archived": true, + "released": true, + "releaseDate": "2014-06-18", + "userReleaseDate": "18/Jun/14", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/14387", + "id": "14387", + "name": "2.1 GA (Dijkstra)", + "archived": true, + "released": true, + "releaseDate": "2014-05-20", + "userReleaseDate": "20/May/14", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/14607", + "id": "14607", + "name": "2.1.1 (Dijkstra SR1)", + "archived": true, + "released": true, + "releaseDate": "2014-06-30", + "userReleaseDate": "30/Jun/14", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/14614", + "id": "14614", + "name": "2.2 M1 (Evans)", + "archived": true, + "released": true, + "releaseDate": "2014-07-10", + "userReleaseDate": "10/Jul/14", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/14656", + "id": "14656", + "name": "2.1.2 (Dijkstra SR2)", + "archived": true, + "released": true, + "releaseDate": "2014-07-28", + "userReleaseDate": "28/Jul/14", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/14675", + "id": "14675", + "name": "2.2 RC1 (Evans)", + "archived": true, + "released": true, + "releaseDate": "2014-08-13", + "userReleaseDate": "13/Aug/14", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/14695", + "id": "14695", + "name": "2.1.4 (Dijkstra SR4)", + "archived": true, + "released": true, + "releaseDate": "2014-08-27", + "userReleaseDate": "27/Aug/14", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/14710", + "id": "14710", + "name": "2.2 GA (Evans)", + "archived": true, + "released": true, + "releaseDate": "2014-09-05", + "userReleaseDate": "05/Sep/14", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/14731", + "id": "14731", + "name": "2.2.1 (Evans SR1)", + "archived": true, + "released": true, + "releaseDate": "2014-10-31", + "userReleaseDate": "31/Oct/14", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/14732", + "id": "14732", + "name": "2.3 M1 (Fowler)", + "archived": true, + "released": true, + "releaseDate": "2014-12-01", + "userReleaseDate": "01/Dec/14", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/14721", + "id": "14721", + "name": "2.1.5 (Dijkstra SR5)", + "archived": true, + "released": true, + "releaseDate": "2015-01-27", + "userReleaseDate": "27/Jan/15", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/14819", + "id": "14819", + "name": "2.2.2 (Evans SR2)", + "archived": true, + "released": true, + "releaseDate": "2015-01-28", + "userReleaseDate": "28/Jan/15", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/14852", + "id": "14852", + "name": "2.3 RC1 (Fowler)", + "archived": true, + "released": true, + "releaseDate": "2015-03-05", + "userReleaseDate": "05/Mar/15", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/14949", + "id": "14949", + "name": "2.3 GA (Fowler)", + "archived": true, + "released": true, + "releaseDate": "2015-03-23", + "userReleaseDate": "23/Mar/15", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/14981", + "id": "14981", + "name": "2.4 M1 (Gosling)", + "archived": true, + "released": true, + "releaseDate": "2015-06-02", + "userReleaseDate": "02/Jun/15", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/14970", + "id": "14970", + "name": "2.3.1 (Fowler SR1)", + "archived": true, + "released": true, + "releaseDate": "2015-07-01", + "userReleaseDate": "01/Jul/15", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/14925", + "id": "14925", + "name": "2.2.3 (Evans SR3)", + "archived": true, + "released": true, + "releaseDate": "2015-07-01", + "userReleaseDate": "01/Jul/15", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/14912", + "id": "14912", + "name": "2.1.6 (Dijkstra SR6)", + "archived": true, + "released": true, + "releaseDate": "2015-07-01", + "userReleaseDate": "01/Jul/15", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/15240", + "id": "15240", + "name": "2.3.2 (Fowler SR2)", + "archived": false, + "released": true, + "releaseDate": "2015-07-28", + "userReleaseDate": "28/Jul/15", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/15205", + "id": "15205", + "name": "2.4 RC1 (Gosling)", + "archived": true, + "released": true, + "releaseDate": "2015-08-04", + "userReleaseDate": "04/Aug/15", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/15279", + "id": "15279", + "name": "2.4 GA (Gosling)", + "archived": true, + "released": true, + "releaseDate": "2015-09-01", + "userReleaseDate": "01/Sep/15", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/15348", + "id": "15348", + "description": "Evans SR4", + "name": "2.2.4 (Evans SR4)", + "archived": false, + "released": true, + "releaseDate": "2015-10-14", + "userReleaseDate": "14/Oct/15", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/15310", + "id": "15310", + "description": "Gosling SR1", + "name": "2.4.1 (Gosling SR1)", + "archived": true, + "released": true, + "releaseDate": "2015-11-15", + "userReleaseDate": "15/Nov/15", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/15416", + "id": "15416", + "description": "Gosling SR2", + "name": "2.4.2 (Gosling SR2)", + "archived": false, + "released": true, + "releaseDate": "2015-12-18", + "userReleaseDate": "18/Dec/15", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/15311", + "id": "15311", + "description": "Hopper M1", + "name": "2.5 M1 (Hopper)", + "archived": false, + "released": true, + "releaseDate": "2016-02-12", + "userReleaseDate": "12/Feb/16", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/15455", + "id": "15455", + "description": "Gosling SR4", + "name": "2.4.4 (Gosling SR4)", + "archived": false, + "released": true, + "releaseDate": "2016-02-23", + "userReleaseDate": "23/Feb/16", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/15475", + "id": "15475", + "description": "Hopper RC1", + "name": "2.5 RC1 (Hopper)", + "archived": false, + "released": true, + "releaseDate": "2016-03-18", + "userReleaseDate": "18/Mar/16", + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/15260", + "id": "15260", + "description": "Fowler SR3", + "name": "2.3.3 (Fowler SR3)", + "archived": false, + "released": false, + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/15512", + "id": "15512", + "name": "2.4.5 (Gosling SR5)", + "archived": false, + "released": false, + "projectId": 10901 + }, + { + "self": "https://jira.spring.io/rest/api/2/version/15513", + "id": "15513", + "name": "2.5 GA (Hopper)", + "archived": false, + "released": false, + "projectId": 10901 + } + ] +} \ No newline at end of file diff --git a/src/test/resources/integration/jira/__files/versionCreated.json b/src/test/resources/integration/jira/__files/versionCreated.json new file mode 100644 index 0000000..c8b50f0 --- /dev/null +++ b/src/test/resources/integration/jira/__files/versionCreated.json @@ -0,0 +1,8 @@ +{ + "description": "An excellent version", + "name": "2.5 RC1 (Hopper)", + "archived": false, + "released": false, + "project": "DATAREST", + "projectId": 10000 +} \ No newline at end of file