Revert "Spring Boot 3 migration (#255)"

This reverts commit aa5d636de3.
This commit is contained in:
Olga MaciaszekSharma
2023-07-03 15:01:26 +02:00
parent 7f525607ef
commit 9082da0a82
143 changed files with 9410 additions and 864 deletions

View File

@@ -5,19 +5,20 @@
<modelVersion>4.0.0</modelVersion>
<artifactId>releaser-projects</artifactId>
<version>3.0.0-SNAPSHOT</version>
<version>2.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.cloud.internal</groupId>
<artifactId>releaser-parent</artifactId>
<version>3.0.0-SNAPSHOT</version>
<version>2.0.0-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<modules>
<module>spring-cloud</module>
<module>spring-cloud-stream</module>
<module>reactor</module>
</modules>
</project>

0
projects/reactor/.jdk8 Normal file
View File

107
projects/reactor/pom.xml Normal file
View File

@@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>reactor</artifactId>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.cloud.internal</groupId>
<artifactId>releaser-projects</artifactId>
<version>2.0.0-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<cloudfoundry-client.version>5.7.0.RELEASE</cloudfoundry-client.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud.internal</groupId>
<artifactId>releaser-spring</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.cloudfoundry</groupId>
<artifactId>cloudfoundry-client-reactor</artifactId>
<version>${cloudfoundry-client.version}</version>
</dependency>
<dependency>
<groupId>org.cloudfoundry</groupId>
<artifactId>cloudfoundry-operations</artifactId>
<version>${cloudfoundry-client.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>sonar</id>
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<executions>
<execution>
<id>pre-unit-test</id>
<goals>
<goal>prepare-agent</goal>
</goals>
<configuration>
<propertyName>surefireArgLine</propertyName>
<destFile>${project.build.directory}/jacoco.exec</destFile>
</configuration>
</execution>
<execution>
<id>post-unit-test</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
<configuration>
<!-- Sets the path to the file which contains the execution data. -->
<dataFile>${project.build.directory}/jacoco.exec</dataFile>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<!-- Sets the VM argument line used when unit tests are run. -->
<argLine>${surefireArgLine}</argLine>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@@ -0,0 +1,40 @@
/*
* 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;
import releaser.internal.options.Parser;
import releaser.internal.spring.ExecutionResultHandler;
import releaser.internal.spring.SpringReleaser;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ReleaserApplication extends ReleaserCommandLineRunner {
public ReleaserApplication(SpringReleaser releaser, ExecutionResultHandler executionResultHandler, Parser parser) {
super(releaser, executionResultHandler, parser);
}
public static void main(String[] args) {
SpringApplication application = new SpringApplication(ReleaserApplication.class);
application.setWebApplicationType(WebApplicationType.NONE);
application.run(args);
}
}

View File

@@ -0,0 +1,78 @@
/*
* 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();
}
}

View File

@@ -0,0 +1,579 @@
/*
* 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 javax.json.JsonValue;
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 toVersionTag = "v" + args.versionFromBom.version;
if (args.options.dryRun == Boolean.TRUE && !gitHandler.findTagSha1(args.project, toVersionTag).isPresent()) {
toVersionTag = "HEAD";
}
// eg. 3.2.1 for current 3.2.2
String fromVersionTag = args.versionFromBom.computePreviousPatchTag("v", "RELEASE").orElseGet(() -> {
Pattern pattern = args.versionFromBom.computePreviousMinorTagPattern("v", "RELEASE")
.orElseGet(() -> args.versionFromBom.computePreviousMajorTagPattern("v", "RELEASE"));
// eg. v3.2.*.RELEASE or v3.*.*.RELEASE
log.info("Couldn't simply compute previous version of {}, looking through tag list with pattern {}",
args.versionFromBom.version, pattern.pattern());
List<String> sortedTags = gitHandler.findTagNamesMatching(args.project, pattern)
.collect(Collectors.toList());
if (sortedTags.isEmpty()) {
throw new IllegalStateException("Couldn't find a tag that matches pattern " + pattern.pattern());
}
sortedTags.forEach(System.out::println);
return sortedTags.get(0);
});
if (Boolean.TRUE == args.options.interactive) {
log.info("\nComputed log range is {}..{}", fromVersionTag, toVersionTag);
log.info("\nForce the FROM in log range if needed [{}]", fromVersionTag);
String modifiedFrom = System.console().readLine();
if (!modifiedFrom.trim().isEmpty()) {
fromVersionTag = modifiedFrom;
}
log.info("\nForce the TO in log range if needed [{}]", toVersionTag);
String modifiedTo = System.console().readLine();
if (!modifiedTo.trim().isEmpty()) {
toVersionTag = modifiedTo;
}
}
log.info("Will fetch 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();
JsonValue.ValueType authorType = commitJson.get("author").getValueType();
if (authorType == JsonValue.ValueType.OBJECT) {
return "@" + commitJson.getJsonObject("author").getString("login");
}
else if (authorType == JsonValue.ValueType.NULL) {
// assume author+committer, look under commit.author.name
if (commitJson.containsKey("commit") && commitJson.getJsonObject("commit").containsKey("author")) {
return "@" + commitJson.getJsonObject("commit").getJsonObject("author").getString("name");
}
}
// in case unexpected json, output the "sha", "commit", "author" and
// "committer"
return "@RAW{\"sha\", " + commitJson.get("sha") + ", \"author\", \"" + commitJson.get("author")
+ ", \"committer\", \"" + commitJson.get("committer") + ", \"commit\", \""
+ commitJson.get("commit") + "}";
}
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;
}
}
}

View File

@@ -0,0 +1,65 @@
/*
* 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.reactor;
import com.jcabi.github.Github;
import com.jcabi.github.RtGithub;
import com.jcabi.http.wire.RetryWire;
import org.cloudfoundry.operations.CloudFoundryOperations;
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.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
CfClient cfClient(CloudFoundryOperations cloudFoundryOperations) {
return new CfClient(cloudFoundryOperations);
}
@Bean
RestartSiteProjectPostReleaseTask restartSiteProjectPostReleaseTask(Releaser releaser, CfClient cfClient,
@Value("${cf.reactorAppName}") String reactorAppName) {
return new RestartSiteProjectPostReleaseTask(releaser, cfClient, reactorAppName);
}
@Bean
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
GenerateReleaseNotesTask releaseNotesTask(Github github, ProjectGitHandler gitHandler) {
return new GenerateReleaseNotesTask(github, gitHandler);
}
}

View File

@@ -0,0 +1,63 @@
/*
* 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.reactor;
import org.cloudfoundry.operations.CloudFoundryOperations;
import org.cloudfoundry.operations.applications.RestartApplicationRequest;
import releaser.internal.Releaser;
import releaser.internal.spring.Arguments;
import releaser.internal.tasks.release.PublishDocsReleaseTask;
import releaser.internal.tech.ExecutionResult;
public class RestartSiteProjectPostReleaseTask extends PublishDocsReleaseTask {
private static final String REACTOR_CORE_PROJECT_NAME = "reactor-core";
private final CfClient cfClient;
private final String reactorAppName;
public RestartSiteProjectPostReleaseTask(Releaser releaser, CfClient cfClient, String reactorAppName) {
super(releaser);
this.cfClient = cfClient;
this.reactorAppName = reactorAppName;
}
@Override
public ExecutionResult runTask(Arguments args) {
if (!REACTOR_CORE_PROJECT_NAME.equals(args.projectToRun.name())) {
return ExecutionResult.success();
}
this.cfClient.restartApp(this.reactorAppName);
return ExecutionResult.success();
}
}
class CfClient {
private final CloudFoundryOperations cloudFoundryOperations;
CfClient(CloudFoundryOperations cloudFoundryOperations) {
this.cloudFoundryOperations = cloudFoundryOperations;
}
void restartApp(String name) {
this.cloudFoundryOperations.applications().restart(RestartApplicationRequest.builder().name(name).build());
}
}

View File

@@ -0,0 +1,38 @@
spring:
main:
web-application-type: none
datasource:
url: jdbc:h2:mem:${random.uuid}
jackson:
deserialization:
FAIL_ON_UNKNOWN_PROPERTIES: true
profiles:
active: production
releaser:
git:
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:
- reactor
git-org-url: https://github.com/reactor
cf:
organization: FrameworksAndRuntimes
space: Reactor
reactorAppName: projectreactor
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

View File

@@ -0,0 +1,46 @@
<!--
~ 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.
-->
<configuration>
<include resource="org/springframework/boot/logging/logback/base.xml"/>
<appender name="COMMANDFILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- <file>${java.io.tmpdir}/reactor-releaser-commands.log</file>-->
<file>logs/reactor-releaser-commands.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<!-- Keep 3 releases worth of history -->
<fileNamePattern>logs/reactor-releaser-commands.%i.log</fileNamePattern>
<minIndex>1</minIndex>
<maxIndex>3</maxIndex>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>5MB</maxFileSize>
</triggeringPolicy>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="releaser.commands" level="DEBUG" additivity="false">
<appender-ref ref="COMMANDFILE"/>
</logger>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="COMMANDFILE"/>
</root>
</configuration>

View File

@@ -0,0 +1,71 @@
/*
* 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;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
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.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.test.context.ActiveProfiles;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(classes = { ReleaserApplication.class })
@ActiveProfiles("test")
class ReleaserApplicationTests {
@Autowired
ApplicationContext context;
@Test
void contextLoads() {
}
@Test
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);
assertThat(inOrder).anySatisfy(task -> assertThat(task).isInstanceOf(GenerateReleaseNotesTask.class));
}
@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);
assertThat(inOrder).anySatisfy(task -> assertThat(task).isInstanceOf(RestartSiteProjectPostReleaseTask.class));
assertThat(inOrder).noneSatisfy(task -> assertThat(task).isInstanceOf(PublishDocsReleaseTask.class)
.isNotInstanceOf(RestartSiteProjectPostReleaseTask.class));
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,275 @@
/*
* 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
}

View File

@@ -0,0 +1,97 @@
/*
* 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);
}
}

View File

@@ -0,0 +1,105 @@
/*
* 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.reactor;
import org.assertj.core.api.BDDAssertions;
import org.junit.jupiter.api.Test;
import org.mockito.BDDMockito;
import releaser.internal.ReleaserProperties;
import releaser.internal.options.Options;
import releaser.internal.project.ProjectVersion;
import releaser.internal.project.Projects;
import releaser.internal.spring.Arguments;
import releaser.internal.spring.ProjectToRun;
import releaser.internal.spring.ProjectsFromBom;
import releaser.internal.tech.ExecutionResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import static org.junit.jupiter.api.Assertions.fail;
@SpringBootTest
@ActiveProfiles("test")
class RestartSiteProjectPostReleaseTaskTests {
@Autowired
RestartSiteProjectPostReleaseTask task;
@Autowired
CfClient cfClient;
@Test
void should_update_the_website() {
Arguments arguments = Arguments.forProject(reactorCoreProject());
ExecutionResult result = task.runTask(arguments);
BDDAssertions.then(result.isSuccess()).isTrue();
BDDMockito.then(this.cfClient).should().restartApp(BDDMockito.eq("projectreactor"));
}
@Test
void should_fail_if_original_version_is_null() {
ProjectToRun p = new ProjectToRun(null, new ProjectsFromBom(new Projects(), new ProjectVersion("foo", "1.0.0")),
null, new ReleaserProperties(), BDDMockito.mock(Options.class)) {
@Override
public String name() {
return "reactor-core";
}
};
try {
Arguments.forProject(p);
fail();
}
catch (Exception e) {
// success
}
}
@Test
void should_not_update_the_website_if_project_not_reactor_core() {
Arguments arguments = Arguments.forProject(nonReactorCoreProject());
ExecutionResult result = task.runTask(arguments);
BDDAssertions.then(result.isSuccess()).isTrue();
BDDMockito.then(this.cfClient).shouldHaveNoInteractions();
}
private ProjectToRun reactorCoreProject() {
return new ProjectToRun(null, new ProjectsFromBom(new Projects(), new ProjectVersion("foo", "1.0.0")),
new ProjectVersion("foo", "1.0.0"), new ReleaserProperties(), BDDMockito.mock(Options.class)) {
@Override
public String name() {
return "reactor-core";
}
};
}
private ProjectToRun nonReactorCoreProject() {
return new ProjectToRun(null, new ProjectsFromBom(new Projects(), new ProjectVersion("foo", "1.0.0")),
new ProjectVersion("foo", "1.0.0"), new ReleaserProperties(), BDDMockito.mock(Options.class)) {
@Override
public String name() {
return "whatever";
}
};
}
}

View File

@@ -0,0 +1,3 @@
cf:
username: foo
password: bar

View File

@@ -10,13 +10,13 @@
<parent>
<groupId>org.springframework.cloud.internal</groupId>
<artifactId>releaser-projects</artifactId>
<version>3.0.0-SNAPSHOT</version>
<version>2.0.0-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>17</java.version>
<java.version>1.8</java.version>
</properties>
<dependencies>

View File

@@ -138,30 +138,30 @@ class SpringCloudStreamMavenBomParser implements CustomBomParser {
public Set<Project> setVersion(Set<Project> projects, String projectName, String version) {
Set<Project> newProjects = new LinkedHashSet<>(projects);
switch (projectName) {
case SPRING_BOOT:
case BOOT_STARTER_ARTIFACT_ID:
case BOOT_STARTER_PARENT_ARTIFACT_ID:
case BOOT_DEPENDENCIES_ARTIFACT_ID:
updateBootVersions(newProjects, version);
break;
case BUILD_ARTIFACT_ID:
case CLOUD_DEPENDENCIES_PARENT_ARTIFACT_ID:
updateBuildVersions(newProjects, version);
break;
case CLOUD_ARTIFACT_ID:
case CLOUD_DEPENDENCIES_ARTIFACT_ID:
case CLOUD_RELEASE_ARTIFACT_ID:
case CLOUD_STARTER_ARTIFACT_ID:
case CLOUD_STARTER_PARENT_ARTIFACT_ID:
updateSpringCloudVersions(newProjects, version);
break;
case STREAM_DEPS_ARTIFACT_ID:
case STREAM_STARTER_ARTIFACT_ID:
case STREAM_STARTER_BUILD_ARTIFACT_ID:
case STREAM_STARTER_PARENT_ARTIFACT_ID:
case STREAM_DOCS_ARTIFACT_ID:
updateStreamVersions(newProjects, version);
break;
case SPRING_BOOT:
case BOOT_STARTER_ARTIFACT_ID:
case BOOT_STARTER_PARENT_ARTIFACT_ID:
case BOOT_DEPENDENCIES_ARTIFACT_ID:
updateBootVersions(newProjects, version);
break;
case BUILD_ARTIFACT_ID:
case CLOUD_DEPENDENCIES_PARENT_ARTIFACT_ID:
updateBuildVersions(newProjects, version);
break;
case CLOUD_ARTIFACT_ID:
case CLOUD_DEPENDENCIES_ARTIFACT_ID:
case CLOUD_RELEASE_ARTIFACT_ID:
case CLOUD_STARTER_ARTIFACT_ID:
case CLOUD_STARTER_PARENT_ARTIFACT_ID:
updateSpringCloudVersions(newProjects, version);
break;
case STREAM_DEPS_ARTIFACT_ID:
case STREAM_STARTER_ARTIFACT_ID:
case STREAM_STARTER_BUILD_ARTIFACT_ID:
case STREAM_STARTER_PARENT_ARTIFACT_ID:
case STREAM_DOCS_ARTIFACT_ID:
updateStreamVersions(newProjects, version);
break;
}
return newProjects;
}

View File

@@ -10,13 +10,13 @@
<parent>
<groupId>org.springframework.cloud.internal</groupId>
<artifactId>releaser-projects</artifactId>
<version>3.0.0-SNAPSHOT</version>
<version>2.0.0-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>17</java.version>
<java.version>1.8</java.version>
</properties>
<dependencies>

View File

@@ -139,31 +139,31 @@ class SpringCloudMavenBomParser implements CustomBomParser {
public Set<Project> setVersion(Set<Project> projects, String projectName, String version) {
Set<Project> newProjects = new LinkedHashSet<>(projects);
switch (projectName) {
case SPRING_BOOT:
case BOOT_STARTER_ARTIFACT_ID:
case BOOT_STARTER_PARENT_ARTIFACT_ID:
case BOOT_DEPENDENCIES_ARTIFACT_ID:
updateBootVersions(newProjects, version);
break;
case BUILD_ARTIFACT_ID:
case CLOUD_DEPENDENCIES_PARENT_ARTIFACT_ID:
updateBuildVersions(newProjects, version);
break;
case CLOUD_ARTIFACT_ID:
case CLOUD_DEPENDENCIES_ARTIFACT_ID:
case CLOUD_RELEASE_ARTIFACT_ID:
case CLOUD_STARTER_ARTIFACT_ID:
case CLOUD_STARTER_PARENT_ARTIFACT_ID:
case CLOUD_STARTER_BUILD_ARTIFACT_ID:
updateSpringCloudVersions(newProjects, version);
break;
case STREAM_DEPS_ARTIFACT_ID:
case STREAM_STARTER_ARTIFACT_ID:
case STREAM_STARTER_BUILD_ARTIFACT_ID:
case STREAM_STARTER_PARENT_ARTIFACT_ID:
case STREAM_DOCS_ARTIFACT_ID:
updateStreamVersions(newProjects, version);
break;
case SPRING_BOOT:
case BOOT_STARTER_ARTIFACT_ID:
case BOOT_STARTER_PARENT_ARTIFACT_ID:
case BOOT_DEPENDENCIES_ARTIFACT_ID:
updateBootVersions(newProjects, version);
break;
case BUILD_ARTIFACT_ID:
case CLOUD_DEPENDENCIES_PARENT_ARTIFACT_ID:
updateBuildVersions(newProjects, version);
break;
case CLOUD_ARTIFACT_ID:
case CLOUD_DEPENDENCIES_ARTIFACT_ID:
case CLOUD_RELEASE_ARTIFACT_ID:
case CLOUD_STARTER_ARTIFACT_ID:
case CLOUD_STARTER_PARENT_ARTIFACT_ID:
case CLOUD_STARTER_BUILD_ARTIFACT_ID:
updateSpringCloudVersions(newProjects, version);
break;
case STREAM_DEPS_ARTIFACT_ID:
case STREAM_STARTER_ARTIFACT_ID:
case STREAM_STARTER_BUILD_ARTIFACT_ID:
case STREAM_STARTER_PARENT_ARTIFACT_ID:
case STREAM_DOCS_ARTIFACT_ID:
updateStreamVersions(newProjects, version);
break;
}
return newProjects;
}

View File

@@ -16,7 +16,7 @@
package releaser.cloud.github;
import org.kohsuke.github.GitHub;
import com.jcabi.github.Github;
import releaser.internal.ReleaserProperties;
import releaser.internal.github.CustomGithubIssues;
import releaser.internal.github.GithubIssueFiler;
@@ -38,41 +38,25 @@ class SpringCloudGithubIssues implements CustomGithubIssues {
this.properties = properties;
}
SpringCloudGithubIssues(GitHub github, ReleaserProperties properties) {
SpringCloudGithubIssues(Github github, ReleaserProperties properties) {
this.githubIssueFiler = new GithubIssueFiler(github, properties);
this.properties = properties;
}
@Override
public void fileIssueInSpringGuides(Projects projects, ProjectVersion version) {
String user = getGuidesOrg();
String repo = getGuidesRepo();
String user = "spring-guides";
String repo = "getting-started-guides";
this.githubIssueFiler.fileAGitHubIssue(user, repo, version, issueTitle(), guidesIssueText(projects));
}
@Override
public void fileIssueInStartSpringIo(Projects projects, ProjectVersion version) {
String user = getStartSpringIoOrg();
String repo = getStartSpringIoRepo();
String user = "spring-io";
String repo = "start.spring.io";
this.githubIssueFiler.fileAGitHubIssue(user, repo, version, issueTitle(), startSpringIoIssueText(projects));
}
String getGuidesOrg() {
return "spring-guides";
}
String getGuidesRepo() {
return "getting-started-guides";
}
String getStartSpringIoOrg() {
return "spring-io";
}
String getStartSpringIoRepo() {
return "start.spring.io";
}
private String issueTitle() {
return String.format(GITHUB_ISSUE_TITLE, StringUtils.capitalize(parsedVersion()));
}

View File

@@ -41,6 +41,10 @@ releaser:
update-release-train-wiki: true
update-all-test-samples: true
all-test-sample-urls:
spring-cloud-sleuth:
- https://github.com/spring-cloud-samples/sleuth-issues
- https://github.com/spring-cloud-samples/sleuth-documentation-apps
- https://github.com/spring-cloud-samples/spring-cloud-sleuth-samples
spring-cloud-contract:
- https://github.com/spring-cloud-samples/spring-cloud-contract-samples
- https://github.com/spring-cloud-samples/the-legacy-app
@@ -97,7 +101,7 @@ releaser:
enabled: true
template-folder: cloud
versions:
all-versions-file-url: https://raw.githubusercontent.com/spring-io/start.spring.io/main/start-site/src/main/resources/application.yml
all-versions-file-url: https://raw.githubusercontent.com/spring-io/start.spring.io/master/start-site/src/main/resources/application.yml
bom-name: spring-cloud
# fixed-versions:
meta-release:

View File

@@ -23,6 +23,8 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import javax.validation.constraints.NotNull;
import org.assertj.core.api.BDDAssertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -43,7 +45,7 @@ import org.springframework.util.FileSystemUtils;
/**
* @author Marcin Grzejszczak
*/
class SpringCloudCustomProjectDocumentationUpdaterTests {
public class SpringCloudCustomProjectDocumentationUpdaterTests {
File project;
@@ -59,7 +61,7 @@ class SpringCloudCustomProjectDocumentationUpdaterTests {
ReleaserProperties properties = SpringCloudReleaserProperties.get();
@BeforeEach
void setup() throws IOException, URISyntaxException {
public void setup() throws IOException, URISyntaxException {
this.project = new File(SpringCloudCustomProjectDocumentationUpdater.class
.getResource("/projects/spring-cloud-static").toURI());
TestUtils.prepareLocalRepo();
@@ -71,17 +73,19 @@ class SpringCloudCustomProjectDocumentationUpdaterTests {
Collections.singletonList(SpringCloudGithubIssuesAccessor.springCloud(this.properties)));
}
@NotNull
private DocumentationUpdater projectDocumentationUpdater(ReleaserProperties properties) {
return new DocumentationUpdater(this.handler, properties, templateGenerator(properties),
Collections.singletonList(new SpringCloudCustomProjectDocumentationUpdater(this.handler, properties)));
}
@NotNull
private TemplateGenerator templateGenerator(ReleaserProperties properties) {
return new TemplateGenerator(properties, this.gitHubHandler);
}
@Test
void should_not_update_current_version_in_the_docs_if_current_release_starts_with_v_and_then_lower_letter_than_the_stored_release()
public void should_not_update_current_version_in_the_docs_if_current_release_starts_with_v_and_then_lower_letter_than_the_stored_release()
throws URISyntaxException, IOException {
ProjectVersion releaseTrainVersion = new ProjectVersion("spring-cloud-release", "Finchley.SR33");
ReleaserProperties properties = new ReleaserProperties();
@@ -110,7 +114,7 @@ class SpringCloudCustomProjectDocumentationUpdaterTests {
}
@Test
void should_not_commit_if_the_same_version_is_already_there() {
public void should_not_commit_if_the_same_version_is_already_there() {
ProjectVersion releaseTrainVersion = new ProjectVersion("spring-cloud-release", "Dalston.SR3");
ReleaserProperties properties = new ReleaserProperties();
properties.getGit().setDocumentationUrl(this.clonedDocProject.toURI().toString());
@@ -123,7 +127,7 @@ class SpringCloudCustomProjectDocumentationUpdaterTests {
}
@Test
void should_do_nothing_when_release_train_docs_update_happen_for_a_project_that_does_not_start_with_spring_cloud() {
public void should_do_nothing_when_release_train_docs_update_happen_for_a_project_that_does_not_start_with_spring_cloud() {
ProjectVersion springBootVersion = new ProjectVersion("spring-boot", "2.2.5");
ReleaserProperties properties = new ReleaserProperties();
properties.getGit().setDocumentationUrl(this.clonedDocProject.toURI().toString());
@@ -136,7 +140,7 @@ class SpringCloudCustomProjectDocumentationUpdaterTests {
}
@Test
void should_do_nothing_when_single_project_docs_update_happen_for_a_project_that_does_not_start_with_spring_cloud() {
public void should_do_nothing_when_single_project_docs_update_happen_for_a_project_that_does_not_start_with_spring_cloud() {
ProjectVersion springBootVersion = new ProjectVersion("spring-boot", "2.2.5");
ReleaserProperties properties = new ReleaserProperties();
properties.getGit().setDocumentationUrl(this.clonedDocProject.toURI().toString());
@@ -149,7 +153,7 @@ class SpringCloudCustomProjectDocumentationUpdaterTests {
}
@Test
void should_not_update_current_version_in_the_docs_if_current_release_starts_with_lower_letter_than_the_stored_release()
public void should_not_update_current_version_in_the_docs_if_current_release_starts_with_lower_letter_than_the_stored_release()
throws IOException {
ProjectVersion releaseTrainVersion = new ProjectVersion("spring-cloud-release", "Angel.SR33");
ReleaserProperties properties = new ReleaserProperties();

View File

@@ -16,13 +16,13 @@
package releaser.cloud.github;
import org.kohsuke.github.GitHub;
import com.jcabi.github.Github;
import releaser.internal.ReleaserProperties;
import releaser.internal.github.CustomGithubIssues;
public class SpringCloudGithubIssuesAccessor {
public static CustomGithubIssues springCloud(GitHub github, ReleaserProperties releaserProperties) {
public static CustomGithubIssues springCloud(Github github, ReleaserProperties releaserProperties) {
return new SpringCloudGithubIssues(github, releaserProperties);
}

View File

@@ -0,0 +1,179 @@
/*
* Copyright 2013-2022 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.cloud.github;
import java.io.IOException;
import java.util.Collections;
import com.jcabi.github.Coordinates;
import com.jcabi.github.Github;
import com.jcabi.github.Issue;
import com.jcabi.github.Repo;
import com.jcabi.github.Repos;
import com.jcabi.github.mock.MkGithub;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.BDDMockito;
import releaser.cloud.SpringCloudReleaserProperties;
import releaser.internal.ReleaserProperties;
import releaser.internal.github.CustomGithubIssues;
import releaser.internal.project.ProjectVersion;
import releaser.internal.project.Projects;
import static org.assertj.core.api.BDDAssertions.then;
import static org.assertj.core.api.BDDAssertions.thenThrownBy;
/**
* @author Marcin Grzejszczak
*/
public class SpringCloudGithubIssuesTests {
ReleaserProperties properties = SpringCloudReleaserProperties.get();
MkGithub github;
Repo repo;
@BeforeEach
public void setup() throws IOException {
this.github = github("spring-guides");
this.properties.getGit().setOauthToken("a");
this.repo = createGettingStartedGuides(this.github);
}
public void setupStartSpringIo() throws IOException {
this.github = github("spring-io");
this.repo = createStartSpringIo(this.github);
}
private MkGithub github(String login) throws IOException {
return new MkGithub(login);
}
@Test
public void should_not_do_anything_for_non_release_train_version() {
Github github = BDDMockito.mock(Github.class);
CustomGithubIssues githubIssues = new SpringCloudGithubIssues(github, properties);
githubIssues.fileIssueInSpringGuides(
new Projects(new ProjectVersion("foo", "1.0.0.BUILD-SNAPSHOT"),
new ProjectVersion("spring-cloud-build", "2.0.0.BUILD-SNAPSHOT")),
new ProjectVersion("sc-release", "Edgware.BUILD-SNAPSHOT"));
BDDMockito.then(github).shouldHaveNoInteractions();
}
@Test
public void should_file_an_issue_for_release_version() throws IOException {
CustomGithubIssues issues = new SpringCloudGithubIssues(github, properties);
properties.getPom().setBranch("vEdgware.RELEASE");
issues.fileIssueInSpringGuides(
new Projects(new ProjectVersion("spring-cloud-foo", "1.0.0.RELEASE"),
new ProjectVersion("spring-cloud-build", "2.0.0.RELEASE"),
new ProjectVersion("bar", "2.0.0.RELEASE"), new ProjectVersion("baz", "3.0.0.RELEASE")),
new ProjectVersion("sc-release", "Edgware.RELEASE"));
Issue issue = this.github.repos().get(new Coordinates.Simple("spring-guides", "getting-started-guides"))
.issues().get(1);
then(issue.exists()).isTrue();
Issue.Smart smartIssue = new Issue.Smart(issue);
then(smartIssue.title()).isEqualTo("Upgrade to Spring Cloud Edgware.RELEASE");
then(smartIssue.body()).contains(
"Release train [spring-cloud-release] in version [Edgware.RELEASE] released with the following projects")
.contains("spring-cloud-foo : `1.0.0.RELEASE`").contains("bar : `2.0.0.RELEASE`")
.contains("baz : `3.0.0.RELEASE`");
}
@Test
public void should_throw_exception_when_no_token_was_passed() {
properties.getGit().setOauthToken("");
CustomGithubIssues issues = new SpringCloudGithubIssues(github, properties);
thenThrownBy(() -> issues.fileIssueInSpringGuides(
new Projects(Collections.singletonList(new ProjectVersion("spring-cloud-build", "2.0.0.RELEASE"))),
nonGaSleuthProject())).isInstanceOf(IllegalArgumentException.class).hasMessageContaining(
"You have to pass Github OAuth token for milestone closing to be operational");
}
@Test
public void should_not_do_anything_for_non_release_train_version_when_updating_startspringio() throws IOException {
setupStartSpringIo();
Github github = BDDMockito.mock(Github.class);
CustomGithubIssues issues = new SpringCloudGithubIssues(github, properties);
issues.fileIssueInStartSpringIo(
new Projects(new ProjectVersion("foo", "1.0.0.BUILD-SNAPSHOT"),
new ProjectVersion("spring-cloud-build", "2.0.0.RELEASE")),
new ProjectVersion("sc-release", "Edgware.BUILD-SNAPSHOT"));
BDDMockito.then(github).shouldHaveNoInteractions();
}
@Test
public void should_file_an_issue_for_release_version_when_updating_startspringio() throws IOException {
setupStartSpringIo();
CustomGithubIssues issues = new SpringCloudGithubIssues(github, properties);
properties.getPom().setBranch("vEdgware.RELEASE");
issues.fileIssueInStartSpringIo(new Projects(new ProjectVersion("spring-cloud-foo", "1.0.0.RELEASE"),
new ProjectVersion("spring-cloud-build", "2.0.0.RELEASE"), new ProjectVersion("bar", "2.0.0.RELEASE"),
new ProjectVersion("baz", "3.0.0.RELEASE"), new ProjectVersion("spring-boot", "1.2.3.RELEASE")),
new ProjectVersion("sc-release", "Edgware.RELEASE"));
Issue issue = this.github.repos().get(new Coordinates.Simple("spring-io", "start.spring.io")).issues().get(1);
then(issue.exists()).isTrue();
Issue.Smart smartIssue = new Issue.Smart(issue);
then(smartIssue.title()).isEqualTo("Upgrade to Spring Cloud Edgware.RELEASE");
then(smartIssue.body()).contains(
"Release train [spring-cloud-release] in version [Edgware.RELEASE] released with the Spring Boot version [`1.2.3.RELEASE`]");
}
@Test
public void should_throw_exception_when_no_token_was_passed_when_updating_startspringio() throws IOException {
setupStartSpringIo();
properties.getGit().setOauthToken("");
CustomGithubIssues issues = new SpringCloudGithubIssues(github, properties);
thenThrownBy(() -> issues.fileIssueInStartSpringIo(
new Projects(Collections.singletonList(new ProjectVersion("spring-cloud-build", "2.0.0.RELEASE"))),
nonGaSleuthProject())).isInstanceOf(IllegalArgumentException.class).hasMessageContaining(
"You have to pass Github OAuth token for milestone closing to be operational");
}
private Repo createGettingStartedGuides(MkGithub github) throws IOException {
return github.repos().create(new Repos.RepoCreate("getting-started-guides", false));
}
private Repo createStartSpringIo(MkGithub github) throws IOException {
return github.repos().create(new Repos.RepoCreate("start.spring.io", false));
}
private ProjectVersion nonGaSleuthProject() {
return new ProjectVersion("spring-cloud-sleuth", "0.2.0.BUILD-SNAPSHOT");
}
ReleaserProperties withToken() {
ReleaserProperties properties = SpringCloudReleaserProperties.get();
properties.getGit().setOauthToken("foo");
properties.getPom().setBranch("vEdgware.RELEASE");
properties.getGit().setUpdateSpringGuides(true);
properties.getGit().setUpdateStartSpringIo(true);
return properties;
}
}