Improvements to common build and new Reactor task (merge with rebase) (#183)
* Reactor: For reference, mention additional inputs in app properties * Polish: fix javadoc copypasta and mark potential problem in GitRepo * Common: Expose method for git log (list revisions between 2 refs) * Common: Only post-process RtGithub beans into CachingGithub * Common: Make Sagan noOp if property is not explicitly set * Common: Fix Process command executor outputting to app's stdout This prevents the command executor from capturing the command's output. * Common: Fix findTagOrBranchHeadRevision and log In findTag... we need to compare name using refs/tags/ and refs/heads/ prefixes. In log we need to peel symbolic tags to get the right ObjectId. Optional.map did seem to cause issues. * Common: Add isMergeCommit to SimpleCommit * Common: Add method to find tag SHA by name * Common: Polish how exit codes are generated and used * Common: Add BuildReportHandler to show report earlier than last step This commit also filters out tasks that haven't run yet, avoiding NPE due to endTime being null. * Reactor: restart task should not be part of dry-runs * Reactor: Alter Gradle build command to include bumpVersionsInReadme task * Reactor: Add GenerateReleaseNotesTask Also added partial tests for the task. Avoids generating notes if snapshot, mark as pre-release if milestone or rc. * Reactor: Split out configurations and use profiles for test * Reactor: Fix org in application.yml * Reactor: Fix some formatting * Reactor: Force github OAuth token at Github client creation * Reactor: Split parseChangelog into several more testable methods * Reactor: Let interactive GenerateReleaseNotesTask force a log range * Reactor: Allow multiple dispatch of note entries Switching from a single TYPE to an EnumSet * Reactor: Extract issue numbers in title too just in case * Reactor: Fix alternative titles markdown and description Also better protect agains Github client failures when fetching more info like title and labels. * Reactor: Polish format (newlines) in tag input, notes output * Reactor: Check tag exists but not release. Check on SHA1 * Reactor: Make checks we can save notes draft BEFORE querying commits * Reactor: Attempt to find existing release draft (max 2 month old), avoid unnecessary calls If an existing draft is found, append notes to it. * Reactor: Ask only for "from" change for interactive log/release notes
This commit is contained in:
committed by
Marcin Grzejszczak
parent
7632b99031
commit
188c079bd6
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2013-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 releaser.reactor;
|
||||
|
||||
import org.cloudfoundry.client.CloudFoundryClient;
|
||||
import org.cloudfoundry.doppler.DopplerClient;
|
||||
import org.cloudfoundry.operations.DefaultCloudFoundryOperations;
|
||||
import org.cloudfoundry.reactor.ConnectionContext;
|
||||
import org.cloudfoundry.reactor.DefaultConnectionContext;
|
||||
import org.cloudfoundry.reactor.TokenProvider;
|
||||
import org.cloudfoundry.reactor.client.ReactorCloudFoundryClient;
|
||||
import org.cloudfoundry.reactor.doppler.ReactorDopplerClient;
|
||||
import org.cloudfoundry.reactor.tokenprovider.PasswordGrantTokenProvider;
|
||||
import org.cloudfoundry.reactor.uaa.ReactorUaaClient;
|
||||
import org.cloudfoundry.uaa.UaaClient;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
|
||||
/**
|
||||
* @author Simon Baslé
|
||||
*/
|
||||
@Configuration
|
||||
@Profile("production")
|
||||
class CfConfiguration {
|
||||
|
||||
@Bean
|
||||
DefaultConnectionContext connectionContext(@Value("${cf.apiHost}") String apiHost) {
|
||||
return DefaultConnectionContext.builder().apiHost(apiHost).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
PasswordGrantTokenProvider tokenProvider(@Value("${cf.username}") String username,
|
||||
@Value("${cf.password}") String password) {
|
||||
return PasswordGrantTokenProvider.builder().password(password).username(username)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactorCloudFoundryClient cloudFoundryClient(ConnectionContext connectionContext,
|
||||
TokenProvider tokenProvider) {
|
||||
return ReactorCloudFoundryClient.builder().connectionContext(connectionContext)
|
||||
.tokenProvider(tokenProvider).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactorDopplerClient dopplerClient(ConnectionContext connectionContext,
|
||||
TokenProvider tokenProvider) {
|
||||
return ReactorDopplerClient.builder().connectionContext(connectionContext)
|
||||
.tokenProvider(tokenProvider).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactorUaaClient uaaClient(ConnectionContext connectionContext,
|
||||
TokenProvider tokenProvider) {
|
||||
return ReactorUaaClient.builder().connectionContext(connectionContext)
|
||||
.tokenProvider(tokenProvider).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
DefaultCloudFoundryOperations defaultCloudFoundryOperations(
|
||||
CloudFoundryClient cloudFoundryClient, DopplerClient dopplerClient,
|
||||
UaaClient uaaClient, @Value("${cf.organization}") String organizationId,
|
||||
@Value("${cf.space}") String spaceId) {
|
||||
return DefaultCloudFoundryOperations.builder()
|
||||
.cloudFoundryClient(cloudFoundryClient).dopplerClient(dopplerClient)
|
||||
.uaaClient(uaaClient).organization(organizationId).space(spaceId).build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,570 @@
|
||||
/*
|
||||
* Copyright 2013-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 releaser.reactor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumMap;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.json.JsonObject;
|
||||
|
||||
import com.jcabi.github.Coordinates;
|
||||
import com.jcabi.github.Github;
|
||||
import com.jcabi.github.Issue;
|
||||
import com.jcabi.github.Issues;
|
||||
import com.jcabi.github.Release;
|
||||
import com.jcabi.github.Repo;
|
||||
import com.jcabi.github.RepoCommit;
|
||||
import com.jcabi.github.RepoCommits;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import releaser.internal.git.ProjectGitHandler;
|
||||
import releaser.internal.git.SimpleCommit;
|
||||
import releaser.internal.spring.Arguments;
|
||||
import releaser.internal.tasks.DryRunReleaseReleaserTask;
|
||||
import releaser.internal.tasks.ProjectPostReleaseReleaserTask;
|
||||
import releaser.internal.tasks.release.PushChangesReleaseTask;
|
||||
import releaser.internal.tech.BuildUnstableException;
|
||||
import releaser.internal.tech.ExecutionResult;
|
||||
|
||||
/**
|
||||
* @author Simon Baslé
|
||||
*/
|
||||
public class GenerateReleaseNotesTask
|
||||
implements ProjectPostReleaseReleaserTask, DryRunReleaseReleaserTask {
|
||||
|
||||
private static final Logger log = LoggerFactory
|
||||
.getLogger(GenerateReleaseNotesTask.class);
|
||||
|
||||
private static final String NAME = "releaseNotes";
|
||||
|
||||
private static final String SHORTNAME = "rn";
|
||||
|
||||
private final Github github;
|
||||
|
||||
private final ProjectGitHandler gitHandler;
|
||||
|
||||
public GenerateReleaseNotesTask(Github github, ProjectGitHandler gitHandler) {
|
||||
this.github = github;
|
||||
this.gitHandler = gitHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return PushChangesReleaseTask.ORDER + 5;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String shortName() {
|
||||
return SHORTNAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String header() {
|
||||
return "Generating release notes";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Generates the release notes as a Github release draft, from issues and commits";
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExecutionResult runTask(Arguments args)
|
||||
throws BuildUnstableException, RuntimeException {
|
||||
if (args.versionFromBom.isSnapshot()) {
|
||||
log.info("\nWon't generate notes for a snapshot version");
|
||||
return ExecutionResult.success();
|
||||
}
|
||||
|
||||
Repo repo = github.repos().get(new Coordinates.Simple(
|
||||
args.properties.getGit().getOrgName(), args.projectToRun.name()));
|
||||
|
||||
String releaseTag = "v" + args.versionFromBom.version;
|
||||
if (args.options.dryRun != Boolean.TRUE) {
|
||||
Optional<String> maybeTagSha1 = gitHandler.findTagSha1(args.project,
|
||||
releaseTag);
|
||||
if (maybeTagSha1.isPresent()) {
|
||||
String tagSha1 = maybeTagSha1.get();
|
||||
try {
|
||||
// call json() to trigger fetch of gh tag by SHA1
|
||||
repo.git().references().get("tags/" + tagSha1).json();
|
||||
}
|
||||
catch (IOException e) {
|
||||
return ExecutionResult.failure(new IllegalStateException(
|
||||
"Shouldn't create a release if tag " + releaseTag + " (sha1="
|
||||
+ tagSha1 + ") not visible in Github",
|
||||
e));
|
||||
}
|
||||
}
|
||||
else {
|
||||
return ExecutionResult.failure(new IllegalStateException(
|
||||
"Attempting to draft release note but tag not found in repository: "
|
||||
+ releaseTag));
|
||||
}
|
||||
}
|
||||
|
||||
Issues issuesClient = repo.issues();
|
||||
EnumMap<Type, List<ChangelogEntry>> entries = new EnumMap<>(Type.class);
|
||||
|
||||
String fromVersionTag = "v" + args.projectToRun.originalVersion.version;
|
||||
String toVersionTag = "v" + args.versionFromBom.version;
|
||||
|
||||
if (Boolean.TRUE == args.options.interactive) {
|
||||
log.info("\nForce the from in log range if needed [{}]: ", fromVersionTag);
|
||||
String modifiedFrom = System.console().readLine();
|
||||
if (!modifiedFrom.trim().isEmpty()) {
|
||||
fromVersionTag = modifiedFrom;
|
||||
}
|
||||
}
|
||||
log.info("Fetching the log for range {}..{}", fromVersionTag, toVersionTag);
|
||||
|
||||
// gather commits. we use tags in the format `vVERSION`
|
||||
final List<SimpleCommit> revCommits = gitHandler.commitsBetween(args.project,
|
||||
fromVersionTag, toVersionTag);
|
||||
|
||||
// parse and link to issues if possible, determining type
|
||||
for (SimpleCommit revCommit : revCommits) {
|
||||
ChangelogEntry entry = parseChangeLogEntry(issuesClient, revCommit);
|
||||
|
||||
for (Type type : entry.types) {
|
||||
entries.computeIfAbsent(type, t -> new ArrayList<>()).add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
// generate the notes
|
||||
String notes = generateNotes(args, entries,
|
||||
extractContributorMentions(repo, revCommits));
|
||||
|
||||
if (args.options.dryRun != null && args.options.dryRun) {
|
||||
// print out
|
||||
log.info("[Dry-Run] Generated release notes:");
|
||||
log.info("\n\n" + notes + "\n\n");
|
||||
return ExecutionResult.success();
|
||||
}
|
||||
else {
|
||||
// WARNING: double check the tag actually exists, otherwise this will create a
|
||||
// tag :( The verification is done early in the task to avoid fetching
|
||||
// commits/issues
|
||||
|
||||
Release.Smart draftRelease = null;
|
||||
JsonObject draftReleaseJson = null;
|
||||
try {
|
||||
// attempt to find gh release with this tag. stop at 2 month old releases
|
||||
Instant oldestReleaseToConsider = ZonedDateTime.now().minusMonths(2)
|
||||
.toInstant();
|
||||
for (Release release : repo.releases().iterate()) {
|
||||
JsonObject releaseJson = release.json();
|
||||
// seems the created release has a wrong tag "untagged-xxxxx", we also
|
||||
// look at name
|
||||
if (releaseJson.getString("tag_name").equals(releaseTag)
|
||||
|| releaseJson.getString("name").equals(releaseTag)) {
|
||||
if (!releaseJson.getBoolean("draft")) {
|
||||
return ExecutionResult.failure(new IllegalStateException(
|
||||
"Release already exists for tag " + releaseTag));
|
||||
}
|
||||
else {
|
||||
draftRelease = new Release.Smart(release);
|
||||
draftReleaseJson = releaseJson;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// we didn't find a matching release. don't go too far back in time!
|
||||
String publishedAtJson = releaseJson.getString("published_at");
|
||||
Instant publishedAt = oldestReleaseToConsider.minusSeconds(1);
|
||||
if (publishedAtJson != null) {
|
||||
publishedAt = ZonedDateTime.parse(publishedAtJson).toInstant();
|
||||
}
|
||||
if (publishedAt.isBefore(oldestReleaseToConsider)) {
|
||||
log.info("OK, didn't find a release matching tag " + releaseTag
|
||||
+ " within the last 2 month, stopping there");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Throwable e) {
|
||||
return ExecutionResult.failure(new IllegalStateException(
|
||||
"Unable to try to match github releases with tag " + releaseTag,
|
||||
e));
|
||||
}
|
||||
|
||||
if (draftReleaseJson == null) {
|
||||
// create a draft release for the tag
|
||||
try {
|
||||
Release.Smart release = new Release.Smart(
|
||||
repo.releases().create(releaseTag));
|
||||
release.draft(true);
|
||||
release.tag(releaseTag);
|
||||
release.name(releaseTag);
|
||||
release.body(notes);
|
||||
if (args.versionFromBom.isMilestone() || args.versionFromBom.isRc()) {
|
||||
release.prerelease(true);
|
||||
}
|
||||
return ExecutionResult.success();
|
||||
}
|
||||
catch (IOException e) {
|
||||
return ExecutionResult.failure(e);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// update the existing draft
|
||||
try {
|
||||
// edit the release
|
||||
String oldAndNewNotes = draftReleaseJson.getString("body")
|
||||
+ "\n\n----\nNew draft added "
|
||||
+ LocalDateTime.now().toString() + "\n----\n" + notes;
|
||||
draftRelease.body(oldAndNewNotes);
|
||||
return ExecutionResult.success();
|
||||
}
|
||||
catch (IOException e) {
|
||||
return ExecutionResult.failure(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the release notes.
|
||||
*/
|
||||
protected String generateNotes(Arguments args,
|
||||
EnumMap<Type, List<ChangelogEntry>> entries,
|
||||
List<String> contributorGithubMentions) {
|
||||
// TODO use a template? handlebars !
|
||||
StringBuilder notes = new StringBuilder().append(args.projectToRun.name())
|
||||
.append(" `").append(args.versionFromBom.version)
|
||||
.append("` is part of **`").append(args.releaseTrain().version)
|
||||
.append("` Release Train**.");
|
||||
|
||||
notes.append("\n\n## :warning: Update considerations and deprecations");
|
||||
for (ChangelogEntry noteworthy : entries.getOrDefault(Type.NOTEWORTHY,
|
||||
Collections.emptyList())) {
|
||||
notes.append("\n - ").append(noteworthy.description);
|
||||
for (String issueTitle : noteworthy.associatedIssueLinksAndTitles.values()) {
|
||||
notes.append("\n\t - ").append(cleanupShortMessage(issueTitle));
|
||||
}
|
||||
}
|
||||
|
||||
notes.append("\n\n## :sparkles: New features and improvements");
|
||||
for (ChangelogEntry feature : entries.getOrDefault(Type.FEATURE,
|
||||
Collections.emptyList())) {
|
||||
notes.append("\n - ").append(feature.description);
|
||||
for (String issueTitle : feature.associatedIssueLinksAndTitles.values()) {
|
||||
notes.append("\n\t - ").append(cleanupShortMessage(issueTitle));
|
||||
}
|
||||
}
|
||||
|
||||
notes.append("\n\n## :beetle: Bug fixes");
|
||||
for (ChangelogEntry bug : entries.getOrDefault(Type.BUG,
|
||||
Collections.emptyList())) {
|
||||
notes.append("\n - ").append(bug.description);
|
||||
for (String issueTitle : bug.associatedIssueLinksAndTitles.values()) {
|
||||
notes.append("\n\t - ").append(cleanupShortMessage(issueTitle));
|
||||
}
|
||||
}
|
||||
|
||||
notes.append("\n\n## :book: Documentation, Tests and Build");
|
||||
for (ChangelogEntry misc : entries.getOrDefault(Type.DOC_MISC,
|
||||
Collections.emptyList())) {
|
||||
notes.append("\n - ").append(misc.description);
|
||||
for (String issueTitle : misc.associatedIssueLinksAndTitles.values()) {
|
||||
notes.append("\n\t - ").append(cleanupShortMessage(issueTitle));
|
||||
}
|
||||
}
|
||||
|
||||
notes.append("\n\n## **TODO DISPATCH THESE**");
|
||||
for (ChangelogEntry unclassified : entries.getOrDefault(Type.UNCLASSIFIED,
|
||||
Collections.emptyList())) {
|
||||
notes.append("\n - ").append(unclassified.description);
|
||||
for (String issueTitle : unclassified.associatedIssueLinksAndTitles
|
||||
.values()) {
|
||||
notes.append("\n\t - ").append(cleanupShortMessage(issueTitle));
|
||||
}
|
||||
}
|
||||
|
||||
// contributors
|
||||
notes.append(
|
||||
"\n\n## :+1: Thanks to the following contributors that also participated to this release\n");
|
||||
notes.append(String.join(", ", contributorGithubMentions));
|
||||
|
||||
return notes.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Categorize into a {@link Type} from a set of labels and the commit's short message
|
||||
* (eg. in case of specific message prefix).
|
||||
* @param labels the set of labels found for issues referenced in the commit
|
||||
* @param shortMessage the commit's title/short message
|
||||
* @return a {@link Type} categorizing the commit, or {@link Type#UNCLASSIFIED} if not
|
||||
* clear
|
||||
*/
|
||||
protected EnumSet<Type> extractTypes(Set<String> labels, String shortMessage) {
|
||||
List<Type> types = new ArrayList<>();
|
||||
for (String label : labels) {
|
||||
switch (label) {
|
||||
case "type/bug":
|
||||
types.add(Type.BUG);
|
||||
break;
|
||||
case "type/enhancement":
|
||||
types.add(Type.FEATURE);
|
||||
break;
|
||||
case "type/documentation":
|
||||
case "type/dependency-upgrade":
|
||||
case "type/chores":
|
||||
types.add(Type.DOC_MISC);
|
||||
break;
|
||||
default:
|
||||
if (label.startsWith("warn/")) {
|
||||
types.add(Type.NOTEWORTHY);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (shortMessage.startsWith("[build]") || shortMessage.startsWith("[polish]")
|
||||
|| shortMessage.startsWith("[doc]")) {
|
||||
types.add(Type.DOC_MISC);
|
||||
}
|
||||
|
||||
if (types.isEmpty()) {
|
||||
return EnumSet.of(Type.UNCLASSIFIED);
|
||||
}
|
||||
return EnumSet.copyOf(types);
|
||||
}
|
||||
|
||||
protected String commitToGithubMention(RepoCommits commitsClient,
|
||||
SimpleCommit revCommit) {
|
||||
RepoCommit dumbCommit = commitsClient.get(revCommit.fullSha1);
|
||||
RepoCommit.Smart smartCommit = new RepoCommit.Smart(dumbCommit);
|
||||
try {
|
||||
JsonObject commitJson = smartCommit.json();
|
||||
return "@" + commitJson.getJsonObject("author").getString("login");
|
||||
}
|
||||
catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract at-mentions of contributors, given a list of commits (will fetch
|
||||
* contributor login from github). The list is deduplicated and sorted in
|
||||
* case-insensitive alphabetical order.
|
||||
*/
|
||||
List<String> extractContributorMentions(Repo repo, List<SimpleCommit> revCommits) {
|
||||
RepoCommits commitsClient = repo.commits();
|
||||
return revCommits.stream().map(c -> commitToGithubMention(commitsClient, c))
|
||||
.filter(Objects::nonNull).distinct().sorted(String.CASE_INSENSITIVE_ORDER)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up a short message, removing issue links in suffix and pr prefix forms.
|
||||
*/
|
||||
protected String cleanupShortMessage(String shortMessage) {
|
||||
final Matcher shortMessageMatcher = SHORT_MESSAGE.matcher(shortMessage);
|
||||
if (shortMessageMatcher.matches()) {
|
||||
return shortMessageMatcher.group(1).trim();
|
||||
}
|
||||
return shortMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up the short message of a {@link SimpleCommit}, ie detect message prefix like
|
||||
* "fix #123 Something something (#124)". The prefix up to the issue link is removed,
|
||||
* and so is the PR reference at the end.
|
||||
*/
|
||||
protected String cleanupShortMessage(SimpleCommit commit) {
|
||||
if (!commit.isMergeCommit) {
|
||||
return cleanupShortMessage(commit.title);
|
||||
}
|
||||
return commit.title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract issue numbers from links like #123 in the whole commit message, in order.
|
||||
*/
|
||||
protected Set<Integer> extractIssueNumbers(SimpleCommit commit) {
|
||||
Set<Integer> issueNumbers = new LinkedHashSet<>();
|
||||
Matcher titleIssueMatcher = ISSUE_REF.matcher(commit.title);
|
||||
while (titleIssueMatcher.find()) {
|
||||
issueNumbers.add(Integer.valueOf(titleIssueMatcher.group(1)));
|
||||
}
|
||||
Matcher bodyIssueMatcher = ISSUE_REF.matcher(commit.fullMessage);
|
||||
while (bodyIssueMatcher.find()) {
|
||||
issueNumbers.add(Integer.valueOf(bodyIssueMatcher.group(1)));
|
||||
}
|
||||
return issueNumbers;
|
||||
}
|
||||
|
||||
/**
|
||||
* From the set of issue numbers (see {@link #extractIssueNumbers(SimpleCommit)}),
|
||||
* extract github client Issue objects and fetch labels and titles from these issues.
|
||||
* The labels and titles are injected in a {@link Set} of labels and {@link Map} of
|
||||
* hashtag issue links to issue titles.
|
||||
*/
|
||||
protected void fetchIssueLabelsAndTitles(Issues issueClient,
|
||||
Set<Integer> issueNumbers, Set<String> labelsTarget,
|
||||
Map<String, String> referencedIssuesTarget) {
|
||||
issueNumbers.forEach(i -> {
|
||||
try {
|
||||
// avoid "Smart" issue here as it makes further requests to the server :(
|
||||
// we have everything handy in the JSON
|
||||
Issue issue = issueClient.get(i);
|
||||
JsonObject issueJson = issue.json();
|
||||
issueJson.getJsonArray("labels").forEach(label -> labelsTarget
|
||||
.add(((JsonObject) label).getString("name")));
|
||||
referencedIssuesTarget.put("#" + issueJson.getInt("number"),
|
||||
issueJson.getString("title"));
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("Could not fetch issue information for #" + i, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract issue information from the commit and generate the {@link ChangelogEntry}.
|
||||
*/
|
||||
protected ChangelogEntry parseChangeLogEntry(Issues issueClient,
|
||||
SimpleCommit commit) {
|
||||
String cleanShortMessage = cleanupShortMessage(commit);
|
||||
Set<Integer> issueNumbers = extractIssueNumbers(commit);
|
||||
|
||||
Set<String> labelsTarget = new HashSet<>();
|
||||
Map<String, String> referencedIssuesTarget = new LinkedHashMap<>();
|
||||
fetchIssueLabelsAndTitles(issueClient, issueNumbers, labelsTarget,
|
||||
referencedIssuesTarget);
|
||||
|
||||
EnumSet<Type> types = extractTypes(labelsTarget, commit.title);
|
||||
|
||||
return new ChangelogEntry(types, commit.abbreviatedSha1, cleanShortMessage,
|
||||
referencedIssuesTarget);
|
||||
}
|
||||
|
||||
/*
|
||||
* Pattern specific to reactor commit message convention: `fix #123 Some short
|
||||
* description (#4567)`. We want to capture the middle part and use that in the
|
||||
* release note. The `fix` part is optional, but occurs most of the time unless there
|
||||
* is no issue associated. The `(#xxx)` part is also optional and much more rare,
|
||||
* reflecting the PR number in case there has been extensive review/discussion in that
|
||||
* PR. Both references would be caught by ISSUE_REF pattern below.
|
||||
*/
|
||||
static Pattern SHORT_MESSAGE = Pattern
|
||||
.compile("(?:[a-zA-Z]+ #[0-9]+)?([^\\(]+)(?:\\(#[0-9]+\\))?");
|
||||
|
||||
/**
|
||||
* A {@link Pattern} to find issue/pr numbers in a commit message, by detecting
|
||||
* {@code #}.
|
||||
*/
|
||||
protected static final Pattern ISSUE_REF = Pattern.compile("#([0-9]+)");
|
||||
|
||||
/**
|
||||
* An enum of the 3 types of changes recognized by the changelog template, plus one
|
||||
* type for noteworthy items (eg. breaking changes) and one additional type for the
|
||||
* commits that couldn't be classified (eg. no relevant prefix and no associated
|
||||
* issue).
|
||||
*/
|
||||
protected enum Type {
|
||||
|
||||
NOTEWORTHY, BUG, FEATURE, DOC_MISC, UNCLASSIFIED;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Representation of an entry in the release notes changelog.
|
||||
*/
|
||||
protected static final class ChangelogEntry {
|
||||
|
||||
/**
|
||||
* The set of types of the change, or singleton {@link Type#UNCLASSIFIED} if
|
||||
* unknown.
|
||||
*/
|
||||
public final EnumSet<Type> types;
|
||||
|
||||
/**
|
||||
* The human-friendly description to put in the release note for that commit.
|
||||
*/
|
||||
public final String description;
|
||||
|
||||
/**
|
||||
* The human-friendly SHA1 (typically abbreviated to 8 chars) to reference the
|
||||
* commit in the release note if there is no associated issue.
|
||||
*/
|
||||
public final String commitSha1;
|
||||
|
||||
/**
|
||||
* An ordered map of github-compatible issue links (including the pound sign) and
|
||||
* their titles, to be added to the draft as possible alternative descriptions.
|
||||
* The links are automatically added to the end of the {@link #description} by the
|
||||
* {@link #ChangelogEntry(EnumSet, String, String, Map)} constructor}.
|
||||
*/
|
||||
public final Map<String, String> associatedIssueLinksAndTitles;
|
||||
|
||||
/**
|
||||
* @param types the set of {@link Type} of the commit
|
||||
* @param commitSha1 the commit's human-friendly sha1 (can and should be the
|
||||
* abbreviated form)
|
||||
* @param cleanShortMessage the commit's cleaned up title, as should be displayed
|
||||
* in the release note entry
|
||||
* @param associatedIssueLinksAndTitles the commit's associated
|
||||
* issue(s)/pull-request(s), in order of importance. If non-empty, each issue will
|
||||
* be mentioned at the end of the entry, and the issue titles will be added to the
|
||||
* {@link #description} on their own lines as potential alternative descriptions
|
||||
*/
|
||||
ChangelogEntry(EnumSet<Type> types, String commitSha1, String cleanShortMessage,
|
||||
Map<String, String> associatedIssueLinksAndTitles) {
|
||||
this.types = types;
|
||||
this.commitSha1 = commitSha1;
|
||||
this.associatedIssueLinksAndTitles = associatedIssueLinksAndTitles;
|
||||
|
||||
if (this.associatedIssueLinksAndTitles.isEmpty()) {
|
||||
description = cleanShortMessage + " (" + commitSha1 + ")";
|
||||
}
|
||||
else {
|
||||
this.description = cleanShortMessage + " ("
|
||||
+ String.join(", ", this.associatedIssueLinksAndTitles.keySet())
|
||||
+ ")";
|
||||
}
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,30 +16,26 @@
|
||||
|
||||
package releaser.reactor;
|
||||
|
||||
import org.cloudfoundry.client.CloudFoundryClient;
|
||||
import org.cloudfoundry.doppler.DopplerClient;
|
||||
import com.jcabi.github.Github;
|
||||
import com.jcabi.github.RtGithub;
|
||||
import com.jcabi.http.wire.RetryWire;
|
||||
import org.cloudfoundry.operations.CloudFoundryOperations;
|
||||
import org.cloudfoundry.operations.DefaultCloudFoundryOperations;
|
||||
import org.cloudfoundry.reactor.ConnectionContext;
|
||||
import org.cloudfoundry.reactor.DefaultConnectionContext;
|
||||
import org.cloudfoundry.reactor.TokenProvider;
|
||||
import org.cloudfoundry.reactor.client.ReactorCloudFoundryClient;
|
||||
import org.cloudfoundry.reactor.doppler.ReactorDopplerClient;
|
||||
import org.cloudfoundry.reactor.tokenprovider.PasswordGrantTokenProvider;
|
||||
import org.cloudfoundry.reactor.uaa.ReactorUaaClient;
|
||||
import org.cloudfoundry.uaa.UaaClient;
|
||||
import releaser.internal.Releaser;
|
||||
import releaser.internal.ReleaserProperties;
|
||||
import releaser.internal.git.ProjectGitHandler;
|
||||
|
||||
import org.springframework.beans.factory.BeanInitializationException;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@Configuration
|
||||
@Profile("production")
|
||||
class ReactorConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
CfClient cfClient(CloudFoundryOperations cloudFoundryOperations) {
|
||||
return new CfClient(cloudFoundryOperations);
|
||||
}
|
||||
@@ -50,52 +46,23 @@ class ReactorConfiguration {
|
||||
return new RestartSiteProjectPostReleaseTask(releaser, cfClient, reactorAppName);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
class CfConfiguration {
|
||||
|
||||
@Bean
|
||||
DefaultConnectionContext connectionContext(@Value("${cf.apiHost}") String apiHost) {
|
||||
return DefaultConnectionContext.builder().apiHost(apiHost).build();
|
||||
Github githubClient(ReleaserProperties properties) {
|
||||
if (!StringUtils.hasText(properties.getGit().getOauthToken())) {
|
||||
throw new BeanInitializationException(
|
||||
"You must set the value of the OAuth token. You can do it "
|
||||
+ "either via the command line [--releaser.git.oauth-token=...] "
|
||||
+ "or put it as an env variable in [~/.bashrc] or "
|
||||
+ "[~/.zshrc] e.g. [export RELEASER_GIT_OAUTH_TOKEN=...]");
|
||||
}
|
||||
return new RtGithub(new RtGithub(properties.getGit().getOauthToken()).entry()
|
||||
.through(RetryWire.class));
|
||||
}
|
||||
|
||||
@Bean
|
||||
PasswordGrantTokenProvider tokenProvider(@Value("${cf.username}") String username,
|
||||
@Value("${cf.password}") String password) {
|
||||
return PasswordGrantTokenProvider.builder().password(password).username(username)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactorCloudFoundryClient cloudFoundryClient(ConnectionContext connectionContext,
|
||||
TokenProvider tokenProvider) {
|
||||
return ReactorCloudFoundryClient.builder().connectionContext(connectionContext)
|
||||
.tokenProvider(tokenProvider).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactorDopplerClient dopplerClient(ConnectionContext connectionContext,
|
||||
TokenProvider tokenProvider) {
|
||||
return ReactorDopplerClient.builder().connectionContext(connectionContext)
|
||||
.tokenProvider(tokenProvider).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ReactorUaaClient uaaClient(ConnectionContext connectionContext,
|
||||
TokenProvider tokenProvider) {
|
||||
return ReactorUaaClient.builder().connectionContext(connectionContext)
|
||||
.tokenProvider(tokenProvider).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
DefaultCloudFoundryOperations defaultCloudFoundryOperations(
|
||||
CloudFoundryClient cloudFoundryClient, DopplerClient dopplerClient,
|
||||
UaaClient uaaClient, @Value("${cf.organization}") String organizationId,
|
||||
@Value("${cf.space}") String spaceId) {
|
||||
return DefaultCloudFoundryOperations.builder()
|
||||
.cloudFoundryClient(cloudFoundryClient).dopplerClient(dopplerClient)
|
||||
.uaaClient(uaaClient).organization(organizationId).space(spaceId).build();
|
||||
GenerateReleaseNotesTask releaseNotesTask(Github github,
|
||||
ProjectGitHandler gitHandler) {
|
||||
return new GenerateReleaseNotesTask(github, gitHandler);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,12 +20,10 @@ import org.cloudfoundry.operations.CloudFoundryOperations;
|
||||
import org.cloudfoundry.operations.applications.RestartApplicationRequest;
|
||||
import releaser.internal.Releaser;
|
||||
import releaser.internal.spring.Arguments;
|
||||
import releaser.internal.tasks.DryRunReleaseReleaserTask;
|
||||
import releaser.internal.tasks.release.PublishDocsReleaseTask;
|
||||
import releaser.internal.tech.ExecutionResult;
|
||||
|
||||
public class RestartSiteProjectPostReleaseTask extends PublishDocsReleaseTask
|
||||
implements DryRunReleaseReleaserTask {
|
||||
public class RestartSiteProjectPostReleaseTask extends PublishDocsReleaseTask {
|
||||
|
||||
private static final String REACTOR_CORE_PROJECT_NAME = "reactor-core";
|
||||
|
||||
|
||||
@@ -6,11 +6,16 @@ spring:
|
||||
jackson:
|
||||
deserialization:
|
||||
FAIL_ON_UNKNOWN_PROPERTIES: true
|
||||
profiles:
|
||||
active: production
|
||||
releaser:
|
||||
git:
|
||||
org-name: spring-cloud
|
||||
org-name: reactor
|
||||
release-train-bom-url: https://github.com/reactor/reactor
|
||||
fetch-versions-from-git: true
|
||||
gradle:
|
||||
build-command: "./gradlew clean bumpVersionsInReadme build publishToMavenLocal --console=plain -PnextVersion={{nextVersion}} -PoldVersion={{oldVersion}} -PcurrentVersion={{version}} {{systemProps}}"
|
||||
|
||||
meta-release:
|
||||
release-train-project-name: reactor
|
||||
release-train-dependency-names:
|
||||
@@ -20,4 +25,14 @@ cf:
|
||||
organization: FrameworksAndRuntimes
|
||||
space: Reactor
|
||||
reactorAppName: projectreactor
|
||||
apiHost: api.run.pivotal.io
|
||||
apiHost: api.run.pivotal.io
|
||||
|
||||
# Boot values to be passed via env/command line:
|
||||
# cf.username
|
||||
# cf.password
|
||||
|
||||
# Gradle project properties to be passed to deploy task somehow
|
||||
# artifactory_publish_contextUrl
|
||||
# artifactory_publish_repoKey
|
||||
# artifactory_publish_username
|
||||
# artifactory_publish_password
|
||||
@@ -21,24 +21,21 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import releaser.internal.options.Parser;
|
||||
import releaser.internal.spring.ExecutionResultHandler;
|
||||
import releaser.internal.spring.SpringReleaser;
|
||||
import releaser.internal.tasks.DryRunReleaseReleaserTask;
|
||||
import releaser.internal.tasks.ReleaserTask;
|
||||
import releaser.internal.tasks.release.PublishDocsReleaseTask;
|
||||
import releaser.reactor.GenerateReleaseNotesTask;
|
||||
import releaser.reactor.RestartSiteProjectPostReleaseTask;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
||||
@SpringBootTest(
|
||||
classes = { ReleaserApplicationTests.Config.class, ReleaserApplication.class },
|
||||
properties = { "releaser.sagan.update-sagan=false" })
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@SpringBootTest(classes = { ReleaserApplication.class })
|
||||
@ActiveProfiles("test")
|
||||
class ReleaserApplicationTests {
|
||||
|
||||
@@ -51,31 +48,27 @@ class ReleaserApplicationTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_load_reactor_version_of_publish_docs() {
|
||||
void should_load_generate_release_notes_in_dry_run() {
|
||||
Map<String, DryRunReleaseReleaserTask> beans = context
|
||||
.getBeansOfType(DryRunReleaseReleaserTask.class);
|
||||
List<ReleaserTask> inOrder = new LinkedList<>(beans.values());
|
||||
inOrder.sort(AnnotationAwareOrderComparator.INSTANCE);
|
||||
System.out.println(inOrder);
|
||||
|
||||
assertThat(inOrder).anySatisfy(
|
||||
task -> assertThat(task).isInstanceOf(GenerateReleaseNotesTask.class));
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class Config {
|
||||
@Test
|
||||
void should_load_restart_site() {
|
||||
Map<String, ReleaserTask> beans = context.getBeansOfType(ReleaserTask.class);
|
||||
List<ReleaserTask> inOrder = new LinkedList<>(beans.values());
|
||||
inOrder.sort(AnnotationAwareOrderComparator.INSTANCE);
|
||||
|
||||
@Bean
|
||||
SpringReleaser mockReleaser() {
|
||||
return Mockito.mock(SpringReleaser.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
ExecutionResultHandler mockExecutionResultHandler() {
|
||||
return Mockito.mock(ExecutionResultHandler.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
Parser mockParser() {
|
||||
return Mockito.mock(Parser.class);
|
||||
}
|
||||
assertThat(inOrder).anySatisfy(task -> assertThat(task)
|
||||
.isInstanceOf(RestartSiteProjectPostReleaseTask.class));
|
||||
assertThat(inOrder).noneSatisfy(
|
||||
task -> assertThat(task).isInstanceOf(PublishDocsReleaseTask.class)
|
||||
.isNotInstanceOf(RestartSiteProjectPostReleaseTask.class));
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2013-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 releaser.reactor;
|
||||
|
||||
import org.cloudfoundry.operations.CloudFoundryOperations;
|
||||
import org.mockito.BDDMockito;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
|
||||
/**
|
||||
* @author Simon Baslé
|
||||
*/
|
||||
@Configuration
|
||||
@Profile("test")
|
||||
class CfTestConfiguration {
|
||||
|
||||
@Bean
|
||||
CloudFoundryOperations mockCloudFoundryOperations() {
|
||||
return BDDMockito.mock(CloudFoundryOperations.class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
/*
|
||||
* Copyright 2013-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 releaser.reactor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.jcabi.github.Issue;
|
||||
import com.jcabi.github.Issues;
|
||||
import com.jcabi.github.mock.MkGithub;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.stubbing.VoidAnswer4;
|
||||
import releaser.internal.git.SimpleCommit;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.AdditionalAnswers.answerVoid;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static releaser.reactor.GenerateReleaseNotesTask.Type;
|
||||
|
||||
/**
|
||||
* @author Simon Baslé
|
||||
*/
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("test")
|
||||
class GenerateReleaseNotesTaskTest {
|
||||
|
||||
@Autowired
|
||||
GenerateReleaseNotesTask task;
|
||||
|
||||
@Autowired
|
||||
MkGithub githubClient;
|
||||
|
||||
@Test
|
||||
void extractTypeFromLabel() {
|
||||
assertThat(task.extractTypes(Collections.singleton("type/bug"), ""))
|
||||
.as("type/bug").containsOnly(Type.BUG);
|
||||
|
||||
assertThat(task.extractTypes(Collections.singleton("type/enhancement"), ""))
|
||||
.as("type/enhancement").containsOnly(Type.FEATURE);
|
||||
|
||||
assertThat(task.extractTypes(Collections.singleton("type/documentation"), ""))
|
||||
.as("type/documentation").containsOnly(Type.DOC_MISC);
|
||||
|
||||
assertThat(task.extractTypes(Collections.singleton("type/chores"), ""))
|
||||
.as("type/chores").containsOnly(Type.DOC_MISC);
|
||||
|
||||
assertThat(task.extractTypes(Collections.singleton("warn/something"), ""))
|
||||
.as("warn/*").containsOnly(Type.NOTEWORTHY);
|
||||
|
||||
assertThat(task.extractTypes(Collections.singleton("type/whatever"), ""))
|
||||
.as("type/whatever").containsOnly(Type.UNCLASSIFIED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void extractTypeFromMultipleLabels() {
|
||||
Set<String> labels = new LinkedHashSet<>();
|
||||
labels.add("type/bug");
|
||||
labels.add("type/enhancement");
|
||||
labels.add("type/documentation");
|
||||
labels.add("whatever");
|
||||
|
||||
assertThat(task.extractTypes(labels, "")).as("multiple labels")
|
||||
.containsOnly(Type.BUG, Type.FEATURE, Type.DOC_MISC);
|
||||
}
|
||||
|
||||
@Test
|
||||
void extractMiscTypeFromBuildMessagePrefix() {
|
||||
String message = "[build] Foo";
|
||||
|
||||
assertThat(task.extractTypes(Collections.emptySet(), message))
|
||||
.containsOnly(Type.DOC_MISC);
|
||||
}
|
||||
|
||||
@Test
|
||||
void extractMiscTypeFromPolishMessagePrefix() {
|
||||
String message = "[polish] Foo";
|
||||
|
||||
assertThat(task.extractTypes(Collections.emptySet(), message))
|
||||
.containsOnly(Type.DOC_MISC);
|
||||
}
|
||||
|
||||
@Test
|
||||
void extractMiscTypeFromDocMessagePrefix() {
|
||||
String message = "[doc] Foo";
|
||||
|
||||
assertThat(task.extractTypes(Collections.emptySet(), message))
|
||||
.containsOnly(Type.DOC_MISC);
|
||||
}
|
||||
|
||||
@Test
|
||||
void extractUnclassifiedTypeFromRandomMessagePrefix() {
|
||||
String message = "fix #123 There was a [bug], needed to [polish] the [doc]";
|
||||
|
||||
assertThat(task.extractTypes(Collections.emptySet(), message))
|
||||
.containsOnly(Type.UNCLASSIFIED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void noTitleCleanupFromMergeCommit() {
|
||||
SimpleCommit mergeCommit = new SimpleCommit("sha1", "fullsha1",
|
||||
"merge #123 into 3.3 (#123)", "merge #123 into 3.3", "Simon Baslé",
|
||||
"sbasle@pivotal.io", "Simon Baslé", "sbasle@pivotal.io", true);
|
||||
|
||||
assertThat(task.cleanupShortMessage(mergeCommit))
|
||||
.isEqualTo("merge #123 into 3.3 (#123)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void titleCleanupFixPrefix() {
|
||||
SimpleCommit commit = new SimpleCommit("sha1", "fullsha1",
|
||||
"fix #123 Text from title", "fix #123 Some more text", "Simon Baslé",
|
||||
"sbasle@pivotal.io", "Simon Baslé", "sbasle@pivotal.io", false);
|
||||
|
||||
assertThat(task.cleanupShortMessage(commit)).isEqualTo("Text from title");
|
||||
}
|
||||
|
||||
@Test
|
||||
void titleCleanupSeePrefix() {
|
||||
SimpleCommit commit = new SimpleCommit("sha1", "fullsha1",
|
||||
"see #123 Text from title", "see #123 Some more text", "Simon Baslé",
|
||||
"sbasle@pivotal.io", "Simon Baslé", "sbasle@pivotal.io", false);
|
||||
|
||||
assertThat(task.cleanupShortMessage(commit)).isEqualTo("Text from title");
|
||||
}
|
||||
|
||||
@Test
|
||||
void titleCleanupPrStyleSuffix() {
|
||||
SimpleCommit commit = new SimpleCommit("sha1", "fullsha1",
|
||||
"Commit without issue (#123)", "fullMessage", "Simon Baslé",
|
||||
"sbasle@pivotal.io", "Simon Baslé", "sbasle@pivotal.io", false);
|
||||
|
||||
assertThat(task.cleanupShortMessage(commit)).isEqualTo("Commit without issue");
|
||||
}
|
||||
|
||||
@Test
|
||||
void titleCleanupPrStyleSuffixNoSpace() {
|
||||
SimpleCommit commit = new SimpleCommit("sha1", "fullsha1",
|
||||
"Commit without issue(#123)", "fullMessage", "Simon Baslé",
|
||||
"sbasle@pivotal.io", "Simon Baslé", "sbasle@pivotal.io", false);
|
||||
|
||||
assertThat(task.cleanupShortMessage(commit)).isEqualTo("Commit without issue");
|
||||
}
|
||||
|
||||
@Test
|
||||
void titleCleanupBothPrefixAndSuffix() {
|
||||
SimpleCommit commit = new SimpleCommit("sha1", "fullsha1",
|
||||
"prefix #123 Commit title (#123)", "fullMessage", "Simon Baslé",
|
||||
"sbasle@pivotal.io", "Simon Baslé", "sbasle@pivotal.io", false);
|
||||
|
||||
assertThat(task.cleanupShortMessage(commit)).isEqualTo("Commit title");
|
||||
}
|
||||
|
||||
@Test
|
||||
void issueNumberTitlePrefix() {
|
||||
SimpleCommit commit = new SimpleCommit("sha1", "fullsha1",
|
||||
"prefix #123 Commit title", "fullMessage", "Simon Baslé",
|
||||
"sbasle@pivotal.io", "Simon Baslé", "sbasle@pivotal.io", false);
|
||||
|
||||
assertThat(task.extractIssueNumbers(commit)).containsOnly(123);
|
||||
}
|
||||
|
||||
@Test
|
||||
void issueNumberTitlePrStyleSuffix() {
|
||||
SimpleCommit commit = new SimpleCommit("sha1", "fullsha1", "Commit title (#123)",
|
||||
"fullMessage", "Simon Baslé", "sbasle@pivotal.io", "Simon Baslé",
|
||||
"sbasle@pivotal.io", false);
|
||||
|
||||
assertThat(task.extractIssueNumbers(commit)).containsOnly(123);
|
||||
}
|
||||
|
||||
@Test
|
||||
void issueNumberTitlePrefixAndSuffix() {
|
||||
SimpleCommit commit = new SimpleCommit("sha1", "fullsha1",
|
||||
"prefix #123 Commit title (#456)", "fullMessage", "Simon Baslé",
|
||||
"sbasle@pivotal.io", "Simon Baslé", "sbasle@pivotal.io", false);
|
||||
|
||||
assertThat(task.extractIssueNumbers(commit)).containsOnly(123, 456);
|
||||
}
|
||||
|
||||
@Test
|
||||
void issueNumberTitleNotSeparatedBySpace() {
|
||||
SimpleCommit commit = new SimpleCommit("sha1", "fullsha1",
|
||||
"prefix#123Commit title(#456)", "fullMessage", "Simon Baslé",
|
||||
"sbasle@pivotal.io", "Simon Baslé", "sbasle@pivotal.io", false);
|
||||
|
||||
assertThat(task.extractIssueNumbers(commit)).containsOnly(123, 456);
|
||||
}
|
||||
|
||||
static final VoidAnswer4<Issues, Set<Integer>, Set<String>, Map<String, String>> MOCK_FETCH_ISSUES = (
|
||||
ignore1, issues, ignore2, resolved) -> issues
|
||||
.forEach(i -> resolved.put("#" + i, "alternative title for " + i));
|
||||
|
||||
static final Issues MOCK_ISSUES = Mockito.mock(Issues.class);
|
||||
|
||||
@Test
|
||||
void generateChangelogDescriptionSingleIssueTwice() throws IOException {
|
||||
final GenerateReleaseNotesTask spy = Mockito.spy(task);
|
||||
doAnswer(answerVoid(MOCK_FETCH_ISSUES)).when(spy).fetchIssueLabelsAndTitles(any(),
|
||||
any(), any(), any());
|
||||
|
||||
SimpleCommit commit = new SimpleCommit("sha1", "fullsha1",
|
||||
"prefix #123 Commit title (#123)", "fullMessage", "Simon Baslé",
|
||||
"sbasle@pivotal.io", "Simon Baslé", "sbasle@pivotal.io", false);
|
||||
|
||||
assertThat(spy.parseChangeLogEntry(githubClient.randomRepo().issues(),
|
||||
commit).description).isEqualTo("Commit title (#123)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateChangelogDescriptionTwoIssues() throws IOException {
|
||||
final GenerateReleaseNotesTask spy = Mockito.spy(task);
|
||||
doAnswer(answerVoid(MOCK_FETCH_ISSUES)).when(spy).fetchIssueLabelsAndTitles(any(),
|
||||
any(), any(), any());
|
||||
|
||||
SimpleCommit commit = new SimpleCommit("sha1", "fullsha1",
|
||||
"prefix #123 Commit title (#456)", "fullMessage", "Simon Baslé",
|
||||
"sbasle@pivotal.io", "Simon Baslé", "sbasle@pivotal.io", false);
|
||||
|
||||
assertThat(spy.parseChangeLogEntry(MOCK_ISSUES, commit).description)
|
||||
.isEqualTo("Commit title (#123, #456)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateChangelogDescriptionTwoIssuesNoSpace() throws IOException {
|
||||
final GenerateReleaseNotesTask spy = Mockito.spy(task);
|
||||
doAnswer(answerVoid(MOCK_FETCH_ISSUES)).when(spy).fetchIssueLabelsAndTitles(any(),
|
||||
any(), any(), any());
|
||||
|
||||
SimpleCommit commit = new SimpleCommit("sha1", "fullsha1",
|
||||
"prefix #123Commit title no space(#456)", "fullMessage", "Simon Baslé",
|
||||
"sbasle@pivotal.io", "Simon Baslé", "sbasle@pivotal.io", false);
|
||||
|
||||
assertThat(spy.parseChangeLogEntry(githubClient.randomRepo().issues(),
|
||||
commit).description).isEqualTo("Commit title no space (#123, #456)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateChangelogDescriptionNoMatchingIssueUsesShortSha1() throws IOException {
|
||||
final GenerateReleaseNotesTask spy = Mockito.spy(task);
|
||||
doNothing().when(spy).fetchIssueLabelsAndTitles(any(), any(), any(), any());
|
||||
|
||||
SimpleCommit commit = new SimpleCommit("sha1", "fullsha1",
|
||||
"prefix #123 Commit title no space(#456)", "fullMessage", "Simon Baslé",
|
||||
"sbasle@pivotal.io", "Simon Baslé", "sbasle@pivotal.io", false);
|
||||
|
||||
assertThat(spy.parseChangeLogEntry(githubClient.randomRepo().issues(),
|
||||
commit).description).isEqualTo("Commit title no space (sha1)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void issueInfoFetchProtectedWhenIssueNotFound() throws IOException {
|
||||
Issues issuesClient = githubClient.randomRepo().issues();
|
||||
Set<String> labels = new HashSet<>();
|
||||
Map<String, String> associatedIssues = new HashMap<>();
|
||||
|
||||
assertThatExceptionOfType(Exception.class).as("github client fails")
|
||||
.isThrownBy(() -> new Issue.Smart(issuesClient.get(123)).title());
|
||||
|
||||
assertThatCode(() -> task.fetchIssueLabelsAndTitles(issuesClient,
|
||||
Collections.singleton(123), labels, associatedIssues))
|
||||
.as("fetching just does nothing").doesNotThrowAnyException();
|
||||
|
||||
assertThat(labels).as("labels").isEmpty();
|
||||
assertThat(associatedIssues).as("associated issues").isEmpty();
|
||||
}
|
||||
|
||||
// TODO test login extraction by mocking the task's commitToGithubMention method
|
||||
|
||||
// TODO find a way to mock commits and thus test extractContributors
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2013-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 releaser.reactor;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import com.jcabi.github.Github;
|
||||
import com.jcabi.github.mock.MkGithub;
|
||||
import org.mockito.BDDMockito;
|
||||
import org.mockito.Mockito;
|
||||
import releaser.internal.Releaser;
|
||||
import releaser.internal.git.ProjectGitHandler;
|
||||
import releaser.internal.options.Parser;
|
||||
import releaser.internal.spring.ExecutionResultHandler;
|
||||
import releaser.internal.spring.SpringReleaser;
|
||||
|
||||
import org.springframework.beans.factory.BeanCreationException;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
|
||||
/**
|
||||
* A configuration for tests, that mocks external clients but reuses the base bean
|
||||
* declaration for tasks.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
*/
|
||||
@Configuration
|
||||
@Profile("test")
|
||||
public class ReactorTestConfiguration {
|
||||
|
||||
@Bean
|
||||
RestartSiteProjectPostReleaseTask restartSiteProjectPostReleaseTask(Releaser releaser,
|
||||
CfClient cfClient, @Value("${cf.reactorAppName}") String reactorAppName) {
|
||||
return new RestartSiteProjectPostReleaseTask(releaser, cfClient, reactorAppName);
|
||||
}
|
||||
|
||||
@Bean
|
||||
GenerateReleaseNotesTask releaseNotesTask(Github github,
|
||||
ProjectGitHandler gitHandler) {
|
||||
return new GenerateReleaseNotesTask(github, gitHandler);
|
||||
}
|
||||
|
||||
@Bean
|
||||
ProjectGitHandler mockGitHandler() {
|
||||
return Mockito.mock(ProjectGitHandler.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
MkGithub mockGithub() {
|
||||
try {
|
||||
return new MkGithub();
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new BeanCreationException("Unable to create mock Github bean", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
CfClient mockCfClient() {
|
||||
return BDDMockito.mock(CfClient.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
SpringReleaser mockReleaser() {
|
||||
return Mockito.mock(SpringReleaser.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
ExecutionResultHandler mockExecutionResultHandler() {
|
||||
return Mockito.mock(ExecutionResultHandler.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
Parser mockParser() {
|
||||
return Mockito.mock(Parser.class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,10 +17,8 @@
|
||||
package releaser.reactor;
|
||||
|
||||
import org.assertj.core.api.BDDAssertions;
|
||||
import org.cloudfoundry.operations.CloudFoundryOperations;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.BDDMockito;
|
||||
import releaser.internal.Releaser;
|
||||
import releaser.internal.ReleaserProperties;
|
||||
import releaser.internal.options.Options;
|
||||
import releaser.internal.project.ProjectVersion;
|
||||
@@ -31,13 +29,10 @@ import releaser.internal.spring.ProjectsFromBom;
|
||||
import releaser.internal.tech.ExecutionResult;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
||||
@SpringBootTest(classes = RestartSiteProjectPostReleaseTaskTests.Config.class)
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("test")
|
||||
class RestartSiteProjectPostReleaseTaskTests {
|
||||
|
||||
@@ -92,25 +87,4 @@ class RestartSiteProjectPostReleaseTaskTests {
|
||||
};
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableAutoConfiguration
|
||||
static class Config extends ReactorConfiguration {
|
||||
|
||||
@Bean
|
||||
Releaser mockReleaser() {
|
||||
return BDDMockito.mock(Releaser.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
CloudFoundryOperations mockCloudFoundryOperations() {
|
||||
return BDDMockito.mock(CloudFoundryOperations.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
CfClient cfClient(CloudFoundryOperations cloudFoundryOperations) {
|
||||
return BDDMockito.mock(CfClient.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
cf:
|
||||
username: foo
|
||||
password: bar
|
||||
password: bar
|
||||
spring:
|
||||
profiles:
|
||||
active: test
|
||||
@@ -446,7 +446,7 @@ public class ReleaserProperties implements Serializable {
|
||||
private String documentationUrl;
|
||||
|
||||
/**
|
||||
* URL to the release train project page repository.
|
||||
* The organization name on Github.
|
||||
*/
|
||||
@NotBlank
|
||||
private String orgName;
|
||||
|
||||
@@ -19,7 +19,9 @@ package releaser.internal.git;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import com.jcraft.jsch.IdentityRepository;
|
||||
import com.jcraft.jsch.JSch;
|
||||
@@ -42,6 +44,7 @@ import org.eclipse.jgit.api.ResetCommand;
|
||||
import org.eclipse.jgit.api.TransportConfigCallback;
|
||||
import org.eclipse.jgit.api.errors.EmptyCommitException;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
import org.eclipse.jgit.transport.CredentialsProvider;
|
||||
@@ -175,6 +178,92 @@ class GitRepo {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to retrieve a tag id from a name, prepending the name with /refs/tags/.
|
||||
*/
|
||||
Optional<ObjectId> findTagIdByName(String tagName, boolean unpeel) {
|
||||
try (Git git = this.gitFactory.open(file(this.basedir))) {
|
||||
return git.tagList().call().stream()
|
||||
.filter(ref -> ref.getName().equals("refs/tags/" + tagName))
|
||||
.findFirst().map(ref -> {
|
||||
if (ref.isPeeled() && unpeel) {
|
||||
return ref.getPeeledObjectId();
|
||||
}
|
||||
return ref.getObjectId();
|
||||
});
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new IllegalStateException(
|
||||
"Unable to fetch git tag id for refs/tags/" + tagName, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs {@link RevCommit} between two tags / branches / hashes.
|
||||
* @param from oldest revision
|
||||
* @param to newest revision
|
||||
*/
|
||||
List<RevCommit> log(String from, String to) {
|
||||
try (Git git = this.gitFactory.open(file(this.basedir))) {
|
||||
final Optional<Ref> fromRevisionOptional = findTagOrBranchHeadRevision(git,
|
||||
from);
|
||||
final Optional<Ref> toRevisionOptional = findTagOrBranchHeadRevision(git, to);
|
||||
|
||||
ObjectId fromRevision;
|
||||
if (fromRevisionOptional.isPresent()) {
|
||||
Ref ref = fromRevisionOptional.get();
|
||||
if (ref.isPeeled()) {
|
||||
fromRevision = ref.getPeeledObjectId();
|
||||
}
|
||||
else {
|
||||
fromRevision = ref.getObjectId();
|
||||
}
|
||||
}
|
||||
else {
|
||||
fromRevision = ObjectId.fromString(from);
|
||||
}
|
||||
|
||||
ObjectId toRevision;
|
||||
if (toRevisionOptional.isPresent()) {
|
||||
Ref ref = toRevisionOptional.get();
|
||||
if (ref.isPeeled()) {
|
||||
toRevision = ref.getPeeledObjectId();
|
||||
}
|
||||
else {
|
||||
toRevision = ref.getObjectId();
|
||||
}
|
||||
}
|
||||
else {
|
||||
toRevision = ObjectId.fromString(to);
|
||||
}
|
||||
|
||||
LinkedList<RevCommit> commits = new LinkedList<>();
|
||||
git.log().addRange(fromRevision, toRevision).call().forEach(commits::add);
|
||||
return commits;
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new IllegalStateException(
|
||||
"Unable to fetch git log for " + from + ".." + to, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for a tag with the given name, and if not found looks for a branch.
|
||||
*/
|
||||
private Optional<Ref> findTagOrBranchHeadRevision(Git git, String tagOrBranch)
|
||||
throws GitAPIException {
|
||||
final Optional<Ref> tag = git.tagList().call().stream()
|
||||
.filter(ref -> ref.getName().equals("refs/tags/" + tagOrBranch))
|
||||
.findFirst();
|
||||
if (tag.isPresent()) {
|
||||
return tag;
|
||||
}
|
||||
|
||||
return git.branchList().setListMode(ListBranchCommand.ListMode.ALL).call()
|
||||
.stream().filter(ref -> ref.getName().equals("refs/heads/" + tagOrBranch))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
boolean hasBranch(String branch) {
|
||||
try (Git git = this.gitFactory.open(file(this.basedir))) {
|
||||
List<Ref> refs = git.branchList().setListMode(ListBranchCommand.ListMode.ALL)
|
||||
@@ -192,6 +281,8 @@ class GitRepo {
|
||||
}
|
||||
|
||||
private String nameOfBranch(String branch) {
|
||||
// TODO careful: this doesn't take into account branches that follow a pattern
|
||||
// like `experiments/foo`
|
||||
return branch.substring(branch.lastIndexOf("/") + 1);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,9 +19,13 @@ package releaser.internal.git;
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jgit.lib.AnyObjectId;
|
||||
import org.eclipse.jgit.transport.URIish;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -191,6 +195,31 @@ public class ProjectGitHandler implements ReleaserPropertiesAware, Closeable {
|
||||
return clonedProject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the commits between two versions. The second version is actually turned into
|
||||
* the head of the relevant branch.
|
||||
* @param clonedProject location of the cloned project
|
||||
* @param fromRef the ref to start from (tag, branch or sha1)
|
||||
* @param toRef the ref to go to (tag, branch or sha1)
|
||||
* @return the list of revisions between these two references
|
||||
*/
|
||||
public List<SimpleCommit> commitsBetween(File clonedProject, String fromRef,
|
||||
String toRef) {
|
||||
return gitRepo(clonedProject).log(fromRef, toRef).stream().map(SimpleCommit::new)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to find the SHA1 of a named tag (without the refs/tags/ prefix).
|
||||
* @param clonedProject location of the cloned project
|
||||
* @param tagName the name of the tag to find
|
||||
* @return an {@link Optional} that is valued with the sha1, if found
|
||||
*/
|
||||
public Optional<String> findTagSha1(File clonedProject, String tagName) {
|
||||
return gitRepo(clonedProject).findTagIdByName(tagName, false)
|
||||
.map(AnyObjectId::getName);
|
||||
}
|
||||
|
||||
private String suffixNonHttpRepo(String orgUrl) {
|
||||
return orgUrl.startsWith("http") || orgUrl.startsWith("git") ? "" : "/";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2013-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 releaser.internal.git;
|
||||
|
||||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
|
||||
/**
|
||||
* A simple representation of a git commit.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
*/
|
||||
public class SimpleCommit {
|
||||
|
||||
/**
|
||||
* The abbreviated SHA1 of the commit, useful for human-readable output.
|
||||
*/
|
||||
public final String abbreviatedSha1;
|
||||
|
||||
/**
|
||||
* The full SHA1 of the commit, useful to identify it.
|
||||
*/
|
||||
public final String fullSha1;
|
||||
|
||||
/**
|
||||
* The title line of the commit (first line of the fullMessage).
|
||||
*/
|
||||
public final String title;
|
||||
|
||||
/**
|
||||
* The full commit message, including the title, body and separating newlines.
|
||||
*/
|
||||
public final String fullMessage;
|
||||
|
||||
/**
|
||||
* The name of the author of the commit, who contributed the code.
|
||||
*/
|
||||
public final String authorName;
|
||||
|
||||
/**
|
||||
* The email of the author of the commit, who contributed the code.
|
||||
*/
|
||||
public final String authorEmail;
|
||||
|
||||
/**
|
||||
* The name of the committer of the commit, who pushed the code to the repository.
|
||||
*/
|
||||
public final String committerName;
|
||||
|
||||
/**
|
||||
* The email of the committer of the commit, who pushed the code to the repository.
|
||||
*/
|
||||
public final String committerEmail;
|
||||
|
||||
/**
|
||||
* Is the commit a merge commit, ie. it has two parents.
|
||||
*/
|
||||
public final boolean isMergeCommit;
|
||||
|
||||
SimpleCommit(RevCommit revCommit) {
|
||||
this(revCommit.abbreviate(8).name(), revCommit.name(),
|
||||
revCommit.getShortMessage(), revCommit.getFullMessage(),
|
||||
revCommit.getAuthorIdent().getName(),
|
||||
revCommit.getAuthorIdent().getEmailAddress(),
|
||||
revCommit.getCommitterIdent().getName(),
|
||||
revCommit.getCommitterIdent().getEmailAddress(),
|
||||
revCommit.getParentCount() > 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link SimpleCommit}.
|
||||
* @param abbreviatedSha1 the short version of the SHA-1, for human-readable output
|
||||
* @param fullSha1 the full SHA-1
|
||||
* @param title the first line of the commit message
|
||||
* @param fullMessage the full commit message, including title
|
||||
* @param authorName the name of the commit's author
|
||||
* @param authorEmail the email of the commit's author
|
||||
* @param committerName the name of the commit's committer
|
||||
* @param committerEmail the email of the commit's committer
|
||||
* @param isMergeCommit is the commit a merge commit (with 2 parents)
|
||||
*/
|
||||
public SimpleCommit(String abbreviatedSha1, String fullSha1, String title,
|
||||
String fullMessage, String authorName, String authorEmail,
|
||||
String committerName, String committerEmail, boolean isMergeCommit) {
|
||||
this.abbreviatedSha1 = abbreviatedSha1;
|
||||
this.fullSha1 = fullSha1;
|
||||
this.title = title;
|
||||
this.fullMessage = fullMessage;
|
||||
this.authorName = authorName;
|
||||
this.authorEmail = authorEmail;
|
||||
this.committerName = committerName;
|
||||
this.committerEmail = committerEmail;
|
||||
this.isMergeCommit = isMergeCommit;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -88,7 +88,7 @@ public class ProjectCommandExecutor implements ReleaserPropertiesAware {
|
||||
try {
|
||||
String projectRoot = this.properties.getWorkingDir();
|
||||
String[] commands = command.split(" ");
|
||||
return runCommand(projectRoot, commands);
|
||||
return runCommand(projectRoot, commands).trim();
|
||||
}
|
||||
catch (IllegalStateException e) {
|
||||
throw e;
|
||||
@@ -319,8 +319,7 @@ class ProcessExecutor implements ReleaserPropertiesAware {
|
||||
commandsToRun = commandToExecute(lastArg);
|
||||
}
|
||||
log.info("Will run the command [{}]", Arrays.toString(commandsToRun));
|
||||
return new ProcessBuilder(commandsToRun).directory(new File(workingDir))
|
||||
.inheritIO();
|
||||
return new ProcessBuilder(commandsToRun).directory(new File(workingDir));
|
||||
}
|
||||
|
||||
String[] commandToExecute(String lastArg) {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package releaser.internal.github;
|
||||
|
||||
import com.jcabi.github.Github;
|
||||
import com.jcabi.github.RtGithub;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
@@ -32,7 +33,7 @@ class GithubConfiguration {
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName)
|
||||
throws BeansException {
|
||||
if (bean instanceof Github) {
|
||||
if (bean instanceof RtGithub) {
|
||||
return new CachingGithub((Github) bean);
|
||||
}
|
||||
return bean;
|
||||
|
||||
@@ -36,7 +36,7 @@ class SaganConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnProperty(value = "releaser.sagan.update-sagan", matchIfMissing = true)
|
||||
@ConditionalOnProperty(value = "releaser.sagan.update-sagan", havingValue = "true")
|
||||
SaganClient saganClient(ReleaserProperties properties) {
|
||||
RestTemplate restTemplate = restTemplate(properties);
|
||||
return new RestTemplateSaganClient(restTemplate, properties);
|
||||
@@ -44,7 +44,8 @@ class SaganConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnProperty(value = "releaser.sagan.update-sagan", havingValue = "false")
|
||||
@ConditionalOnProperty(value = "releaser.sagan.update-sagan", havingValue = "false",
|
||||
matchIfMissing = true)
|
||||
SaganClient noOpSaganClient() {
|
||||
return new SaganClient() {
|
||||
@Override
|
||||
|
||||
@@ -56,8 +56,8 @@ class BatchConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
ExecutionResultHandler springBatchExecutionResultHandler(JobExplorer jobExplorer,
|
||||
ConfigurableApplicationContext context) {
|
||||
SpringBatchExecutionResultHandler springBatchExecutionResultHandler(
|
||||
JobExplorer jobExplorer, ConfigurableApplicationContext context) {
|
||||
return new SpringBatchExecutionResultHandler(jobExplorer, context);
|
||||
}
|
||||
|
||||
@@ -74,11 +74,11 @@ class BatchConfiguration {
|
||||
JobBuilderFactory jobBuilderFactory,
|
||||
ProjectsToRunFactory projectsToRunFactory, JobLauncher jobLauncher,
|
||||
FlowRunnerTaskExecutorSupplier flowRunnerTaskExecutorSupplier,
|
||||
ConfigurableApplicationContext context,
|
||||
ReleaserProperties releaserProperties) {
|
||||
ConfigurableApplicationContext context, ReleaserProperties releaserProperties,
|
||||
BuildReportHandler reportHandler) {
|
||||
return new SpringBatchFlowRunner(stepBuilderFactory, jobBuilderFactory,
|
||||
projectsToRunFactory, jobLauncher, flowRunnerTaskExecutorSupplier,
|
||||
context, releaserProperties);
|
||||
context, releaserProperties, reportHandler);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2013-2019 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 releaser.internal.spring;
|
||||
|
||||
/**
|
||||
* Handles reporting the results of the build. Similar to {@link ExecutionResultHandler}
|
||||
* with the difference that it could be invoked before the final result is determined, and
|
||||
* shouldn't handle exiting the application.
|
||||
*/
|
||||
public interface BuildReportHandler {
|
||||
|
||||
/**
|
||||
* Display a report summarizing the state of the build so far. Can be invoked during
|
||||
* normal execution, so it should filter out eg. tasks that are running.
|
||||
*/
|
||||
void reportBuildSummary();
|
||||
|
||||
}
|
||||
@@ -43,7 +43,8 @@ import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.core.NestedExceptionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
class SpringBatchExecutionResultHandler implements ExecutionResultHandler {
|
||||
class SpringBatchExecutionResultHandler
|
||||
implements ExecutionResultHandler, BuildReportHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory
|
||||
.getLogger(SpringBatchExecutionResultHandler.class);
|
||||
@@ -60,7 +61,7 @@ class SpringBatchExecutionResultHandler implements ExecutionResultHandler {
|
||||
|
||||
@Override
|
||||
public void accept(ExecutionResult executionResult) {
|
||||
buildSummaryTable();
|
||||
reportBuildSummary();
|
||||
if (executionResult.isFailure()) {
|
||||
log.error("At least one failure occurred while running the release process",
|
||||
executionResult.foundExceptions());
|
||||
@@ -79,21 +80,21 @@ class SpringBatchExecutionResultHandler implements ExecutionResultHandler {
|
||||
}
|
||||
|
||||
void exitSuccessfully() {
|
||||
SpringApplication.exit(this.context, () -> 0);
|
||||
System.exit(0);
|
||||
System.exit(SpringApplication.exit(this.context, () -> 0));
|
||||
}
|
||||
|
||||
void exitWithException() {
|
||||
SpringApplication.exit(this.context, () -> 1);
|
||||
System.exit(1);
|
||||
System.exit(SpringApplication.exit(this.context, () -> 1));
|
||||
}
|
||||
|
||||
private void buildSummaryTable() {
|
||||
@Override
|
||||
public void reportBuildSummary() {
|
||||
List<String> jobNames = this.jobExplorer.getJobNames();
|
||||
List<JobExecution> sortedJobExecutions = jobNames.stream()
|
||||
.flatMap(name -> this.jobExplorer.findJobInstancesByJobName(name, 0, 100)
|
||||
.stream())
|
||||
.flatMap(instance -> this.jobExplorer.getJobExecutions(instance).stream())
|
||||
.filter(j -> !j.isRunning())
|
||||
.sorted(Comparator.comparing(JobExecution::getCreateTime))
|
||||
.collect(Collectors.toList());
|
||||
List<StepExecution> stepContexts = sortedJobExecutions.stream()
|
||||
|
||||
@@ -93,14 +93,14 @@ class SpringBatchFlowRunner implements FlowRunner, Closeable {
|
||||
JobBuilderFactory jobBuilderFactory,
|
||||
ProjectsToRunFactory projectsToRunFactory, JobLauncher jobLauncher,
|
||||
FlowRunnerTaskExecutorSupplier flowRunnerTaskExecutorSupplier,
|
||||
ConfigurableApplicationContext context,
|
||||
ReleaserProperties releaserProperties) {
|
||||
ConfigurableApplicationContext context, ReleaserProperties releaserProperties,
|
||||
BuildReportHandler reportHandler) {
|
||||
this.stepBuilderFactory = stepBuilderFactory;
|
||||
this.jobBuilderFactory = jobBuilderFactory;
|
||||
this.projectsToRunFactory = projectsToRunFactory;
|
||||
this.jobLauncher = jobLauncher;
|
||||
this.flowRunnerTaskExecutorSupplier = flowRunnerTaskExecutorSupplier;
|
||||
this.stepSkipper = new ConsoleInputStepSkipper(context);
|
||||
this.stepSkipper = new ConsoleInputStepSkipper(context, reportHandler);
|
||||
this.releaserProperties = releaserProperties;
|
||||
this.executorService = Executors.newFixedThreadPool(
|
||||
this.releaserProperties.getMetaRelease().getReleaseGroupThreadCount());
|
||||
@@ -534,8 +534,12 @@ class ConsoleInputStepSkipper {
|
||||
|
||||
private final ConfigurableApplicationContext context;
|
||||
|
||||
ConsoleInputStepSkipper(ConfigurableApplicationContext context) {
|
||||
private final BuildReportHandler reportHandler;
|
||||
|
||||
ConsoleInputStepSkipper(ConfigurableApplicationContext context,
|
||||
BuildReportHandler reportHandler) {
|
||||
this.context = context;
|
||||
this.reportHandler = reportHandler;
|
||||
}
|
||||
|
||||
public boolean skipStep() {
|
||||
@@ -544,8 +548,8 @@ class ConsoleInputStepSkipper {
|
||||
case "s":
|
||||
return true;
|
||||
case "q":
|
||||
SpringApplication.exit(this.context, () -> 0);
|
||||
System.exit(0);
|
||||
reportHandler.reportBuildSummary();
|
||||
System.exit(SpringApplication.exit(this.context, () -> 0));
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user