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

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
java: ["17"]
java: ["8"]
steps:
- uses: actions/checkout@v2

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>org.springframework.cloud.internal</groupId>
<artifactId>releaser-parent</artifactId>
<version>3.0.0-SNAPSHOT</version>
<version>2.0.0-SNAPSHOT</version>
</parent>
<artifactId>releaser-docs</artifactId>
<packaging>pom</packaging>
@@ -16,11 +16,11 @@
<configprops.inclusionPattern>releaser.*</configprops.inclusionPattern>
</properties>
<dependencies>
<!--<dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-info</artifactId>
<version>${project.version}</version>
</dependency>-->
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>releaser-core</artifactId>

123
pom.xml
View File

@@ -6,13 +6,13 @@
<groupId>org.springframework.cloud.internal</groupId>
<artifactId>releaser-parent</artifactId>
<version>3.0.0-SNAPSHOT</version>
<version>2.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-build</artifactId>
<version>4.0.3</version>
<version>3.1.1</version>
<relativePath/>
<!-- lookup parent from repository -->
</parent>
@@ -28,17 +28,18 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>17</java.version>
<java.version>1.8</java.version>
<spring-cloud-bom.version>2022.0.3</spring-cloud-bom.version>
<github-api.version>1.315</github-api.version>
<spring-cloud-bom.version>2021.0.1</spring-cloud-bom.version>
<jcabi-github.version>1.1.2</jcabi-github.version>
<jsch-agent.version>0.0.9</jsch-agent.version>
<hibernate-validator.version>7.0.4.Final</hibernate-validator.version>
<!--Matches JGit in Boot:-->
<org.eclipse.jgit-version>6.5.0.202303070854-r</org.eclipse.jgit-version>
<org.eclipse.jgit-version>5.12.0.202106070339-r</org.eclipse.jgit-version>
<!-- Versions plugin uses this version -->
<maven-model.version>3.8.5</maven-model.version>
<versions-maven-plugin.version>2.10.0</versions-maven-plugin.version>
<handlebars.version>4.3.1</handlebars.version>
<handlebars.version>4.3.0</handlebars.version>
<initializr-metadata.version>0.12.0</initializr-metadata.version>
<awaitility.version>4.2.0</awaitility.version>
<sagan-site.version>1.0.0-SNAPSHOT</sagan-site.version>
@@ -46,7 +47,6 @@
<jopt-simple.version>5.0.4</jopt-simple.version>
<fliptables.version>1.1.0</fliptables.version>
<zt.exec.version>1.12</zt.exec.version>
<okhttp.version>4.11.0</okhttp.version>
</properties>
<dependencyManagement>
@@ -58,93 +58,6 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>${org.eclipse.jgit-version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit.ssh.jsch</artifactId>
<version>${org.eclipse.jgit-version}</version>
</dependency>
<!-- a proxy to ssh-agent and Pageant in Java -->
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch.agentproxy.sshagent</artifactId>
<version>${jsch-agent.version}</version>
</dependency>
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch.agentproxy.jsch</artifactId>
<version>${jsch-agent.version}</version>
</dependency>
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch.agentproxy.usocket-jna</artifactId>
<version>${jsch-agent.version}</version>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-model</artifactId>
<!--<version>3.3.9</version>-->
<!-- Versions plugin uses this version -->
<version>${maven-model.version}</version>
</dependency>
<dependency>
<groupId>org.codehaus.mojo</groupId>
<artifactId>versions-maven-plugin</artifactId>
<version>${versions-maven-plugin.version}</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.kohsuke</groupId>
<artifactId>github-api</artifactId>
<version>${github-api.version}</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>${okhttp.version}</version>
</dependency>
<dependency>
<groupId>com.github.jknack</groupId>
<artifactId>handlebars</artifactId>
<version>${handlebars.version}</version>
</dependency>
<dependency>
<groupId>io.spring.initializr</groupId>
<artifactId>initializr-metadata</artifactId>
<version>${initializr-metadata.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.json</artifactId>
<version>${javax.json.version}</version>
</dependency>
<dependency>
<groupId>org.zeroturnaround</groupId>
<artifactId>zt-exec</artifactId>
<version>${zt.exec.version}</version>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>${awaitility.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
@@ -190,12 +103,20 @@
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/release</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<url>https://repo.spring.io/libs-snapshot-local</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
@@ -206,7 +127,15 @@
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<url>https://repo.spring.io/libs-milestone-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/libs-release-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>

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

View File

@@ -10,7 +10,7 @@
<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>
@@ -35,31 +35,40 @@
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit.ssh.jsch</artifactId>
<version>${org.eclipse.jgit-version}</version>
</dependency>
<!-- a proxy to ssh-agent and Pageant in Java -->
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch.agentproxy.sshagent</artifactId>
<version>${jsch-agent.version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit.ssh.jsch</artifactId>
<version>${org.eclipse.jgit-version}</version>
</dependency>
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch.agentproxy.jsch</artifactId>
<version>${jsch-agent.version}</version>
</dependency>
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch.agentproxy.usocket-jna</artifactId>
<version>${jsch-agent.version}</version>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-model</artifactId>
<!--<version>3.3.9</version>-->
<!-- Versions plugin uses this version -->
<version>${maven-model.version}</version>
</dependency>
<dependency>
<groupId>org.codehaus.mojo</groupId>
<artifactId>versions-maven-plugin</artifactId>
<version>${versions-maven-plugin.version}</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
@@ -76,20 +85,19 @@
</exclusions>
</dependency>
<dependency>
<groupId>org.kohsuke</groupId>
<artifactId>github-api</artifactId>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<groupId>com.jcabi</groupId>
<artifactId>jcabi-github</artifactId>
<version>${jcabi-github.version}</version>
</dependency>
<dependency>
<groupId>com.github.jknack</groupId>
<artifactId>handlebars</artifactId>
<version>${handlebars.version}</version>
</dependency>
<dependency>
<groupId>io.spring.initializr</groupId>
<artifactId>initializr-metadata</artifactId>
<version>${initializr-metadata.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
@@ -104,6 +112,7 @@
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>${awaitility.version}</version>
<scope>test</scope>
</dependency>
<!--<dependency>
@@ -122,11 +131,13 @@
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.json</artifactId>
<version>${javax.json.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.zeroturnaround</groupId>
<artifactId>zt-exec</artifactId>
<version>${zt.exec.version}</version>
</dependency>
</dependencies>

View File

@@ -17,7 +17,6 @@
package releaser.internal;
import java.io.Serializable;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -27,7 +26,8 @@ import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
import jakarta.validation.constraints.NotBlank;
import javax.validation.constraints.NotBlank;
import org.apache.commons.lang.SerializationUtils;
import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -428,12 +428,6 @@ public class ReleaserProperties implements Serializable {
public static class Git implements Serializable {
/**
* Absolute path to a directory with cache for OkHTTP calls to GitHub.
*/
@NotBlank
private String cacheDirectory = temporaryDirectory();
/**
* URL to a release train repository.
*/
@@ -822,14 +816,6 @@ public class ReleaserProperties implements Serializable {
this.orgName = orgName;
}
public String getCacheDirectory() {
return cacheDirectory;
}
public void setCacheDirectory(String cacheDirectory) {
this.cacheDirectory = cacheDirectory;
}
@Override
public String toString() {
return "Git{" + "releaseTrainBomUrl='" + this.releaseTrainBomUrl + '\'' + ", documentationUrl='"
@@ -844,15 +830,6 @@ public class ReleaserProperties implements Serializable {
+ this.updateSpringProject + ", sampleUrlsSize=" + this.allTestSampleUrls.size() + '}';
}
private static String temporaryDirectory() {
try {
return Files.createTempDirectory("github-cache").toAbsolutePath().toString();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
}
public static class Pom implements Serializable {
@@ -1217,7 +1194,7 @@ public class ReleaserProperties implements Serializable {
* A mapping that should be applied to {@code gradle.properties} in order to
* perform a substitution of properties. The mapping is from a property inside
* {@code gradle.properties} to the projects name. Example.
* <p>
*
* In {@code gradle.properties} you have {@code verifierVersion=1.0.0} . You want
* this property to get updated with the value of {@code spring-cloud-contract}
* version. Then it's enough to do the mapping like this for this Releaser's
@@ -1389,7 +1366,7 @@ public class ReleaserProperties implements Serializable {
/**
* URL to the Sagan API.
*/
private String baseUrl = "https://api.spring.io";
private String baseUrl = "https://spring.io";
/**
* Folder with asciidoctor files for docs.

View File

@@ -56,12 +56,12 @@ import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.FetchResult;
import org.eclipse.jgit.transport.JschConfigSessionFactory;
import org.eclipse.jgit.transport.OpenSshConfig;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.SshTransport;
import org.eclipse.jgit.transport.URIish;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
import org.eclipse.jgit.transport.ssh.jsch.JschConfigSessionFactory;
import org.eclipse.jgit.transport.ssh.jsch.OpenSshConfig;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.FileUtils;
import org.slf4j.Logger;

View File

@@ -16,39 +16,365 @@
package releaser.internal.github;
import java.io.File;
import java.io.Closeable;
import java.io.IOException;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import okhttp3.Cache;
import okhttp3.OkHttpClient;
import org.kohsuke.github.GitHub;
import org.kohsuke.github.GitHubBuilder;
import org.kohsuke.github.extras.okhttp3.OkHttpGitHubConnector;
import javax.json.JsonObject;
final class CachingGithub {
import com.jcabi.github.Assignees;
import com.jcabi.github.Branches;
import com.jcabi.github.Collaborators;
import com.jcabi.github.Contents;
import com.jcabi.github.Coordinates;
import com.jcabi.github.DeployKeys;
import com.jcabi.github.Forks;
import com.jcabi.github.Gists;
import com.jcabi.github.Git;
import com.jcabi.github.Github;
import com.jcabi.github.Gitignores;
import com.jcabi.github.Hooks;
import com.jcabi.github.IssueEvents;
import com.jcabi.github.Issues;
import com.jcabi.github.Labels;
import com.jcabi.github.Language;
import com.jcabi.github.Limits;
import com.jcabi.github.Markdown;
import com.jcabi.github.Milestones;
import com.jcabi.github.Notifications;
import com.jcabi.github.Organizations;
import com.jcabi.github.Pulls;
import com.jcabi.github.Releases;
import com.jcabi.github.Repo;
import com.jcabi.github.RepoCommits;
import com.jcabi.github.Repos;
import com.jcabi.github.Search;
import com.jcabi.github.Stars;
import com.jcabi.github.Users;
import com.jcabi.http.Request;
private CachingGithub() {
throw new IllegalStateException("Can't instantiate this");
class CachingGithub implements Github, Closeable {
private final AtomicReference<Repos> repos = new AtomicReference<>();
private final Github delegate;
CachingGithub(Github delegate) {
this.delegate = delegate;
}
static GitHub INSTANCE;
static GitHub getInstance(String oauthToken, String cacheDirectory) {
if (INSTANCE == null) {
INSTANCE = github(oauthToken, cacheDirectory);
}
return INSTANCE;
@Override
public Request entry() {
return this.delegate.entry();
}
private static GitHub github(String oauthToken, String cacheDirectory) {
Cache cache = new Cache(new File(cacheDirectory), 10 * 1024 * 1024); // 10MB cache
try {
return new GitHubBuilder().withOAuthToken(oauthToken)
.withConnector(new OkHttpGitHubConnector(new OkHttpClient.Builder().cache(cache).build())).build();
@Override
public Repos repos() {
Repos repos = this.repos.get();
if (repos == null) {
this.repos.set(new CachingRepos(this.delegate.repos()));
}
catch (IOException e) {
throw new RuntimeException(e);
return this.repos.get();
}
@Override
public Gists gists() {
return this.delegate.gists();
}
@Override
public Users users() {
return this.delegate.users();
}
@Override
public Organizations organizations() {
return this.delegate.organizations();
}
@Override
public Markdown markdown() {
return this.delegate.markdown();
}
@Override
public Limits limits() {
return this.delegate.limits();
}
@Override
public Search search() {
return this.delegate.search();
}
@Override
public Gitignores gitignores() throws IOException {
return this.delegate.gitignores();
}
@Override
public JsonObject meta() throws IOException {
return this.delegate.meta();
}
@Override
public JsonObject emojis() throws IOException {
return this.delegate.emojis();
}
@Override
public boolean equals(Object o) {
return this.delegate.equals(o);
}
@Override
public int hashCode() {
return this.delegate.hashCode();
}
@Override
public void close() throws IOException {
Repos repos = this.repos.get();
if (repos instanceof Closeable) {
((Closeable) repos).close();
}
}
}
class CachingRepos implements Repos, Closeable {
private static final Map<Object, CachingRepo> CACHE = new ConcurrentHashMap<>();
private final Repos delegate;
CachingRepos(Repos delegate) {
this.delegate = delegate;
}
@Override
public Github github() {
return this.delegate.github();
}
@Override
public Repo create(RepoCreate repoCreate) throws IOException {
return this.delegate.create(repoCreate);
}
@Override
public Repo get(Coordinates coordinates) {
return CACHE.computeIfAbsent(coordinates, o -> new CachingRepo(this.delegate.get(coordinates)));
}
@Override
public void remove(Coordinates coordinates) throws IOException {
this.delegate.remove(coordinates);
}
@Override
public Iterable<Repo> iterate(String s) {
return this.delegate.iterate(s);
}
@Override
public boolean exists(Coordinates coordinates) throws IOException {
return delegate.exists(coordinates);
}
@Override
public boolean equals(Object o) {
return this.delegate.equals(o);
}
@Override
public int hashCode() {
return this.delegate.hashCode();
}
@Override
public void close() throws IOException {
CACHE.forEach((o, repo) -> repo.close());
CACHE.clear();
}
}
class CachingRepo implements Repo, Closeable {
private static final Map<RepoKey, Object> CACHE = new ConcurrentHashMap<>();
private final Repo delegate;
CachingRepo(Repo delegate) {
this.delegate = delegate;
}
@Override
public Github github() {
return this.delegate.github();
}
@Override
public Coordinates coordinates() {
return (Coordinates) CACHE.computeIfAbsent(new RepoKey(this.delegate, "coordinates"),
s -> this.delegate.coordinates());
}
@Override
public Issues issues() {
return (Issues) CACHE.computeIfAbsent(new RepoKey(this.delegate, "issues"), s -> this.delegate.issues());
}
@Override
public Milestones milestones() {
return (Milestones) CACHE.computeIfAbsent(new RepoKey(this.delegate, "milestones"),
s -> this.delegate.milestones());
}
@Override
public Pulls pulls() {
return (Pulls) CACHE.computeIfAbsent(new RepoKey(this.delegate, "pulls"), s -> this.delegate.pulls());
}
@Override
public Hooks hooks() {
return (Hooks) CACHE.computeIfAbsent(new RepoKey(this.delegate, "hooks"), s -> this.delegate.hooks());
}
@Override
public IssueEvents issueEvents() {
return (IssueEvents) CACHE.computeIfAbsent(new RepoKey(this.delegate, "issueEvents"),
s -> this.delegate.issueEvents());
}
@Override
public Labels labels() {
return (Labels) CACHE.computeIfAbsent(new RepoKey(this.delegate, "labels"), s -> this.delegate.labels());
}
@Override
public Assignees assignees() {
return (Assignees) CACHE.computeIfAbsent(new RepoKey(this.delegate, "assignees"),
s -> this.delegate.assignees());
}
@Override
public Releases releases() {
return (Releases) CACHE.computeIfAbsent(new RepoKey(this.delegate, "releases"), s -> this.delegate.releases());
}
@Override
public DeployKeys keys() {
return (DeployKeys) CACHE.computeIfAbsent(new RepoKey(this.delegate, "keys"), s -> this.delegate.keys());
}
@Override
public Forks forks() {
return (Forks) CACHE.computeIfAbsent(new RepoKey(this.delegate, "forks"), s -> this.delegate.forks());
}
@Override
public RepoCommits commits() {
return (RepoCommits) CACHE.computeIfAbsent(new RepoKey(this.delegate, "repoCommits"),
s -> this.delegate.commits());
}
@Override
public Branches branches() {
return (Branches) CACHE.computeIfAbsent(new RepoKey(this.delegate, "branches"), s -> this.delegate.branches());
}
@Override
public Contents contents() {
return (Contents) CACHE.computeIfAbsent(new RepoKey(this.delegate, "contents"), s -> this.delegate.contents());
}
@Override
public Collaborators collaborators() {
return (Collaborators) CACHE.computeIfAbsent(new RepoKey(this.delegate, "collaborators"),
s -> this.delegate.collaborators());
}
@Override
public Git git() {
return (Git) CACHE.computeIfAbsent(new RepoKey(this.delegate, "git"), s -> this.delegate.git());
}
@Override
public Stars stars() {
return (Stars) CACHE.computeIfAbsent(new RepoKey(this.delegate, "stars"), s -> this.delegate.stars());
}
@Override
public Notifications notifications() {
return (Notifications) CACHE.computeIfAbsent(new RepoKey(this.delegate, "notifications"),
s -> this.delegate.notifications());
}
@Override
public Iterable<Language> languages() throws IOException {
return this.delegate.languages();
}
@Override
public JsonObject json() throws IOException {
return (JsonObject) CACHE.computeIfAbsent(new RepoKey(this.delegate, "json"), s -> {
try {
return this.delegate.json();
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
});
}
@Override
public void patch(JsonObject jsonObject) throws IOException {
this.delegate.patch(jsonObject);
}
@Override
public int compareTo(Repo o) {
return this.delegate.compareTo(o);
}
@Override
public void close() {
CACHE.clear();
}
}
class RepoKey {
final Repo repo;
final String key;
RepoKey(Repo repo, String key) {
this.repo = repo;
this.key = key;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
RepoKey repoKey = (RepoKey) o;
return Objects.equals(this.repo, repoKey.repo) && Objects.equals(this.key, repoKey.key);
}
@Override
public int hashCode() {
return Objects.hash(this.repo, this.key);
}
}

View File

@@ -18,13 +18,14 @@ package releaser.internal.github;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.kohsuke.github.GHIssue;
import org.kohsuke.github.GHIssueState;
import org.kohsuke.github.GHRepository;
import org.kohsuke.github.GitHub;
import com.jcabi.github.Coordinates;
import com.jcabi.github.Github;
import com.jcabi.github.Issue;
import com.jcabi.github.Repo;
import com.jcabi.github.RtGithub;
import com.jcabi.http.wire.RetryWire;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import releaser.internal.ReleaserProperties;
@@ -39,19 +40,19 @@ import org.springframework.util.Assert;
*/
public class GithubIssueFiler {
private static final Logger log = LoggerFactory.getLogger(GithubIssueFiler.class);
private static final Logger log = LoggerFactory.getLogger(GithubIssues.class);
private final GitHub github;
private final Github github;
private final ReleaserProperties properties;
public GithubIssueFiler(ReleaserProperties properties) {
this(CachingGithub.getInstance(properties.getGit().getOauthToken(), properties.getGit().getCacheDirectory()),
this(new RtGithub(new RtGithub(properties.getGit().getOauthToken()).entry().through(RetryWire.class)),
properties);
}
public GithubIssueFiler(GitHub github, ReleaserProperties properties) {
this.github = github;
public GithubIssueFiler(Github github, ReleaserProperties properties) {
this.github = new CachingGithub(github);
this.properties = properties;
}
@@ -69,16 +70,15 @@ public class GithubIssueFiler {
}
private void fileAGithubIssue(String user, String repo, String issueTitle, String issueText) {
Repo ghRepo = this.github.repos().get(new Coordinates.Simple(user, repo));
// check if the issue is not already there
boolean issueAlreadyFiled = issueAlreadyFiled(ghRepo, issueTitle);
if (issueAlreadyFiled) {
log.info("Issue already filed, will not do that again");
return;
}
try {
GHRepository ghRepo = github.getRepository(user + "/" + repo);
// check if the issue is not already there
boolean issueAlreadyFiled = issueAlreadyFiled(ghRepo, issueTitle);
if (issueAlreadyFiled) {
log.info("Issue already filed, will not do that again");
return;
}
GHIssue ghIssue = ghRepo.createIssue(issueTitle).body(issueText).create();
int number = ghIssue.getNumber();
int number = ghRepo.issues().create(issueTitle, issueText).number();
log.info("Successfully created an issue with " + "title [{}] for the [{}/{}] GitHub repository" + number,
issueTitle, user, repo);
}
@@ -95,20 +95,23 @@ public class GithubIssueFiler {
return version;
}
boolean issueAlreadyFiled(GHRepository springGuides, String issueTitle) throws IOException {
private boolean issueAlreadyFiled(Repo springGuides, String issueTitle) {
Map<String, String> map = new HashMap<>();
map.put("state", "open");
int counter = 0;
int maxIssues = 10;
for (Iterator<GHIssue> it = springGuides.getIssues(GHIssueState.OPEN).iterator(); it.hasNext();) {
GHIssue issue = it.next();
for (Issue issue : springGuides.issues().iterate(map)) {
if (counter >= maxIssues) {
return false;
}
String title = issue.getTitle();
if (issueTitle.equals(title)) {
return true;
Issue.Smart smartIssue = new Issue.Smart(issue);
try {
if (issueTitle.equals(smartIssue.title())) {
return true;
}
}
catch (IOException e) {
return false;
}
counter = counter + 1;
}

View File

@@ -18,12 +18,16 @@ package releaser.internal.github;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.kohsuke.github.GHIssueState;
import org.kohsuke.github.GHMilestone;
import org.kohsuke.github.GitHub;
import com.jcabi.github.Coordinates;
import com.jcabi.github.Github;
import com.jcabi.github.Milestone;
import com.jcabi.github.RtGithub;
import com.jcabi.http.wire.RetryWire;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import releaser.internal.ReleaserProperties;
@@ -38,21 +42,21 @@ import org.springframework.util.StringUtils;
class GithubMilestones {
static final Map<ProjectVersion, String> MILESTONE_URL_CACHE = new ConcurrentHashMap<>();
static final Map<ProjectVersion, GHMilestone> MILESTONE_CACHE = new ConcurrentHashMap<>();
static final Map<ProjectVersion, Milestone.Smart> MILESTONE_CACHE = new ConcurrentHashMap<>();
private static final Logger log = LoggerFactory.getLogger(GithubMilestones.class);
private final GitHub github;
private final Github github;
private final ReleaserProperties properties;
GithubMilestones(ReleaserProperties properties) {
this(CachingGithub.getInstance(properties.getGit().getOauthToken(), properties.getGit().getCacheDirectory()),
this(new RtGithub(new RtGithub(properties.getGit().getOauthToken()).entry().through(RetryWire.class)),
properties);
}
GithubMilestones(GitHub github, ReleaserProperties properties) {
this.github = github;
GithubMilestones(Github github, ReleaserProperties properties) {
this.github = new CachingGithub(github);
this.properties = properties;
}
@@ -62,7 +66,7 @@ class GithubMilestones {
+ "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=...]");
GHMilestone foundMilestone = MILESTONE_CACHE.get(version);
Milestone.Smart foundMilestone = MILESTONE_CACHE.get(version);
String tagVersion = version.version;
if (foundMilestone == null) {
foundMilestone = matchingMilestone(tagVersion, openMilestones(version));
@@ -85,26 +89,27 @@ class GithubMilestones {
}
}
GHMilestone matchingMilestone(String tagVersion, Iterable<GHMilestone> milestones) {
Milestone.Smart matchingMilestone(String tagVersion, Iterable<Milestone> milestones) {
log.debug("Successfully received list of milestones [{}]", milestones);
log.info("Will try to match against tag version [{}]", tagVersion);
try {
int counter = 0;
for (GHMilestone milestone : milestones) {
for (Milestone milestone : milestones) {
if (counter++ >= this.properties.getGit().getNumberOfCheckedMilestones()) {
log.warn(
"No matching milestones were found within the provided threshold [{}] of checked milestones",
this.properties.getGit().getNumberOfCheckedMilestones());
return null;
}
String title = milestone.getTitle();
Milestone.Smart smartMilestone = new Milestone.Smart(milestone);
String title = milestoneTitle(smartMilestone);
if (tagVersion.equals(title) || numericVersion(tagVersion).equals(title)) {
log.info("Found a matching milestone [{}]", milestone.getNumber());
return milestone;
log.info("Found a matching milestone [{}]", smartMilestone.number());
return smartMilestone;
}
}
}
catch (Exception e) {
catch (AssertionError | IOException e) {
log.error("Exception occurred while trying to retrieve the milestone", e);
return null;
}
@@ -120,7 +125,7 @@ class GithubMilestones {
Assert.hasText(this.properties.getGit().getOauthToken(),
"You have to pass Github OAuth token for milestone closing to be operational");
String tagVersion = version.version;
GHMilestone foundMilestone = matchingMilestone(tagVersion, closedMilestones(version));
Milestone.Smart foundMilestone = matchingMilestone(tagVersion, closedMilestones(version));
String foundUrl = "";
if (foundMilestone != null) {
try {
@@ -144,36 +149,51 @@ class GithubMilestones {
return version.contains("RELEASE") ? version.substring(0, version.lastIndexOf(".")) : "";
}
String milestoneTitle(GHMilestone milestone) throws IOException {
return milestone.getTitle();
String milestoneTitle(Milestone.Smart milestone) throws IOException {
return milestone.title();
}
URL foundMilestoneUrl(GHMilestone milestone) throws IOException {
return milestone.getUrl();
URL foundMilestoneUrl(Milestone.Smart milestone) throws IOException {
return milestone.url();
}
private Iterable<GHMilestone> openMilestones(ProjectVersion version) {
private Iterable<Milestone> getMilestones(ProjectVersion version, Map<String, String> map) {
try {
return this.github.getRepository(org() + "/" + version.projectName).listMilestones(GHIssueState.OPEN)
.toList();
return this.github.repos().get(new Coordinates.Simple(org(), version.projectName)).milestones()
.iterate(map);
}
catch (IOException e) {
throw new RuntimeException(e);
catch (AssertionError e) {
log.error("Exception occurred while trying to fetch milestones", e);
return new ArrayList<>();
}
}
private Iterable<GHMilestone> closedMilestones(ProjectVersion version) {
try {
return this.github.getRepository(org() + "/" + version.projectName).listMilestones(GHIssueState.CLOSED)
.toList();
}
catch (IOException e) {
throw new RuntimeException(e);
}
private Iterable<Milestone> openMilestones(ProjectVersion version) {
return getMilestones(version, openMilestones());
}
private Iterable<Milestone> closedMilestones(ProjectVersion version) {
return getMilestones(version, closedMilestones());
}
String org() {
return this.properties.getGit().getOrgName();
}
private Map<String, String> openMilestones() {
Map<String, String> params = new HashMap<>();
params.put("state", "open");
params.put("sort", "due_on");
params.put("direction", "desc");
return params;
}
private Map<String, String> closedMilestones() {
Map<String, String> params = new HashMap<>();
params.put("state", "closed");
params.put("sort", "due_on");
params.put("direction", "desc");
return params;
}
}

View File

@@ -52,13 +52,13 @@ class RestTemplateSaganClient implements SaganClient {
public Project getProject(String projectName) {
HttpHeaders headers = new HttpHeaders();
headers.put("Accept", Collections.singletonList("application/hal+json"));
Project project = this.restTemplate.exchange(this.baseUrl + "/projects/{projectName}", HttpMethod.GET,
Project project = this.restTemplate.exchange(this.baseUrl + "/api/projects/{projectName}", HttpMethod.GET,
new HttpEntity<>(headers), Project.class, projectName).getBody();
if (project == null) {
return null;
}
EmbeddedProjectReleases body = this.restTemplate.exchange(this.baseUrl + "/projects/{projectName}/releases",
EmbeddedProjectReleases body = this.restTemplate.exchange(this.baseUrl + "/api/projects/{projectName}/releases",
HttpMethod.GET, new HttpEntity<>(headers), EmbeddedProjectReleases.class, projectName).getBody();
project.setReleases(body._embedded.releases);
return project;
@@ -68,14 +68,14 @@ class RestTemplateSaganClient implements SaganClient {
public Release getRelease(String projectName, String releaseVersion) {
HttpHeaders headers = new HttpHeaders();
headers.put("Accept", Collections.singletonList("application/hal+json"));
return this.restTemplate.exchange(this.baseUrl + "/projects/{projectName}/releases/{releaseVersion}",
return this.restTemplate.exchange(this.baseUrl + "/api/projects/{projectName}/releases/{releaseVersion}",
HttpMethod.GET, new HttpEntity<>(headers), Release.class, projectName, releaseVersion).getBody();
}
@Override
public boolean deleteRelease(String projectName, String releaseVersion) {
ResponseEntity<Release> entity = this.restTemplate.exchange(
this.baseUrl + "/projects/{projectName}/releases/{releaseVersion}", HttpMethod.DELETE,
this.baseUrl + "/api/projects/{projectName}/releases/{releaseVersion}", HttpMethod.DELETE,
new HttpEntity<>(""), Release.class, projectName, releaseVersion);
boolean deleted = entity.getStatusCode().is2xxSuccessful();
log.info("Response from Sagan\n\n[{}] \n with status [{}]", entity, entity.getStatusCode());
@@ -85,7 +85,7 @@ class RestTemplateSaganClient implements SaganClient {
@Override
public boolean addRelease(String projectName, ReleaseInput releaseInput) {
RequestEntity<ReleaseInput> request = RequestEntity
.post(URI.create(this.baseUrl + "/projects/" + projectName + "/releases"))
.post(URI.create(this.baseUrl + "/api/projects/" + projectName + "/releases"))
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE).body(releaseInput);
ResponseEntity<Project> entity = this.restTemplate.exchange(request, Project.class);
boolean added = entity.getStatusCode().is2xxSuccessful();
@@ -94,12 +94,19 @@ class RestTemplateSaganClient implements SaganClient {
}
@Override
public void patchProjectDetails(String projectName, ProjectDetails details) {
RequestEntity<ProjectDetails> request = RequestEntity
.patch(URI.create(this.baseUrl + "/projects/" + projectName + "/details"))
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE).body(details);
ResponseEntity<Object> entity = this.restTemplate.exchange(request, Object.class);
log.info("Response from Sagan\n\n[{}]", entity);
public Project patchProject(Project project) {
// RequestEntity<Project> request = RequestEntity
// .patch(URI.create(this.baseUrl + "/project_metadata/" + project.getSlug()))
// .header(HttpHeaders.CONTENT_TYPE,
// MediaType.APPLICATION_JSON_UTF8_VALUE).body(project);
// ResponseEntity<Project> entity = this.restTemplate.exchange(request,
// Project.class);
// Project updatedProject = entity.getBody();
// log.info("Response from Sagan\n\n[{}] \n with body [{}]", entity,
// updatedProject);
// return updatedProject;
// FIXME: no api yet https://github.com/spring-io/sagan/issues/1052
return null;
}
private static class EmbeddedProjectReleases {

View File

@@ -29,6 +29,6 @@ public interface SaganClient {
boolean addRelease(String projectName, ReleaseInput releaseInput);
void patchProjectDetails(String projectName, ProjectDetails details);
Project patchProject(Project project);
}

View File

@@ -95,27 +95,30 @@ public class SaganUpdater {
File docsModule = docsModule(projectFile);
File indexDoc = new File(docsModule, this.releaserProperties.getSagan().getIndexSectionFileName());
File bootDoc = new File(docsModule, this.releaserProperties.getSagan().getBootSectionFileName());
ProjectDetails projectDetails = new ProjectDetails();
if (indexDoc.exists()) {
log.debug("Index adoc file exists");
String fileText = fileToText(indexDoc);
if (StringUtils.hasText(fileText)) {
log.info("Index adoc content differs from the previously stored, will update it");
projectDetails.setBody(fileText);
shouldUpdate = true;
}
// if (StringUtils.hasText(fileText) && !fileText.equals(project.rawOverview))
// {
// log.info("Index adoc content differs from the previously stored, will
// update it");
// project.rawOverview = fileText;
// shouldUpdate = true;
// }
}
if (bootDoc.exists()) {
log.debug("Boot adoc file exists");
String fileText = fileToText(bootDoc);
if (StringUtils.hasText(fileText)) {
log.info("Boot adoc content differs from the previously stored, will update it");
projectDetails.setBootConfig(fileText);
shouldUpdate = true;
}
// if (StringUtils.hasText(fileText) &&
// !fileText.equals(project.rawBootConfig)) {
// log.info("Boot adoc content differs from the previously stored, will update
// it");
// project.rawBootConfig = fileText;
// shouldUpdate = true;
// }
}
if (shouldUpdate) {
this.saganClient.patchProjectDetails(project.getName(), projectDetails);
this.saganClient.patchProject(project);
log.info("Updating Sagan project with adoc data.");
}
else {

View File

@@ -0,0 +1,219 @@
/*
* 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.github;
import com.jcabi.github.Coordinates;
import com.jcabi.github.Github;
import com.jcabi.github.Issues;
import com.jcabi.github.Milestones;
import com.jcabi.github.Releases;
import com.jcabi.github.Repo;
import com.jcabi.github.Repos;
import org.junit.jupiter.api.Test;
import org.mockito.BDDMockito;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.only;
import static org.mockito.Mockito.verify;
class CachingGithubTests {
@Test
void should_call_repos_only_once_for_same_github() {
Github github = mock(Github.class);
Repos repos = mock(Repos.class);
given(github.repos()).willReturn(repos);
CachingGithub cachingGithub = new CachingGithub(github);
cachingGithub.repos();
cachingGithub.repos();
cachingGithub.repos();
verify(github, only()).repos();
}
@Test
void should_not_cache_repos_calls_for_different_githubs() {
Github github1 = mock(Github.class);
Github github2 = mock(Github.class);
Github github3 = mock(Github.class);
new CachingGithub(github1).repos();
new CachingGithub(github2).repos();
new CachingGithub(github3).repos();
verify(github1).repos();
verify(github2).repos();
verify(github3).repos();
}
@Test
void should_get_repo_only_once_for_same_coordinates() {
Repos repos = mock(Repos.class);
CachingRepos cachingRepos = new CachingRepos(repos);
Coordinates.Simple simple = new Coordinates.Simple("foo", "bar");
cachingRepos.get(simple);
cachingRepos.get(simple);
cachingRepos.get(simple);
verify(repos, only()).get(simple);
}
@Test
void should_not_cache_calls_for_different_coordinates() {
Repos repos = mock(Repos.class);
CachingRepos cachingRepos = new CachingRepos(repos);
Coordinates.Simple simple1 = new Coordinates.Simple("foo", "bar1");
Coordinates.Simple simple2 = new Coordinates.Simple("foo", "bar2");
Coordinates.Simple simple3 = new Coordinates.Simple("foo", "bar3");
cachingRepos.get(simple1);
cachingRepos.get(simple2);
cachingRepos.get(simple3);
verify(repos).get(simple1);
verify(repos).get(simple2);
verify(repos).get(simple3);
}
@Test
void should_coordinates_only_once_for_same_repo() {
Repo repo = mock(Repo.class);
given(repo.coordinates()).willReturn(new Coordinates.Simple("foo", "bar"));
CachingRepo cachingRepo = new CachingRepo(repo);
cachingRepo.coordinates();
cachingRepo.coordinates();
cachingRepo.coordinates();
verify(repo, only()).coordinates();
}
@Test
void should_not_cache_calls_for_different_coordinates_for_repo() {
Repo repo1 = mock(Repo.class);
given(repo1.coordinates()).willReturn(new Coordinates.Simple("foo", "bar"));
Repo repo2 = mock(Repo.class);
given(repo2.coordinates()).willReturn(new Coordinates.Simple("foo", "bar"));
Repo repo3 = mock(Repo.class);
given(repo3.coordinates()).willReturn(new Coordinates.Simple("foo", "bar"));
new CachingRepo(repo1).coordinates();
new CachingRepo(repo2).coordinates();
new CachingRepo(repo3).coordinates();
verify(repo1).coordinates();
verify(repo2).coordinates();
verify(repo3).coordinates();
}
@Test
void should_issues_only_once_for_same_repo() {
Repo repo = mock(Repo.class);
given(repo.issues()).willReturn(BDDMockito.mock(Issues.class));
CachingRepo cachingRepo = new CachingRepo(repo);
cachingRepo.issues();
cachingRepo.issues();
cachingRepo.issues();
verify(repo, only()).issues();
}
@Test
void should_not_cache_calls_for_different_issues_for_repo() {
Repo repo1 = mock(Repo.class);
given(repo1.issues()).willReturn(BDDMockito.mock(Issues.class));
Repo repo2 = mock(Repo.class);
given(repo2.issues()).willReturn(BDDMockito.mock(Issues.class));
Repo repo3 = mock(Repo.class);
given(repo3.issues()).willReturn(BDDMockito.mock(Issues.class));
new CachingRepo(repo1).issues();
new CachingRepo(repo2).issues();
new CachingRepo(repo3).issues();
verify(repo1).issues();
verify(repo2).issues();
verify(repo3).issues();
}
@Test
void should_call_milestones_only_once_for_same_repo() {
Repo repo = mock(Repo.class);
given(repo.milestones()).willReturn(BDDMockito.mock(Milestones.class));
CachingRepo cachingRepo = new CachingRepo(repo);
cachingRepo.milestones();
cachingRepo.milestones();
cachingRepo.milestones();
verify(repo, only()).milestones();
}
@Test
void should_not_cache_calls_for_different_milestones_for_repo() {
Repo repo1 = mock(Repo.class);
given(repo1.milestones()).willReturn(BDDMockito.mock(Milestones.class));
Repo repo2 = mock(Repo.class);
given(repo2.milestones()).willReturn(BDDMockito.mock(Milestones.class));
Repo repo3 = mock(Repo.class);
given(repo3.milestones()).willReturn(BDDMockito.mock(Milestones.class));
new CachingRepo(repo1).milestones();
new CachingRepo(repo2).milestones();
new CachingRepo(repo3).milestones();
verify(repo1).milestones();
verify(repo2).milestones();
verify(repo3).milestones();
}
@Test
void should_call_releases_only_once_for_same_repo() {
Repo repo = mock(Repo.class);
given(repo.releases()).willReturn(BDDMockito.mock(Releases.class));
CachingRepo cachingRepo = new CachingRepo(repo);
cachingRepo.releases();
cachingRepo.releases();
cachingRepo.releases();
verify(repo, only()).releases();
}
@Test
void should_not_cache_calls_for_different_releases_for_repo() {
Repo repo1 = mock(Repo.class);
given(repo1.releases()).willReturn(BDDMockito.mock(Releases.class));
Repo repo2 = mock(Repo.class);
given(repo2.releases()).willReturn(BDDMockito.mock(Releases.class));
Repo repo3 = mock(Repo.class);
given(repo3.releases()).willReturn(BDDMockito.mock(Releases.class));
new CachingRepo(repo1).releases();
new CachingRepo(repo2).releases();
new CachingRepo(repo3).releases();
verify(repo1).releases();
verify(repo2).releases();
verify(repo3).releases();
}
}

View File

@@ -1,111 +0,0 @@
/*
* 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.internal.github;
import java.io.IOException;
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
import org.assertj.core.api.BDDAssertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.kohsuke.github.GHRepository;
import org.kohsuke.github.GitHub;
import org.mockito.BDDMockito;
import releaser.internal.ReleaserProperties;
import releaser.internal.project.ProjectVersion;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import static org.assertj.core.api.BDDAssertions.thenThrownBy;
/**
* @author Marcin Grzejszczak
*/
@ExtendWith(OutputCaptureExtension.class)
@WireMockTest(httpPort = 12346)
class GithubIssueFillerTests {
private static final String ORG = "marcingrzejszczak";
private static final String REPO = "test-repo";
private static final String TOKEN = "FOO";
GitHub github;
ReleaserProperties properties = withToken();
GithubIssueFiler filer;
@BeforeEach
void setup() throws IOException {
this.github = GitHub.connectToEnterprise("http://localhost:12346", TOKEN);
this.filer = new GithubIssueFiler(github, properties);
}
@Test
void should_not_do_anything_for_non_release_train_version() {
GitHub github = BDDMockito.mock(GitHub.class);
filer.fileAGitHubIssue(ORG, REPO, new ProjectVersion("foo", "1.0.0-SNAPSHOT"), "foo", "bar");
BDDMockito.then(github).shouldHaveNoInteractions();
}
@Test
void should_file_an_issue_for_release_version(CapturedOutput capturedOutput) throws IOException {
filer.fileAGitHubIssue(ORG, REPO, new ProjectVersion("foo", "1.0.0"), "foo", "bar");
BDDAssertions.then(capturedOutput.toString()).contains("Successfully created an issue");
}
@Test
void should_not_file_an_issue_for_release_version_if_one_is_already_created(CapturedOutput capturedOutput)
throws IOException {
new GithubIssueFiler(github, properties) {
@Override
boolean issueAlreadyFiled(GHRepository springGuides, String issueTitle) throws IOException {
return true;
}
}.fileAGitHubIssue(ORG, REPO, new ProjectVersion("foo", "1.0.0"), "foo", "bar");
BDDAssertions.then(capturedOutput.toString()).doesNotContain("Successfully created an issue")
.contains("Issue already filed, will not do that again");
}
@Test
void should_throw_exception_when_no_token_was_passed() {
properties.getGit().setOauthToken("");
thenThrownBy(() -> filer.fileAGitHubIssue(ORG, REPO, new ProjectVersion("foo", "1.0.0"), "foo", "bar"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("You have to pass Github OAuth token for milestone closing to be operational");
}
ReleaserProperties withToken() {
ReleaserProperties properties = new ReleaserProperties();
properties.getGit().setOrgName(ORG);
properties.getGit().setOauthToken(TOKEN);
properties.getPom().setBranch("vEdgware.RELEASE");
properties.getGit().setUpdateSpringGuides(true);
properties.getGit().setUpdateStartSpringIo(true);
return properties;
}
}

View File

@@ -0,0 +1,118 @@
/*
* 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.internal.github;
import java.io.IOException;
import java.util.Collections;
import com.jcabi.github.Github;
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.internal.project.ProjectVersion;
import releaser.internal.project.Projects;
/**
* @author Marcin Grzejszczak
*/
public class GithubIssuesTests {
MkGithub github;
Repo repo;
@BeforeEach
public void setup() throws IOException {
this.github = github("spring-guides");
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);
GithubIssues issues = new GithubIssues(Collections.emptyList());
issues.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_not_do_anything_if_not_applicable() {
Github github = BDDMockito.mock(Github.class);
GithubIssues issues = new GithubIssues(Collections.emptyList());
issues.fileIssueInSpringGuides(
new Projects(new ProjectVersion("foo", "1.0.0.RELEASE"), new ProjectVersion("bar", "2.0.0.RELEASE"),
new ProjectVersion("baz", "3.0.0.RELEASE")),
new ProjectVersion("sc-release", "Edgware.RELEASE"));
BDDMockito.then(github).shouldHaveNoInteractions();
}
@Test
public void should_not_do_anything_for_non_release_train_version_when_updating_startspringio() throws IOException {
setupStartSpringIo();
Github github = BDDMockito.mock(Github.class);
GithubIssues issues = new GithubIssues(Collections.emptyList());
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_not_do_anything_if_not_applicable_when_updating_startspringio() throws IOException {
setupStartSpringIo();
Github github = BDDMockito.mock(Github.class);
GithubIssues issues = new GithubIssues(Collections.emptyList());
issues.fileIssueInStartSpringIo(
new Projects(new ProjectVersion("foo", "1.0.0.RELEASE"), new ProjectVersion("bar", "2.0.0.RELEASE"),
new ProjectVersion("baz", "3.0.0.RELEASE")),
new ProjectVersion("sc-release", "Edgware.RELEASE"));
BDDMockito.then(github).shouldHaveNoInteractions();
}
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));
}
}

View File

@@ -19,12 +19,13 @@ package releaser.internal.github;
import java.io.IOException;
import java.net.URL;
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
import com.jcabi.github.Milestone;
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.junit.jupiter.api.extension.ExtendWith;
import org.kohsuke.github.GHMilestone;
import org.kohsuke.github.GitHub;
import releaser.internal.ReleaserProperties;
import releaser.internal.project.ProjectVersion;
@@ -38,23 +39,32 @@ import static org.assertj.core.api.BDDAssertions.thenThrownBy;
* @author Marcin Grzejszczak
*/
@ExtendWith(OutputCaptureExtension.class)
@WireMockTest(httpPort = 12345)
class GithubMilestonesTests {
public class GithubMilestonesTests {
private static final String ORG = "marcingrzejszczak";
MkGithub github;
private static final String TOKEN = "foo";
GitHub github;
Repo repo;
@BeforeEach
public void setup() throws IOException {
this.github = GitHub.connectToEnterprise("http://localhost:12345", TOKEN);
this.github = new MkGithub();
this.repo = createSleuthRepo(this.github);
}
@Test
void should_close_milestone_if_there_is_one(CapturedOutput capturedOutput) throws IOException {
GithubMilestones milestones = new GithubMilestones(this.github, withToken());
public void should_close_milestone_if_there_is_one(CapturedOutput capturedOutput) throws IOException {
GithubMilestones milestones = new GithubMilestones(this.github, withToken()) {
@Override
String org() {
return GithubMilestonesTests.this.repo.coordinates().user();
}
@Override
String milestoneTitle(Milestone.Smart milestone) {
return "0.2.0.BUILD-SNAPSHOT";
}
};
this.repo.milestones().create("0.2.0.BUILD-SNAPSHOT");
milestones.closeMilestone(nonGaSleuthProject());
@@ -62,126 +72,183 @@ class GithubMilestonesTests {
}
@Test
void should_close_milestone_when_the_milestone_contains_numeric_version_only_and_version_is_ga(
public void should_close_milestone_when_the_milestone_contains_numeric_version_only_and_version_is_ga(
CapturedOutput capturedOutput) throws IOException {
GithubMilestones milestones = new GithubMilestones(this.github, withToken());
GithubMilestones milestones = new GithubMilestones(this.github, withToken()) {
@Override
String org() {
return GithubMilestonesTests.this.repo.coordinates().user();
}
milestones.closeMilestone(gaProject());
@Override
String milestoneTitle(Milestone.Smart milestone) {
return "0.2.0";
}
};
this.repo.milestones().create("0.2.0");
milestones.closeMilestone(gaSleuthProject());
then(capturedOutput.toString()).doesNotContain("No matching milestone was found");
}
private ProjectVersion closedProject() {
return new ProjectVersion("test-repo", "0.1.0");
}
private ProjectVersion gaProject() {
return new ProjectVersion("test-repo", "0.0.1");
private ProjectVersion gaSleuthProject() {
return new ProjectVersion("spring-cloud-sleuth", "0.2.0.RELEASE");
}
@Test
void should_not_close_milestone_when_the_milestone_contains_numeric_version_only(CapturedOutput capturedOutput)
throws IOException {
GithubMilestones milestones = new GithubMilestones(this.github, withToken());
public void should_not_close_milestone_when_the_milestone_contains_numeric_version_only(
CapturedOutput capturedOutput) throws IOException {
GithubMilestones milestones = new GithubMilestones(this.github, withToken()) {
@Override
String org() {
return GithubMilestonesTests.this.repo.coordinates().user();
}
milestones.closeMilestone(notMatchingProjectVersion());
@Override
String milestoneTitle(Milestone.Smart milestone) {
return "0.2.0";
}
};
this.repo.milestones().create("0.2.0");
milestones.closeMilestone(nonGaSleuthProject());
then(capturedOutput.toString()).contains("No matching milestone was found");
}
@Test
void should_fetch_url_of_a_closed_matching_milestone() throws IOException {
GithubMilestones milestones = new GithubMilestones(this.github, withToken());
String url = milestones.milestoneUrl(closedProject());
then(url).isEqualTo("https://github.com/marcingrzejszczak/test-repo/milestone/3?closed=1");
}
@Test
void should_fetch_url_of_a_closed_matching_milestone_from_cache() {
public void should_fetch_url_of_a_closed_matching_milestone() throws IOException {
GithubMilestones milestones = new GithubMilestones(this.github, withToken()) {
@Override
GHMilestone matchingMilestone(String tagVersion, Iterable<GHMilestone> milestones) {
throw new AssertionError("This should not be called");
String org() {
return GithubMilestonesTests.this.repo.coordinates().user();
}
@Override
String milestoneTitle(Milestone.Smart milestone) {
return "0.2.0.RELEASE";
}
@Override
URL foundMilestoneUrl(Milestone.Smart milestone) throws IOException {
return new URL("https://api.github.com/repos/spring-cloud/spring-cloud-sleuth/milestones/33");
}
};
GithubMilestones.MILESTONE_URL_CACHE.put(gaProject(),
"https://github.com/marcingrzejszczak/test-repo/milestone/3?closed=1");
this.repo.milestones().create("0.2.0.RELEASE");
String url = milestones.milestoneUrl(gaProject());
String url = milestones.milestoneUrl(gaSleuthProject());
then(url).isEqualTo("https://github.com/marcingrzejszczak/test-repo/milestone/3?closed=1");
then(url).isEqualTo("https://github.com/spring-cloud/spring-cloud-sleuth/milestone/33?closed=1");
}
@Test
void should_return_null_if_no_matching_milestone_was_found() {
public void should_fetch_url_of_a_closed_matching_milestone_from_cache() {
GithubMilestones milestones = new GithubMilestones(this.github, withToken());
GithubMilestones.MILESTONE_URL_CACHE.put(gaSleuthProject(),
"https://github.com/spring-cloud/spring-cloud-sleuth/milestone/33?closed=1");
String url = milestones.milestoneUrl(gaSleuthProject());
then(url).isEqualTo("https://github.com/spring-cloud/spring-cloud-sleuth/milestone/33?closed=1");
}
@Test
public void should_return_null_if_no_matching_milestone_was_found() {
GithubMilestones milestones = new GithubMilestones(this.github, withToken()) {
@Override
String milestoneTitle(GHMilestone milestone) {
return "0.9.0";
String org() {
return GithubMilestonesTests.this.repo.coordinates().user();
}
@Override
URL foundMilestoneUrl(GHMilestone milestone) throws IOException {
String milestoneTitle(Milestone.Smart milestone) {
return "0.9.0.RELEASE";
}
@Override
URL foundMilestoneUrl(Milestone.Smart milestone) throws IOException {
return new URL("http://www.foo.com/bar");
}
};
String url = milestones.milestoneUrl(gaProject());
String url = milestones.milestoneUrl(gaSleuthProject());
then(url).isEmpty();
}
@Test
void should_return_null_if_no_matching_milestone_was_found_within_threshold(CapturedOutput capturedOutput)
public void should_return_null_if_no_matching_milestone_was_found_within_threshold(CapturedOutput capturedOutput)
throws IOException {
GithubMilestones milestones = new GithubMilestones(this.github, withThreshold());
GithubMilestones milestones = new GithubMilestones(this.github, withThreshold()) {
@Override
String org() {
return GithubMilestonesTests.this.repo.coordinates().user();
}
milestones.closeMilestone(gaProject());
@Override
String milestoneTitle(Milestone.Smart milestone) {
return "0.2.0";
}
};
this.repo.milestones().create("0.2.0");
milestones.closeMilestone(gaSleuthProject());
then(capturedOutput.toString()).contains("No matching milestones were found within the provided threshold [0]");
}
private ProjectVersion nonGaSleuthProject() {
return new ProjectVersion("test-repo", "0.0.1-SNAPSHOT");
}
private ProjectVersion notMatchingProjectVersion() {
return new ProjectVersion("test-repo", "0.0.100-SNAPSHOT");
return new ProjectVersion("spring-cloud-sleuth", "0.2.0.BUILD-SNAPSHOT");
}
@Test
void should_throw_exception_when_there_is_no_matching_milestone(CapturedOutput capturedOutput) throws IOException {
public void should_throw_exception_when_there_is_no_matching_milestone(CapturedOutput capturedOutput)
throws IOException {
GithubMilestones milestones = new GithubMilestones(this.github, withToken()) {
@Override
String org() {
return ORG;
return GithubMilestonesTests.this.repo.coordinates().user();
}
@Override
String milestoneTitle(Milestone.Smart milestone) {
return "0.1.0.BUILD-SNAPSHOT";
}
};
this.repo.milestones().create("v0.2.0.BUILD-SNAPSHOT");
milestones.closeMilestone(notMatchingProjectVersion());
milestones.closeMilestone(nonGaSleuthProject());
then(capturedOutput.toString()).contains("No matching milestone was found");
}
@Test
void should_print_that_no_milestones_were_found_when_io_problems_occurred(CapturedOutput capturedOutput)
public void should_print_that_no_milestones_were_found_when_io_problems_occurred(CapturedOutput capturedOutput)
throws IOException {
GithubMilestones milestones = new GithubMilestones(this.github, withToken()) {
@Override
String org() {
return GithubMilestonesTests.this.repo.coordinates().user();
}
@Override
GHMilestone matchingMilestone(String tagVersion, Iterable<GHMilestone> milestones) {
return null;
String milestoneTitle(Milestone.Smart milestone) throws IOException {
throw new IOException("foo");
}
};
this.repo.milestones().create("v0.2.0.BUILD-SNAPSHOT");
milestones.closeMilestone(nonGaSleuthProject());
then(capturedOutput.toString()).contains("No matching milestone was found");
}
private Repo createSleuthRepo(MkGithub github) throws IOException {
return github.repos().create(new Repos.RepoCreate("spring-cloud-sleuth", false));
}
@Test
void should_throw_exception_when_no_token_was_passed() {
public void should_throw_exception_when_no_token_was_passed() {
GithubMilestones milestones = new GithubMilestones(new ReleaserProperties());
thenThrownBy(() -> milestones.closeMilestone(nonGaSleuthProject())).isInstanceOf(IllegalArgumentException.class)
@@ -190,16 +257,13 @@ class GithubMilestonesTests {
ReleaserProperties withToken() {
ReleaserProperties properties = new ReleaserProperties();
properties.getGit().setOauthToken(TOKEN);
properties.getGit().setOrgName(ORG);
properties.getGit().setOauthToken("foo");
return properties;
}
ReleaserProperties withThreshold() {
ReleaserProperties properties = withToken();
properties.getGit().setNumberOfCheckedMilestones(0);
properties.getGit().setOauthToken(TOKEN);
properties.getGit().setOrgName(ORG);
return properties;
}

View File

@@ -18,13 +18,19 @@ package releaser.internal.sagan;
import java.io.IOException;
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
import org.assertj.core.api.BDDAssertions;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import releaser.internal.ReleaserProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.BDDAssertions.then;
@@ -32,21 +38,30 @@ import static org.assertj.core.api.BDDAssertions.then;
/**
* @author Marcin Grzejszczak
*/
@WireMockTest(httpPort = 23456)
class RestTemplateSaganClientTests {
@Disabled
@SpringBootTest(classes = RestTemplateSaganClientTests.Config.class)
@AutoConfigureStubRunner(ids = "io.spring.sagan:sagan-site")
public class RestTemplateSaganClientTests {
RestTemplateSaganClient client;
@Value("${stubrunner.runningstubs.sagan-site.port}")
Integer saganPort;
@Autowired
ObjectMapper objectMapper;
SaganClient client;
@BeforeEach
void setup() {
public void setup() {
ReleaserProperties properties = new ReleaserProperties();
properties.getGit().setOauthToken("foo");
properties.getSagan().setBaseUrl("http://localhost:23456");
properties.getSagan().setBaseUrl("http://localhost:" + this.saganPort);
this.client = saganClient(properties);
}
@Test
void should_get_a_project() {
@Disabled("TODO: The API has changed")
public void should_get_a_project() {
Project project = this.client.getProject("spring-boot");
then(project.getSlug()).isEqualTo("spring-boot");
@@ -55,54 +70,82 @@ class RestTemplateSaganClientTests {
then(project.getStatus()).isEqualTo("ACTIVE");
then(project.getReleases()).isNotEmpty();
Release release = project.getReleases().get(0);
then(release.getStatus()).isEqualTo("GENERAL_AVAILABILITY");
then(release.getStatus()).isEqualTo("PRERELEASE");
then(release.getReferenceDocUrl())
.isEqualTo("https://docs.spring.io/spring-boot/docs/{version}/reference/html/");
then(release.getApiDocUrl()).isEqualTo("https://docs.spring.io/spring-boot/docs/{version}/api/");
then(release.getVersion()).isEqualTo("2.5.14");
.isEqualTo("https://docs.spring.io/spring-boot/docs/2.4.0-M1/reference/html/");
then(release.getApiDocUrl()).isEqualTo("https://docs.spring.io/spring-boot/docs/2.4.0-M1/api/");
then(release.getVersion()).isEqualTo("2.4.0-M1");
}
@Test
void should_get_a_release() {
Release release = this.client.getRelease("spring-boot", "2.5.14");
@Disabled("TODO: The API has changed")
public void should_get_a_release() {
Release release = this.client.getRelease("spring-boot", "2.3.0.RELEASE");
then(release.getStatus()).isEqualTo("GENERAL_AVAILABILITY");
then(release.getReferenceDocUrl())
.isEqualTo("https://docs.spring.io/spring-boot/docs/{version}/reference/html/");
then(release.getApiDocUrl()).isEqualTo("https://docs.spring.io/spring-boot/docs/{version}/api/");
then(release.getVersion()).isEqualTo("2.5.14");
then(release.getReferenceDocUrl()).isEqualTo("https://docs.spring.io/spring-boot/docs/current/reference/html/");
then(release.getApiDocUrl()).isEqualTo("https://docs.spring.io/spring-boot/docs/current/api/");
then(release.getVersion()).isEqualTo("2.3.0.RELEASE");
}
@Test
void should_delete_a_release() {
boolean deleted = this.client.deleteRelease("spring-cloud-contract", "4.0.3-SNAPSHOT");
@Disabled("TODO: The API has changed")
public void should_delete_a_release() {
boolean deleted = this.client.deleteRelease("spring-boot", "2.3.0.RELEASE");
then(deleted).isTrue();
}
@Test
void should_update_a_release() {
@Disabled("TODO: The API has changed")
public void should_update_a_release() {
ReleaseInput releaseInput = new ReleaseInput();
releaseInput.setVersion("4.0.3-SNAPSHOT");
releaseInput.setReferenceDocUrl("https://docs.spring.io/spring-cloud-contract/docs/{version}/reference/html/");
releaseInput.setApiDocUrl("https://docs.spring.io/spring-cloud-contract/docs/{version}/api/");
boolean added = this.client.addRelease("spring-cloud-contract", releaseInput);
releaseInput.setVersion("2.2.0.RELEASE");
releaseInput.setReferenceDocUrl("https://docs.spring.io/spring-boot/docs/{version}/reference/html/");
releaseInput.setApiDocUrl("https://docs.spring.io/spring-boot/docs/{version}/api/");
boolean added = this.client.addRelease("spring-boot", releaseInput);
then(added).isTrue();
}
@Test
void should_patch_a_project() throws IOException {
ProjectDetails projectDetails = new ProjectDetails();
projectDetails.setBody("new body");
projectDetails.setBootConfig("new sbc");
@Disabled("no api yet https://github.com/spring-io/sagan/issues/1052")
public void should_patch_a_project() throws IOException {
String projectJson = "{\n \"id\" : \"spring-framework\",\n "
+ "\"rawBootConfig\" : \"rawBootConfig\",\n \"rawOverview\" : \"rawOverview\",\n "
+ "\"displayOrder\" : 2147483647,\n \"projectReleases\" : [ ],"
+ "\n \"projectSamples\" : [ ],\n \"mostCurrentRelease\" : {\n \"present\" : false\n },"
+ "\n \"nonMostCurrentReleases\" : [ ],\n \"stackOverflowTagList\" : [ ],\n \"topLevelProject\" : true\n}";
Project project = this.objectMapper.readValue(projectJson, Project.class);
BDDAssertions.thenNoException()
.isThrownBy(() -> this.client.patchProjectDetails("spring-cloud-contract", projectDetails));
Project patchedProject = this.client.patchProject(project);
// then(patchedProject.id).isEqualTo("spring-framework");
// then(patchedProject.name).isEqualTo("Spring Framework");
// then(patchedProject.rawBootConfig).isEqualTo("rawBootConfig");
// then(patchedProject.rawOverview).isEqualTo("rawOverview");
}
private RestTemplateSaganClient saganClient(ReleaserProperties properties) {
private Repository milestone() {
Repository milestone = new Repository();
milestone.id = "spring-milestones";
milestone.name = "Spring Milestones";
milestone.url = "https://repo.spring.io/libs-milestone";
milestone.snapshotsEnabled = false;
return milestone;
}
private Repository snapshots() {
Repository snapshots = new Repository();
snapshots.id = "spring-snapshots";
snapshots.name = "Spring Snapshots";
snapshots.url = "https://repo.spring.io/libs-snapshot";
snapshots.snapshotsEnabled = true;
return snapshots;
}
private SaganClient saganClient(ReleaserProperties properties) {
RestTemplate restTemplate = restTemplate(properties);
return new RestTemplateSaganClient(restTemplate, properties);
}
@@ -111,4 +154,10 @@ class RestTemplateSaganClientTests {
return new RestTemplateBuilder().basicAuthentication(properties.getGit().getOauthToken(), "").build();
}
@Configuration
@EnableAutoConfiguration
static class Config {
}
}

View File

@@ -57,7 +57,7 @@ public class SaganUpdaterTest {
private Project project;
@BeforeEach
void setup() {
public void setup() {
project = new Project();
project.setReleases(Arrays.asList(release("2.2.0-RC1"), release("2.3.0-SNAPSHOT"), release("2.2.0-M4")));
this.properties.getSagan().setUpdateSagan(true);
@@ -72,7 +72,7 @@ public class SaganUpdaterTest {
}
@Test
void should_not_update_sagan_when_switch_is_off() {
public void should_not_update_sagan_when_switch_is_off() {
this.properties.getSagan().setUpdateSagan(false);
ExecutionResult result = this.saganUpdater.updateSagan(new File("."), "main", version("2.2.0-M1"),
@@ -83,7 +83,7 @@ public class SaganUpdaterTest {
}
@Test
void should_update_sagan_releases_for_milestone() {
public void should_update_sagan_releases_for_milestone() {
given(this.saganClient.addRelease(eq("foo"), any())).willReturn(true);
given(this.saganClient.getProject("foo")).willReturn(projectWithNewRelease("2.2.0-M1"));
@@ -97,7 +97,7 @@ public class SaganUpdaterTest {
}
@Test
void should_update_sagan_releases_for_rc() {
public void should_update_sagan_releases_for_rc() {
given(this.saganClient.addRelease(anyString(), any())).willReturn(true);
given(this.saganClient.getProject("foo")).willReturn(projectWithNewRelease("2.2.0-RC1"));
@@ -111,7 +111,7 @@ public class SaganUpdaterTest {
}
@Test
void should_not_update_docs_for_sagan_when_current_version_older() {
public void should_not_update_docs_for_sagan_when_current_version_older() {
given(this.saganClient.addRelease(anyString(), any())).willReturn(true);
given(this.saganClient.getProject("foo")).willReturn(projectWithNewRelease("2.2.0-RC1"));
@@ -120,12 +120,12 @@ public class SaganUpdaterTest {
assertThat(result.isSkipped()).isFalse();
assertThat(result.isSuccess()).isTrue();
then(this.saganClient).should(never()).patchProjectDetails(anyString(), any(ProjectDetails.class));
then(this.saganClient).should(never()).patchProject(any(Project.class));
}
@Test
void should_not_update_docs_for_sagan_when_files_exist_but_content_does_not_differ() throws IOException {
public void should_not_update_docs_for_sagan_when_files_exist_but_content_does_not_differ() throws IOException {
given(this.saganClient.addRelease(anyString(), any())).willReturn(true);
given(this.saganClient.getProject("foo")).willReturn(projectWithNewRelease("3.0.0-RC1"));
@@ -144,11 +144,12 @@ public class SaganUpdaterTest {
assertThat(result.isSkipped()).isFalse();
assertThat(result.isSuccess()).isTrue();
then(this.saganClient).should(never()).patchProjectDetails(anyString(), any(ProjectDetails.class));
then(this.saganClient).should(never()).patchProject(any(Project.class));
}
@Test
void should_update_docs_for_sagan_when_current_version_newer_and_only_overview_adoc_exists() throws IOException {
public void should_update_docs_for_sagan_when_current_version_newer_and_only_overview_adoc_exists()
throws IOException {
given(this.saganClient.addRelease(anyString(), any())).willReturn(true);
given(this.saganClient.getProject("foo")).willReturn(projectWithNewRelease("3.0.0-RC1"));
@@ -171,7 +172,7 @@ public class SaganUpdaterTest {
}
@Test
void should_update_docs_for_sagan_when_current_version_newer_and_only_boot_adoc_exists() throws IOException {
public void should_update_docs_for_sagan_when_current_version_newer_and_only_boot_adoc_exists() throws IOException {
given(this.saganClient.addRelease(anyString(), any())).willReturn(true);
given(this.saganClient.getProject("foo")).willReturn(projectWithNewRelease("3.0.0-RC1"));
@@ -204,7 +205,7 @@ public class SaganUpdaterTest {
}
@Test
void should_update_sagan_from_main() {
public void should_update_sagan_from_main() {
ProjectVersion projectVersion = version("2.4.0-SNAPSHOT");
given(this.saganClient.addRelease(anyString(), any())).willReturn(true);
given(this.saganClient.getProject("foo")).willReturn(projectWithNewRelease("2.4.0-SNAPSHOT"));
@@ -224,7 +225,7 @@ public class SaganUpdaterTest {
}
@Test
void should_update_sagan_from_release_version() {
public void should_update_sagan_from_release_version() {
ProjectVersion projectVersion = version("2.2.0");
given(this.saganClient.addRelease(eq("foo"), any())).willReturn(true);
given(this.saganClient.deleteRelease("foo", "2.2.0-RC1")).willReturn(true);
@@ -247,7 +248,7 @@ public class SaganUpdaterTest {
}
@Test
void should_update_sagan_from_non_main() {
public void should_update_sagan_from_non_main() {
ProjectVersion projectVersion = version("2.3.0-SNAPSHOT");
given(this.saganClient.addRelease(eq("foo"), any())).willReturn(true);
given(this.saganClient.getProject("foo")).willReturn(projectWithNewRelease("2.3.0-SNAPSHOT"));

View File

@@ -1,20 +0,0 @@
{
"id" : "0b389dc6-1b73-487d-8122-41f1c974647f",
"name" : "repos_marcingrzejszczak_test-repo_issues",
"request" : {
"url" : "/repos/marcingrzejszczak/test-repo/issues",
"method" : "POST",
"bodyPatterns" : [ {
"equalToJson" : "{\"assignees\":[],\"title\":\"foo\",\"body\":\"bar\",\"labels\":[]}",
"ignoreArrayOrder" : true,
"ignoreExtraElements" : true
} ]
},
"response" : {
"status" : 201,
"body" : "{\"url\":\"https://api.github.com/repos/marcingrzejszczak/test-repo/issues/2\",\"repository_url\":\"https://api.github.com/repos/marcingrzejszczak/test-repo\",\"labels_url\":\"https://api.github.com/repos/marcingrzejszczak/test-repo/issues/2/labels{/name}\",\"comments_url\":\"https://api.github.com/repos/marcingrzejszczak/test-repo/issues/2/comments\",\"events_url\":\"https://api.github.com/repos/marcingrzejszczak/test-repo/issues/2/events\",\"html_url\":\"https://github.com/marcingrzejszczak/test-repo/issues/2\",\"id\":1700204230,\"node_id\":\"I_kwDOJgNfic5lVw7G\",\"number\":2,\"title\":\"foo\",\"user\":{\"login\":\"marcingrzejszczak\",\"id\":3297437,\"node_id\":\"MDQ6VXNlcjMyOTc0Mzc=\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/3297437?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/marcingrzejszczak\",\"html_url\":\"https://github.com/marcingrzejszczak\",\"followers_url\":\"https://api.github.com/users/marcingrzejszczak/followers\",\"following_url\":\"https://api.github.com/users/marcingrzejszczak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/marcingrzejszczak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/marcingrzejszczak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/marcingrzejszczak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/marcingrzejszczak/orgs\",\"repos_url\":\"https://api.github.com/users/marcingrzejszczak/repos\",\"events_url\":\"https://api.github.com/users/marcingrzejszczak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/marcingrzejszczak/received_events\",\"type\":\"User\",\"site_admin\":false},\"labels\":[],\"state\":\"open\",\"locked\":false,\"assignee\":null,\"assignees\":[],\"milestone\":null,\"comments\":0,\"created_at\":\"2023-05-08T12:57:04Z\",\"updated_at\":\"2023-05-08T12:57:04Z\",\"closed_at\":null,\"author_association\":\"OWNER\",\"active_lock_reason\":null,\"body\":\"bar\",\"closed_by\":null,\"reactions\":{\"url\":\"https://api.github.com/repos/marcingrzejszczak/test-repo/issues/2/reactions\",\"total_count\":0,\"+1\":0,\"-1\":0,\"laugh\":0,\"hooray\":0,\"confused\":0,\"heart\":0,\"rocket\":0,\"eyes\":0},\"timeline_url\":\"https://api.github.com/repos/marcingrzejszczak/test-repo/issues/2/timeline\",\"performed_via_github_app\":null,\"state_reason\":null}"
},
"uuid" : "0b389dc6-1b73-487d-8122-41f1c974647f",
"persistent" : true,
"insertionIndex" : 63
}

View File

@@ -1,15 +0,0 @@
{
"id" : "a494c3df-1e2a-45da-bd13-fec861e0bf80",
"name" : "repos_marcingrzejszczak_test-repo_issues",
"request" : {
"url" : "/repos/marcingrzejszczak/test-repo/issues?state=open",
"method" : "GET"
},
"response" : {
"status" : 200,
"body" : "[{\"url\":\"https://api.github.com/repos/marcingrzejszczak/test-repo/issues/1\",\"repository_url\":\"https://api.github.com/repos/marcingrzejszczak/test-repo\",\"labels_url\":\"https://api.github.com/repos/marcingrzejszczak/test-repo/issues/1/labels{/name}\",\"comments_url\":\"https://api.github.com/repos/marcingrzejszczak/test-repo/issues/1/comments\",\"events_url\":\"https://api.github.com/repos/marcingrzejszczak/test-repo/issues/1/events\",\"html_url\":\"https://github.com/marcingrzejszczak/test-repo/issues/1\",\"id\":1700018397,\"node_id\":\"I_kwDOJgNfic5lVDjd\",\"number\":1,\"title\":\"Issue\",\"user\":{\"login\":\"marcingrzejszczak\",\"id\":3297437,\"node_id\":\"MDQ6VXNlcjMyOTc0Mzc=\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/3297437?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/marcingrzejszczak\",\"html_url\":\"https://github.com/marcingrzejszczak\",\"followers_url\":\"https://api.github.com/users/marcingrzejszczak/followers\",\"following_url\":\"https://api.github.com/users/marcingrzejszczak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/marcingrzejszczak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/marcingrzejszczak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/marcingrzejszczak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/marcingrzejszczak/orgs\",\"repos_url\":\"https://api.github.com/users/marcingrzejszczak/repos\",\"events_url\":\"https://api.github.com/users/marcingrzejszczak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/marcingrzejszczak/received_events\",\"type\":\"User\",\"site_admin\":false},\"labels\":[],\"state\":\"open\",\"locked\":false,\"assignee\":null,\"assignees\":[],\"milestone\":{\"url\":\"https://api.github.com/repos/marcingrzejszczak/test-repo/milestones/1\",\"html_url\":\"https://github.com/marcingrzejszczak/test-repo/milestone/1\",\"labels_url\":\"https://api.github.com/repos/marcingrzejszczak/test-repo/milestones/1/labels\",\"id\":9372216,\"node_id\":\"MI_kwDOJgNfic4AjwI4\",\"number\":1,\"title\":\"0.0.1\",\"description\":null,\"creator\":{\"login\":\"marcingrzejszczak\",\"id\":3297437,\"node_id\":\"MDQ6VXNlcjMyOTc0Mzc=\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/3297437?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/marcingrzejszczak\",\"html_url\":\"https://github.com/marcingrzejszczak\",\"followers_url\":\"https://api.github.com/users/marcingrzejszczak/followers\",\"following_url\":\"https://api.github.com/users/marcingrzejszczak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/marcingrzejszczak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/marcingrzejszczak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/marcingrzejszczak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/marcingrzejszczak/orgs\",\"repos_url\":\"https://api.github.com/users/marcingrzejszczak/repos\",\"events_url\":\"https://api.github.com/users/marcingrzejszczak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/marcingrzejszczak/received_events\",\"type\":\"User\",\"site_admin\":false},\"open_issues\":1,\"closed_issues\":0,\"state\":\"open\",\"created_at\":\"2023-05-08T10:44:51Z\",\"updated_at\":\"2023-05-08T11:49:24Z\",\"due_on\":null,\"closed_at\":null},\"comments\":0,\"created_at\":\"2023-05-08T10:44:53Z\",\"updated_at\":\"2023-05-08T10:44:53Z\",\"closed_at\":null,\"author_association\":\"OWNER\",\"active_lock_reason\":null,\"body\":null,\"reactions\":{\"url\":\"https://api.github.com/repos/marcingrzejszczak/test-repo/issues/1/reactions\",\"total_count\":0,\"+1\":0,\"-1\":0,\"laugh\":0,\"hooray\":0,\"confused\":0,\"heart\":0,\"rocket\":0,\"eyes\":0},\"timeline_url\":\"https://api.github.com/repos/marcingrzejszczak/test-repo/issues/1/timeline\",\"performed_via_github_app\":null,\"state_reason\":null}]"
},
"uuid" : "a494c3df-1e2a-45da-bd13-fec861e0bf80",
"persistent" : true,
"insertionIndex" : 62
}

View File

@@ -1,15 +0,0 @@
{
"id" : "384801a9-2b6a-4f8b-a259-fc554383b4b5",
"name" : "repos_marcingrzejszczak_test-repo_milestones",
"request" : {
"url" : "/repos/marcingrzejszczak/test-repo/milestones?state=open",
"method" : "GET"
},
"response" : {
"status" : 200,
"body" : "[{\"url\":\"https://api.github.com/repos/marcingrzejszczak/test-repo/milestones/1\",\"html_url\":\"https://github.com/marcingrzejszczak/test-repo/milestone/1\",\"labels_url\":\"https://api.github.com/repos/marcingrzejszczak/test-repo/milestones/1/labels\",\"id\":9372216,\"node_id\":\"MI_kwDOJgNfic4AjwI4\",\"number\":1,\"title\":\"0.0.1\",\"description\":null,\"creator\":{\"login\":\"marcingrzejszczak\",\"id\":3297437,\"node_id\":\"MDQ6VXNlcjMyOTc0Mzc=\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/3297437?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/marcingrzejszczak\",\"html_url\":\"https://github.com/marcingrzejszczak\",\"followers_url\":\"https://api.github.com/users/marcingrzejszczak/followers\",\"following_url\":\"https://api.github.com/users/marcingrzejszczak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/marcingrzejszczak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/marcingrzejszczak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/marcingrzejszczak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/marcingrzejszczak/orgs\",\"repos_url\":\"https://api.github.com/users/marcingrzejszczak/repos\",\"events_url\":\"https://api.github.com/users/marcingrzejszczak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/marcingrzejszczak/received_events\",\"type\":\"User\",\"site_admin\":false},\"open_issues\":1,\"closed_issues\":0,\"state\":\"open\",\"created_at\":\"2023-05-08T10:44:51Z\",\"updated_at\":\"2023-05-08T11:49:24Z\",\"due_on\":null,\"closed_at\":null},{\"url\":\"https://api.github.com/repos/marcingrzejszczak/test-repo/milestones/2\",\"html_url\":\"https://github.com/marcingrzejszczak/test-repo/milestone/2\",\"labels_url\":\"https://api.github.com/repos/marcingrzejszczak/test-repo/milestones/2/labels\",\"id\":9372221,\"node_id\":\"MI_kwDOJgNfic4AjwI9\",\"number\":2,\"title\":\"0.0.1-SNAPSHOT\",\"description\":\"\",\"creator\":{\"login\":\"marcingrzejszczak\",\"id\":3297437,\"node_id\":\"MDQ6VXNlcjMyOTc0Mzc=\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/3297437?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/marcingrzejszczak\",\"html_url\":\"https://github.com/marcingrzejszczak\",\"followers_url\":\"https://api.github.com/users/marcingrzejszczak/followers\",\"following_url\":\"https://api.github.com/users/marcingrzejszczak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/marcingrzejszczak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/marcingrzejszczak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/marcingrzejszczak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/marcingrzejszczak/orgs\",\"repos_url\":\"https://api.github.com/users/marcingrzejszczak/repos\",\"events_url\":\"https://api.github.com/users/marcingrzejszczak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/marcingrzejszczak/received_events\",\"type\":\"User\",\"site_admin\":false},\"open_issues\":0,\"closed_issues\":0,\"state\":\"open\",\"created_at\":\"2023-05-08T10:47:21Z\",\"updated_at\":\"2023-05-08T11:50:14Z\",\"due_on\":null,\"closed_at\":null}]"
},
"uuid" : "384801a9-2b6a-4f8b-a259-fc554383b4b5",
"persistent" : true,
"insertionIndex" : 44
}

View File

@@ -1,15 +0,0 @@
{
"id" : "ec859ab2-e3fc-45ab-b913-3c6094548d0b",
"name" : "repos_marcingrzejszczak_test-repo_milestones",
"request" : {
"url" : "/repos/marcingrzejszczak/test-repo/milestones?state=closed",
"method" : "GET"
},
"response" : {
"status" : 200,
"body" : "[{\"url\":\"https://api.github.com/repos/marcingrzejszczak/test-repo/milestones/3\",\"html_url\":\"https://github.com/marcingrzejszczak/test-repo/milestone/3\",\"labels_url\":\"https://api.github.com/repos/marcingrzejszczak/test-repo/milestones/3/labels\",\"id\":9372427,\"node_id\":\"MI_kwDOJgNfic4AjwML\",\"number\":3,\"title\":\"0.1.0\",\"description\":\"Closed\",\"creator\":{\"login\":\"marcingrzejszczak\",\"id\":3297437,\"node_id\":\"MDQ6VXNlcjMyOTc0Mzc=\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/3297437?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/marcingrzejszczak\",\"html_url\":\"https://github.com/marcingrzejszczak\",\"followers_url\":\"https://api.github.com/users/marcingrzejszczak/followers\",\"following_url\":\"https://api.github.com/users/marcingrzejszczak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/marcingrzejszczak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/marcingrzejszczak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/marcingrzejszczak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/marcingrzejszczak/orgs\",\"repos_url\":\"https://api.github.com/users/marcingrzejszczak/repos\",\"events_url\":\"https://api.github.com/users/marcingrzejszczak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/marcingrzejszczak/received_events\",\"type\":\"User\",\"site_admin\":false},\"open_issues\":0,\"closed_issues\":0,\"state\":\"closed\",\"created_at\":\"2023-05-08T11:50:54Z\",\"updated_at\":\"2023-05-08T11:51:14Z\",\"due_on\":null,\"closed_at\":\"2023-05-08T11:51:14Z\"}]"
},
"uuid" : "ec859ab2-e3fc-45ab-b913-3c6094548d0b",
"persistent" : true,
"insertionIndex" : 40
}

View File

@@ -1,20 +0,0 @@
{
"id" : "1ffed929-9781-4f60-a942-c71a5bbb8550",
"name" : "repos_marcingrzejszczak_test-repo_milestones_1",
"request" : {
"url" : "/repos/marcingrzejszczak/test-repo/milestones/1",
"method" : "PATCH",
"bodyPatterns" : [ {
"equalToJson" : "{\"state\":\"closed\"}",
"ignoreArrayOrder" : true,
"ignoreExtraElements" : true
} ]
},
"response" : {
"status" : 200,
"body" : "{\"url\":\"https://api.github.com/repos/marcingrzejszczak/test-repo/milestones/1\",\"html_url\":\"https://github.com/marcingrzejszczak/test-repo/milestone/1\",\"labels_url\":\"https://api.github.com/repos/marcingrzejszczak/test-repo/milestones/1/labels\",\"id\":9372216,\"node_id\":\"MI_kwDOJgNfic4AjwI4\",\"number\":1,\"title\":\"0.0.1\",\"description\":null,\"creator\":{\"login\":\"marcingrzejszczak\",\"id\":3297437,\"node_id\":\"MDQ6VXNlcjMyOTc0Mzc=\",\"avatar_url\":\"https://avatars.githubusercontent.com/u/3297437?v=4\",\"gravatar_id\":\"\",\"url\":\"https://api.github.com/users/marcingrzejszczak\",\"html_url\":\"https://github.com/marcingrzejszczak\",\"followers_url\":\"https://api.github.com/users/marcingrzejszczak/followers\",\"following_url\":\"https://api.github.com/users/marcingrzejszczak/following{/other_user}\",\"gists_url\":\"https://api.github.com/users/marcingrzejszczak/gists{/gist_id}\",\"starred_url\":\"https://api.github.com/users/marcingrzejszczak/starred{/owner}{/repo}\",\"subscriptions_url\":\"https://api.github.com/users/marcingrzejszczak/subscriptions\",\"organizations_url\":\"https://api.github.com/users/marcingrzejszczak/orgs\",\"repos_url\":\"https://api.github.com/users/marcingrzejszczak/repos\",\"events_url\":\"https://api.github.com/users/marcingrzejszczak/events{/privacy}\",\"received_events_url\":\"https://api.github.com/users/marcingrzejszczak/received_events\",\"type\":\"User\",\"site_admin\":false},\"open_issues\":1,\"closed_issues\":0,\"state\":\"closed\",\"created_at\":\"2023-05-08T10:44:51Z\",\"updated_at\":\"2023-05-08T11:45:14Z\",\"due_on\":null,\"closed_at\":\"2023-05-08T11:45:14Z\"}"
},
"uuid" : "1ffed929-9781-4f60-a942-c71a5bbb8550",
"persistent" : true,
"insertionIndex" : 26
}

View File

@@ -1,14 +0,0 @@
{
"id" : "ab37a4f8-9c96-46d0-8aab-07a352e8ea3f",
"name" : "user",
"request" : {
"url" : "/user",
"method" : "GET"
},
"response" : {
"status" : 200
},
"uuid" : "ab37a4f8-9c96-46d0-8aab-07a352e8ea3f",
"persistent" : true,
"insertionIndex" : 23
}

View File

@@ -1,18 +0,0 @@
{
"id" : "a4099665-5a81-4767-80f0-ed9b5a60121e",
"name" : "projects_spring-boot",
"request" : {
"url" : "/projects/spring-boot",
"method" : "GET"
},
"response" : {
"status" : 200,
"body" : "{\"name\":\"Spring Boot\",\"slug\":\"spring-boot\",\"repositoryUrl\":\"https://github.com/spring-projects/spring-boot\",\"status\":\"ACTIVE\",\"_links\":{\"releases\":{\"href\":\"https://api.spring.io/projects/spring-boot/releases\"},\"generations\":{\"href\":\"https://api.spring.io/projects/spring-boot/generations\"},\"self\":{\"href\":\"https://api.spring.io/projects/spring-boot\"}}}",
"headers" : {
"Content-Type" : "application/hal+json"
}
},
"uuid" : "a4099665-5a81-4767-80f0-ed9b5a60121e",
"persistent" : true,
"insertionIndex" : 1
}

View File

@@ -1,18 +0,0 @@
{
"id" : "740428fb-e86b-46fb-9544-68cfbc7f335e",
"name" : "projects_spring-boot_releases",
"request" : {
"url" : "/projects/spring-boot/releases",
"method" : "GET"
},
"response" : {
"status" : 200,
"body" : "{\"_embedded\":{\"releases\":[{\"version\":\"2.5.14\",\"apiDocUrl\":\"https://docs.spring.io/spring-boot/docs/{version}/api/\",\"referenceDocUrl\":\"https://docs.spring.io/spring-boot/docs/{version}/reference/html/\",\"status\":\"GENERAL_AVAILABILITY\",\"current\":false,\"_links\":{\"repository\":{\"href\":\"https://api.spring.io/repositories/spring-releases\"},\"self\":{\"href\":\"https://api.spring.io/projects/spring-boot/releases/2.5.14\"}}},{\"version\":\"2.6.14\",\"apiDocUrl\":\"https://docs.spring.io/spring-boot/docs/{version}/api/\",\"referenceDocUrl\":\"https://docs.spring.io/spring-boot/docs/{version}/reference/html/\",\"status\":\"GENERAL_AVAILABILITY\",\"current\":false,\"_links\":{\"repository\":{\"href\":\"https://api.spring.io/repositories/spring-releases\"},\"self\":{\"href\":\"https://api.spring.io/projects/spring-boot/releases/2.6.14\"}}},{\"version\":\"2.7.12-SNAPSHOT\",\"apiDocUrl\":\"https://docs.spring.io/spring-boot/docs/{version}/api/\",\"referenceDocUrl\":\"https://docs.spring.io/spring-boot/docs/{version}/reference/html/\",\"status\":\"SNAPSHOT\",\"current\":false,\"_links\":{\"repository\":{\"href\":\"https://api.spring.io/repositories/spring-snapshots\"},\"self\":{\"href\":\"https://api.spring.io/projects/spring-boot/releases/2.7.12-SNAPSHOT\"}}},{\"version\":\"3.1.0-SNAPSHOT\",\"apiDocUrl\":\"https://docs.spring.io/spring-boot/docs/{version}/api/\",\"referenceDocUrl\":\"https://docs.spring.io/spring-boot/docs/{version}/reference/html/\",\"status\":\"SNAPSHOT\",\"current\":false,\"_links\":{\"repository\":{\"href\":\"https://api.spring.io/repositories/spring-snapshots\"},\"self\":{\"href\":\"https://api.spring.io/projects/spring-boot/releases/3.1.0-SNAPSHOT\"}}},{\"version\":\"3.1.0-M2\",\"apiDocUrl\":\"https://docs.spring.io/spring-boot/docs/{version}/api/\",\"referenceDocUrl\":\"https://docs.spring.io/spring-boot/docs/{version}/reference/html/\",\"status\":\"PRERELEASE\",\"current\":false,\"_links\":{\"repository\":{\"href\":\"https://api.spring.io/repositories/spring-milestones\"},\"self\":{\"href\":\"https://api.spring.io/projects/spring-boot/releases/3.1.0-M2\"}}},{\"version\":\"3.0.6\",\"apiDocUrl\":\"https://docs.spring.io/spring-boot/docs/{version}/api/\",\"referenceDocUrl\":\"https://docs.spring.io/spring-boot/docs/{version}/reference/html/\",\"status\":\"GENERAL_AVAILABILITY\",\"current\":true,\"_links\":{\"repository\":{\"href\":\"https://api.spring.io/repositories/spring-releases\"},\"self\":{\"href\":\"https://api.spring.io/projects/spring-boot/releases/3.0.6\"}}},{\"version\":\"3.0.7-SNAPSHOT\",\"apiDocUrl\":\"https://docs.spring.io/spring-boot/docs/{version}/api/\",\"referenceDocUrl\":\"https://docs.spring.io/spring-boot/docs/{version}/reference/html/\",\"status\":\"SNAPSHOT\",\"current\":false,\"_links\":{\"repository\":{\"href\":\"https://api.spring.io/repositories/spring-snapshots\"},\"self\":{\"href\":\"https://api.spring.io/projects/spring-boot/releases/3.0.7-SNAPSHOT\"}}},{\"version\":\"2.7.11\",\"apiDocUrl\":\"https://docs.spring.io/spring-boot/docs/{version}/api/\",\"referenceDocUrl\":\"https://docs.spring.io/spring-boot/docs/{version}/reference/html/\",\"status\":\"GENERAL_AVAILABILITY\",\"current\":false,\"_links\":{\"repository\":{\"href\":\"https://api.spring.io/repositories/spring-releases\"},\"self\":{\"href\":\"https://api.spring.io/projects/spring-boot/releases/2.7.11\"}}},{\"version\":\"3.1.0-RC2\",\"apiDocUrl\":\"https://docs.spring.io/spring-boot/docs/{version}/api/\",\"referenceDocUrl\":\"https://docs.spring.io/spring-boot/docs/{version}/reference/html/\",\"status\":\"PRERELEASE\",\"current\":false,\"_links\":{\"repository\":{\"href\":\"https://api.spring.io/repositories/spring-milestones\"},\"self\":{\"href\":\"https://api.spring.io/projects/spring-boot/releases/3.1.0-RC2\"}}}]},\"_links\":{\"project\":{\"href\":\"https://api.spring.io/projects/spring-boot\"},\"current\":{\"href\":\"https://api.spring.io/projects/spring-boot/releases/current\"}}}",
"headers" : {
"Content-Type" : "application/hal+json"
}
},
"uuid" : "740428fb-e86b-46fb-9544-68cfbc7f335e",
"persistent" : true,
"insertionIndex" : 2
}

View File

@@ -1,18 +0,0 @@
{
"id" : "5e584f53-bb48-4c2f-ad6d-c56c5b1fb0c5",
"name" : "projects_spring-boot_releases_2514",
"request" : {
"url" : "/projects/spring-boot/releases/2.5.14",
"method" : "GET"
},
"response" : {
"status" : 200,
"body" : "{\"version\":\"2.5.14\",\"apiDocUrl\":\"https://docs.spring.io/spring-boot/docs/{version}/api/\",\"referenceDocUrl\":\"https://docs.spring.io/spring-boot/docs/{version}/reference/html/\",\"status\":\"GENERAL_AVAILABILITY\",\"current\":false,\"_links\":{\"repository\":{\"href\":\"https://api.spring.io/repositories/spring-releases\"},\"self\":{\"href\":\"https://api.spring.io/projects/spring-boot/releases/2.5.14\"}}}",
"headers" : {
"Content-Type" : "application/hal+json"
}
},
"uuid" : "5e584f53-bb48-4c2f-ad6d-c56c5b1fb0c5",
"persistent" : true,
"insertionIndex" : 4
}

View File

@@ -1,19 +0,0 @@
{
"id" : "048e10c5-8a54-4e77-8ee4-0c1cc14b1eff",
"name" : "projects_spring-cloud-contract_details",
"request" : {
"url" : "/projects/spring-cloud-contract/details",
"method" : "PATCH",
"bodyPatterns" : [ {
"equalToJson" : "{\"bootConfig\":\"new sbc\",\"body\":\"new body\"}",
"ignoreArrayOrder" : true,
"ignoreExtraElements" : true
} ]
},
"response" : {
"status" : 204
},
"uuid" : "048e10c5-8a54-4e77-8ee4-0c1cc14b1eff",
"persistent" : true,
"insertionIndex" : 16
}

View File

@@ -1,19 +0,0 @@
{
"id" : "d26df67c-4167-4e85-bab9-dfa95dabe806",
"name" : "projects_spring-cloud-contract_releases",
"request" : {
"url" : "/projects/spring-cloud-contract/releases",
"method" : "POST",
"bodyPatterns" : [ {
"equalToJson" : "{\"version\":\"4.0.3-SNAPSHOT\",\"referenceDocUrl\":\"https://docs.spring.io/spring-cloud-contract/docs/{version}/reference/html/\",\"apiDocUrl\":\"https://docs.spring.io/spring-cloud-contract/docs/{version}/api/\"}",
"ignoreArrayOrder" : true,
"ignoreExtraElements" : true
} ]
},
"response" : {
"status" : 201
},
"uuid" : "d26df67c-4167-4e85-bab9-dfa95dabe806",
"persistent" : true,
"insertionIndex" : 15
}

View File

@@ -1,14 +0,0 @@
{
"id" : "d6fa2037-b021-44ab-9019-ec70c702f623",
"name" : "projects_spring-cloud-contract_releases_403-snapshot",
"request" : {
"url" : "/projects/spring-cloud-contract/releases/4.0.3-SNAPSHOT",
"method" : "DELETE"
},
"response" : {
"status" : 204
},
"uuid" : "d6fa2037-b021-44ab-9019-ec70c702f623",
"persistent" : true,
"insertionIndex" : 14
}

View File

@@ -10,13 +10,13 @@
<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>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>17</java.version>
<java.version>1.8</java.version>
</properties>
<dependencies>

View File

@@ -0,0 +1,43 @@
/*
* 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.github;
import com.jcabi.github.Github;
import com.jcabi.github.RtGithub;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
class GithubConfiguration {
@Bean
BeanPostProcessor cachingGithubBeanPostProcessor() {
return new BeanPostProcessor() {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof RtGithub) {
return new CachingGithub((Github) bean);
}
return bean;
}
};
}
}

View File

@@ -66,8 +66,8 @@ class SaganConfiguration {
}
@Override
public void patchProjectDetails(String projectName, ProjectDetails details) {
public Project patchProject(Project project) {
return null;
}
};
}

View File

@@ -16,15 +16,24 @@
package releaser.internal.spring;
import javax.sql.DataSource;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import releaser.internal.ReleaserProperties;
import org.springframework.batch.core.configuration.annotation.BatchConfigurer;
import org.springframework.batch.core.configuration.annotation.DefaultBatchConfigurer;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.explore.support.JobExplorerFactoryBean;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.repository.dao.Jackson2ExecutionContextStringSerializer;
import org.springframework.batch.core.repository.support.JobRepositoryFactoryBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
@@ -34,6 +43,7 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
@EnableBatchProcessing
class BatchConfiguration {
@Bean
@@ -66,11 +76,11 @@ class BatchConfiguration {
@Bean
@ConditionalOnMissingBean
FlowRunner flowRunner(JobRepository jobRepository, PlatformTransactionManager manager,
FlowRunner flowRunner(StepBuilderFactory stepBuilderFactory, JobBuilderFactory jobBuilderFactory,
ProjectsToRunFactory projectsToRunFactory, JobLauncher jobLauncher,
FlowRunnerTaskExecutorSupplier flowRunnerTaskExecutorSupplier, ConfigurableApplicationContext context,
ReleaserProperties releaserProperties, BuildReportHandler reportHandler) {
return new SpringBatchFlowRunner(jobRepository, manager, projectsToRunFactory, jobLauncher,
return new SpringBatchFlowRunner(stepBuilderFactory, jobBuilderFactory, projectsToRunFactory, jobLauncher,
flowRunnerTaskExecutorSupplier, context, releaserProperties, reportHandler);
}
@@ -85,28 +95,32 @@ class BatchConfiguration {
serializer.setObjectMapper(objectMapper);
return serializer;
}
/*
* // Needed to add this to serialize the exceptions
*
* @Bean BatchConfigurer myBatchConfigurer(DataSource dataSource,
* Jackson2ExecutionContextStringSerializer
* myJackson2ExecutionContextStringSerializer, PlatformTransactionManager
* transactionManager) { return new DefaultBatchConfigurer(dataSource) {
*
* @Override protected JobExplorer createJobExplorer() throws Exception {
* JobExplorerFactoryBean jobExplorerFactoryBean = new JobExplorerFactoryBean();
* jobExplorerFactoryBean.setDataSource(dataSource);
* jobExplorerFactoryBean.setSerializer(myJackson2ExecutionContextStringSerializer);
* jobExplorerFactoryBean.afterPropertiesSet(); return
* jobExplorerFactoryBean.getObject(); }
*
* @Override protected JobRepository createJobRepository() throws Exception {
* JobRepositoryFactoryBean jobRepositoryFactoryBean = new JobRepositoryFactoryBean();
* jobRepositoryFactoryBean.setDataSource(dataSource);
* jobRepositoryFactoryBean.setSerializer(myJackson2ExecutionContextStringSerializer);
* jobRepositoryFactoryBean.setTransactionManager(transactionManager);
* jobRepositoryFactoryBean.afterPropertiesSet(); return
* jobRepositoryFactoryBean.getObject(); } }; }
*/
// Needed to add this to serialize the exceptions
@Bean
BatchConfigurer myBatchConfigurer(DataSource dataSource,
Jackson2ExecutionContextStringSerializer myJackson2ExecutionContextStringSerializer,
PlatformTransactionManager transactionManager) {
return new DefaultBatchConfigurer(dataSource) {
@Override
protected JobExplorer createJobExplorer() throws Exception {
JobExplorerFactoryBean jobExplorerFactoryBean = new JobExplorerFactoryBean();
jobExplorerFactoryBean.setDataSource(dataSource);
jobExplorerFactoryBean.setSerializer(myJackson2ExecutionContextStringSerializer);
jobExplorerFactoryBean.afterPropertiesSet();
return jobExplorerFactoryBean.getObject();
}
@Override
protected JobRepository createJobRepository() throws Exception {
JobRepositoryFactoryBean jobRepositoryFactoryBean = new JobRepositoryFactoryBean();
jobRepositoryFactoryBean.setDataSource(dataSource);
jobRepositoryFactoryBean.setSerializer(myJackson2ExecutionContextStringSerializer);
jobRepositoryFactoryBean.setTransactionManager(transactionManager);
jobRepositoryFactoryBean.afterPropertiesSet();
return jobRepositoryFactoryBean.getObject();
}
};
}
}

View File

@@ -16,7 +16,6 @@
package releaser.internal.spring;
import java.io.Serializable;
import java.util.List;
import releaser.internal.tasks.ReleaserTask;
@@ -25,7 +24,7 @@ import releaser.internal.tasks.ReleaserTask;
* A report from running a task. Can be mapped to a row in a table for a single execution
* of a release task.
*/
public class ExecutionResultReport implements Serializable {
public class ExecutionResultReport {
private String projectName;

View File

@@ -16,8 +16,7 @@
package releaser.internal.spring;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatterBuilder;
import java.text.SimpleDateFormat;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Comparator;
@@ -62,10 +61,8 @@ class SpringBatchBuildReportHandler implements BuildReportHandler {
private List<Table> buildTable(List<StepExecution> stepContexts) {
return stepContexts.stream().map(step -> {
String date = step.getStartTime()
.format(new DateTimeFormatterBuilder().appendPattern("yyyy-MM-dd HH:mm:ss.SSS").toFormatter());
long millis = ChronoUnit.MILLIS.between(step.getStartTime().toInstant(ZoneOffset.UTC),
step.getEndTime().toInstant(ZoneOffset.UTC));
String date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(step.getStartTime());
long millis = ChronoUnit.MILLIS.between(step.getStartTime().toInstant(), step.getEndTime().toInstant());
ExecutionContext context = step.getExecutionContext();
ExecutionResultReport entity = (ExecutionResultReport) context.get("entity");
if (entity == null) {

View File

@@ -50,6 +50,8 @@ import org.springframework.batch.core.Step;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.StepExecutionListener;
import org.springframework.batch.core.UnexpectedJobExecutionException;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.job.builder.FlowBuilder;
import org.springframework.batch.core.job.builder.FlowJobBuilder;
import org.springframework.batch.core.job.builder.JobBuilder;
@@ -57,14 +59,11 @@ import org.springframework.batch.core.job.builder.JobFlowBuilder;
import org.springframework.batch.core.job.flow.Flow;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.listener.StepExecutionListenerSupport;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.task.TaskExecutor;
import org.springframework.transaction.PlatformTransactionManager;
class SpringBatchFlowRunner implements FlowRunner, Closeable {
@@ -74,9 +73,9 @@ class SpringBatchFlowRunner implements FlowRunner, Closeable {
private final ConsoleInputStepSkipper stepSkipper;
private final JobRepository jobRepository;
private final StepBuilderFactory stepBuilderFactory;
private final PlatformTransactionManager manager;
private final JobBuilderFactory jobBuilderFactory;
private final ProjectsToRunFactory projectsToRunFactory;
@@ -90,12 +89,12 @@ class SpringBatchFlowRunner implements FlowRunner, Closeable {
private final ReleaserProperties releaserProperties;
SpringBatchFlowRunner(JobRepository jobRepository, PlatformTransactionManager manager,
SpringBatchFlowRunner(StepBuilderFactory stepBuilderFactory, JobBuilderFactory jobBuilderFactory,
ProjectsToRunFactory projectsToRunFactory, JobLauncher jobLauncher,
FlowRunnerTaskExecutorSupplier flowRunnerTaskExecutorSupplier, ConfigurableApplicationContext context,
ReleaserProperties releaserProperties, BuildReportHandler reportHandler) {
this.jobRepository = jobRepository;
this.manager = manager;
this.stepBuilderFactory = stepBuilderFactory;
this.jobBuilderFactory = jobBuilderFactory;
this.projectsToRunFactory = projectsToRunFactory;
this.jobLauncher = jobLauncher;
this.flowRunnerTaskExecutorSupplier = flowRunnerTaskExecutorSupplier;
@@ -111,7 +110,7 @@ class SpringBatchFlowRunner implements FlowRunner, Closeable {
}
private Step createStep(ReleaserTask releaserTask, NamedArgumentsSupplier argsSupplier) {
return new StepBuilder(argsSupplier.projectName + "_" + releaserTask.name(), jobRepository)
return this.stepBuilderFactory.get(argsSupplier.projectName + "_" + releaserTask.name())
.tasklet((contribution, chunkContext) -> {
Arguments args = argsSupplier.get();
FlowRunner.Decision decision = beforeTask(args.options, args.properties, releaserTask);
@@ -139,7 +138,7 @@ class SpringBatchFlowRunner implements FlowRunner, Closeable {
log.info("Skipping step [{}]", releaserTask.name());
}
return RepeatStatus.FINISHED;
}, this.manager).listener(releaserListener(argsSupplier, releaserTask)).build();
}).listener(releaserListener(argsSupplier, releaserTask)).build();
}
private List<Throwable> addExceptionToErrors(List<Throwable> errors, RuntimeException exception) {
@@ -305,7 +304,7 @@ class SpringBatchFlowRunner implements FlowRunner, Closeable {
}
private Job buildJobForFlows(Iterator<StuffToRun> flowsIterator) {
JobBuilder release = new JobBuilder("release_" + System.currentTimeMillis(), this.jobRepository);
JobBuilder release = this.jobBuilderFactory.get("release_" + System.currentTimeMillis());
StuffToRun stuffToRun = flowsIterator.next();
Flow first = stuffToRun.flow;
JobFlowBuilder start = release.start(first);
@@ -363,7 +362,7 @@ class SpringBatchFlowRunner implements FlowRunner, Closeable {
log.info("No release train post release tasks to run, will do nothing");
return ExecutionResult.success();
}
Job job = new JobBuilder(name, this.jobRepository).start(flow).build().build();
Job job = this.jobBuilderFactory.get(name).start(flow).build().build();
return runJob(job);
}
@@ -512,14 +511,14 @@ class ConsoleInputStepSkipper {
public boolean skipStep() {
String input = chosenOption();
switch (input.toLowerCase()) {
case "s":
return true;
case "q":
reportHandler.reportBuildSummary();
System.exit(SpringApplication.exit(this.context, () -> 0));
return true;
default:
return false;
case "s":
return true;
case "q":
reportHandler.reportBuildSummary();
System.exit(SpringApplication.exit(this.context, () -> 0));
return true;
default:
return false;
}
}

View File

@@ -41,7 +41,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.web.client.HttpServerErrorException;
@SpringBootTest(properties = "spring.batch.jdbc.initialize-schema=always")
@SpringBootTest
@ActiveProfiles("batch")
class SpringBatchFlowRunnerTests {

View File

@@ -4,5 +4,4 @@ releaser:
spring:
batch:
jdbc:
initialize-schema: always
initialize-schema: always

View File

@@ -10,13 +10,13 @@
<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>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>17</java.version>
<java.version>1.8</java.version>
</properties>
<dependencies>

View File

@@ -50,14 +50,14 @@ import releaser.internal.sagan.Project;
import releaser.internal.sagan.Release;
import releaser.internal.tasks.ReleaserTask;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.util.FileSystemUtils;
import static org.assertj.core.api.BDDAssertions.then;
@@ -320,11 +320,11 @@ public abstract class AbstractSpringAcceptanceTests {
public static class DefaultTestConfiguration {
@Bean
SpringBatchFlowRunner mySpringBatchFlowRunner(JobRepository jobRepository, PlatformTransactionManager manager,
ProjectsToRunFactory projectsToRunFactory, JobLauncher jobLauncher,
SpringBatchFlowRunner mySpringBatchFlowRunner(StepBuilderFactory stepBuilderFactory,
JobBuilderFactory jobBuilderFactory, ProjectsToRunFactory projectsToRunFactory, JobLauncher jobLauncher,
FlowRunnerTaskExecutorSupplier flowRunnerTaskExecutorSupplier, ConfigurableApplicationContext context,
ReleaserProperties releaserProperties, BuildReportHandler reportHandler) {
return new SpringBatchFlowRunner(jobRepository, manager, projectsToRunFactory, jobLauncher,
return new SpringBatchFlowRunner(stepBuilderFactory, jobBuilderFactory, projectsToRunFactory, jobLauncher,
flowRunnerTaskExecutorSupplier, context, releaserProperties, reportHandler) {
@Override
Decision decide(Options options, ReleaserTask task) {

View File

@@ -0,0 +1,31 @@
# Spring Cloud Info
## Why?
The Spring Cloud Team gets a lot of inquiries from users like the following:
* What version of Spring Cloud should I use if I am using Spring Boot `x.x.x`?
* When will Spring Cloud `[RELEASE]` be released?
* What version of `spring-cloud-*` is in Spring Cloud `[RELEASE]`?
The answers to these questions can easily be found if you know where to look. The purpose of
Spring Cloud Info is to make these answers easily accessible via a REST API that we can then use
to expose this information in a user friendly way.
## The Nitty Gritty Details
Spring Cloud Info is deployed on Pivotal Cloud Foundry in the org `spring.io` and space `
spring-cloud-issuebot-production`.
There is a `manifest.yml` file in the root of the project you can use to deploy the app
simply by running `cf push`. It requires a Spring Cloud Services Config Server service
named `config-server`. The config server should point to the configuration in the branch
`spring-cloud-info-config` of the repo https://github.com/spring-cloud/spring-cloud-release-tools.
Within the `application.yml` in that branch there needs to be one an encrypted oauth token for GitHub
that is set via `spring.cloud.info.git.oauthtoken`. This token is used by Spring Cloud Info to fetch
data from GitHub.
## REST API
The rest API is documented via Spring Rest Docs and is published along with the app to Pivotal
Cloud Foundry. You can find the REST API documentation at
https://spring-cloud-info.apps.pcfone.io/docs/spring-cloud-info.html.

View File

@@ -0,0 +1,7 @@
---
applications:
- name: spring-cloud-info
memory: 2048M
path: ./target/spring-cloud-info-2.0.0-SNAPSHOT.jar
services:
- config-server

157
spring-cloud-info/pom.xml Normal file
View File

@@ -0,0 +1,157 @@
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>releaser-parent</artifactId>
<groupId>org.springframework.cloud.internal</groupId>
<version>2.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-info</artifactId>
<properties>
<maven-core.version>3.8.5</maven-core.version>
<javax.json-api.version>1.1.4</javax.json-api.version>
<maven-model.version>3.8.5</maven-model.version>
<scs.version>3.1.6.RELEASE</scs.version>
<spring-asciidoctor-extensions.version>0.2.0-RELEASE</spring-asciidoctor-extensions.version>
<spring-cloud-bom.version>2021.0.1</spring-cloud-bom.version>
<spring-rest-docs.version>2.0.6.RELEASE</spring-rest-docs.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-core</artifactId>
<version>${maven-core.version}</version>
</dependency>
<dependency>
<groupId>com.jcabi</groupId>
<artifactId>jcabi-github</artifactId>
<version>${jcabi-github.version}</version>
</dependency>
<dependency>
<groupId>javax.json</groupId>
<artifactId>javax.json-api</artifactId>
<version>${javax.json-api.version}</version>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-model</artifactId>
<version>${maven-model.version}</version>
</dependency>
<dependency>
<groupId>io.pivotal.spring.cloud</groupId>
<artifactId>spring-cloud-services-starter-config-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.pivotal.spring.cloud</groupId>
<artifactId>spring-cloud-services-dependencies</artifactId>
<version>${scs.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud-bom.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<version>2.2.2</version>
<dependencies>
<dependency>
<groupId>io.spring.asciidoctor</groupId>
<artifactId>spring-asciidoctor-extensions</artifactId>
<version>0.6.0</version>
</dependency>
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-asciidoctor</artifactId>
<version>2.0.6.RELEASE</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>${maven-resources-plugin.version}</version>
<executions>
<execution>
<id>copy-resources-for-fatjar</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>
${project.basedir}/src/main/resources/static/docs
</outputDirectory>
<resources>
<resource>
<directory>
${project.build.directory}/generated-docs
</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,121 @@
# Rest API
## Spring Cloud Versions
Gets all the available Spring Cloud release trains.
### cURL Request
include::{snippets}/springcloudversions/curl-request.adoc[]
### HTTPie Request
include::{snippets}/springcloudversions/httpie-request.adoc[]
### HTTP Request
include::{snippets}/springcloudversions/http-request.adoc[]
### Response
include::{snippets}/springcloudversions/http-response.adoc[]
### Response Fields
include::{snippets}/springcloudversions/response-fields.adoc[]
## Spring Cloud Version Given Spring Boot Version
Gets the Spring Cloud release train version given a Spring Boot version.
### Path Parameters
include::{snippets}/springcloudversion/path-parameters.adoc[]
### cURL Request
include::{snippets}/springcloudversion/curl-request.adoc[]
### HTTPie Request
include::{snippets}/springcloudversion/httpie-request.adoc[]
### HTTP Request
include::{snippets}/springcloudversion/http-request.adoc[]
### HTTP Response
include::{snippets}/springcloudversion/http-response.adoc[]
### Response Fields
include::{snippets}/springcloudversion/response-fields.adoc[]
## Spring Cloud Project Versions
Get the Spring Cloud project versions for a given Spring Cloud release train.
### cURL Request
include::{snippets}/bomversions/curl-request.adoc[]
### HTTPie Request
include::{snippets}/bomversions/httpie-request.adoc[]
### HTTP Request
include::{snippets}/bomversions/http-request.adoc[]
### HTTP Response
include::{snippets}/bomversions/http-response.adoc[]
## Upcoming Spring Cloud Releases
Gets all the upcoming Spring Cloud releases.
### cURL Request
include::{snippets}/milestones/curl-request.adoc[]
### HTTPie Request
include::{snippets}/milestones/httpie-request.adoc[]
### HTTP Request
include::{snippets}/milestones/http-request.adoc[]
### HTTP Response
include::{snippets}/milestones/http-response.adoc[]
## Get Spring Cloud Release Date
Gets the tentative date given an upcoming Spring Cloud release train name.
### Path Parameters
include::{snippets}/milestoneduedate/path-parameters.adoc[]
### cURL Request
include::{snippets}/milestoneduedate/curl-request.adoc[]
### HTTPie Request
include::{snippets}/milestoneduedate/httpie-request.adoc[]
### HTTP Request
include::{snippets}/milestoneduedate/http-request.adoc[]
### HTTP Response
include::{snippets}/milestoneduedate/http-response.adoc[]
### Response Fields
include::{snippets}/milestoneduedate/response-fields.adoc[]

View File

@@ -0,0 +1,52 @@
/*
* 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 org.springframework.cloud.info;
import java.io.IOException;
import java.io.StringReader;
import org.apache.maven.model.Model;
import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
import org.springframework.web.client.RestTemplate;
/**
* @author Ryan Baxter
*/
public class GithubPomReader {
private MavenXpp3Reader reader;
private RestTemplate rest;
public GithubPomReader(MavenXpp3Reader reader, RestTemplate rest) {
this.reader = reader;
this.rest = rest;
}
public Model readPomFromUrl(String url) throws IOException, XmlPullParserException {
StringReader pomString = new StringReader(rest.getForObject(url, String.class));
try {
return reader.read(pomString);
}
finally {
pomString.close();
}
}
}

View File

@@ -0,0 +1,101 @@
/*
* 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 org.springframework.cloud.info;
import java.util.HashMap;
import java.util.Map;
import com.jcabi.github.Github;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cloud.info.exceptions.InitializrParseException;
import org.springframework.cloud.info.exceptions.SpringCloudVersionNotFoundException;
import org.springframework.web.client.RestTemplate;
/**
* @author Ryan Baxter
*/
public class InitializrSpringCloudInfoService extends SpringCloudRelease {
private static final String INITIALIZR_URL = "https://start.spring.io/actuator/info";
private RestTemplate rest;
public InitializrSpringCloudInfoService(RestTemplate rest, Github github, GithubPomReader reader) {
super(github, reader);
this.rest = rest;
}
@Override
@Cacheable("springCloudViaBoot")
public SpringCloudVersion getSpringCloudVersion(String springBootVersion)
throws SpringCloudVersionNotFoundException {
Map<String, SpringBootAndCloudVersion> cache = new HashMap<>();
Map<String, Object> response = rest.getForObject(INITIALIZR_URL, Map.class);
if (!response.containsKey("bom-ranges")) {
throw new SpringCloudVersionNotFoundException(
new InitializrParseException("bom-ranges key not found in Initializr info endpoint"));
}
Map<String, Object> bomRanges = (Map<String, Object>) response.get("bom-ranges");
if (!bomRanges.containsKey("spring-cloud")) {
throw new SpringCloudVersionNotFoundException(
new InitializrParseException("spring-cloud key not found in Initializr info endpoint"));
}
Map<String, String> springCloud = (Map<String, String>) bomRanges.get("spring-cloud");
for (String key : springCloud.keySet()) {
String rangeString = springCloud.get(key);
cache.put(key, parseRangeString(rangeString, key));
}
for (String key : cache.keySet()) {
if (cache.get(key).matchesSpringBootVersion(springBootVersion)) {
return new SpringCloudVersion(key);
}
}
throw new SpringCloudVersionNotFoundException(springBootVersion);
}
private SpringBootAndCloudVersion parseRangeString(String rangeString, String springCloudVersion) {
// Example of rangeString Spring Boot >=2.0.0.M3 and <2.0.0.M5
String versions = rangeString.substring(13);
boolean startVersionInclusive = true;
if (versions.charAt(0) == '=') {
versions = versions.substring(1);
}
else {
startVersionInclusive = false;
}
// Example of versions 2.0.0.M3 and <2.0.0.M5 or 2.0.0.M3 and <=2.0.0.M5
String[] cleanedVersions;
boolean endVersionInclusive = true;
if (versions.contains("=")) {
cleanedVersions = versions.split(" and <=");
}
else {
endVersionInclusive = false;
cleanedVersions = versions.split(" and <");
}
if (cleanedVersions.length == 1) {
return new SpringBootAndCloudVersion(cleanedVersions[0], startVersionInclusive, "99999.99999.99999.RELEASE",
endVersionInclusive, springCloudVersion);
}
return new SpringBootAndCloudVersion(cleanedVersions[0], startVersionInclusive, cleanedVersions[1],
endVersionInclusive, springCloudVersion);
}
}

View File

@@ -0,0 +1,86 @@
/*
* 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 org.springframework.cloud.info;
import org.apache.maven.artifact.versioning.ComparableVersion;
/**
* @author Ryan Baxter
*/
public class SpringBootAndCloudVersion {
private String bootStartVersion;
private ComparableVersion comparableBootStartVersion;
private boolean startVersionInclusive;
private String bootEndVersion;
private ComparableVersion comparableBootEndVersion;
private boolean endVersionInclusive;
private String springCloudVersion;
public SpringBootAndCloudVersion(String bootStartVersion, boolean statVersionInclusive, String bootEndVersion,
boolean endVersionInclusive, String springCloudVersion) {
this.bootEndVersion = bootEndVersion;
this.comparableBootEndVersion = new ComparableVersion(bootEndVersion);
this.endVersionInclusive = endVersionInclusive;
this.bootStartVersion = bootStartVersion;
this.comparableBootStartVersion = new ComparableVersion(bootStartVersion);
this.startVersionInclusive = statVersionInclusive;
this.springCloudVersion = springCloudVersion;
}
String getBootStartVersion() {
return bootStartVersion;
}
void setBootStartVersion(String bootStartVersion) {
this.bootStartVersion = bootStartVersion;
}
String getBootEndVersion() {
return bootEndVersion;
}
void setBootEndVersion(String bootEndVersion) {
this.bootEndVersion = bootEndVersion;
}
String getSpringCloudVersion() {
return springCloudVersion;
}
void setSpringCloudVersion(String springCloudVersion) {
this.springCloudVersion = springCloudVersion;
}
boolean matchesSpringBootVersion(String versionToCheck) {
return matchesSpringBootVersion(new ComparableVersion(versionToCheck));
}
boolean matchesSpringBootVersion(ComparableVersion versionToCheck) {
int startVersionComparison = comparableBootStartVersion.compareTo(versionToCheck);
int endVersionComparison = versionToCheck.compareTo(comparableBootEndVersion);
return ((startVersionInclusive && startVersionComparison == 0) || startVersionComparison <= -1)
&& ((endVersionInclusive && endVersionComparison == 0) || endVersionComparison <= -1);
}
}

View File

@@ -0,0 +1,61 @@
/*
* 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 org.springframework.cloud.info;
import com.jcabi.github.Github;
import com.jcabi.github.RtGithub;
import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
@EnableCaching
@EnableConfigurationProperties({ SpringCloudInfoConfigurationProperties.class })
public class SpringCloudInfoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringCloudInfoApplication.class, args);
}
@Bean
public SpringCloudInfoService initializrSpringCloudVersionService(
SpringCloudInfoConfigurationProperties properties) {
Github github = new RtGithub(properties.getGit().getOauthToken());
RestTemplate rest = new RestTemplateBuilder().build();
return new InitializrSpringCloudInfoService(rest, github, new GithubPomReader(new MavenXpp3Reader(), rest));
}
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().permitAll().and().httpBasic().disable().csrf().disable();
}
}
}

View File

@@ -14,39 +14,38 @@
* limitations under the License.
*/
package releaser.internal.sagan;
package org.springframework.cloud.info;
import com.fasterxml.jackson.annotation.JsonInclude;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author Marcin Grzejszczak
* @author Ryan Baxter
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ProjectDetails {
@ConfigurationProperties("spring.cloud.info")
public class SpringCloudInfoConfigurationProperties {
private String bootConfig;
private Git git = new Git();
private String body;
public String getBootConfig() {
return bootConfig;
public Git getGit() {
return git;
}
public void setBootConfig(String bootConfig) {
this.bootConfig = bootConfig;
public void setGit(Git git) {
this.git = git;
}
public String getBody() {
return body;
}
public static class Git {
public void setBody(String body) {
this.body = body;
}
private String oauthToken = "";
public String getOauthToken() {
return oauthToken;
}
public void setOauthToken(String oauthToken) {
this.oauthToken = oauthToken;
}
@Override
public String toString() {
return "ProjectDetails{" + "bootConfig='" + bootConfig + '\'' + ", body='" + body + '\'' + '}';
}
}

View File

@@ -0,0 +1,85 @@
/*
* 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 org.springframework.cloud.info;
import java.io.IOException;
import java.util.Collection;
import java.util.Map;
import org.springframework.cloud.info.SpringCloudInfoService.SpringCloudVersion;
import org.springframework.cloud.info.exceptions.SpringCloudMilestoneNotFoundException;
import org.springframework.cloud.info.exceptions.SpringCloudVersionNotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
/**
* @author Ryan Baxter
*/
@RestController
public class SpringCloudInfoRestController {
private SpringCloudInfoService versionService;
public SpringCloudInfoRestController(SpringCloudInfoService versionService) {
this.versionService = versionService;
}
@GetMapping("/springcloudversion/springboot/{bootVersion}")
public SpringCloudVersion version(@PathVariable String bootVersion) {
try {
return versionService.getSpringCloudVersion(bootVersion);
}
catch (SpringCloudVersionNotFoundException e) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage());
}
}
@GetMapping("/springcloudversions")
public Collection<String> versions() throws IOException {
return versionService.getSpringCloudVersions();
}
@GetMapping("/bomversions/{bomVersion}")
public Map<String, String> bomVersions(@PathVariable String bomVersion) throws IOException {
try {
return versionService.getReleaseVersions(bomVersion);
}
catch (SpringCloudVersionNotFoundException e) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage());
}
}
@GetMapping("/milestones")
public Collection<String> milestones() throws IOException {
return versionService.getMilestones();
}
@GetMapping("/milestones/{name}/duedate")
public SpringCloudInfoService.Milestone milestoneDueDate(@PathVariable String name) throws IOException {
try {
return versionService.getMilestoneDueDate(name);
}
catch (SpringCloudMilestoneNotFoundException e) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage());
}
}
}

View File

@@ -0,0 +1,118 @@
/*
* 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 org.springframework.cloud.info;
import java.io.IOException;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import org.springframework.cloud.info.exceptions.SpringCloudMilestoneNotFoundException;
import org.springframework.cloud.info.exceptions.SpringCloudVersionNotFoundException;
/**
* @author Ryan Baxter
*/
public interface SpringCloudInfoService {
SpringCloudVersion getSpringCloudVersion(String bootVersion) throws SpringCloudVersionNotFoundException;
Collection<String> getSpringCloudVersions() throws IOException;
Map<String, String> getReleaseVersions(String bomVersion) throws SpringCloudVersionNotFoundException, IOException;
Collection<String> getMilestones() throws IOException;
Milestone getMilestoneDueDate(String name) throws SpringCloudMilestoneNotFoundException, IOException;
class Milestone {
private String dueDate;
public Milestone() {
}
public Milestone(String dueDate) {
this.dueDate = dueDate;
}
public String getDueDate() {
return dueDate;
}
public void setDueDate(String dueDate) {
this.dueDate = dueDate;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Milestone milestone = (Milestone) o;
return Objects.equals(getDueDate(), milestone.getDueDate());
}
@Override
public int hashCode() {
return Objects.hash(getDueDate());
}
}
class SpringCloudVersion {
private String version;
public SpringCloudVersion() {
}
public SpringCloudVersion(String version) {
this.version = version;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
SpringCloudVersion that = (SpringCloudVersion) o;
return Objects.equals(getVersion(), that.getVersion());
}
@Override
public int hashCode() {
return Objects.hash(getVersion());
}
}
}

View File

@@ -0,0 +1,177 @@
/*
* 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 org.springframework.cloud.info;
import java.io.IOException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonReader;
import com.jcabi.github.Coordinates;
import com.jcabi.github.Github;
import com.jcabi.http.response.JsonResponse;
import org.apache.maven.model.Model;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cloud.info.exceptions.SpringCloudMilestoneNotFoundException;
import org.springframework.cloud.info.exceptions.SpringCloudVersionNotFoundException;
/**
* @author Ryan Baxter
*/
public abstract class SpringCloudRelease implements SpringCloudInfoService {
/**
* Coordinates to Spring Cloud Release in Github.
*/
public static final String SPRING_CLOUD_RELEASE_COORDINATES = "spring-cloud/spring-cloud-release";
/**
* Github tags path.
*/
public static final String SPRING_CLOUD_RELEASE_TAGS_PATH = "repos/" + SPRING_CLOUD_RELEASE_COORDINATES + "/tags";
private static final String SPRING_CLOUD_RELEASE_RAW = "https://raw.githubusercontent.com/"
+ SPRING_CLOUD_RELEASE_COORDINATES;
/**
* URL to the raw spring-cloud-dependencies file on Github.
*/
public static final String SPRING_CLOUD_RELEASE_DEPENDENCIES_RAW = SPRING_CLOUD_RELEASE_RAW
+ "/%s/spring-cloud-dependencies/pom.xml";
/**
* URL to the raw spring-cloud-starter-parent file on Github.
*/
public static final String SPRING_CLOUD_STARTER_PARENT_RAW = SPRING_CLOUD_RELEASE_RAW
+ "/%s/spring-cloud-starter-parent/pom.xml";
private Github github;
private GithubPomReader reader;
public SpringCloudRelease(Github github, GithubPomReader reader) {
this.github = github;
this.reader = reader;
}
@Override
@Cacheable("springCloudVersions")
public Collection<String> getSpringCloudVersions() throws IOException {
List<String> releaseVersions = new ArrayList<>();
JsonReader reader = github.entry().uri().path(SPRING_CLOUD_RELEASE_TAGS_PATH).back().fetch()
.as(JsonResponse.class).json();
JsonArray tags = reader.readArray();
reader.close();
List<JsonObject> tagsList = tags.getValuesAs(JsonObject.class);
for (JsonObject obj : tagsList) {
releaseVersions.add(obj.getString("name").replaceFirst("v", ""));
}
return releaseVersions;
}
@Override
@Cacheable("releaseVersions")
public Map<String, String> getReleaseVersions(String bomVersion)
throws SpringCloudVersionNotFoundException, IOException {
bomVersion = formatBomVersion(bomVersion);
if (!getSpringCloudVersions().contains(bomVersion)) {
throw new SpringCloudVersionNotFoundException();
}
try {
Map<String, String> versions = new HashMap<>();
Model model = reader.readPomFromUrl(String.format(SPRING_CLOUD_RELEASE_DEPENDENCIES_RAW, bomVersion));
for (String name : model.getProperties().stringPropertyNames()) {
if (name.startsWith("spring-cloud-")) {
versions.put(name.replace(".version", ""), model.getProperties().getProperty(name));
}
}
versions.put("spring-boot", getSpringCloudBootVersion(bomVersion));
return versions;
}
catch (XmlPullParserException e) {
throw new SpringCloudVersionNotFoundException(e);
}
}
@Override
@Cacheable("milestones")
public Collection<String> getMilestones() throws IOException {
Set<String> milestones = new HashSet<>();
Iterable<com.jcabi.github.Milestone> githubMilestones = getMilestonesFromGithub();
for (com.jcabi.github.Milestone milestone : githubMilestones) {
JsonObject json = milestone.json();
milestones.add(json.getString("title"));
}
return milestones;
}
@Override
@Cacheable("milestoneDueDate")
public Milestone getMilestoneDueDate(String name) throws SpringCloudMilestoneNotFoundException, IOException {
Iterable<com.jcabi.github.Milestone> milestones = getMilestonesFromGithub();
for (com.jcabi.github.Milestone milestone : milestones) {
JsonObject json = milestone.json();
if (json.getString("title").equalsIgnoreCase(name)) {
if (json.isNull("due_on")) {
return new Milestone("No Due Date");
}
else {
Instant instant = Instant.parse(json.getString("due_on"));
return new Milestone(LocalDateTime.ofInstant(instant, ZoneId.of(ZoneOffset.UTC.getId()))
.toLocalDate().toString());
}
}
}
throw new SpringCloudMilestoneNotFoundException(name);
}
private Iterable<com.jcabi.github.Milestone> getMilestonesFromGithub() {
Map<String, String> params = new HashMap<>();
params.put("state", "open");
return github.repos().get(new Coordinates.Simple(SPRING_CLOUD_RELEASE_COORDINATES)).milestones()
.iterate(params);
}
private String getSpringCloudBootVersion(String bomVersion) throws IOException, XmlPullParserException {
Model model = reader.readPomFromUrl(String.format(SPRING_CLOUD_STARTER_PARENT_RAW, bomVersion));
return model.getParent().getVersion();
}
private String formatBomVersion(String bomVersion) {
if (bomVersion.charAt(0) != 'v') {
return "v" + bomVersion;
}
else {
return bomVersion;
}
}
}

View File

@@ -0,0 +1,28 @@
/*
* 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 org.springframework.cloud.info.exceptions;
/**
* @author Ryan Baxter
*/
public class InitializrParseException extends Exception {
public InitializrParseException(String msg) {
super(msg);
}
}

View File

@@ -0,0 +1,28 @@
/*
* 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 org.springframework.cloud.info.exceptions;
/**
* @author Ryan Baxter
*/
public class SpringCloudMilestoneNotFoundException extends Exception {
public SpringCloudMilestoneNotFoundException(String name) {
super("Spring Cloud milestone " + name + " was not found in Spring Cloud Release");
}
}

View File

@@ -0,0 +1,36 @@
/*
* 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 org.springframework.cloud.info.exceptions;
/**
* @author Ryan Baxter
*/
public class SpringCloudVersionNotFoundException extends Exception {
public SpringCloudVersionNotFoundException(String springBootVersion) {
super("Spring Cloud version not found for Spring Boot Version " + springBootVersion);
}
public SpringCloudVersionNotFoundException(Exception e) {
super("Spring Cloud version not found", e);
}
public SpringCloudVersionNotFoundException() {
super("Spring Cloud version not found");
}
}

View File

@@ -0,0 +1,5 @@
#management:
# endpoints:
# web:
# exposure:
# include: "*"

View File

@@ -0,0 +1,6 @@
spring:
application:
name: spring-cloud-info
cloud:
config:
label: spring-cloud-info-config

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,99 @@
/* a11y-dark theme */
/* Based on the Tomorrow Night Eighties theme: https://github.com/isagalaev/highlight.js/blob/master/src/styles/tomorrow-night-eighties.css */
/* @author: ericwbailey */
/* Comment */
.hljs-comment,
.hljs-quote {
color: #d4d0ab;
}
/* Red */
.hljs-variable,
.hljs-template-variable,
.hljs-tag,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class,
.hljs-regexp,
.hljs-deletion {
color: #ffa07a;
}
/* Orange */
.hljs-number,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params,
.hljs-meta,
.hljs-link {
color: #f5ab35;
}
/* Yellow */
.hljs-attribute {
color: #ffd700;
}
/* Green */
.hljs-string,
.hljs-symbol,
.hljs-bullet,
.hljs-addition {
color: #abe338;
}
/* Blue */
.hljs-title,
.hljs-section {
color: #00e0e0;
}
/* Purple */
.hljs-keyword,
.hljs-selector-tag {
color: #dcc6e0;
}
.hljs {
display: block;
overflow-x: auto;
background: #2b2b2b;
color: #f8f8f2;
padding: 0.5em;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
@media screen and (-ms-high-contrast: active) {
.hljs-addition,
.hljs-attribute,
.hljs-built_in,
.hljs-builtin-name,
.hljs-bullet,
.hljs-comment,
.hljs-link,
.hljs-literal,
.hljs-meta,
.hljs-number,
.hljs-params,
.hljs-string,
.hljs-symbol,
.hljs-type,
.hljs-quote {
color: highlight;
}
.hljs-keyword,
.hljs-selector-tag {
font-weight: bold;
}
}

View File

@@ -0,0 +1,89 @@
/*
An Old Hope Star Wars Syntax (c) Gustavo Costa <gusbemacbe@gmail.com>
Original theme - Ocean Dark Theme by https://github.com/gavsiu
Based on Jesse Leite's Atom syntax theme 'An Old Hope' https://github.com/JesseLeite/an-old-hope-syntax-atom
*/
/* Death Star Comment */
.hljs-comment,
.hljs-quote
{
color: #B6B18B;
}
/* Darth Vader */
.hljs-variable,
.hljs-template-variable,
.hljs-tag,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class,
.hljs-regexp,
.hljs-deletion
{
color: #EB3C54;
}
/* Threepio */
.hljs-number,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params,
.hljs-meta,
.hljs-link
{
color: #E7CE56;
}
/* Luke Skywalker */
.hljs-attribute
{
color: #EE7C2B;
}
/* Obi Wan Kenobi */
.hljs-string,
.hljs-symbol,
.hljs-bullet,
.hljs-addition
{
color: #4FB4D7;
}
/* Yoda */
.hljs-title,
.hljs-section
{
color: #78BB65;
}
/* Mace Windu */
.hljs-keyword,
.hljs-selector-tag
{
color: #B45EA4;
}
/* Millenium Falcon */
.hljs
{
display: block;
overflow-x: auto;
background: #1C1D21;
color: #c0c5ce;
padding: 0.5em;
}
.hljs-emphasis
{
font-style: italic;
}
.hljs-strong
{
font-weight: bold;
}

View File

@@ -0,0 +1,77 @@
/*
Atom One Dark With support for ReasonML by Gidi Morris, based off work by Daniel Gamage
Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
line-height: 1.3em;
color: #abb2bf;
background: #282c34;
border-radius: 5px;
}
.hljs-keyword, .hljs-operator {
color: #F92672;
}
.hljs-pattern-match {
color: #F92672;
}
.hljs-pattern-match .hljs-constructor {
color: #61aeee;
}
.hljs-function {
color: #61aeee;
}
.hljs-function .hljs-params {
color: #A6E22E;
}
.hljs-function .hljs-params .hljs-typing {
color: #FD971F;
}
.hljs-module-access .hljs-module {
color: #7e57c2;
}
.hljs-constructor {
color: #e2b93d;
}
.hljs-constructor .hljs-string {
color: #9CCC65;
}
.hljs-comment, .hljs-quote {
color: #b18eb1;
font-style: italic;
}
.hljs-doctag, .hljs-formula {
color: #c678dd;
}
.hljs-section, .hljs-name, .hljs-selector-tag, .hljs-deletion, .hljs-subst {
color: #e06c75;
}
.hljs-literal {
color: #56b6c2;
}
.hljs-string, .hljs-regexp, .hljs-addition, .hljs-attribute, .hljs-meta-string {
color: #98c379;
}
.hljs-built_in, .hljs-class .hljs-title {
color: #e6c07b;
}
.hljs-attr, .hljs-variable, .hljs-template-variable, .hljs-type, .hljs-selector-class, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-number {
color: #d19a66;
}
.hljs-symbol, .hljs-bullet, .hljs-link, .hljs-meta, .hljs-selector-id, .hljs-title {
color: #61aeee;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
.hljs-link {
text-decoration: underline;
}

View File

@@ -0,0 +1,96 @@
/*
Atom One Dark by Daniel Gamage
Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax
base: #282c34
mono-1: #abb2bf
mono-2: #818896
mono-3: #5c6370
hue-1: #56b6c2
hue-2: #61aeee
hue-3: #c678dd
hue-4: #98c379
hue-5: #e06c75
hue-5-2: #be5046
hue-6: #d19a66
hue-6-2: #e6c07b
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
color: #abb2bf;
background: #282c34;
}
.hljs-comment,
.hljs-quote {
color: #5c6370;
font-style: italic;
}
.hljs-doctag,
.hljs-keyword,
.hljs-formula {
color: #c678dd;
}
.hljs-section,
.hljs-name,
.hljs-selector-tag,
.hljs-deletion,
.hljs-subst {
color: #e06c75;
}
.hljs-literal {
color: #56b6c2;
}
.hljs-string,
.hljs-regexp,
.hljs-addition,
.hljs-attribute,
.hljs-meta-string {
color: #98c379;
}
.hljs-built_in,
.hljs-class .hljs-title {
color: #e6c07b;
}
.hljs-attr,
.hljs-variable,
.hljs-template-variable,
.hljs-type,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-number {
color: #d19a66;
}
.hljs-symbol,
.hljs-bullet,
.hljs-link,
.hljs-meta,
.hljs-selector-id,
.hljs-title {
color: #61aeee;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
.hljs-link {
text-decoration: underline;
}

View File

@@ -0,0 +1,96 @@
/*
Atom One Light by Daniel Gamage
Original One Light Syntax theme from https://github.com/atom/one-light-syntax
base: #fafafa
mono-1: #383a42
mono-2: #686b77
mono-3: #a0a1a7
hue-1: #0184bb
hue-2: #4078f2
hue-3: #a626a4
hue-4: #50a14f
hue-5: #e45649
hue-5-2: #c91243
hue-6: #986801
hue-6-2: #c18401
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
color: #383a42;
background: #fafafa;
}
.hljs-comment,
.hljs-quote {
color: #a0a1a7;
font-style: italic;
}
.hljs-doctag,
.hljs-keyword,
.hljs-formula {
color: #a626a4;
}
.hljs-section,
.hljs-name,
.hljs-selector-tag,
.hljs-deletion,
.hljs-subst {
color: #e45649;
}
.hljs-literal {
color: #0184bb;
}
.hljs-string,
.hljs-regexp,
.hljs-addition,
.hljs-attribute,
.hljs-meta-string {
color: #50a14f;
}
.hljs-built_in,
.hljs-class .hljs-title {
color: #c18401;
}
.hljs-attr,
.hljs-variable,
.hljs-template-variable,
.hljs-type,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-number {
color: #986801;
}
.hljs-symbol,
.hljs-bullet,
.hljs-link,
.hljs-meta,
.hljs-selector-id,
.hljs-title {
color: #4078f2;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
.hljs-link {
text-decoration: underline;
}

View File

@@ -0,0 +1,76 @@
/*
Dracula Theme v1.2.0
https://github.com/zenorocha/dracula-theme
Copyright 2015, All rights reserved
Code licensed under the MIT license
http://zenorocha.mit-license.org
@author Éverton Ribeiro <nuxlli@gmail.com>
@author Zeno Rocha <hi@zenorocha.com>
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
background: #282a36;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-literal,
.hljs-section,
.hljs-link {
color: #8be9fd;
}
.hljs-function .hljs-keyword {
color: #ff79c6;
}
.hljs,
.hljs-subst {
color: #f8f8f2;
}
.hljs-string,
.hljs-title,
.hljs-name,
.hljs-type,
.hljs-attribute,
.hljs-symbol,
.hljs-bullet,
.hljs-addition,
.hljs-variable,
.hljs-template-tag,
.hljs-template-variable {
color: #f1fa8c;
}
.hljs-comment,
.hljs-quote,
.hljs-deletion,
.hljs-meta {
color: #6272a4;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-literal,
.hljs-title,
.hljs-section,
.hljs-doctag,
.hljs-type,
.hljs-name,
.hljs-strong {
font-weight: bold;
}
.hljs-emphasis {
font-style: italic;
}

View File

@@ -0,0 +1,99 @@
/*
github.com style (c) Vasily Polovnyov <vast@whiteants.net>
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
color: #333;
background: #f8f8f8;
}
.hljs-comment,
.hljs-quote {
color: #998;
font-style: italic;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-subst {
color: #333;
font-weight: bold;
}
.hljs-number,
.hljs-literal,
.hljs-variable,
.hljs-template-variable,
.hljs-tag .hljs-attr {
color: #008080;
}
.hljs-string,
.hljs-doctag {
color: #d14;
}
.hljs-title,
.hljs-section,
.hljs-selector-id {
color: #900;
font-weight: bold;
}
.hljs-subst {
font-weight: normal;
}
.hljs-type,
.hljs-class .hljs-title {
color: #458;
font-weight: bold;
}
.hljs-tag,
.hljs-name,
.hljs-attribute {
color: #000080;
font-weight: normal;
}
.hljs-regexp,
.hljs-link {
color: #009926;
}
.hljs-symbol,
.hljs-bullet {
color: #990073;
}
.hljs-built_in,
.hljs-builtin-name {
color: #0086b3;
}
.hljs-meta {
color: #999;
font-weight: bold;
}
.hljs-deletion {
background: #fdd;
}
.hljs-addition {
background: #dfd;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}

View File

@@ -0,0 +1,83 @@
/*
Monokai Sublime style. Derived from Monokai by noformnocontent http://nn.mit-license.org/
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
background: #23241f;
}
.hljs,
.hljs-tag,
.hljs-subst {
color: #f8f8f2;
}
.hljs-strong,
.hljs-emphasis {
color: #a8a8a2;
}
.hljs-bullet,
.hljs-quote,
.hljs-number,
.hljs-regexp,
.hljs-literal,
.hljs-link {
color: #ae81ff;
}
.hljs-code,
.hljs-title,
.hljs-section,
.hljs-selector-class {
color: #a6e22e;
}
.hljs-strong {
font-weight: bold;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-name,
.hljs-attr {
color: #f92672;
}
.hljs-symbol,
.hljs-attribute {
color: #66d9ef;
}
.hljs-params,
.hljs-class .hljs-title {
color: #f8f8f2;
}
.hljs-string,
.hljs-type,
.hljs-built_in,
.hljs-builtin-name,
.hljs-selector-id,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-addition,
.hljs-variable,
.hljs-template-variable {
color: #e6db74;
}
.hljs-comment,
.hljs-deletion,
.hljs-meta {
color: #75715e;
}

View File

@@ -0,0 +1,70 @@
/*
Monokai style - ported by Luigi Maselli - http://grigio.org
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
background: #272822; color: #ddd;
}
.hljs-tag,
.hljs-keyword,
.hljs-selector-tag,
.hljs-literal,
.hljs-strong,
.hljs-name {
color: #f92672;
}
.hljs-code {
color: #66d9ef;
}
.hljs-class .hljs-title {
color: white;
}
.hljs-attribute,
.hljs-symbol,
.hljs-regexp,
.hljs-link {
color: #bf79db;
}
.hljs-string,
.hljs-bullet,
.hljs-subst,
.hljs-title,
.hljs-section,
.hljs-emphasis,
.hljs-type,
.hljs-built_in,
.hljs-builtin-name,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-addition,
.hljs-variable,
.hljs-template-tag,
.hljs-template-variable {
color: #a6e22e;
}
.hljs-comment,
.hljs-quote,
.hljs-deletion,
.hljs-meta {
color: #75715e;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-literal,
.hljs-doctag,
.hljs-title,
.hljs-section,
.hljs-type,
.hljs-selector-id {
font-weight: bold;
}

View File

@@ -0,0 +1,84 @@
/*
Orginal Style from ethanschoonover.com/solarized (c) Jeremy Hull <sourdrums@gmail.com>
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
background: #fdf6e3;
color: #657b83;
}
.hljs-comment,
.hljs-quote {
color: #93a1a1;
}
/* Solarized Green */
.hljs-keyword,
.hljs-selector-tag,
.hljs-addition {
color: #859900;
}
/* Solarized Cyan */
.hljs-number,
.hljs-string,
.hljs-meta .hljs-meta-string,
.hljs-literal,
.hljs-doctag,
.hljs-regexp {
color: #2aa198;
}
/* Solarized Blue */
.hljs-title,
.hljs-section,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class {
color: #268bd2;
}
/* Solarized Yellow */
.hljs-attribute,
.hljs-attr,
.hljs-variable,
.hljs-template-variable,
.hljs-class .hljs-title,
.hljs-type {
color: #b58900;
}
/* Solarized Orange */
.hljs-symbol,
.hljs-bullet,
.hljs-subst,
.hljs-meta,
.hljs-meta .hljs-keyword,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-link {
color: #cb4b16;
}
/* Solarized Red */
.hljs-built_in,
.hljs-deletion {
color: #dc322f;
}
.hljs-formula {
background: #eee8d5;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}

View File

@@ -0,0 +1,80 @@
/*
Zenburn style from voldmar.ru (c) Vladimir Epifanov <voldmar@voldmar.ru>
based on dark.css by Ivan Sagalaev
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
background: #3f3f3f;
color: #dcdcdc;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-tag {
color: #e3ceab;
}
.hljs-template-tag {
color: #dcdcdc;
}
.hljs-number {
color: #8cd0d3;
}
.hljs-variable,
.hljs-template-variable,
.hljs-attribute {
color: #efdcbc;
}
.hljs-literal {
color: #efefaf;
}
.hljs-subst {
color: #8f8f8f;
}
.hljs-title,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class,
.hljs-section,
.hljs-type {
color: #efef8f;
}
.hljs-symbol,
.hljs-bullet,
.hljs-link {
color: #dca3a3;
}
.hljs-deletion,
.hljs-string,
.hljs-built_in,
.hljs-builtin-name {
color: #cc9393;
}
.hljs-addition,
.hljs-comment,
.hljs-quote,
.hljs-meta {
color: #7f9f7f;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}

View File

@@ -0,0 +1,107 @@
var toctitle = document.getElementById('toctitle');
var path = window.location.pathname;
if (toctitle != null) {
var oldtoc = toctitle.nextElementSibling;
var newtoc = document.createElement('div');
newtoc.setAttribute('id', 'tocbot');
newtoc.setAttribute('class', 'js-toc desktop-toc');
oldtoc.setAttribute('class', 'mobile-toc');
oldtoc.parentNode.appendChild(newtoc);
tocbot.init({
contentSelector: '#content',
headingSelector: 'h1, h2, h3, h4, h5',
positionFixedSelector: 'body',
fixedSidebarOffset: 90,
smoothScroll: false
});
if (!path.endsWith("index.html") && !path.endsWith("/")) {
var link = document.createElement("a");
link.setAttribute("href", "index.html");
link.innerHTML = "<span><i class=\"fa fa-chevron-left\" aria-hidden=\"true\"></i></span> Back to index";
var block = document.createElement("div");
block.setAttribute('class', 'back-action');
block.appendChild(link);
var toc = document.getElementById('toc');
var next = document.getElementById('toctitle').nextElementSibling;
toc.insertBefore(block, next);
}
}
var headerHtml = '<div id="header-spring">\n' +
'<h1>\n' +
'<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0"\n' +
'viewBox="0 0 245.8 45.3" style="enable-background:new 0 0 245.8 45.3;" xml:space="preserve">\n' +
'<g id="logos">\n' +
'<g>\n' +
'<path class="st0" d="M39.4,3.7c-0.6,1.5-1.4,2.8-2.3,4c-3.9-4-9.3-6.4-15.2-6.4c-11.7,0-21.3,9.5-21.3,21.3\n' +
'c0,6.2,2.6,11.7,6.8,15.6l0.8,0.7c3.7,3.1,8.5,5,13.7,5c11.2,0,20.4-8.7,21.2-19.8C43.7,18.7,42.1,11.8,39.4,3.7z M10.5,38.3\n' +
'c-0.6,0.8-1.8,0.9-2.6,0.3C7.1,37.9,7,36.8,7.6,36c0.6-0.8,1.8-0.9,2.6-0.3C11,36.4,11.1,37.5,10.5,38.3z M39.3,31.9\n' +
'c-5.2,7-16.5,4.6-23.6,5c0,0-1.3,0.1-2.6,0.3c0,0,0.5-0.2,1.1-0.4c5-1.7,7.4-2.1,10.5-3.7c5.8-3,11.5-9.4,12.7-16.1\n' +
'c-2.2,6.4-8.9,12-14.9,14.2c-4.2,1.5-11.7,3-11.7,3c0,0-0.3-0.2-0.3-0.2c-5.1-2.5-5.3-13.6,4-17.1c4.1-1.6,8-0.7,12.4-1.8\n' +
'C31.6,14.1,37,10.6,39.2,6C41.7,13.3,44.7,24.8,39.3,31.9z"/>\n' +
'<g>\n' +
'<path class="st0" d="M55.2,30.9c-0.5-0.3-0.9-0.9-0.9-1.6c0-1.1,0.8-1.9,1.9-1.9c0.4,0,0.7,0.1,1,0.3c2,1.3,4.1,2,5.9,2\n' +
'c2,0,3.2-0.9,3.2-2.2v-0.1c0-1.6-2.2-2.2-4.6-2.9c-3-0.9-6.5-2.1-6.5-6.1v-0.1c0-3.9,3.2-6.3,7.4-6.3c2.2,0,4.5,0.6,6.5,1.7\n' +
'c0.7,0.4,1.1,1,1.1,1.8c0,1.1-0.9,1.9-2,1.9c-0.4,0-0.6-0.1-0.9-0.2c-1.7-0.9-3.4-1.4-4.9-1.4c-1.8,0-2.9,0.9-2.9,2v0.1\n' +
'c0,1.5,2.2,2.2,4.7,2.9c3,0.9,6.4,2.3,6.4,6v0.1c0,4.3-3.4,6.5-7.7,6.5C60.4,33.3,57.6,32.5,55.2,30.9z"/>\n' +
'<path class="st0" d="M72.5,14.3c0-1.3,1-2.4,2.3-2.4c1.3,0,2.4,1.1,2.4,2.4v1.4c1.5-2.2,3.7-3.9,7-3.9c4.8,0,9.6,3.8,9.6,10.7\n' +
'v0.1c0,6.8-4.7,10.7-9.6,10.7c-3.4,0-5.6-1.7-7-3.6V37c0,1.3-1.1,2.4-2.4,2.4c-1.3,0-2.3-1-2.3-2.4V14.3z M89.1,22.7L89.1,22.7\n' +
'c0-4.1-2.7-6.7-5.9-6.7c-3.2,0-6,2.7-6,6.6v0.1c0,4,2.8,6.6,6,6.6C86.4,29.3,89.1,26.7,89.1,22.7z"/>\n' +
'<path class="st0" d="M95.7,14.3c0-1.3,1-2.4,2.3-2.4c1.3,0,2.4,1.1,2.4,2.4v1.1c0.2-1.8,3.1-3.5,5.2-3.5c1.5,0,2.3,1,2.3,2.3\n' +
'c0,1.3-0.8,2.1-1.9,2.3c-3.4,0.6-5.7,3.5-5.7,7.6V31c0,1.3-1.1,2.3-2.4,2.3c-1.3,0-2.3-1-2.3-2.3V14.3z"/>\n' +
'<path class="st0" d="M109.7,14.3c0-1.3,1-2.4,2.3-2.4c1.3,0,2.4,1.1,2.4,2.4V31c0,1.3-1.1,2.3-2.4,2.3c-1.3,0-2.3-1-2.3-2.3V14.3\n' +
'z"/>\n' +
'<path class="st0" d="M116.9,14.3c0-1.3,1-2.4,2.3-2.4c1.3,0,2.4,1.1,2.4,2.4v1c1.3-1.9,3.2-3.4,6.5-3.4c4.7,0,7.4,3.1,7.4,7.9V31\n' +
'c0,1.3-1,2.3-2.3,2.3c-1.3,0-2.4-1-2.4-2.3v-9.7c0-3.2-1.6-5-4.4-5c-2.7,0-4.7,1.9-4.7,5.1V31c0,1.3-1.1,2.3-2.4,2.3\n' +
'c-1.3,0-2.3-1-2.3-2.3V14.3z"/>\n' +
'<path class="st0" d="M156.2,11.9c-1.3,0-2.4,1.1-2.4,2.4v1.4c-1.5-2.2-3.7-3.9-7-3.9c-4.9,0-9.6,3.8-9.6,10.7v0.1\n' +
'c0,6.8,4.7,10.7,9.6,10.7c3.4,0,5.6-1.7,7-3.6c-0.2,3.7-2.5,5.7-6.5,5.7c-2.4,0-4.5-0.6-6.3-1.6c-0.2-0.1-0.5-0.2-0.9-0.2\n' +
'c-1.1,0-2,0.9-2,2c0,0.9,0.5,1.6,1.3,1.9c2.5,1.2,5.1,1.8,8,1.8c3.7,0,6.6-0.9,8.5-2.8c1.7-1.7,2.7-4.3,2.7-7.8V14.3\n' +
'C158.5,13,157.5,11.9,156.2,11.9z M147.9,29.2c-3.2,0-5.9-2.5-5.9-6.6v-0.1c0-4,2.7-6.6,5.9-6.6c3.2,0,6,2.7,6,6.6v0.1\n' +
'C153.9,26.6,151.1,29.2,147.9,29.2z"/>\n' +
'<path class="st0" d="M114.5,6.3c0,1.3-1.1,2.4-2.4,2.4c-1.3,0-2.4-1.1-2.4-2.4c0-1.3,1.1-2.4,2.4-2.4\n' +
'C113.4,3.9,114.5,4.9,114.5,6.3z"/>\n' +
'</g>\n' +
'</g>\n' +
'<g class="st1">\n' +
'<g>\n' +
'<g>\n' +
'<g>\n' +
'<path class="st2" d="M200.1,21.1H198V19h2.1V21.1z M200.1,32.9H198V22.6h2.1V32.9z"/>\n' +
'</g>\n' +
'<g>\n' +
'<g>\n' +
'<path class="st2" d="M212.5,22.6l-3,8.9c-0.5,1.5-1.4,1.6-2.2,1.6c-1.1,0-1.8-0.5-2.2-1.6l-2.5-7.4h-1v-1.5h2.6l2.6,8.3\n' +
'c0.1,0.4,0.2,0.6,0.5,0.6c0.3,0,0.4-0.2,0.5-0.6l2.6-8.3H212.5z"/>\n' +
'<path class="st2" d="M217.8,22.6c2.8,0,4.7,1.8,4.7,4.5v1.6c0,2.6-1.9,4.5-4.7,4.5c-2.8,0-4.7-1.8-4.7-4.5v-1.6\n' +
'C213,24.4,215,22.6,217.8,22.6 M217.8,31.4c1.7,0,2.7-1.3,2.7-2.8v-1.6c0-1.5-1-2.8-2.7-2.8c-1.8,0-2.7,1.3-2.7,2.8v1.6\n' +
'C215.1,30.2,216,31.4,217.8,31.4"/>\n' +
'<path class="st2" d="M239.6,22.9c-1.1-0.3-2.7-0.5-4-0.5c-2.8,0-4.6,1.8-4.6,4.6v1.1c0,2.9,1.7,4.7,4.6,4.7c0.1,0,0.6,0,0.8,0\n' +
'v-1.7c-0.1,0-0.7,0-0.8,0c-1.5,0-2.6-1.2-2.6-2.9v-1.1c0-1.8,1-2.9,2.6-2.9c0.7,0,1.7,0.1,2.1,0.1l0.1,0l0,8.6h2.1v-9.6\n' +
'C240,23.1,240,23,239.6,22.9"/>\n' +
'<rect x="242.1" y="19" class="st2" width="2.1" height="13.9"/>\n' +
'<path class="st2" d="M190.5,19h-3.8v13.9h2.2V20.9h1.3c0.3,0,0.5,0,0.8,0c1.9,0,2.9,0.8,2.9,2.3c0,0.1,0,0.1,0,0.2\n' +
'c0,1.4-0.8,2.3-2.9,2.3c-0.2,0-0.4,0-0.6,0c0,0.5,0,1.5,0,1.9c0.2,0,0.4,0,0.6,0c3,0,5.2-1.2,5.2-4.2c0-0.1,0-0.1,0-0.2\n' +
'C196.2,20.2,193.9,19,190.5,19"/>\n' +
'<path class="st2" d="M226.3,20.4v2.2h3.5v1.7h-3.5v6c0,0.9,0.6,1,1.5,1h2v1.7H227c-2,0-2.9-0.8-2.9-2.6v-9.6L226.3,20.4z"/>\n' +
'</g>\n' +
'</g>\n' +
'</g>\n' +
'</g>\n' +
'<g>\n' +
'<path class="st2" d="M167.7,32.9v-10h1.1v3.8c0.6-0.8,1.5-1.3,2.4-1.3c1.9,0,3.2,1.5,3.2,3.8c0,2.4-1.3,3.8-3.2,3.8\n' +
'c-1,0-1.9-0.5-2.4-1.3v1.1H167.7z M171,32.1c1.5,0,2.3-1.2,2.3-2.8c0-1.6-0.9-2.8-2.3-2.8c-0.9,0-1.8,0.5-2.2,1.2v3.3\n' +
'C169.2,31.6,170.1,32.1,171,32.1z"/>\n' +
'<path class="st2" d="M175.9,34.7c0.2,0.1,0.4,0.1,0.6,0.1c0.5,0,0.8-0.2,1.1-0.8l0.5-1.1l-3-7.3h1.2l2.4,5.9l2.4-5.9h1.2\n' +
'l-3.6,8.7c-0.4,1-1.2,1.5-2.1,1.5c-0.2,0-0.6,0-0.8-0.1L175.9,34.7z"/>\n' +
'</g>\n' +
'</g>\n' +
'</g>\n' +
'</svg>\n' +
'\n' +
'</h1>\n' +
'</div>';
var header = document.createElement("div");
header.innerHTML = headerHtml;
document.body.insertBefore(header, document.body.firstChild);

View File

@@ -0,0 +1 @@
.toc{overflow-y:auto}.toc>.toc-list{overflow:hidden;position:relative}.toc>.toc-list li{list-style:none}.toc-list{margin:0;padding-left:10px}a.toc-link{color:currentColor;height:100%}.is-collapsible{max-height:1000px;overflow:hidden;transition:all 300ms ease-in-out}.is-collapsed{max-height:0}.is-position-fixed{position:fixed !important;top:0}.is-active-link{font-weight:700}.toc-link::before{background-color:#EEE;content:' ';display:inline-block;height:inherit;left:0;margin-top:-1px;position:absolute;width:2px}.is-active-link::before{background-color:#54BC4B}

Some files were not shown because too many files have changed in this diff Show More