#160 - Attach changelog to GitHub release.

This commit is contained in:
Mark Paluch
2020-11-22 15:12:42 +01:00
parent 8e5601dc64
commit e1f220fe25
13 changed files with 688 additions and 54 deletions

View File

@@ -48,6 +48,8 @@ $ release build $trainIteration
$ release conclude $trainIteration
$ git push $trainIteration
$ git push $trainIteration --tags
# After GitHub issues migration:
# github push $trainIteration
$ git backport changelog $trainIteration --target $targets
$ foreach $target -> git push $target
```

View File

@@ -18,6 +18,7 @@ package org.springframework.data.release.git;
import lombok.EqualsAndHashCode;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
@@ -69,13 +70,23 @@ public class VersionTags implements Streamable<Tag> {
sorted().collect(Collectors.toList());
}
/**
* Creates an empty {@link VersionTags} object for {@link Project}.
*
* @param project must not be {@literal null}.
* @return
*/
public static VersionTags empty(Project project) {
return new VersionTags(project, Collections.emptyList());
}
/**
* Returns the latest {@link Tag}.
*
* @return
*/
public Tag getLatest() {
return tags.get(tags.size() - 1);
return tags.isEmpty() ? null : tags.get(tags.size() - 1);
}
public Tag createTag(ModuleIteration iteration) {

View File

@@ -0,0 +1,138 @@
/*
* Copyright 2020 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.issues.github;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.springframework.stereotype.Component;
/**
* Generates a changelog markdown file which includes bug fixes, enhancements and contributors for a given milestone.
*
* @author Madhura Bhave
* @author Phillip Webb
*/
@Component
public class ChangelogGenerator {
private static final Pattern ghUserMentionPattern = Pattern.compile("(^|[^\\w`])(@[\\w-]+)");
private final Set<String> excludeLabels;
private final Set<String> excludeContributors;
private final String contributorsTitle;
private final ChangelogSections sections;
public ChangelogGenerator() {
this.excludeLabels = new HashSet<>(Arrays.asList("type: task"));
this.excludeContributors = Collections.emptySet();
this.contributorsTitle = null;
this.sections = new ChangelogSections();
}
/**
* Generates a file at the given path which includes bug fixes, enhancements and contributors for the given milestone.
*
* @param issues the issues to generate the changelog for
* @param sectionContentPostProcessor the postprocessor for a changelog section
*/
public String generate(List<GitHubIssue> issues,
BiFunction<ChangelogSection, String, String> sectionContentPostProcessor) {
return generateContent(issues, sectionContentPostProcessor);
}
private boolean isExcluded(GitHubIssue issue) {
return issue.getLabels().stream().anyMatch(this::isExcluded);
}
private boolean isExcluded(Label label) {
return this.excludeLabels.contains(label.getName());
}
private String generateContent(List<GitHubIssue> issues,
BiFunction<ChangelogSection, String, String> sectionContentPostProcessor) {
StringBuilder content = new StringBuilder();
addSectionContent(content, this.sections.collate(issues), sectionContentPostProcessor);
Set<GitHubUser> contributors = getContributors(issues);
if (!contributors.isEmpty()) {
addContributorsContent(content, contributors);
}
return content.toString();
}
private void addSectionContent(StringBuilder result, Map<ChangelogSection, List<GitHubIssue>> sectionIssues,
BiFunction<ChangelogSection, String, String> sectionContentPostProcessor) {
sectionIssues.forEach((section, issues) -> {
issues.sort(Comparator.reverseOrder());
StringBuilder content = new StringBuilder();
content.append((content.length() != 0) ? String.format("%n") : "");
content.append("## ").append(section).append(String.format("%n%n"));
issues.stream().map(this::getFormattedIssue).forEach(content::append);
result.append(sectionContentPostProcessor.apply(section, content.toString()));
});
}
private String getFormattedIssue(GitHubIssue issue) {
String title = issue.getTitle();
title = ghUserMentionPattern.matcher(title).replaceAll("$1`$2`");
return String.format("- %s %s%n", title, getLinkToIssue(issue));
}
private String getLinkToIssue(GitHubIssue issue) {
return "[" + issue.getId() + "]" + "(" + issue.getUrl() + ")";
}
private Set<GitHubUser> getContributors(List<GitHubIssue> issues) {
if (this.excludeContributors.contains("*")) {
return Collections.emptySet();
}
return issues.stream().filter((issue) -> issue.getPullRequest() != null).map(GitHubIssue::getUser)
.filter(this::isIncludedContributor).collect(Collectors.toSet());
}
private boolean isIncludedContributor(GitHubUser user) {
return !this.excludeContributors.contains(user.getName());
}
private void addContributorsContent(StringBuilder content, Set<GitHubUser> contributors) {
content.append(String.format("%n## "));
content.append((this.contributorsTitle != null) ? this.contributorsTitle : ":heart: Contributors");
content.append(String.format("%n%nWe'd like to thank all the contributors who worked on this release!%n%n"));
contributors.stream().map(this::formatContributors).forEach(content::append);
}
private String formatContributors(GitHubUser c) {
return String.format("- [@%s](%s)%n", c.getName(), c.getUrl());
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2020 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.issues.github;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* A single section of a changelog report.
*
* @author Phillip Webb
*/
class ChangelogSection {
private final String title;
private final String group;
private final Set<String> labels;
ChangelogSection(String title, String group, String... labels) {
this(title, group, new LinkedHashSet<>(Arrays.asList(labels)));
}
ChangelogSection(String title, String group, Set<String> labels) {
Assert.hasText(title, "Title must not be empty");
Assert.isTrue(!CollectionUtils.isEmpty(labels), "Labels must not be empty");
this.title = title;
this.group = group;
this.labels = labels;
}
String getGroup() {
return this.group;
}
boolean isMatchFor(GitHubIssue issue) {
for (String candidate : this.labels) {
for (Label label : issue.getLabels()) {
if (label.getName().contains(candidate)) {
return true;
}
}
}
return false;
}
public boolean hasLabel(String label) {
return this.labels.contains(label);
}
@Override
public String toString() {
return this.title;
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2020 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.issues.github;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* Manages sections of the changelog report.
*
* @author Phillip Webb
*/
class ChangelogSections {
private static final List<ChangelogSection> DEFAULT_SECTIONS;
static {
List<ChangelogSection> sections = new ArrayList<>();
add(sections, ":star: New Features", "enhancement");
add(sections, ":beetle: Bug Fixes", "bug", "regression");
add(sections, ":notebook_with_decorative_cover: Documentation", "documentation");
add(sections, ":hammer: Dependency Upgrades", "dependency-upgrade");
DEFAULT_SECTIONS = Collections.unmodifiableList(sections);
}
private static void add(List<ChangelogSection> sections, String title, String... labels) {
sections.add(new ChangelogSection(title, null, labels));
}
private final List<ChangelogSection> sections;
ChangelogSections() {
this.sections = DEFAULT_SECTIONS;
}
Map<ChangelogSection, List<GitHubIssue>> collate(List<GitHubIssue> issues) {
SortedMap<ChangelogSection, List<GitHubIssue>> collated = new TreeMap<>(
Comparator.comparing(this.sections::indexOf));
for (GitHubIssue issue : issues) {
List<ChangelogSection> sections = getSections(issue);
for (ChangelogSection section : sections) {
collated.computeIfAbsent(section, (key) -> new ArrayList<>());
collated.get(section).add(issue);
}
}
return collated;
}
private List<ChangelogSection> getSections(GitHubIssue issue) {
List<ChangelogSection> result = new ArrayList<>();
Set<String> groupClaimes = new HashSet<>();
for (ChangelogSection section : this.sections) {
if (section.isMatchFor(issue) && groupClaimes.add(section.getGroup())) {
result.add(section);
}
}
return result;
}
}

View File

@@ -34,12 +34,17 @@ import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.data.release.git.GitProject;
import org.springframework.data.release.git.Tag;
import org.springframework.data.release.git.VersionTags;
import org.springframework.data.release.issues.Changelog;
import org.springframework.data.release.issues.IssueTracker;
import org.springframework.data.release.issues.Ticket;
import org.springframework.data.release.issues.TicketReference;
import org.springframework.data.release.issues.Tickets;
import org.springframework.data.release.issues.github.GitHubIssue.Milestone;
import org.springframework.data.release.model.ArtifactVersion;
import org.springframework.data.release.model.DocumentationMetadata;
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.Tracker;
@@ -69,6 +74,9 @@ class GitHub extends GitHubSupport implements IssueTracker {
private static final String MILESTONE_BY_ID_URI_TEMPLATE = "/repos/spring-projects/{repoName}/milestones/{id}";
private static final String ISSUE_BY_ID_URI_TEMPLATE = "/repos/spring-projects/{repoName}/issues/{id}";
private static final String ISSUES_URI_TEMPLATE = "/repos/spring-projects/{repoName}/issues";
private static final String RELEASE_BY_TAG_URI_TEMPLATE = "/repos/spring-projects/{repoName}/releases/tags/{tag}";
private static final String RELEASE_URI_TEMPLATE = "/repos/spring-projects/{repoName}/releases";
private static final String RELEASE_BY_ID_URI_TEMPLATE = "/repos/spring-projects/{repoName}/releases/{id}";
private static final ParameterizedTypeReference<List<Milestone>> MILESTONES_TYPE = new ParameterizedTypeReference<List<Milestone>>() {};
private static final ParameterizedTypeReference<List<GitHubIssue>> ISSUES_TYPE = new ParameterizedTypeReference<List<GitHubIssue>>() {};
@@ -436,6 +444,12 @@ class GitHub extends GitHubSupport implements IssueTracker {
@Override
public Tickets findTickets(ModuleIteration moduleIteration, List<TicketReference> ticketReferences) {
return findGitHubIssues(moduleIteration, ticketReferences).stream().map(GitHub::toTicket)
.collect(Tickets.toTicketsCollector());
}
List<GitHubIssue> findGitHubIssues(ModuleIteration moduleIteration, List<TicketReference> ticketReferences) {
logger.log(moduleIteration, "Looking up GitHub issues from milestone …");
Map<String, GitHubIssue> issues = getIssuesFor(moduleIteration, false, true)
@@ -448,13 +462,14 @@ class GitHub extends GitHubSupport implements IssueTracker {
Streamable.of(() -> ticketReferences.stream().filter(it -> it.getId().startsWith("#"))),
ticketReference -> getTicket(issues, repositoryName, ticketReference));
Tickets tickets = foundIssues.stream().map(GitHub::toTicket)
.filter(it -> it.isReleaseTicketFor(moduleIteration) || !it.isReleaseTicket())
.collect(Tickets.toTicketsCollector());
List<GitHubIssue> gitHubIssues = foundIssues.stream().filter(it -> {
Ticket ticket = toTicket(it);
return !ticket.isReleaseTicketFor(moduleIteration) && !ticket.isReleaseTicket();
}).collect(Collectors.toList());
logger.log(moduleIteration, "Resolved %s tickets.", tickets.getOverallTotal());
logger.log(moduleIteration, "Resolved %s tickets.", gitHubIssues.size());
return tickets;
return gitHubIssues;
}
private GitHubIssue getTicket(Map<String, GitHubIssue> cache, String repositoryName, TicketReference reference) {
@@ -486,11 +501,9 @@ class GitHub extends GitHubSupport implements IssueTracker {
try {
GitHubIssue gitHubIssue = operations.exchange(ISSUE_BY_ID_URI_TEMPLATE, HttpMethod.GET,
return operations.exchange(ISSUE_BY_ID_URI_TEMPLATE, HttpMethod.GET,
new HttpEntity<>(new HttpHeaders()), ISSUE_TYPE, parameters).getBody();
return gitHubIssue;
} catch (HttpStatusCodeException e) {
if (e.getStatusCode() == HttpStatus.NOT_FOUND) {
@@ -501,6 +514,100 @@ class GitHub extends GitHubSupport implements IssueTracker {
}
}
/**
* @param module
* @param ticketReferences
*/
public void createOrUpdateRelease(ModuleIteration module, List<TicketReference> ticketReferences) {
logger.log(module, "Preparing GitHub Release …");
List<GitHubIssue> gitHubIssues = findGitHubIssues(module, ticketReferences);
ArtifactVersion version = ArtifactVersion.of(module);
DocumentationMetadata documentation = DocumentationMetadata.of(module.getProject(), version);
ChangelogGenerator generator = new ChangelogGenerator();
String releaseBody = generator.generate(gitHubIssues, (changelogSection, s) -> s);
String documentationLinks = getDocumentationLinks(module, documentation);
createOrUpdateRelease(module, String.format("## :green_book: Links%n%s%n%s%n", documentationLinks, releaseBody));
logger.log(module, "GitHub Release up to date");
}
private String getDocumentationLinks(ModuleIteration module, DocumentationMetadata documentation) {
String referenceDocUrl = documentation.getReferenceDocUrl(module.getTrain());
String apiDocUrl = documentation.getApiDocUrl(module.getTrain());
String reference = String.format("* [%s %s Reference documentation](%s)", module.getProject().getFullName(),
documentation.getVersion(module.getTrain()), referenceDocUrl);
String apidoc = String.format("* [%s %s Javadoc](%s)", module.getProject().getFullName(),
documentation.getVersion(module.getTrain()), apiDocUrl);
return String.format("%s%n%s%n", reference, apidoc);
}
private void createOrUpdateRelease(ModuleIteration module, String body) {
String repositoryName = GitProject.of(module.getProject()).getRepositoryName();
Tag tag = VersionTags.empty(module.getProject()).createTag(module);
logger.log(module, "Looking up GitHub Release …");
Iteration iteration = module.getTrainIteration().getIteration();
boolean prerelase = iteration.isPreview();
GitHubRelease release = findRelease(repositoryName, tag.getName());
if (release == null) {
release = new GitHubRelease(null, tag.getName(), tag.getName(), body, false, prerelase);
logger.log(module, "Creating new Release …");
createRelease(repositoryName, release);
} else {
release = release.withPrerelease(prerelase).withBody(body);
logger.log(module, "Updating new Release …");
updateRelease(repositoryName, release);
}
}
private GitHubRelease findRelease(String repositoryName, String tagName) {
Map<String, Object> parameters = newUrlTemplateVariables();
parameters.put("repoName", repositoryName);
parameters.put("tag", tagName);
try {
return operations.exchange(RELEASE_BY_TAG_URI_TEMPLATE, HttpMethod.GET, new HttpEntity<>(new HttpHeaders()),
GitHubRelease.class, parameters).getBody();
} catch (HttpStatusCodeException e) {
if (e.getStatusCode() == HttpStatus.NOT_FOUND) {
return null;
}
throw e;
}
}
private void createRelease(String repositoryName, GitHubRelease release) {
Map<String, Object> parameters = newUrlTemplateVariables();
parameters.put("repoName", repositoryName);
operations.exchange(RELEASE_URI_TEMPLATE, HttpMethod.POST, new HttpEntity<>(release), GitHubRelease.class,
parameters);
}
private void updateRelease(String repositoryName, GitHubRelease release) {
Map<String, Object> parameters = newUrlTemplateVariables();
parameters.put("repoName", repositoryName);
parameters.put("id", release.getId());
operations.exchange(RELEASE_BY_ID_URI_TEMPLATE, HttpMethod.PATCH, new HttpEntity<>(release), GitHubRelease.class,
parameters);
}
private Stream<GitHubIssue> getIssuesFor(ModuleIteration moduleIteration, boolean forCurrentUser,
boolean ignoreMissingMilestone) {
@@ -556,4 +663,5 @@ class GitHub extends GitHubSupport implements IssueTracker {
private static Ticket toTicket(GitHubIssue issue) {
return new Ticket(issue.getId(), issue.getTitle(), issue.getHtmlUrl(), new GithubTicketStatus(issue.getState()));
}
}

View File

@@ -20,17 +20,25 @@ import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import java.util.List;
import java.util.concurrent.Executor;
import org.springframework.data.release.CliComponent;
import org.springframework.data.release.TimedCommand;
import org.springframework.data.release.git.GitOperations;
import org.springframework.data.release.issues.IssueTracker;
import org.springframework.data.release.issues.TicketReference;
import org.springframework.data.release.model.Project;
import org.springframework.data.release.model.Tracker;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.data.release.utils.ExecutionUtils;
import org.springframework.plugin.core.PluginRegistry;
import org.springframework.shell.core.annotation.CliCommand;
import org.springframework.shell.core.annotation.CliOption;
/**
* Component to execute GitHub related operations.
*
* @author Mark Paluch
*/
@CliComponent
@@ -39,6 +47,8 @@ import org.springframework.shell.core.annotation.CliOption;
class GitHubCommands extends TimedCommand {
@NonNull PluginRegistry<IssueTracker, Project> tracker;
@NonNull GitHub gitHub;
@NonNull GitOperations git;
@NonNull GitHubLabels gitHubLabels;
@NonNull Executor executor;
@@ -47,4 +57,29 @@ class GitHubCommands extends TimedCommand {
gitHubLabels.createOrUpdateLabels(project);
}
@CliCommand(value = "github push")
public void push(@CliOption(key = "", mandatory = true) TrainIteration iteration) {
git.push(iteration);
git.pushTags(iteration.getTrain());
createOrUpdateLabels(iteration);
}
@CliCommand(value = "github create release")
public void createOrUpdateLabels(@CliOption(key = "", mandatory = true) TrainIteration iteration) {
TrainIteration previousIteration = git.getPreviousIteration(iteration);
ExecutionUtils.run(executor, iteration, it -> {
if (it.getProject().getTracker() == Tracker.GITHUB) {
List<TicketReference> ticketReferences = git.getTicketReferencesBetween(it.getProject(), previousIteration,
iteration);
gitHub.createOrUpdateRelease(it, ticketReferences);
}
});
}
}

View File

@@ -31,41 +31,59 @@ import com.fasterxml.jackson.annotation.JsonProperty;
* @author Mark Paluch
*/
@Value
class GitHubIssue {
class GitHubIssue implements Comparable<GitHubIssue> {
String number, title, state;
String number, title, state, url;
GitHubUser user;
List<Object> assignees;
String htmlUrl;
Object milestone;
PullRequest pullRequest;
List<Label> labels;
public GitHubIssue(String number, String title, String state, List<Object> assignees,
@JsonProperty("html_url") String htmlUrl, Object milestone) {
public GitHubIssue(String number, String title, String state, String url, GitHubUser user, List<Object> assignees,
@JsonProperty("html_url") String htmlUrl, Object milestone, @JsonProperty("pull_request") PullRequest pullRequest,
List<Label> labels) {
this.number = number;
this.title = title;
this.state = state;
this.url = url;
this.user = user;
this.assignees = assignees;
this.htmlUrl = htmlUrl;
this.milestone = milestone;
this.pullRequest = pullRequest;
this.labels = labels;
}
public static GitHubIssue of(String title, Milestone milestone) {
return new GitHubIssue(null, title, null, null, null, milestone.number);
return new GitHubIssue(null, title, null, null, null, null, null, milestone.number, null, null);
}
public static GitHubIssue assignedTo(String username) {
Assert.hasText(username, "Username must not be null or empty!");
return new GitHubIssue(null, null, null, Collections.singletonList(username), null, null);
return new GitHubIssue(null, null, null, null, null, Collections.singletonList(username), null, null, null, null);
}
public GitHubIssue close() {
return new GitHubIssue(this.number, this.title, "closed", this.assignees, null, this.milestone);
return new GitHubIssue(this.number, this.title, "closed", null, null, this.assignees, null, this.milestone, null,
null);
}
public String getId() {
return number == null ? null : "#".concat(number);
}
@Override
public int compareTo(GitHubIssue o) {
int number = this.number != null ? Integer.parseInt(this.number) : -1;
int other = o.number != null ? Integer.parseInt(o.number) : -1;
return Integer.compare(number, other);
}
/**
* @author Oliver Gierke
* @author Mark Paluch

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2020 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.issues.github;
import lombok.Value;
import lombok.With;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Details of a Github release.
*
* @author Mark Paluch
*/
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE)
@Value
class GitHubRelease {
Integer id;
@JsonProperty("tag_name") String tagName;
String name;
@With String body;
@With boolean draft;
@With boolean prerelease;
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2018-2020 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
*
* https://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.issues.github;
import lombok.Value;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Details of a Github user.
*
* @author Madhura Bhave
*/
@Value
class GitHubUser {
String name;
String url;
public GitHubUser(@JsonProperty("login") String name, @JsonProperty("html_url") String url) {
this.name = name;
this.url = url;
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2018-2020 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
*
* https://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.issues.github;
import lombok.Value;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Represents a GitHub PullRequest.
*
* @author Mark Paluch
*/
@Value
class PullRequest {
String url;
public PullRequest(@JsonProperty("url") String url) {
this.url = url;
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2020 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.model;
import lombok.Value;
import java.util.Locale;
/**
* Value object providing documentation links.
*
* @author Mark Paluch
*/
@Value(staticConstructor = "of")
public class DocumentationMetadata {
private static String DOCS_BASE = "https://docs.spring.io/spring-data/%s/docs/%s";
private static String DOCS = DOCS_BASE.concat("/reference/html/");
private static String JAVADOC = DOCS_BASE.concat("/api");
Project project;
ArtifactVersion version;
/**
* Returns the JavaDoc URL for non-snapshot versions and not the build project.
*
* @return
*/
public String getApiDocUrl(Train train) {
return version.isSnapshotVersion() || Projects.BUILD.equals(project) //
? "" //
: String.format(JAVADOC, project.getName().toLowerCase(Locale.US), getVersion(train));
}
/**
* Returns the reference documentation URL for non-snapshot versions and not the build project.
*
* @return
*/
public String getReferenceDocUrl(Train train) {
return version.isSnapshotVersion() || Projects.BUILD.equals(project) //
? "" //
: String.format(DOCS, project.getName().toLowerCase(Locale.US), getVersion(train));
}
public String getVersion(Train train) {
if (Projects.BUILD.equals(project)) {
if (train.usesCalver()) {
if (version.isBugFixVersion() || version.isReleaseVersion()) {
return train.getCalver().toMajorMinorBugfix();
}
return String.format("%s-%s", train.getCalver().toMajorMinorBugfix(), version.getReleaseTrainSuffix());
}
return String.format("%s-%s", train.getName(),
version.isReleaseVersion() && !version.isBugFixVersion() ? "RELEASE" : version.getReleaseTrainSuffix());
}
return version.toString();
}
}

View File

@@ -15,11 +15,7 @@
*/
package org.springframework.data.release.sagan;
import java.util.Locale;
import org.springframework.data.release.model.ArtifactVersion;
import org.springframework.data.release.model.Projects;
import org.springframework.data.release.model.Train;
import org.springframework.data.release.model.DocumentationMetadata;
import org.springframework.util.Assert;
import com.fasterxml.jackson.annotation.JsonInclude;
@@ -35,12 +31,9 @@ import com.fasterxml.jackson.annotation.JsonProperty;
@JsonInclude(Include.NON_NULL)
class ProjectMetadata {
private static String DOCS_BASE = "https://docs.spring.io/spring-data/%s/docs/{version}";
private static String DOCS = DOCS_BASE.concat("/reference/html/");
private static String JAVADOC = DOCS_BASE.concat("/api");
private final MaintainedVersion version;
private final MaintainedVersions versions;
private final DocumentationMetadata documentation;
/**
* Creates a new {@link ProjectMetadata} instance from the given {@link MaintainedVersion}.
@@ -54,6 +47,7 @@ class ProjectMetadata {
this.version = version;
this.versions = versions;
this.documentation = DocumentationMetadata.of(version.getProject(), version.getVersion());
}
/**
@@ -62,10 +56,7 @@ class ProjectMetadata {
* @return
*/
public String getReferenceDocUrl() {
return version.getVersion().isSnapshotVersion() || Projects.BUILD.equals(version.getProject()) //
? "" //
: String.format(DOCS, version.getProject().getName().toLowerCase(Locale.US));
return documentation.getReferenceDocUrl(version.getTrain());
}
/**
@@ -84,10 +75,7 @@ class ProjectMetadata {
* @return
*/
public String getApiDocUrl() {
return version.getVersion().isSnapshotVersion() || Projects.BUILD.equals(version.getProject()) //
? "" //
: String.format(JAVADOC, version.getProject().getName().toLowerCase(Locale.US));
return documentation.getApiDocUrl(version.getTrain());
}
/**
@@ -97,26 +85,6 @@ class ProjectMetadata {
* @return
*/
public String getVersion() {
ArtifactVersion version = this.version.getVersion();
if (Projects.BUILD.equals(this.version.getProject())) {
Train train = this.version.getTrain();
if (train.usesCalver()) {
if (version.isBugFixVersion() || version.isReleaseVersion()) {
return train.getCalver().toMajorMinorBugfix();
}
return String.format("%s-%s", train.getCalver().toMajorMinorBugfix(), version.getReleaseTrainSuffix());
}
return String.format("%s-%s", train.getName(),
version.isReleaseVersion() && !version.isBugFixVersion() ? "RELEASE" : version.getReleaseTrainSuffix());
}
return version.toString();
return documentation.getVersion(version.getTrain());
}
}