#160 - Attach changelog to GitHub release.
This commit is contained in:
@@ -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
|
||||
```
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user