Commit 98ee724e authored by Andy Wilkinson's avatar Andy Wilkinson

Stop using Bintray to publish to Maven Central

This commit reworks the CI pipeline to remove the use of Bintray for
publishing to Maven Central. In its place it adds a new
publishToCentral command to the release scripts. This command can be
used to publish a directory tree of artifacts to the Maven Central
gateway hosted by Sonatype.

Publishing consists of 4 steps:

1. Create the staging repository
2. Deploy artifacts to the repository
3. Close the repository
4. Release the repository

The command requires 3 arguments:

1. The type of release being performed
2. Location of a build info JSON file that describes the release
   that is to be deployed
3. Root of a directory structure, in Maven repository layout, that
   contains the artifacts to be deployed

Closes gh-25107
parent 29d46c86
......@@ -19,6 +19,11 @@
<spring-javaformat.version>0.0.26</spring-javaformat.version>
</properties>
<dependencies>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpg-jdk15to18</artifactId>
<version>1.68</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
......
......@@ -17,14 +17,10 @@
package io.spring.concourse.releasescripts.artifactory;
import java.net.URI;
import java.time.Duration;
import java.util.Set;
import io.spring.concourse.releasescripts.ReleaseInfo;
import io.spring.concourse.releasescripts.artifactory.payload.BuildInfoResponse;
import io.spring.concourse.releasescripts.artifactory.payload.DistributionRequest;
import io.spring.concourse.releasescripts.artifactory.payload.PromotionRequest;
import io.spring.concourse.releasescripts.bintray.BintrayService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -53,17 +49,11 @@ public class ArtifactoryService {
private static final String BUILD_INFO_URL = ARTIFACTORY_URL + "/api/build/";
private static final String DISTRIBUTION_URL = ARTIFACTORY_URL + "/api/build/distribute/";
private static final String STAGING_REPO = "libs-staging-local";
private final RestTemplate restTemplate;
private final BintrayService bintrayService;
public ArtifactoryService(RestTemplateBuilder builder, ArtifactoryProperties artifactoryProperties,
BintrayService bintrayService) {
this.bintrayService = bintrayService;
public ArtifactoryService(RestTemplateBuilder builder, ArtifactoryProperties artifactoryProperties) {
String username = artifactoryProperties.getUsername();
String password = artifactoryProperties.getPassword();
if (StringUtils.hasLength(username)) {
......@@ -116,37 +106,6 @@ public class ArtifactoryService {
}
}
/**
* Deploy builds from Artifactory to Bintray.
* @param sourceRepo the source repo in Artifactory.
* @param releaseInfo the resease info
* @param artifactDigests the artifact digests
*/
public void distribute(String sourceRepo, ReleaseInfo releaseInfo, Set<String> artifactDigests) {
logger.debug("Attempting distribute via Artifactory");
if (!this.bintrayService.isDistributionStarted(releaseInfo)) {
startDistribute(sourceRepo, releaseInfo);
}
if (!this.bintrayService.isDistributionComplete(releaseInfo, artifactDigests, Duration.ofMinutes(60))) {
throw new DistributionTimeoutException("Distribution timed out.");
}
}
private void startDistribute(String sourceRepo, ReleaseInfo releaseInfo) {
DistributionRequest request = new DistributionRequest(new String[] { sourceRepo });
RequestEntity<DistributionRequest> requestEntity = RequestEntity
.post(URI.create(DISTRIBUTION_URL + releaseInfo.getBuildName() + "/" + releaseInfo.getBuildNumber()))
.contentType(MediaType.APPLICATION_JSON).body(request);
try {
this.restTemplate.exchange(requestEntity, Object.class);
logger.debug("Distribute call completed");
}
catch (HttpClientErrorException ex) {
logger.info("Failed to distribute.");
throw ex;
}
}
private PromotionRequest getPromotionRequest(String targetRepo) {
return new PromotionRequest("staged", STAGING_REPO, targetRepo);
}
......
/*
* Copyright 2012-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 io.spring.concourse.releasescripts.bintray;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* {@link ConfigurationProperties @ConfigurationProperties} for the Bintray API.
*
* @author Madhura Bhave
*/
@ConfigurationProperties(prefix = "bintray")
public class BintrayProperties {
private String username;
private String apiKey;
private String repo;
private String subject;
public String getUsername() {
return this.username;
}
public void setUsername(String username) {
this.username = username;
}
public String getApiKey() {
return this.apiKey;
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
public String getRepo() {
return this.repo;
}
public void setRepo(String repo) {
this.repo = repo;
}
public String getSubject() {
return this.subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
}
/*
* Copyright 2012-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 io.spring.concourse.releasescripts.bintray;
import java.net.URI;
import java.time.Duration;
import java.util.HashSet;
import java.util.Set;
import io.spring.concourse.releasescripts.ReleaseInfo;
import io.spring.concourse.releasescripts.sonatype.SonatypeProperties;
import io.spring.concourse.releasescripts.sonatype.SonatypeService;
import org.awaitility.core.ConditionTimeoutException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import static org.awaitility.Awaitility.waitAtMost;
/**
* Central class for interacting with Bintray's REST API.
*
* @author Madhura Bhave
*/
@Component
public class BintrayService {
private static final Logger logger = LoggerFactory.getLogger(BintrayService.class);
private static final String BINTRAY_URL = "https://api.bintray.com/";
private static final String GRADLE_PLUGIN_REQUEST = "[ { \"name\": \"gradle-plugin\", \"values\": [\"org.springframework.boot:org.springframework.boot:spring-boot-gradle-plugin\"] } ]";
private final RestTemplate restTemplate;
private final BintrayProperties bintrayProperties;
private final SonatypeProperties sonatypeProperties;
private final SonatypeService sonatypeService;
public BintrayService(RestTemplateBuilder builder, BintrayProperties bintrayProperties,
SonatypeProperties sonatypeProperties, SonatypeService sonatypeService) {
this.bintrayProperties = bintrayProperties;
this.sonatypeProperties = sonatypeProperties;
this.sonatypeService = sonatypeService;
String username = bintrayProperties.getUsername();
String apiKey = bintrayProperties.getApiKey();
if (StringUtils.hasLength(username)) {
builder = builder.basicAuthentication(username, apiKey);
}
this.restTemplate = builder.build();
}
public boolean isDistributionStarted(ReleaseInfo releaseInfo) {
logger.debug("Checking if distribution is started");
RequestEntity<Void> request = getPackageFilesRequest(releaseInfo, 1);
try {
logger.debug("Checking Bintray");
this.restTemplate.exchange(request, PackageFile[].class).getBody();
return true;
}
catch (HttpClientErrorException ex) {
if (ex.getStatusCode() != HttpStatus.NOT_FOUND) {
throw ex;
}
return false;
}
}
public boolean isDistributionComplete(ReleaseInfo releaseInfo, Set<String> requiredDigests, Duration timeout) {
return isDistributionComplete(releaseInfo, requiredDigests, timeout, Duration.ofSeconds(20));
}
public boolean isDistributionComplete(ReleaseInfo releaseInfo, Set<String> requiredDigests, Duration timeout,
Duration pollInterval) {
logger.debug("Checking if distribution is complete");
RequestEntity<Void> request = getPackageFilesRequest(releaseInfo, 1);
try {
waitAtMost(timeout).with().pollDelay(Duration.ZERO).pollInterval(pollInterval).until(() -> {
logger.debug("Checking Bintray");
try {
PackageFile[] published = this.restTemplate.exchange(request, PackageFile[].class).getBody();
return hasPublishedAll(published, requiredDigests);
}
catch (HttpClientErrorException.NotFound ex) {
return false;
}
});
}
catch (ConditionTimeoutException ex) {
logger.debug("Timeout checking Bintray");
return false;
}
return true;
}
private boolean hasPublishedAll(PackageFile[] published, Set<String> requiredDigests) {
if (published == null || published.length == 0) {
logger.debug("Bintray returned no published files");
return false;
}
Set<String> remaining = new HashSet<>(requiredDigests);
for (PackageFile publishedFile : published) {
logger.debug(
"Found published file " + publishedFile.getName() + " with digest " + publishedFile.getSha256());
remaining.remove(publishedFile.getSha256());
}
if (remaining.isEmpty()) {
logger.debug("Found all required digests");
return true;
}
logger.debug(remaining.size() + " digests have not been published:");
remaining.forEach(logger::debug);
return false;
}
private RequestEntity<Void> getPackageFilesRequest(ReleaseInfo releaseInfo, int includeUnpublished) {
return RequestEntity.get(URI.create(BINTRAY_URL + "packages/" + this.bintrayProperties.getSubject() + "/"
+ this.bintrayProperties.getRepo() + "/" + releaseInfo.getGroupId() + "/versions/"
+ releaseInfo.getVersion() + "/files?include_unpublished=" + includeUnpublished)).build();
}
/**
* Add attributes to Spring Boot's Gradle plugin.
* @param releaseInfo the release information
*/
public void publishGradlePlugin(ReleaseInfo releaseInfo) {
logger.debug("Publishing Gradle plugin");
RequestEntity<String> requestEntity = RequestEntity
.post(URI.create(BINTRAY_URL + "packages/" + this.bintrayProperties.getSubject() + "/"
+ this.bintrayProperties.getRepo() + "/" + releaseInfo.getGroupId() + "/versions/"
+ releaseInfo.getVersion() + "/attributes"))
.contentType(MediaType.APPLICATION_JSON).body(GRADLE_PLUGIN_REQUEST);
try {
this.restTemplate.exchange(requestEntity, Object.class);
logger.debug("Publishing Gradle plugin complete");
}
catch (HttpClientErrorException ex) {
logger.info("Failed to add attribute to Gradle plugin");
throw ex;
}
}
/**
* Sync artifacts from Bintray to Maven Central.
* @param releaseInfo the release information
*/
public void syncToMavenCentral(ReleaseInfo releaseInfo) {
logger.info("Calling Bintray to sync to Sonatype");
if (this.sonatypeService.artifactsPublished(releaseInfo)) {
logger.info("Artifacts already published");
return;
}
RequestEntity<SonatypeProperties> requestEntity = RequestEntity
.post(URI.create(String.format(BINTRAY_URL + "maven_central_sync/%s/%s/%s/versions/%s",
this.bintrayProperties.getSubject(), this.bintrayProperties.getRepo(), releaseInfo.getGroupId(),
releaseInfo.getVersion())))
.contentType(MediaType.APPLICATION_JSON).body(this.sonatypeProperties);
try {
this.restTemplate.exchange(requestEntity, Object.class);
logger.debug("Sync complete");
}
catch (HttpClientErrorException ex) {
logger.info("Failed to sync");
throw ex;
}
}
}
/*
* Copyright 2012-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 io.spring.concourse.releasescripts.command;
import java.io.File;
import java.nio.file.Files;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.spring.concourse.releasescripts.ReleaseInfo;
import io.spring.concourse.releasescripts.ReleaseType;
import io.spring.concourse.releasescripts.artifactory.ArtifactoryService;
import io.spring.concourse.releasescripts.artifactory.payload.BuildInfoResponse;
import io.spring.concourse.releasescripts.artifactory.payload.BuildInfoResponse.Artifact;
import io.spring.concourse.releasescripts.artifactory.payload.BuildInfoResponse.BuildInfo;
import io.spring.concourse.releasescripts.artifactory.payload.BuildInfoResponse.Module;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
/**
* Command used to deploy builds from Artifactory to Bintray.
*
* @author Madhura Bhave
* @author Phillip Webb
*/
@Component
public class DistributeCommand implements Command {
private static final Logger logger = LoggerFactory.getLogger(DistributeCommand.class);
private final ArtifactoryService artifactoryService;
private final ObjectMapper objectMapper;
private final List<Pattern> optionalDeployments;
public DistributeCommand(ArtifactoryService artifactoryService, ObjectMapper objectMapper,
DistributeProperties distributeProperties) {
this.artifactoryService = artifactoryService;
this.objectMapper = objectMapper;
this.optionalDeployments = distributeProperties.getOptionalDeployments().stream().map(Pattern::compile)
.collect(Collectors.toList());
}
@Override
public void run(ApplicationArguments args) throws Exception {
logger.debug("Running 'distribute' command");
List<String> nonOptionArgs = args.getNonOptionArgs();
Assert.state(!nonOptionArgs.isEmpty(), "No command argument specified");
Assert.state(nonOptionArgs.size() == 3, "Release type or build info not specified");
String releaseType = nonOptionArgs.get(1);
ReleaseType type = ReleaseType.from(releaseType);
if (!ReleaseType.RELEASE.equals(type)) {
logger.info("Skipping distribution of " + type + " type");
return;
}
String buildInfoLocation = nonOptionArgs.get(2);
logger.debug("Loading build-info from " + buildInfoLocation);
byte[] content = Files.readAllBytes(new File(buildInfoLocation).toPath());
BuildInfoResponse buildInfoResponse = this.objectMapper.readValue(content, BuildInfoResponse.class);
BuildInfo buildInfo = buildInfoResponse.getBuildInfo();
logger.debug("Loading build info:");
for (Module module : buildInfo.getModules()) {
logger.debug(module.getId());
for (Artifact artifact : module.getArtifacts()) {
logger.debug(artifact.getSha256() + " " + artifact.getName());
}
}
ReleaseInfo releaseInfo = ReleaseInfo.from(buildInfo);
Set<String> artifactDigests = buildInfo.getArtifactDigests(this::isIncluded);
this.artifactoryService.distribute(type.getRepo(), releaseInfo, artifactDigests);
}
private boolean isIncluded(Artifact artifact) {
String path = artifact.getName();
for (Pattern optionalDeployment : this.optionalDeployments) {
if (optionalDeployment.matcher(path).matches()) {
return false;
}
}
return true;
}
}
/*
* Copyright 2020-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 io.spring.concourse.releasescripts.command;
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Distribution properties.
*
* @author Phillip Webb
*/
@ConfigurationProperties(prefix = "distribute")
public class DistributeProperties {
private List<String> optionalDeployments = new ArrayList<>();
public List<String> getOptionalDeployments() {
return this.optionalDeployments;
}
public void setOptionalDeployments(List<String> optionalDeployments) {
this.optionalDeployments = optionalDeployments;
}
}
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 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.
......@@ -24,7 +24,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import io.spring.concourse.releasescripts.ReleaseInfo;
import io.spring.concourse.releasescripts.ReleaseType;
import io.spring.concourse.releasescripts.artifactory.payload.BuildInfoResponse;
import io.spring.concourse.releasescripts.bintray.BintrayService;
import io.spring.concourse.releasescripts.artifactory.payload.BuildInfoResponse.BuildInfo;
import io.spring.concourse.releasescripts.sonatype.SonatypeService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -33,47 +34,46 @@ import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
/**
* Command used to add attributes to the gradle plugin.
* Command used to publish a release to Maven Central.
*
* @author Madhura Bhave
* @author Andy Wilkinson
*/
@Component
public class PublishGradlePlugin implements Command {
public class PublishToCentralCommand implements Command {
private static final Logger logger = LoggerFactory.getLogger(PublishGradlePlugin.class);
private static final Logger logger = LoggerFactory.getLogger(PublishToCentralCommand.class);
private static final String PUBLISH_GRADLE_PLUGIN_COMMAND = "publishGradlePlugin";
private final BintrayService service;
private final SonatypeService sonatype;
private final ObjectMapper objectMapper;
public PublishGradlePlugin(BintrayService service, ObjectMapper objectMapper) {
this.service = service;
public PublishToCentralCommand(SonatypeService sonatype, ObjectMapper objectMapper) {
this.sonatype = sonatype;
this.objectMapper = objectMapper;
}
@Override
public String getName() {
return PUBLISH_GRADLE_PLUGIN_COMMAND;
return "publishToCentral";
}
@Override
public void run(ApplicationArguments args) throws Exception {
logger.debug("Running 'publish gradle' command");
List<String> nonOptionArgs = args.getNonOptionArgs();
Assert.state(!nonOptionArgs.isEmpty(), "No command argument specified");
Assert.state(nonOptionArgs.size() == 3, "Release type or build info not specified");
Assert.state(nonOptionArgs.size() == 4,
"Release type, build info location, or artifacts location not specified");
String releaseType = nonOptionArgs.get(1);
ReleaseType type = ReleaseType.from(releaseType);
if (!ReleaseType.RELEASE.equals(type)) {
return;
}
String buildInfoLocation = nonOptionArgs.get(2);
logger.debug("Loading build-info from " + buildInfoLocation);
byte[] content = Files.readAllBytes(new File(buildInfoLocation).toPath());
BuildInfoResponse buildInfoResponse = this.objectMapper.readValue(content, BuildInfoResponse.class);
ReleaseInfo releaseInfo = ReleaseInfo.from(buildInfoResponse.getBuildInfo());
this.service.publishGradlePlugin(releaseInfo);
BuildInfo buildInfo = buildInfoResponse.getBuildInfo();
String artifactsLocation = nonOptionArgs.get(3);
this.sonatype.publish(ReleaseInfo.from(buildInfo), new File(artifactsLocation).toPath());
}
}
/*
* Copyright 2012-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 io.spring.concourse.releasescripts.command;
import java.io.File;
import java.nio.file.Files;
import java.util.List;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.spring.concourse.releasescripts.ReleaseInfo;
import io.spring.concourse.releasescripts.ReleaseType;
import io.spring.concourse.releasescripts.artifactory.payload.BuildInfoResponse;
import io.spring.concourse.releasescripts.bintray.BintrayService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
/**
* Command used to sync artifacts to Maven Central.
*
* @author Madhura Bhave
*/
@Component
public class SyncToCentralCommand implements Command {
private static final Logger logger = LoggerFactory.getLogger(SyncToCentralCommand.class);
private static final String SYNC_TO_CENTRAL_COMMAND = "syncToCentral";
private final BintrayService service;
private final ObjectMapper objectMapper;
public SyncToCentralCommand(BintrayService service, ObjectMapper objectMapper) {
this.service = service;
this.objectMapper = objectMapper;
}
@Override
public String getName() {
return SYNC_TO_CENTRAL_COMMAND;
}
@Override
public void run(ApplicationArguments args) throws Exception {
logger.debug("Running 'sync to central' command");
List<String> nonOptionArgs = args.getNonOptionArgs();
Assert.state(!nonOptionArgs.isEmpty(), "No command argument specified");
Assert.state(nonOptionArgs.size() == 3, "Release type or build info not specified");
String releaseType = nonOptionArgs.get(1);
ReleaseType type = ReleaseType.from(releaseType);
if (!ReleaseType.RELEASE.equals(type)) {
return;
}
String buildInfoLocation = nonOptionArgs.get(2);
byte[] content = Files.readAllBytes(new File(buildInfoLocation).toPath());
BuildInfoResponse buildInfoResponse = this.objectMapper.readValue(content, BuildInfoResponse.class);
ReleaseInfo releaseInfo = ReleaseInfo.from(buildInfoResponse.getBuildInfo());
this.service.syncToMavenCentral(releaseInfo);
}
}
/*
* Copyright 2021 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 io.spring.concourse.releasescripts.sonatype;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.core.io.PathResource;
/**
* Collects artifacts to be deployed.
*
* @author Andy Wilkinson
*/
class ArtifactCollector {
private final Predicate<Path> excludeFilter;
ArtifactCollector(List<String> exclude) {
this.excludeFilter = excludeFilter(exclude);
}
private Predicate<Path> excludeFilter(List<String> exclude) {
Predicate<String> patternFilter = exclude.stream().map(Pattern::compile).map(Pattern::asPredicate)
.reduce((path) -> false, Predicate::or).negate();
return (path) -> patternFilter.test(path.toString());
}
Collection<DeployableArtifact> collectArtifacts(Path root) {
try (Stream<Path> artifacts = Files.walk(root)) {
return artifacts.filter(Files::isRegularFile).filter(this.excludeFilter)
.map((artifact) -> deployableArtifact(artifact, root)).collect(Collectors.toList());
}
catch (IOException ex) {
throw new RuntimeException("Could not read artifacts from '" + root + "'");
}
}
private DeployableArtifact deployableArtifact(Path artifact, Path root) {
return new DeployableArtifact(new PathResource(artifact), root.relativize(artifact).toString());
}
}
/*
* Copyright 2020-2020 the original author or authors.
* Copyright 2012-2021 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.
......@@ -14,25 +14,32 @@
* limitations under the License.
*/
package io.spring.concourse.releasescripts.bintray;
package io.spring.concourse.releasescripts.sonatype;
import org.springframework.core.io.Resource;
/**
* Details for a single packaged file.
* An artifact that can be deployed.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
public class PackageFile {
class DeployableArtifact {
private final Resource resource;
private String name;
private final String path;
private String sha256;
DeployableArtifact(Resource resource, String path) {
this.resource = resource;
this.path = path;
}
public String getName() {
return this.name;
Resource getResource() {
return this.resource;
}
public String getSha256() {
return this.sha256;
String getPath() {
return this.path;
}
}
}
\ No newline at end of file
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2021 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.
......@@ -16,6 +16,10 @@
package io.spring.concourse.releasescripts.sonatype;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
......@@ -34,6 +38,32 @@ public class SonatypeProperties {
@JsonProperty("password")
private String passwordToken;
/**
* URL of the Nexus instance used to publish releases.
*/
private String url;
/**
* ID of the staging profile used to publish releases.
*/
private String stagingProfileId;
/**
* Time between requests made to determine if the closing of a staging repository has
* completed.
*/
private Duration pollingInterval = Duration.ofSeconds(15);
/**
* Number of threads used to upload artifacts to the staging repository.
*/
private int uploadThreads = 8;
/**
* Regular expression patterns of artifacts to exclude
*/
private List<String> exclude = new ArrayList<>();
public String getUserToken() {
return this.userToken;
}
......@@ -50,4 +80,44 @@ public class SonatypeProperties {
this.passwordToken = passwordToken;
}
public String getUrl() {
return this.url;
}
public void setUrl(String url) {
this.url = url;
}
public String getStagingProfileId() {
return this.stagingProfileId;
}
public void setStagingProfileId(String stagingProfileId) {
this.stagingProfileId = stagingProfileId;
}
public Duration getPollingInterval() {
return this.pollingInterval;
}
public void setPollingInterval(Duration pollingInterval) {
this.pollingInterval = pollingInterval;
}
public int getUploadThreads() {
return this.uploadThreads;
}
public void setUploadThreads(int uploadThreads) {
this.uploadThreads = uploadThreads;
}
public List<String> getExclude() {
return this.exclude;
}
public void setExclude(List<String> exclude) {
this.exclude = exclude;
}
}
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 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.
......@@ -16,7 +16,28 @@
package io.spring.concourse.releasescripts.sonatype;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonCreator.Mode;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.spring.concourse.releasescripts.ReleaseInfo;
import org.apache.logging.log4j.util.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -32,23 +53,39 @@ import org.springframework.web.client.RestTemplate;
* Central class for interacting with Sonatype.
*
* @author Madhura Bhave
* @author Andy Wilkinson
*/
@Component
public class SonatypeService {
private static final Logger logger = LoggerFactory.getLogger(SonatypeService.class);
private static final String SONATYPE_REPOSITORY_URI = "https://oss.sonatype.org/service/local/repositories/releases/content/org/springframework/boot/spring-boot/";
private static final String NEXUS_REPOSITORY_PATH = "/service/local/repositories/releases/content/org/springframework/boot/spring-boot/";
private static final String NEXUS_STAGING_PATH = "/service/local/staging/";
private final ArtifactCollector artifactCollector;
private final RestTemplate restTemplate;
private final String stagingProfileId;
private final Duration pollingInterval;
private final int threads;
public SonatypeService(RestTemplateBuilder builder, SonatypeProperties sonatypeProperties) {
String username = sonatypeProperties.getUserToken();
String password = sonatypeProperties.getPasswordToken();
if (StringUtils.hasLength(username)) {
builder = builder.basicAuthentication(username, password);
}
this.restTemplate = builder.build();
this.restTemplate = builder.rootUri(sonatypeProperties.getUrl()).build();
this.stagingProfileId = sonatypeProperties.getStagingProfileId();
this.pollingInterval = sonatypeProperties.getPollingInterval();
this.threads = sonatypeProperties.getUploadThreads();
this.artifactCollector = new ArtifactCollector(sonatypeProperties.getExclude());
}
/**
......@@ -59,7 +96,7 @@ public class SonatypeService {
public boolean artifactsPublished(ReleaseInfo releaseInfo) {
try {
ResponseEntity<?> entity = this.restTemplate
.getForEntity(String.format(SONATYPE_REPOSITORY_URI + "%s/spring-boot-%s.jar.sha1",
.getForEntity(String.format(NEXUS_REPOSITORY_PATH + "%s/spring-boot-%s.jar.sha1",
releaseInfo.getVersion(), releaseInfo.getVersion()), byte[].class);
if (HttpStatus.OK.equals(entity.getStatusCode())) {
logger.info("Already published to Sonatype.");
......@@ -72,4 +109,200 @@ public class SonatypeService {
return false;
}
/**
* Publishes the release by creating a staging repository and deploying to it the
* artifacts at the given {@code artifactsRoot}. The repository is then closed and,
* upon successfully closure, it is released.
* @param releaseInfo the release information
* @param artifactsRoot the root directory of the artifacts to stage
*/
public void publish(ReleaseInfo releaseInfo, Path artifactsRoot) {
logger.info("Creating staging repository");
String buildId = releaseInfo.getBuildNumber();
String repositoryId = createStagingRepository(buildId);
Collection<DeployableArtifact> artifacts = this.artifactCollector.collectArtifacts(artifactsRoot);
logger.info("Staging repository {} created. Deploying {} artifacts", repositoryId, artifacts.size());
deploy(artifacts, repositoryId);
logger.info("Deploy complete. Closing staging repository");
close(repositoryId);
logger.info("Staging repository closed");
release(repositoryId, buildId);
logger.info("Staging repository released");
}
private String createStagingRepository(String buildId) {
Map<String, Object> body = new HashMap<>();
body.put("data", Collections.singletonMap("description", buildId));
PromoteResponse response = this.restTemplate.postForObject(
String.format(NEXUS_STAGING_PATH + "profiles/%s/start", this.stagingProfileId), body,
PromoteResponse.class);
String repositoryId = response.data.stagedRepositoryId;
return repositoryId;
}
private void deploy(Collection<DeployableArtifact> artifacts, String repositoryId) {
ExecutorService executor = Executors.newFixedThreadPool(this.threads);
try {
CompletableFuture.allOf(artifacts.stream()
.map((artifact) -> CompletableFuture.runAsync(() -> deploy(artifact, repositoryId), executor))
.toArray(CompletableFuture[]::new)).get(60, TimeUnit.MINUTES);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted during artifact deploy");
}
catch (ExecutionException ex) {
throw new RuntimeException("Deploy failed", ex);
}
catch (TimeoutException ex) {
throw new RuntimeException("Deploy timed out", ex);
}
finally {
executor.shutdown();
}
}
private void deploy(DeployableArtifact deployableArtifact, String repositoryId) {
try {
this.restTemplate.put(
NEXUS_STAGING_PATH + "deployByRepositoryId/" + repositoryId + "/" + deployableArtifact.getPath(),
deployableArtifact.getResource());
logger.info("Deloyed {}", deployableArtifact.getPath());
}
catch (HttpClientErrorException ex) {
logger.error("Failed to deploy {}. Error response: {}", deployableArtifact.getPath(),
ex.getResponseBodyAsString());
throw ex;
}
}
private void close(String stagedRepositoryId) {
Map<String, Object> body = new HashMap<>();
body.put("data", Collections.singletonMap("stagedRepositoryId", stagedRepositoryId));
this.restTemplate.postForEntity(String.format(NEXUS_STAGING_PATH + "profiles/%s/finish", this.stagingProfileId),
body, Void.class);
logger.info("Close requested. Awaiting result");
while (true) {
StagingRepository repository = this.restTemplate
.getForObject(NEXUS_STAGING_PATH + "repository/" + stagedRepositoryId, StagingRepository.class);
if (!repository.transitioning) {
if ("open".equals(repository.type)) {
logFailures(stagedRepositoryId);
throw new RuntimeException("Close failed");
}
return;
}
try {
Thread.sleep(this.pollingInterval.toMillis());
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted while waiting for staging repository to close", ex);
}
}
}
private void logFailures(String stagedRepositoryId) {
try {
StagingRepositoryActivity[] activities = this.restTemplate.getForObject(
NEXUS_STAGING_PATH + "repository/" + stagedRepositoryId + "/activity",
StagingRepositoryActivity[].class);
List<String> failureMessages = Stream.of(activities).flatMap((activity) -> activity.events.stream())
.filter((event) -> event.severity > 0).flatMap((event) -> event.properties.stream())
.filter((property) -> "failureMessage".equals(property.name))
.map((property) -> " " + property.value).collect(Collectors.toList());
if (failureMessages.isEmpty()) {
logger.error("Close failed for unknown reasons");
}
logger.error("Close failed:\n{}", Strings.join(failureMessages, '\n'));
}
catch (Exception ex) {
logger.error("Failed to determine causes of close failure", ex);
}
}
private void release(String stagedRepositoryId, String buildId) {
Map<String, Object> data = new HashMap<>();
data.put("stagedRepositoryIds", Arrays.asList(stagedRepositoryId));
data.put("description", "Releasing " + buildId);
data.put("autoDropAfterRelease", true);
Map<String, Object> body = Collections.singletonMap("data", data);
this.restTemplate.postForEntity(NEXUS_STAGING_PATH + "bulk/promote", body, Void.class);
}
private static final class PromoteResponse {
private final Data data;
@JsonCreator(mode = Mode.PROPERTIES)
private PromoteResponse(@JsonProperty("data") Data data) {
this.data = data;
}
private static final class Data {
private final String stagedRepositoryId;
@JsonCreator(mode = Mode.PROPERTIES)
Data(@JsonProperty("stagedRepositoryId") String stagedRepositoryId) {
this.stagedRepositoryId = stagedRepositoryId;
}
}
}
private static final class StagingRepository {
private final String type;
private final boolean transitioning;
private StagingRepository(String type, boolean transitioning) {
this.type = type;
this.transitioning = transitioning;
}
}
private static final class StagingRepositoryActivity {
private final List<Event> events;
@JsonCreator
private StagingRepositoryActivity(@JsonProperty("events") List<Event> events) {
this.events = events;
}
private static class Event {
private final List<Property> properties;
private final int severity;
@JsonCreator
public Event(@JsonProperty("name") String name, @JsonProperty("properties") List<Property> properties,
@JsonProperty("severity") int severity) {
this.properties = properties;
this.severity = severity;
}
private static class Property {
private final String name;
private final String value;
@JsonCreator
private Property(@JsonProperty("name") String name, @JsonProperty("value") String value) {
this.name = name;
this.value = value;
}
}
}
}
}
spring.main.banner-mode=off
distribute.optional-deployments[0]=.*\\.zip
distribute.optional-deployments[1]=spring-boot-project-\\d+\\.\\d+\\.\\d+(?:\\.RELEASE)?\\.pom
sonatype.exclude[0]=build-info\\.json
sonatype.exclude[1]=org/springframework/boot/spring-boot-docs/.*
logging.level.io.spring.concourse=DEBUG
\ No newline at end of file
......@@ -16,20 +16,13 @@
package io.spring.concourse.releasescripts.artifactory;
import java.time.Duration;
import java.util.Collections;
import java.util.Set;
import io.spring.concourse.releasescripts.ReleaseInfo;
import io.spring.concourse.releasescripts.bintray.BintrayService;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
......@@ -40,13 +33,6 @@ import org.springframework.util.Base64Utils;
import org.springframework.web.client.HttpClientErrorException;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.content;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.header;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
......@@ -63,14 +49,9 @@ import static org.springframework.test.web.client.response.MockRestResponseCreat
@EnableConfigurationProperties(ArtifactoryProperties.class)
class ArtifactoryServiceTests {
private static final Duration TIMEOUT = Duration.ofMinutes(60);
@Autowired
private ArtifactoryService service;
@MockBean
private BintrayService bintrayService;
@Autowired
private ArtifactoryProperties properties;
......@@ -127,68 +108,6 @@ class ArtifactoryServiceTests {
this.server.verify();
}
@Test
@SuppressWarnings("unchecked")
void distributeWhenSuccessful() throws Exception {
ReleaseInfo releaseInfo = getReleaseInfo();
given(this.bintrayService.isDistributionStarted(eq(releaseInfo))).willReturn(false);
given(this.bintrayService.isDistributionComplete(eq(releaseInfo), (Set<String>) any(), any())).willReturn(true);
this.server.expect(requestTo("https://repo.spring.io/api/build/distribute/example-build/example-build-1"))
.andExpect(method(HttpMethod.POST))
.andExpect(content().json(
"{\"sourceRepos\": [\"libs-release-local\"], \"targetRepo\" : \"spring-distributions\", \"async\":\"true\"}"))
.andExpect(header("Authorization", "Basic " + Base64Utils.encodeToString(String
.format("%s:%s", this.properties.getUsername(), this.properties.getPassword()).getBytes())))
.andExpect(header("Content-Type", MediaType.APPLICATION_JSON.toString())).andRespond(withSuccess());
Set<String> artifactDigests = Collections.singleton("602e20176706d3cc7535f01ffdbe91b270ae5014");
this.service.distribute("libs-release-local", releaseInfo, artifactDigests);
this.server.verify();
InOrder ordered = inOrder(this.bintrayService);
ordered.verify(this.bintrayService).isDistributionComplete(releaseInfo, artifactDigests, TIMEOUT);
}
@Test
void distributeWhenFailure() throws Exception {
ReleaseInfo releaseInfo = getReleaseInfo();
this.server.expect(requestTo("https://repo.spring.io/api/build/distribute/example-build/example-build-1"))
.andExpect(method(HttpMethod.POST))
.andExpect(content().json(
"{\"sourceRepos\": [\"libs-release-local\"], \"targetRepo\" : \"spring-distributions\", \"async\":\"true\"}"))
.andExpect(header("Authorization", "Basic " + Base64Utils.encodeToString(String
.format("%s:%s", this.properties.getUsername(), this.properties.getPassword()).getBytes())))
.andExpect(header("Content-Type", MediaType.APPLICATION_JSON.toString()))
.andRespond(withStatus(HttpStatus.FORBIDDEN));
Set<String> artifactDigests = Collections.singleton("602e20176706d3cc7535f01ffdbe91b270ae5014");
assertThatExceptionOfType(HttpClientErrorException.class)
.isThrownBy(() -> this.service.distribute("libs-release-local", releaseInfo, artifactDigests));
this.server.verify();
verify(this.bintrayService, times(1)).isDistributionStarted(releaseInfo);
verifyNoMoreInteractions(this.bintrayService);
}
@Test
@SuppressWarnings("unchecked")
void distributeWhenGettingPackagesTimesOut() throws Exception {
ReleaseInfo releaseInfo = getReleaseInfo();
given(this.bintrayService.isDistributionComplete(eq(releaseInfo), (Set<String>) any(), any()))
.willReturn(false);
given(this.bintrayService.isDistributionComplete(eq(releaseInfo), (Set<String>) any(), any()))
.willReturn(false);
this.server.expect(requestTo("https://repo.spring.io/api/build/distribute/example-build/example-build-1"))
.andExpect(method(HttpMethod.POST))
.andExpect(content().json(
"{\"sourceRepos\": [\"libs-release-local\"], \"targetRepo\" : \"spring-distributions\", \"async\":\"true\"}"))
.andExpect(header("Authorization", "Basic " + Base64Utils.encodeToString(String
.format("%s:%s", this.properties.getUsername(), this.properties.getPassword()).getBytes())))
.andExpect(header("Content-Type", MediaType.APPLICATION_JSON.toString())).andRespond(withSuccess());
Set<String> artifactDigests = Collections.singleton("602e20176706d3cc7535f01ffdbe91b270ae5014");
assertThatExceptionOfType(DistributionTimeoutException.class)
.isThrownBy(() -> this.service.distribute("libs-release-local", releaseInfo, artifactDigests));
this.server.verify();
InOrder ordered = inOrder(this.bintrayService);
ordered.verify(this.bintrayService).isDistributionComplete(releaseInfo, artifactDigests, TIMEOUT);
}
private ReleaseInfo getReleaseInfo() {
ReleaseInfo releaseInfo = new ReleaseInfo();
releaseInfo.setBuildName("example-build");
......
/*
* Copyright 2012-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 io.spring.concourse.releasescripts.bintray;
import java.time.Duration;
import java.util.LinkedHashSet;
import java.util.Set;
import io.spring.concourse.releasescripts.ReleaseInfo;
import io.spring.concourse.releasescripts.sonatype.SonatypeProperties;
import io.spring.concourse.releasescripts.sonatype.SonatypeService;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.test.web.client.ExpectedCount;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.test.web.client.response.DefaultResponseCreator;
import org.springframework.util.Base64Utils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.content;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.header;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
/**
* Tests for {@link BintrayService}.
*
* @author Madhura Bhave
*/
@RestClientTest(BintrayService.class)
@EnableConfigurationProperties({ BintrayProperties.class, SonatypeProperties.class })
class BintrayServiceTests {
@Autowired
private BintrayService service;
@Autowired
private BintrayProperties properties;
@Autowired
private SonatypeProperties sonatypeProperties;
@MockBean
private SonatypeService sonatypeService;
@Autowired
private MockRestServiceServer server;
@AfterEach
void tearDown() {
this.server.reset();
}
@Test
void isDistributionComplete() throws Exception {
setupGetPackageFiles(1, "no-package-files.json");
setupGetPackageFiles(1, "some-package-files.json");
setupGetPackageFiles(1, "all-package-files.json");
Set<String> digests = new LinkedHashSet<>();
digests.add("602e20176706d3cc7535f01ffdbe91b270ae5012");
digests.add("602e20176706d3cc7535f01ffdbe91b270ae5013");
digests.add("602e20176706d3cc7535f01ffdbe91b270ae5014");
assertThat(this.service.isDistributionComplete(getReleaseInfo(), digests, Duration.ofMinutes(1), Duration.ZERO))
.isTrue();
this.server.verify();
}
private void setupGetPackageFiles(int includeUnpublished, String path) {
this.server
.expect(requestTo(String.format(
"https://api.bintray.com/packages/%s/%s/%s/versions/%s/files?include_unpublished=%s",
this.properties.getSubject(), this.properties.getRepo(), "example", "1.1.0.RELEASE",
includeUnpublished)))
.andExpect(method(HttpMethod.GET))
.andExpect(header("Authorization", "Basic " + Base64Utils.encodeToString(
String.format("%s:%s", this.properties.getUsername(), this.properties.getApiKey()).getBytes())))
.andRespond(withJsonFrom(path));
}
@Test
void publishGradlePluginWhenSuccessful() {
this.server
.expect(requestTo(String.format("https://api.bintray.com/packages/%s/%s/%s/versions/%s/attributes",
this.properties.getSubject(), this.properties.getRepo(), "example", "1.1.0.RELEASE")))
.andExpect(method(HttpMethod.POST))
.andExpect(content().json(
"[ { \"name\": \"gradle-plugin\", \"values\": [\"org.springframework.boot:org.springframework.boot:spring-boot-gradle-plugin\"] } ]"))
.andExpect(header("Authorization", "Basic " + Base64Utils.encodeToString(
String.format("%s:%s", this.properties.getUsername(), this.properties.getApiKey()).getBytes())))
.andExpect(header("Content-Type", MediaType.APPLICATION_JSON.toString())).andRespond(withSuccess());
this.service.publishGradlePlugin(getReleaseInfo());
this.server.verify();
}
@Test
void syncToMavenCentralWhenSuccessful() {
ReleaseInfo releaseInfo = getReleaseInfo();
given(this.sonatypeService.artifactsPublished(releaseInfo)).willReturn(false);
this.server
.expect(requestTo(String.format("https://api.bintray.com/maven_central_sync/%s/%s/%s/versions/%s",
this.properties.getSubject(), this.properties.getRepo(), "example", "1.1.0.RELEASE")))
.andExpect(method(HttpMethod.POST))
.andExpect(content().json(String.format("{\"username\": \"%s\", \"password\": \"%s\"}",
this.sonatypeProperties.getUserToken(), this.sonatypeProperties.getPasswordToken())))
.andExpect(header("Authorization", "Basic " + Base64Utils.encodeToString(
String.format("%s:%s", this.properties.getUsername(), this.properties.getApiKey()).getBytes())))
.andExpect(header("Content-Type", MediaType.APPLICATION_JSON.toString())).andRespond(withSuccess());
this.service.syncToMavenCentral(releaseInfo);
this.server.verify();
}
@Test
void syncToMavenCentralWhenArtifactsAlreadyPublished() {
ReleaseInfo releaseInfo = getReleaseInfo();
given(this.sonatypeService.artifactsPublished(releaseInfo)).willReturn(true);
this.server.expect(ExpectedCount.never(),
requestTo(String.format("https://api.bintray.com/maven_central_sync/%s/%s/%s/versions/%s",
this.properties.getSubject(), this.properties.getRepo(), "example", "1.1.0.RELEASE")));
this.service.syncToMavenCentral(releaseInfo);
this.server.verify();
}
private ReleaseInfo getReleaseInfo() {
ReleaseInfo releaseInfo = new ReleaseInfo();
releaseInfo.setBuildName("example-build");
releaseInfo.setBuildNumber("example-build-1");
releaseInfo.setGroupId("example");
releaseInfo.setVersion("1.1.0.RELEASE");
return releaseInfo;
}
private DefaultResponseCreator withJsonFrom(String path) {
return withSuccess(getClassPathResource(path), MediaType.APPLICATION_JSON);
}
private ClassPathResource getClassPathResource(String path) {
return new ClassPathResource(path, getClass());
}
}
\ No newline at end of file
/*
* Copyright 2012-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 io.spring.concourse.releasescripts.command;
import java.util.Arrays;
import java.util.Collections;
import org.junit.jupiter.api.Test;
import org.springframework.boot.DefaultApplicationArguments;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link CommandProcessor}.
*
* @author Madhura Bhave
*/
class CommandProcessorTests {
private static final String[] NO_ARGS = {};
@Test
void runWhenNoArgumentThrowsException() {
CommandProcessor processor = new CommandProcessor(Collections.singletonList(mock(Command.class)));
assertThatIllegalStateException().isThrownBy(() -> processor.run(new DefaultApplicationArguments(NO_ARGS)))
.withMessage("No command argument specified");
}
@Test
void runWhenUnknownCommandThrowsException() {
Command fooCommand = mock(Command.class);
given(fooCommand.getName()).willReturn("foo");
CommandProcessor processor = new CommandProcessor(Collections.singletonList(fooCommand));
DefaultApplicationArguments args = new DefaultApplicationArguments(new String[] { "bar", "go" });
assertThatIllegalStateException().isThrownBy(() -> processor.run(args)).withMessage("Unknown command 'bar'");
}
@Test
void runDelegatesToCommand() throws Exception {
Command fooCommand = mock(Command.class);
given(fooCommand.getName()).willReturn("foo");
Command barCommand = mock(Command.class);
given(barCommand.getName()).willReturn("bar");
CommandProcessor processor = new CommandProcessor(Arrays.asList(fooCommand, barCommand));
DefaultApplicationArguments args = new DefaultApplicationArguments(new String[] { "bar", "go" });
processor.run(args);
verify(fooCommand, never()).run(any());
verify(barCommand).run(args);
}
}
\ No newline at end of file
/*
* Copyright 2012-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 io.spring.concourse.releasescripts.command;
import java.util.Arrays;
import java.util.Set;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.spring.concourse.releasescripts.ReleaseInfo;
import io.spring.concourse.releasescripts.ReleaseType;
import io.spring.concourse.releasescripts.artifactory.ArtifactoryService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.DefaultApplicationArguments;
import org.springframework.core.io.ClassPathResource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
/**
* Tests for {@link DistributeCommand}.
*
* @author Madhura Bhave
* @author Phillip Webb
*/
class DistributeCommandTests {
@Mock
private ArtifactoryService service;
private DistributeCommand command;
private ObjectMapper objectMapper;
@BeforeEach
void setup() {
MockitoAnnotations.initMocks(this);
DistributeProperties distributeProperties = new DistributeProperties();
distributeProperties.setOptionalDeployments(Arrays.asList(".*\\.zip", "demo-\\d\\.\\d\\.\\d\\.doc"));
this.objectMapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
this.command = new DistributeCommand(this.service, this.objectMapper, distributeProperties);
}
@Test
void distributeWhenReleaseTypeNotSpecifiedShouldThrowException() {
Assertions.assertThatIllegalStateException()
.isThrownBy(() -> this.command.run(new DefaultApplicationArguments("distribute")));
}
@Test
void distributeWhenReleaseTypeMilestoneShouldDoNothing() throws Exception {
this.command.run(new DefaultApplicationArguments("distribute", "M", getBuildInfoLocation()));
verifyNoInteractions(this.service);
}
@Test
void distributeWhenReleaseTypeRCShouldDoNothing() throws Exception {
this.command.run(new DefaultApplicationArguments("distribute", "RC", getBuildInfoLocation()));
verifyNoInteractions(this.service);
}
@Test
@SuppressWarnings("unchecked")
void distributeWhenReleaseTypeReleaseShouldCallService() throws Exception {
ArgumentCaptor<ReleaseInfo> releaseInfoCaptor = ArgumentCaptor.forClass(ReleaseInfo.class);
ArgumentCaptor<Set<String>> artifactDigestCaptor = ArgumentCaptor.forClass(Set.class);
this.command.run(new DefaultApplicationArguments("distribute", "RELEASE", getBuildInfoLocation()));
verify(this.service).distribute(eq(ReleaseType.RELEASE.getRepo()), releaseInfoCaptor.capture(),
artifactDigestCaptor.capture());
ReleaseInfo releaseInfo = releaseInfoCaptor.getValue();
assertThat(releaseInfo.getBuildName()).isEqualTo("example");
assertThat(releaseInfo.getBuildNumber()).isEqualTo("example-build-1");
assertThat(releaseInfo.getGroupId()).isEqualTo("org.example.demo");
assertThat(releaseInfo.getVersion()).isEqualTo("2.2.0");
Set<String> artifactDigests = artifactDigestCaptor.getValue();
assertThat(artifactDigests).containsExactly("aaaaaaaaa85f5c5093721f3ed0edda8ff8290yyyyyyyyyy");
}
@Test
@SuppressWarnings("unchecked")
void distributeWhenReleaseTypeReleaseAndFilteredShouldCallService() throws Exception {
ArgumentCaptor<ReleaseInfo> releaseInfoCaptor = ArgumentCaptor.forClass(ReleaseInfo.class);
ArgumentCaptor<Set<String>> artifactDigestCaptor = ArgumentCaptor.forClass(Set.class);
this.command.run(new DefaultApplicationArguments("distribute", "RELEASE",
getBuildInfoLocation("filtered-build-info-response.json")));
verify(this.service).distribute(eq(ReleaseType.RELEASE.getRepo()), releaseInfoCaptor.capture(),
artifactDigestCaptor.capture());
ReleaseInfo releaseInfo = releaseInfoCaptor.getValue();
assertThat(releaseInfo.getBuildName()).isEqualTo("example");
assertThat(releaseInfo.getBuildNumber()).isEqualTo("example-build-1");
assertThat(releaseInfo.getGroupId()).isEqualTo("org.example.demo");
assertThat(releaseInfo.getVersion()).isEqualTo("2.2.0");
Set<String> artifactDigests = artifactDigestCaptor.getValue();
assertThat(artifactDigests).containsExactly("aaaaaaaaa85f5c5093721f3ed0edda8ff8290yyyyyyyyyy");
}
private String getBuildInfoLocation() throws Exception {
return getBuildInfoLocation("build-info-response.json");
}
private String getBuildInfoLocation(String file) throws Exception {
return new ClassPathResource(file, ArtifactoryService.class).getFile().getAbsolutePath();
}
}
\ No newline at end of file
/*
* Copyright 2012-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 io.spring.concourse.releasescripts.command;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.spring.concourse.releasescripts.ReleaseInfo;
import io.spring.concourse.releasescripts.ReleaseType;
import io.spring.concourse.releasescripts.artifactory.ArtifactoryService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.DefaultApplicationArguments;
import org.springframework.core.io.ClassPathResource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
/**
* @author Madhura Bhave
*/
class PromoteCommandTests {
@Mock
private ArtifactoryService service;
private PromoteCommand command;
private ObjectMapper objectMapper;
@BeforeEach
void setup() {
MockitoAnnotations.initMocks(this);
this.objectMapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
this.command = new PromoteCommand(this.service, this.objectMapper);
}
@Test
void runWhenReleaseTypeNotSpecifiedShouldThrowException() {
assertThatIllegalStateException()
.isThrownBy(() -> this.command.run(new DefaultApplicationArguments("promote")));
}
@Test
void runWhenReleaseTypeMilestoneShouldCallService() throws Exception {
this.command.run(new DefaultApplicationArguments("promote", "M", getBuildInfoLocation()));
verify(this.service).promote(eq(ReleaseType.MILESTONE.getRepo()), any(ReleaseInfo.class));
}
@Test
void runWhenReleaseTypeRCShouldCallService() throws Exception {
this.command.run(new DefaultApplicationArguments("promote", "RC", getBuildInfoLocation()));
verify(this.service).promote(eq(ReleaseType.RELEASE_CANDIDATE.getRepo()), any(ReleaseInfo.class));
}
@Test
void runWhenReleaseTypeReleaseShouldCallService() throws Exception {
this.command.run(new DefaultApplicationArguments("promote", "RELEASE", getBuildInfoLocation()));
verify(this.service).promote(eq(ReleaseType.RELEASE.getRepo()), any(ReleaseInfo.class));
}
@Test
void runWhenBuildInfoNotSpecifiedShouldThrowException() {
assertThatIllegalStateException()
.isThrownBy(() -> this.command.run(new DefaultApplicationArguments("promote", "M")));
}
@Test
void runShouldParseBuildInfoProperly() throws Exception {
ArgumentCaptor<ReleaseInfo> captor = ArgumentCaptor.forClass(ReleaseInfo.class);
this.command.run(new DefaultApplicationArguments("promote", "RELEASE", getBuildInfoLocation()));
verify(this.service).promote(eq(ReleaseType.RELEASE.getRepo()), captor.capture());
ReleaseInfo releaseInfo = captor.getValue();
assertThat(releaseInfo.getBuildName()).isEqualTo("example");
assertThat(releaseInfo.getBuildNumber()).isEqualTo("example-build-1");
assertThat(releaseInfo.getGroupId()).isEqualTo("org.example.demo");
assertThat(releaseInfo.getVersion()).isEqualTo("2.2.0");
}
private String getBuildInfoLocation() throws Exception {
return new ClassPathResource("build-info-response.json", ArtifactoryService.class).getFile().getAbsolutePath();
}
}
\ No newline at end of file
/*
* Copyright 2012-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 io.spring.concourse.releasescripts.command;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.spring.concourse.releasescripts.ReleaseInfo;
import io.spring.concourse.releasescripts.artifactory.ArtifactoryService;
import io.spring.concourse.releasescripts.bintray.BintrayService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.DefaultApplicationArguments;
import org.springframework.core.io.ClassPathResource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
/**
* Tests for {@link PublishGradlePlugin}.
*
* @author Madhura Bhave
*/
class PublishGradlePluginTests {
@Mock
private BintrayService service;
private PublishGradlePlugin command;
private ObjectMapper objectMapper;
@BeforeEach
void setup() {
MockitoAnnotations.initMocks(this);
this.objectMapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
this.command = new PublishGradlePlugin(this.service, objectMapper);
}
@Test
void runWhenReleaseTypeNotSpecifiedShouldThrowException() throws Exception {
Assertions.assertThatIllegalStateException()
.isThrownBy(() -> this.command.run(new DefaultApplicationArguments("publishGradlePlugin")));
}
@Test
void runWhenReleaseTypeMilestoneShouldDoNothing() throws Exception {
this.command.run(new DefaultApplicationArguments("publishGradlePlugin", "M", getBuildInfoLocation()));
verifyNoInteractions(this.service);
}
@Test
void runWhenReleaseTypeRCShouldDoNothing() throws Exception {
this.command.run(new DefaultApplicationArguments("publishGradlePlugin", "RC", getBuildInfoLocation()));
verifyNoInteractions(this.service);
}
@Test
void runWhenReleaseTypeReleaseShouldCallService() throws Exception {
ArgumentCaptor<ReleaseInfo> captor = ArgumentCaptor.forClass(ReleaseInfo.class);
this.command.run(new DefaultApplicationArguments("promote", "RELEASE", getBuildInfoLocation()));
verify(this.service).publishGradlePlugin(captor.capture());
ReleaseInfo releaseInfo = captor.getValue();
assertThat(releaseInfo.getBuildName()).isEqualTo("example");
assertThat(releaseInfo.getBuildNumber()).isEqualTo("example-build-1");
assertThat(releaseInfo.getGroupId()).isEqualTo("org.example.demo");
assertThat(releaseInfo.getVersion()).isEqualTo("2.2.0");
}
private String getBuildInfoLocation() throws Exception {
return new ClassPathResource("build-info-response.json", ArtifactoryService.class).getFile().getAbsolutePath();
}
}
\ No newline at end of file
/*
* Copyright 2012-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 io.spring.concourse.releasescripts.command;
import io.spring.concourse.releasescripts.sdkman.SdkmanService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.DefaultApplicationArguments;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
/**
* Tests for {@link PublishToSdkmanCommand}.
*
* @author Madhura Bhave
*/
class PublishToSdkmanCommandTests {
@Mock
private SdkmanService service;
private PublishToSdkmanCommand command;
@BeforeEach
void setup() {
MockitoAnnotations.initMocks(this);
this.command = new PublishToSdkmanCommand(this.service);
}
@Test
void runWhenReleaseTypeNotSpecifiedShouldThrowException() throws Exception {
Assertions.assertThatIllegalStateException()
.isThrownBy(() -> this.command.run(new DefaultApplicationArguments("publishToSdkman")));
}
@Test
void runWhenVersionNotSpecifiedShouldThrowException() throws Exception {
Assertions.assertThatIllegalStateException()
.isThrownBy(() -> this.command.run(new DefaultApplicationArguments("publishToSdkman", "RELEASE")));
}
@Test
void runWhenReleaseTypeMilestoneShouldDoNothing() throws Exception {
this.command.run(new DefaultApplicationArguments("publishToSdkman", "M", "1.2.3"));
verifyNoInteractions(this.service);
}
@Test
void runWhenReleaseTypeRCShouldDoNothing() throws Exception {
this.command.run(new DefaultApplicationArguments("publishToSdkman", "RC", "1.2.3"));
verifyNoInteractions(this.service);
}
@Test
void runWhenLatestGANotSpecifiedShouldCallServiceWithMakeDefaultFalse() throws Exception {
DefaultApplicationArguments args = new DefaultApplicationArguments("promote", "RELEASE", "1.2.3");
testRun(args, false);
}
@Test
void runWhenReleaseTypeReleaseShouldCallService() throws Exception {
DefaultApplicationArguments args = new DefaultApplicationArguments("promote", "RELEASE", "1.2.3", "true");
testRun(args, true);
}
private void testRun(DefaultApplicationArguments args, boolean makeDefault) throws Exception {
ArgumentCaptor<String> versionCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<Boolean> makeDefaultCaptor = ArgumentCaptor.forClass(Boolean.class);
this.command.run(args);
verify(this.service).publish(versionCaptor.capture(), makeDefaultCaptor.capture());
String version = versionCaptor.getValue();
Boolean makeDefaultValue = makeDefaultCaptor.getValue();
assertThat(version).isEqualTo("1.2.3");
assertThat(makeDefaultValue).isEqualTo(makeDefault);
}
}
/*
* Copyright 2012-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 io.spring.concourse.releasescripts.command;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.spring.concourse.releasescripts.ReleaseInfo;
import io.spring.concourse.releasescripts.artifactory.ArtifactoryService;
import io.spring.concourse.releasescripts.bintray.BintrayService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.DefaultApplicationArguments;
import org.springframework.core.io.ClassPathResource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
/**
* Tests for {@link SyncToCentralCommand}.
*
* @author Madhura Bhave
*/
class SyncToCentralCommandTests {
@Mock
private BintrayService service;
private SyncToCentralCommand command;
private ObjectMapper objectMapper;
@BeforeEach
void setup() {
MockitoAnnotations.initMocks(this);
this.objectMapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
this.command = new SyncToCentralCommand(this.service, objectMapper);
}
@Test
void runWhenReleaseTypeNotSpecifiedShouldThrowException() throws Exception {
Assertions.assertThatIllegalStateException()
.isThrownBy(() -> this.command.run(new DefaultApplicationArguments("syncToCentral")));
}
@Test
void runWhenReleaseTypeMilestoneShouldDoNothing() throws Exception {
this.command.run(new DefaultApplicationArguments("syncToCentral", "M", getBuildInfoLocation()));
verifyNoInteractions(this.service);
}
@Test
void runWhenReleaseTypeRCShouldDoNothing() throws Exception {
this.command.run(new DefaultApplicationArguments("syncToCentral", "RC", getBuildInfoLocation()));
verifyNoInteractions(this.service);
}
@Test
void runWhenReleaseTypeReleaseShouldCallService() throws Exception {
ArgumentCaptor<ReleaseInfo> captor = ArgumentCaptor.forClass(ReleaseInfo.class);
this.command.run(new DefaultApplicationArguments("syncToCentral", "RELEASE", getBuildInfoLocation()));
verify(this.service).syncToMavenCentral(captor.capture());
ReleaseInfo releaseInfo = captor.getValue();
assertThat(releaseInfo.getBuildName()).isEqualTo("example");
assertThat(releaseInfo.getBuildNumber()).isEqualTo("example-build-1");
assertThat(releaseInfo.getGroupId()).isEqualTo("org.example.demo");
assertThat(releaseInfo.getVersion()).isEqualTo("2.2.0");
}
private String getBuildInfoLocation() throws Exception {
return new ClassPathResource("build-info-response.json", ArtifactoryService.class).getFile().getAbsolutePath();
}
}
\ No newline at end of file
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 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.
......@@ -16,6 +16,17 @@
package io.spring.concourse.releasescripts.sonatype;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import io.spring.concourse.releasescripts.ReleaseInfo;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
......@@ -23,11 +34,20 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.test.web.client.ExpectedCount;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.test.web.client.RequestMatcher;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.hamcrest.Matchers.equalTo;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.header;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.jsonPath;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
......@@ -38,7 +58,7 @@ import static org.springframework.test.web.client.response.MockRestResponseCreat
*
* @author Madhura Bhave
*/
@RestClientTest(SonatypeService.class)
@RestClientTest(components = SonatypeService.class, properties = "sonatype.url=https://nexus.example.org")
@EnableConfigurationProperties(SonatypeProperties.class)
class SonatypeServiceTests {
......@@ -56,9 +76,9 @@ class SonatypeServiceTests {
@Test
void artifactsPublishedWhenPublishedShouldReturnTrue() {
this.server.expect(requestTo(String.format(
"https://oss.sonatype.org/service/local/repositories/releases/content/org/springframework/boot/spring-boot/%s/spring-boot-%s.jar.sha1",
"/service/local/repositories/releases/content/org/springframework/boot/spring-boot/%s/spring-boot-%s.jar.sha1",
"1.1.0.RELEASE", "1.1.0.RELEASE"))).andExpect(method(HttpMethod.GET))
.andRespond(withSuccess().body("ce8d8b6838ecceb68962b975b18682f4237ccf71".getBytes()));
.andRespond(withSuccess().body("ce8d8b6838ecceb68962b9150b18682f4237ccf71".getBytes()));
boolean published = this.service.artifactsPublished(getReleaseInfo());
assertThat(published).isTrue();
this.server.verify();
......@@ -67,7 +87,7 @@ class SonatypeServiceTests {
@Test
void artifactsPublishedWhenNotPublishedShouldReturnFalse() {
this.server.expect(requestTo(String.format(
"https://oss.sonatype.org/service/local/repositories/releases/content/org/springframework/boot/spring-boot/%s/spring-boot-%s.jar.sha1",
"/service/local/repositories/releases/content/org/springframework/boot/spring-boot/%s/spring-boot-%s.jar.sha1",
"1.1.0.RELEASE", "1.1.0.RELEASE"))).andExpect(method(HttpMethod.GET))
.andRespond(withStatus(HttpStatus.NOT_FOUND));
boolean published = this.service.artifactsPublished(getReleaseInfo());
......@@ -75,6 +95,102 @@ class SonatypeServiceTests {
this.server.verify();
}
@Test
void publishWithSuccessfulClose() throws IOException {
this.server.expect(requestTo("/service/local/staging/profiles/1a2b3c4d/start"))
.andExpect(method(HttpMethod.POST)).andExpect(header("Content-Type", "application/json"))
.andExpect(header("Accept", "application/json, application/*+json"))
.andExpect(jsonPath("$.data.description").value("example-build-1"))
.andRespond(withStatus(HttpStatus.CREATED).contentType(MediaType.APPLICATION_JSON).body(
"{\"data\":{\"stagedRepositoryId\":\"example-6789\", \"description\":\"example-build\"}}"));
Path artifactsRoot = new File("src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo")
.toPath();
try (Stream<Path> artifacts = Files.walk(artifactsRoot)) {
Set<RequestMatcher> uploads = artifacts.filter(Files::isRegularFile)
.map((artifact) -> artifactsRoot.relativize(artifact))
.filter((artifact) -> !artifact.startsWith("build-info.json"))
.map((artifact) -> requestTo(
"/service/local/staging/deployByRepositoryId/example-6789/" + artifact.toString()))
.collect(Collectors.toCollection(HashSet::new));
AnyOfRequestMatcher uploadRequestsMatcher = anyOf(uploads);
assertThat(uploadRequestsMatcher.candidates).hasSize(150);
this.server.expect(ExpectedCount.times(150), uploadRequestsMatcher).andExpect(method(HttpMethod.PUT))
.andRespond(withSuccess());
this.server.expect(requestTo("/service/local/staging/profiles/1a2b3c4d/finish"))
.andExpect(method(HttpMethod.POST)).andExpect(header("Content-Type", "application/json"))
.andExpect(header("Accept", "application/json, application/*+json"))
.andRespond(withStatus(HttpStatus.CREATED));
this.server.expect(ExpectedCount.times(2), requestTo("/service/local/staging/repository/example-6789"))
.andExpect(method(HttpMethod.GET))
.andExpect(header("Accept", "application/json, application/*+json"))
.andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON)
.body("{\"type\":\"open\", \"transitioning\":true}"));
this.server.expect(requestTo("/service/local/staging/repository/example-6789"))
.andExpect(method(HttpMethod.GET))
.andExpect(header("Accept", "application/json, application/*+json"))
.andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON)
.body("{\"type\":\"closed\", \"transitioning\":false}"));
this.server.expect(requestTo("/service/local/staging/bulk/promote")).andExpect(method(HttpMethod.POST))
.andExpect(header("Content-Type", "application/json"))
.andExpect(header("Accept", "application/json, application/*+json"))
.andExpect(jsonPath("$.data.description").value("Releasing example-build-1"))
.andExpect(jsonPath("$.data.autoDropAfterRelease").value(true))
.andExpect(jsonPath("$.data.stagedRepositoryIds").value(equalTo(Arrays.asList("example-6789"))))
.andRespond(withSuccess());
this.service.publish(getReleaseInfo(), artifactsRoot);
this.server.verify();
assertThat(uploadRequestsMatcher.candidates).hasSize(0);
}
}
@Test
void publishWithCloseFailureDueToRuleViolations() throws IOException {
this.server.expect(requestTo("/service/local/staging/profiles/1a2b3c4d/start"))
.andExpect(method(HttpMethod.POST)).andExpect(header("Content-Type", "application/json"))
.andExpect(header("Accept", "application/json, application/*+json"))
.andExpect(jsonPath("$.data.description").value("example-build-1"))
.andRespond(withStatus(HttpStatus.CREATED).contentType(MediaType.APPLICATION_JSON).body(
"{\"data\":{\"stagedRepositoryId\":\"example-6789\", \"description\":\"example-build\"}}"));
Path artifactsRoot = new File("src/test/resources/io/spring/concourse/releasescripts/sonatype/artifactory-repo")
.toPath();
try (Stream<Path> artifacts = Files.walk(artifactsRoot)) {
Set<RequestMatcher> uploads = artifacts.filter(Files::isRegularFile)
.map((artifact) -> artifactsRoot.relativize(artifact))
.filter((artifact) -> !"build-info.json".equals(artifact.toString()))
.map((artifact) -> requestTo(
"/service/local/staging/deployByRepositoryId/example-6789/" + artifact.toString()))
.collect(Collectors.toCollection(HashSet::new));
AnyOfRequestMatcher uploadRequestsMatcher = anyOf(uploads);
assertThat(uploadRequestsMatcher.candidates).hasSize(150);
this.server.expect(ExpectedCount.times(150), uploadRequestsMatcher).andExpect(method(HttpMethod.PUT))
.andRespond(withSuccess());
this.server.expect(requestTo("/service/local/staging/profiles/1a2b3c4d/finish"))
.andExpect(method(HttpMethod.POST)).andExpect(header("Content-Type", "application/json"))
.andExpect(header("Accept", "application/json, application/*+json"))
.andRespond(withStatus(HttpStatus.CREATED));
this.server.expect(ExpectedCount.times(2), requestTo("/service/local/staging/repository/example-6789"))
.andExpect(method(HttpMethod.GET))
.andExpect(header("Accept", "application/json, application/*+json"))
.andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON)
.body("{\"type\":\"open\", \"transitioning\":true}"));
this.server.expect(requestTo("/service/local/staging/repository/example-6789"))
.andExpect(method(HttpMethod.GET))
.andExpect(header("Accept", "application/json, application/*+json"))
.andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON)
.body("{\"type\":\"open\", \"transitioning\":false}"));
this.server.expect(requestTo("/service/local/staging/repository/example-6789/activity"))
.andExpect(method(HttpMethod.GET))
.andExpect(header("Accept", "application/json, application/*+json"))
.andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON).body(new FileSystemResource(
new File("src/test/resources/io/spring/concourse/releasescripts/sonatype/activity.json"))));
assertThatExceptionOfType(RuntimeException.class)
.isThrownBy(() -> this.service.publish(getReleaseInfo(), artifactsRoot))
.withMessage("Close failed");
this.server.verify();
assertThat(uploadRequestsMatcher.candidates).hasSize(0);
}
}
private ReleaseInfo getReleaseInfo() {
ReleaseInfo releaseInfo = new ReleaseInfo();
releaseInfo.setBuildName("example-build");
......@@ -84,4 +200,39 @@ class SonatypeServiceTests {
return releaseInfo;
}
}
\ No newline at end of file
private AnyOfRequestMatcher anyOf(Set<RequestMatcher> candidates) {
return new AnyOfRequestMatcher(candidates);
}
private static class AnyOfRequestMatcher implements RequestMatcher {
private final Object monitor = new Object();
private final Set<RequestMatcher> candidates;
private AnyOfRequestMatcher(Set<RequestMatcher> candidates) {
this.candidates = candidates;
}
@Override
public void match(ClientHttpRequest request) throws IOException, AssertionError {
synchronized (this.monitor) {
Iterator<RequestMatcher> iterator = this.candidates.iterator();
while (iterator.hasNext()) {
try {
iterator.next().match(request);
iterator.remove();
return;
}
catch (AssertionError ex) {
// Continue
}
}
throw new AssertionError(
"No matching request matcher was found for request to '" + request.getURI() + "'");
}
}
}
}
......@@ -9,6 +9,8 @@ bintray:
sonatype:
user-token: sonatype-user
password-token: sonatype-password
polling-interval: 1s
staging-profile-id: 1a2b3c4d
sdkman:
consumer-key: sdkman-consumer-key
consumer-token: sdkman-consumer-token
[
{
"name": "nutcracker-1.1-sources.jar",
"path": "org/jfrog/powerutils/nutcracker/1.1/nutcracker-1.1-sources.jar",
"package": "jfrog-power-utils",
"version": "1.1",
"repo": "jfrog-jars",
"owner": "jfrog",
"created": "ISO8601 (yyyy-MM-dd'T'HH:mm:ss.SSSZ)",
"size": 1234,
"sha256": "602e20176706d3cc7535f01ffdbe91b270ae5012"
},
{
"name": "nutcracker-1.1.pom",
"path": "org/jfrog/powerutils/nutcracker/1.1/nutcracker-1.1.pom",
"package": "jfrog-power-utils",
"version": "1.1",
"repo": "jfrog-jars",
"owner": "jfrog",
"created": "ISO8601 (yyyy-MM-dd'T'HH:mm:ss.SSSZ)",
"size": 1234,
"sha256": "602e20176706d3cc7535f01ffdbe91b270ae5013"
},
{
"name": "nutcracker-1.1.jar",
"path": "org/jfrog/powerutils/nutcracker/1.1/nutcracker-1.1.jar",
"package": "jfrog-power-utils",
"version": "1.1",
"repo": "jfrog-jars",
"owner": "jfrog",
"created": "ISO8601 (yyyy-MM-dd'T'HH:mm:ss.SSSZ)",
"size": 1234,
"sha256": "602e20176706d3cc7535f01ffdbe91b270ae5014"
}
]
\ No newline at end of file
[
{
"name": "nutcracker-1.1-sources.jar",
"path": "org/jfrog/powerutils/nutcracker/1.1/nutcracker-1.1-sources.jar",
"package": "jfrog-power-utils",
"version": "1.1",
"repo": "jfrog-jars",
"owner": "jfrog",
"created": "ISO8601 (yyyy-MM-dd'T'HH:mm:ss.SSSZ)",
"size": 1234,
"sha256": "602e20176706d3cc7535f01ffdbe91b270ae5012"
}
]
\ No newline at end of file
[
{
"events": [
{
"name": "repositoryCreated",
"properties": [
{
"name": "id",
"value": "orgspringframework-7161"
},
{
"name": "user",
"value": "user"
},
{
"name": "ip",
"value": "127.0.0.1"
}
],
"severity": 0,
"timestamp": "2021-02-08T14:31:13.523Z"
}
],
"name": "open",
"started": "2021-02-08T14:31:00.662Z",
"stopped": "2021-02-08T14:31:14.855Z"
},
{
"events": [
{
"name": "rulesEvaluate",
"properties": [
{
"name": "id",
"value": "5e9e8e6f8d20a3"
},
{
"name": "rule",
"value": "no-traversal-paths-in-archive-file"
},
{
"name": "rule",
"value": "profile-target-matching-staging"
},
{
"name": "rule",
"value": "sbom-report"
},
{
"name": "rule",
"value": "checksum-staging"
},
{
"name": "rule",
"value": "javadoc-staging"
},
{
"name": "rule",
"value": "pom-staging"
},
{
"name": "rule",
"value": "signature-staging"
},
{
"name": "rule",
"value": "sources-staging"
}
],
"severity": 0,
"timestamp": "2021-02-08T14:31:37.327Z"
},
{
"name": "ruleEvaluate",
"properties": [
{
"name": "typeId",
"value": "no-traversal-paths-in-archive-file"
}
],
"severity": 0,
"timestamp": "2021-02-08T14:31:41.254Z"
},
{
"name": "rulePassed",
"properties": [
{
"name": "typeId",
"value": "no-traversal-paths-in-archive-file"
}
],
"severity": 0,
"timestamp": "2021-02-08T14:31:47.498Z"
},
{
"name": "ruleEvaluate",
"properties": [
{
"name": "typeId",
"value": "javadoc-staging"
}
],
"severity": 0,
"timestamp": "2021-02-08T14:31:53.438Z"
},
{
"name": "rulePassed",
"properties": [
{
"name": "typeId",
"value": "javadoc-staging"
}
],
"severity": 0,
"timestamp": "2021-02-08T14:31:54.623Z"
},
{
"name": "ruleEvaluate",
"properties": [
{
"name": "typeId",
"value": "pom-staging"
}
],
"severity": 0,
"timestamp": "2021-02-08T14:31:58.091Z"
},
{
"name": "ruleFailed",
"properties": [
{
"name": "typeId",
"value": "pom-staging"
},
{
"name": "failureMessage",
"value": "Invalid POM: /org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom: Project name missing, Project description missing, Project URL missing, License information missing, SCM URL missing, Developer information missing"
},
{
"name": "failureMessage",
"value": "Invalid POM: /org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom: Project name missing, Project description missing, Project URL missing, License information missing, SCM URL missing, Developer information missing"
},
{
"name": "failureMessage",
"value": "Invalid POM: /org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom: Project name missing, Project description missing, Project URL missing, License information missing, SCM URL missing, Developer information missing"
}
],
"severity": 1,
"timestamp": "2021-02-08T14:31:59.403Z"
},
{
"name": "ruleEvaluate",
"properties": [
{
"name": "typeId",
"value": "profile-target-matching-staging"
}
],
"severity": 0,
"timestamp": "2021-02-08T14:32:05.322Z"
},
{
"name": "rulePassed",
"properties": [
{
"name": "typeId",
"value": "profile-target-matching-staging"
}
],
"severity": 0,
"timestamp": "2021-02-08T14:32:06.492Z"
},
{
"name": "ruleEvaluate",
"properties": [
{
"name": "typeId",
"value": "checksum-staging"
}
],
"severity": 0,
"timestamp": "2021-02-08T14:32:12.415Z"
},
{
"name": "rulePassed",
"properties": [
{
"name": "typeId",
"value": "checksum-staging"
}
],
"severity": 0,
"timestamp": "2021-02-08T14:32:13.568Z"
},
{
"name": "ruleEvaluate",
"properties": [
{
"name": "typeId",
"value": "signature-staging"
}
],
"severity": 0,
"timestamp": "2021-02-08T14:32:18.288Z"
},
{
"name": "ruleFailed",
"properties": [
{
"name": "typeId",
"value": "signature-staging"
},
{
"name": "failureMessage",
"value": "Missing Signature: '/org/springframework/example/module-one/1.0.0/module-one-1.0.0-javadoc.jar.asc' does not exist for 'module-one-1.0.0-javadoc.jar'."
},
{
"name": "failureMessage",
"value": "Missing Signature: '/org/springframework/example/module-one/1.0.0/module-one-1.0.0.jar.asc' does not exist for 'module-one-1.0.0.jar'."
},
{
"name": "failureMessage",
"value": "Missing Signature: '/org/springframework/example/module-one/1.0.0/module-one-1.0.0-sources.jar.asc' does not exist for 'module-one-1.0.0-sources.jar'."
},
{
"name": "failureMessage",
"value": "Missing Signature: '/org/springframework/example/module-one/1.0.0/module-one-1.0.0.module.asc' does not exist for 'module-one-1.0.0.module'."
},
{
"name": "failureMessage",
"value": "Missing Signature: '/org/springframework/example/module-one/1.0.0/module-one-1.0.0.pom.asc' does not exist for 'module-one-1.0.0.pom'."
},
{
"name": "failureMessage",
"value": "Missing Signature: '/org/springframework/example/module-two/1.0.0/module-two-1.0.0.module.asc' does not exist for 'module-two-1.0.0.module'."
},
{
"name": "failureMessage",
"value": "Missing Signature: '/org/springframework/example/module-two/1.0.0/module-two-1.0.0.pom.asc' does not exist for 'module-two-1.0.0.pom'."
},
{
"name": "failureMessage",
"value": "Missing Signature: '/org/springframework/example/module-two/1.0.0/module-two-1.0.0-sources.jar.asc' does not exist for 'module-two-1.0.0-sources.jar'."
},
{
"name": "failureMessage",
"value": "Missing Signature: '/org/springframework/example/module-two/1.0.0/module-two-1.0.0.jar.asc' does not exist for 'module-two-1.0.0.jar'."
},
{
"name": "failureMessage",
"value": "Missing Signature: '/org/springframework/example/module-two/1.0.0/module-two-1.0.0-javadoc.jar.asc' does not exist for 'module-two-1.0.0-javadoc.jar'."
},
{
"name": "failureMessage",
"value": "Missing Signature: '/org/springframework/example/module-three/1.0.0/module-three-1.0.0.module.asc' does not exist for 'module-three-1.0.0.module'."
},
{
"name": "failureMessage",
"value": "Missing Signature: '/org/springframework/example/module-three/1.0.0/module-three-1.0.0-javadoc.jar.asc' does not exist for 'module-three-1.0.0-javadoc.jar'."
},
{
"name": "failureMessage",
"value": "Missing Signature: '/org/springframework/example/module-three/1.0.0/module-three-1.0.0-sources.jar.asc' does not exist for 'module-three-1.0.0-sources.jar'."
},
{
"name": "failureMessage",
"value": "Missing Signature: '/org/springframework/example/module-three/1.0.0/module-three-1.0.0.jar.asc' does not exist for 'module-three-1.0.0.jar'."
},
{
"name": "failureMessage",
"value": "Missing Signature: '/org/springframework/example/module-three/1.0.0/module-three-1.0.0.pom.asc' does not exist for 'module-three-1.0.0.pom'."
}
],
"severity": 1,
"timestamp": "2021-02-08T14:32:19.443Z"
},
{
"name": "ruleEvaluate",
"properties": [
{
"name": "typeId",
"value": "sources-staging"
}
],
"severity": 0,
"timestamp": "2021-02-08T14:32:24.175Z"
},
{
"name": "rulePassed",
"properties": [
{
"name": "typeId",
"value": "sources-staging"
}
],
"severity": 0,
"timestamp": "2021-02-08T14:32:28.940Z"
},
{
"name": "ruleEvaluate",
"properties": [
{
"name": "typeId",
"value": "sbom-report"
}
],
"severity": 0,
"timestamp": "2021-02-08T14:32:34.906Z"
},
{
"name": "rulePassed",
"properties": [
{
"name": "typeId",
"value": "sbom-report"
},
{
"name": "successMessage",
"value": "Successfully requested SBOM report"
}
],
"severity": 0,
"timestamp": "2021-02-08T14:32:36.520Z"
},
{
"name": "rulesFailed",
"properties": [
{
"name": "id",
"value": "5e9e8e6f8d20a3"
},
{
"name": "failureCount",
"value": "2"
}
],
"severity": 1,
"timestamp": "2021-02-08T14:32:42.068Z"
},
{
"name": "repositoryCloseFailed",
"properties": [
{
"name": "id",
"value": "orgspringframework-7161"
},
{
"name": "cause",
"value": "com.sonatype.nexus.staging.StagingRulesFailedException: One or more rules have failed"
}
],
"severity": 1,
"timestamp": "2021-02-08T14:32:43.218Z"
}
],
"name": "close",
"started": "2021-02-08T14:31:34.943Z",
"startedByIpAddress": "127.0.0.1",
"startedByUserId": "user",
"stopped": "2021-02-08T14:32:47.138Z"
}
]
{
"buildInfo": {
"version": "1.0.1",
"name": "example",
"number": "example-build-1",
"started": "2019-09-10T12:18:05.430+0000",
"durationMillis": 0,
"artifactoryPrincipal": "user",
"url": "https://my-ci.com",
"modules": [
{
"id": "org.example.demo:demo:2.2.0",
"artifacts": [
{
"type": "jar",
"sha1": "ayyyya9151a22cb3145538e523dbbaaaaaaaa",
"sha256": "aaaaaaaaa85f5c5093721f3ed0edda8ff8290yyyyyyyyyy",
"md5": "aaaaaacddea1724b0b69d8yyyyyyy",
"name": "demo-2.2.0.jar"
}
]
}
],
"statuses": [
{
"status": "staged",
"repository": "libs-release-local",
"timestamp": "2019-09-10T12:42:24.716+0000",
"user": "user",
"timestampDate": 1568119344716
}
]
},
"uri": "https://my-artifactory-repo.com/api/build/example/example-build-1"
}
\ No newline at end of file
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT3qtgf8CeDvxzi7lPghlrZtmYTTjaic4FZsGRWTPky3H24i4wSRDhG0L5sj4uPK
eLLlITr5a9j26UCas9HRSthiC8+EgIMAhSN0X482SQhUZHAW67ErIvaHlwL+ixMD
0T5pmsW8PKN3lV1TFMhNYSEC2GRG/4GF+3yQA8LR+BgeEu/E5nmysIH8vuQMkOD6
3pKA8VKNBml591j6UTqxoHtPX+rThaziz3Hy3+ekf5iWslllTTGPd2SWqTvnj2Ae
GvRzsbli+FEM0Aj/v8jUQnQzOz891QSvWR+fMfCqZimiJMc+GBzJ9umbcyQsB5tY
e26mAoYd9KEpGXMKN4biHbJZNp1GGw==
=x/MY
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT1PSwf+InTdlh+BVVy3RFszEL5vN7waSPYMImXhgd5EZ1d4Lxd6EeOwtNgWNKpG
E+Ps/Kw0jZDvoD49WJlcUjzDHBNHcE7C/L3GAWHV6WwklhtQaJ4EegsynWdSXz6k
fqJY6r58aGKGjpKPutRWAjvfcdC170+ZRsc2oi9xrAgHCpvXzTjq4+O9Ah0t5jwW
jcZ/Xubcw4vjsw774OucHbtwGsvRN5SDJ3IONOH8WCwhUP5vEEKvA6MYX0KGoTdS
3wTCyZTzU3qtTWxcbTCpiJIWbYwRR7TzLB/uydWHlAMzuz6coIiBpYsGiO6wkmfg
W+QvcE7wyW2jtb22pCImLyObyZ21VA==
=VjDv
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT2HQgf+MTUEnwzXK4zi76VI7C5cchGan26lIA2Ebq4vtYGKDqNSISOAAdWs9+nT
U6ZA6OIFo5qdeD6F/45s91IoDbxbhMDMFEsSijKASqiuZN5TZM1U2h2kWFAl/sEl
EI1RTygn+xDw/ah4V3/duuMFC+jRgvJ/LgemIF4KBvECWaTQKNu0fu5d4dPXMpp+
jrxMEZPQZsivpOvklzV8O7wAkf/ZQhJdcB2m8uOfSPlJ91a4EEtXF9/GzzkXUi1P
bzt4NsmOag3227B3mO1Bc6yZdDBNu8wQ9apiJVCpqsxB9Dz0PCL4dHNa1u9g6Xo6
ElRgneV4HZp+LB125VoNabKuNH00bw==
=2yDl
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT0ScQf7Bip+quFq1CzDCTDxUhdTTOIpQcCfMKo1Jegpa2Hlm63XuK+6zVI9u6S+
dBXPdYeneMOqMPQUAaw3M06ZqohaDOt8Ti1EiQFGXCdOvbonTA52Lrd4EEZxwNnK
BdPuIh/8qCfozm5KbZe1bFyGVRAdNyf27KvzHgfBTirLtI+3MiOdL4bvNZbWRPfh
J84Ko+Ena8jPFgyz6nJv2Q2U/V3dCooLJAXs2vEG6owwk5J9zvSysWpHaJbXas5v
KXO9TOBBjf3+vxb1WVQa8ZYUU3+FIFes0RFVgOWghJXIooOcWrwOV2Q8z9qWXwoK
mMZ2oLS+z/7clXibK45KeRUeCX5DvQ==
=5oO1
-----END PGP SIGNATURE-----
2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00
\ No newline at end of file
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT3d+AgAwQvlwnKQLLuiZADGL+I922/YG317N2Re1EjC6WlMRZUKXH54fckRTyPm
4ZLyxVHy8LlUD2Q10g69opb7HRd/tV0miBJhn5OU1wIM3hqTgxNp9EFckK4md45k
osnhQJNDsFToxJL8zPP+KRs/aWPZs+FrRcH6k26lwLl2gTfyBDsaU11HFRVEN9yi
X41obVyKiVNlc9efSSvlLtRBSVt0VhAFhck+3t61H6D9H09QxaDGAqmduDua3Tg3
t5eqURuDfv3TfSztYgK3JBmG/6gVMsZodCgyC+8rhDDs6vSoDG30apx5Leg2rPbj
xuk2wi/WNzc94IgY9tVS3tAfT2k6yQ==
=6+Cv
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT3qtgf8CeDvxzi7lPghlrZtmYTTjaic4FZsGRWTPky3H24i4wSRDhG0L5sj4uPK
eLLlITr5a9j26UCas9HRSthiC8+EgIMAhSN0X482SQhUZHAW67ErIvaHlwL+ixMD
0T5pmsW8PKN3lV1TFMhNYSEC2GRG/4GF+3yQA8LR+BgeEu/E5nmysIH8vuQMkOD6
3pKA8VKNBml591j6UTqxoHtPX+rThaziz3Hy3+ekf5iWslllTTGPd2SWqTvnj2Ae
GvRzsbli+FEM0Aj/v8jUQnQzOz891QSvWR+fMfCqZimiJMc+GBzJ9umbcyQsB5tY
e26mAoYd9KEpGXMKN4biHbJZNp1GGw==
=x/MY
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT1PSwf+InTdlh+BVVy3RFszEL5vN7waSPYMImXhgd5EZ1d4Lxd6EeOwtNgWNKpG
E+Ps/Kw0jZDvoD49WJlcUjzDHBNHcE7C/L3GAWHV6WwklhtQaJ4EegsynWdSXz6k
fqJY6r58aGKGjpKPutRWAjvfcdC170+ZRsc2oi9xrAgHCpvXzTjq4+O9Ah0t5jwW
jcZ/Xubcw4vjsw774OucHbtwGsvRN5SDJ3IONOH8WCwhUP5vEEKvA6MYX0KGoTdS
3wTCyZTzU3qtTWxcbTCpiJIWbYwRR7TzLB/uydWHlAMzuz6coIiBpYsGiO6wkmfg
W+QvcE7wyW2jtb22pCImLyObyZ21VA==
=VjDv
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT2HQgf+MTUEnwzXK4zi76VI7C5cchGan26lIA2Ebq4vtYGKDqNSISOAAdWs9+nT
U6ZA6OIFo5qdeD6F/45s91IoDbxbhMDMFEsSijKASqiuZN5TZM1U2h2kWFAl/sEl
EI1RTygn+xDw/ah4V3/duuMFC+jRgvJ/LgemIF4KBvECWaTQKNu0fu5d4dPXMpp+
jrxMEZPQZsivpOvklzV8O7wAkf/ZQhJdcB2m8uOfSPlJ91a4EEtXF9/GzzkXUi1P
bzt4NsmOag3227B3mO1Bc6yZdDBNu8wQ9apiJVCpqsxB9Dz0PCL4dHNa1u9g6Xo6
ElRgneV4HZp+LB125VoNabKuNH00bw==
=2yDl
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT0ScQf7Bip+quFq1CzDCTDxUhdTTOIpQcCfMKo1Jegpa2Hlm63XuK+6zVI9u6S+
dBXPdYeneMOqMPQUAaw3M06ZqohaDOt8Ti1EiQFGXCdOvbonTA52Lrd4EEZxwNnK
BdPuIh/8qCfozm5KbZe1bFyGVRAdNyf27KvzHgfBTirLtI+3MiOdL4bvNZbWRPfh
J84Ko+Ena8jPFgyz6nJv2Q2U/V3dCooLJAXs2vEG6owwk5J9zvSysWpHaJbXas5v
KXO9TOBBjf3+vxb1WVQa8ZYUU3+FIFes0RFVgOWghJXIooOcWrwOV2Q8z9qWXwoK
mMZ2oLS+z/7clXibK45KeRUeCX5DvQ==
=5oO1
-----END PGP SIGNATURE-----
2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00
\ No newline at end of file
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT3d+AgAwQvlwnKQLLuiZADGL+I922/YG317N2Re1EjC6WlMRZUKXH54fckRTyPm
4ZLyxVHy8LlUD2Q10g69opb7HRd/tV0miBJhn5OU1wIM3hqTgxNp9EFckK4md45k
osnhQJNDsFToxJL8zPP+KRs/aWPZs+FrRcH6k26lwLl2gTfyBDsaU11HFRVEN9yi
X41obVyKiVNlc9efSSvlLtRBSVt0VhAFhck+3t61H6D9H09QxaDGAqmduDua3Tg3
t5eqURuDfv3TfSztYgK3JBmG/6gVMsZodCgyC+8rhDDs6vSoDG30apx5Leg2rPbj
xuk2wi/WNzc94IgY9tVS3tAfT2k6yQ==
=6+Cv
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT3qtgf8CeDvxzi7lPghlrZtmYTTjaic4FZsGRWTPky3H24i4wSRDhG0L5sj4uPK
eLLlITr5a9j26UCas9HRSthiC8+EgIMAhSN0X482SQhUZHAW67ErIvaHlwL+ixMD
0T5pmsW8PKN3lV1TFMhNYSEC2GRG/4GF+3yQA8LR+BgeEu/E5nmysIH8vuQMkOD6
3pKA8VKNBml591j6UTqxoHtPX+rThaziz3Hy3+ekf5iWslllTTGPd2SWqTvnj2Ae
GvRzsbli+FEM0Aj/v8jUQnQzOz891QSvWR+fMfCqZimiJMc+GBzJ9umbcyQsB5tY
e26mAoYd9KEpGXMKN4biHbJZNp1GGw==
=x/MY
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT1PSwf+InTdlh+BVVy3RFszEL5vN7waSPYMImXhgd5EZ1d4Lxd6EeOwtNgWNKpG
E+Ps/Kw0jZDvoD49WJlcUjzDHBNHcE7C/L3GAWHV6WwklhtQaJ4EegsynWdSXz6k
fqJY6r58aGKGjpKPutRWAjvfcdC170+ZRsc2oi9xrAgHCpvXzTjq4+O9Ah0t5jwW
jcZ/Xubcw4vjsw774OucHbtwGsvRN5SDJ3IONOH8WCwhUP5vEEKvA6MYX0KGoTdS
3wTCyZTzU3qtTWxcbTCpiJIWbYwRR7TzLB/uydWHlAMzuz6coIiBpYsGiO6wkmfg
W+QvcE7wyW2jtb22pCImLyObyZ21VA==
=VjDv
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT2HQgf+MTUEnwzXK4zi76VI7C5cchGan26lIA2Ebq4vtYGKDqNSISOAAdWs9+nT
U6ZA6OIFo5qdeD6F/45s91IoDbxbhMDMFEsSijKASqiuZN5TZM1U2h2kWFAl/sEl
EI1RTygn+xDw/ah4V3/duuMFC+jRgvJ/LgemIF4KBvECWaTQKNu0fu5d4dPXMpp+
jrxMEZPQZsivpOvklzV8O7wAkf/ZQhJdcB2m8uOfSPlJ91a4EEtXF9/GzzkXUi1P
bzt4NsmOag3227B3mO1Bc6yZdDBNu8wQ9apiJVCpqsxB9Dz0PCL4dHNa1u9g6Xo6
ElRgneV4HZp+LB125VoNabKuNH00bw==
=2yDl
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT0ScQf7Bip+quFq1CzDCTDxUhdTTOIpQcCfMKo1Jegpa2Hlm63XuK+6zVI9u6S+
dBXPdYeneMOqMPQUAaw3M06ZqohaDOt8Ti1EiQFGXCdOvbonTA52Lrd4EEZxwNnK
BdPuIh/8qCfozm5KbZe1bFyGVRAdNyf27KvzHgfBTirLtI+3MiOdL4bvNZbWRPfh
J84Ko+Ena8jPFgyz6nJv2Q2U/V3dCooLJAXs2vEG6owwk5J9zvSysWpHaJbXas5v
KXO9TOBBjf3+vxb1WVQa8ZYUU3+FIFes0RFVgOWghJXIooOcWrwOV2Q8z9qWXwoK
mMZ2oLS+z/7clXibK45KeRUeCX5DvQ==
=5oO1
-----END PGP SIGNATURE-----
2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00
\ No newline at end of file
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT3d+AgAwQvlwnKQLLuiZADGL+I922/YG317N2Re1EjC6WlMRZUKXH54fckRTyPm
4ZLyxVHy8LlUD2Q10g69opb7HRd/tV0miBJhn5OU1wIM3hqTgxNp9EFckK4md45k
osnhQJNDsFToxJL8zPP+KRs/aWPZs+FrRcH6k26lwLl2gTfyBDsaU11HFRVEN9yi
X41obVyKiVNlc9efSSvlLtRBSVt0VhAFhck+3t61H6D9H09QxaDGAqmduDua3Tg3
t5eqURuDfv3TfSztYgK3JBmG/6gVMsZodCgyC+8rhDDs6vSoDG30apx5Leg2rPbj
xuk2wi/WNzc94IgY9tVS3tAfT2k6yQ==
=6+Cv
-----END PGP SIGNATURE-----
{
"formatVersion": "1.1",
"component": {
"group": "org.springframework.example",
"module": "module-one",
"version": "1.0.0",
"attributes": {
"org.gradle.status": "release"
}
},
"createdBy": {
"gradle": {
"version": "6.5.1",
"buildId": "mvqepqsdqjcahjl7cii6b6ucoe"
}
},
"variants": [
{
"name": "apiElements",
"attributes": {
"org.gradle.category": "library",
"org.gradle.dependency.bundling": "external",
"org.gradle.jvm.version": 8,
"org.gradle.libraryelements": "jar",
"org.gradle.usage": "java-api"
},
"files": [
{
"name": "module-one-1.0.0.jar",
"url": "module-one-1.0.0.jar",
"size": 261,
"sha512": "2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00",
"sha256": "10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385",
"sha1": "8992b17455ce660da9c5fe47226b7ded9e872637",
"md5": "e84da489be91de821c95d41b8f0e0a0a"
}
]
},
{
"name": "runtimeElements",
"attributes": {
"org.gradle.category": "library",
"org.gradle.dependency.bundling": "external",
"org.gradle.jvm.version": 8,
"org.gradle.libraryelements": "jar",
"org.gradle.usage": "java-runtime"
},
"files": [
{
"name": "module-one-1.0.0.jar",
"url": "module-one-1.0.0.jar",
"size": 261,
"sha512": "2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00",
"sha256": "10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385",
"sha1": "8992b17455ce660da9c5fe47226b7ded9e872637",
"md5": "e84da489be91de821c95d41b8f0e0a0a"
}
]
},
{
"name": "javadocElements",
"attributes": {
"org.gradle.category": "documentation",
"org.gradle.dependency.bundling": "external",
"org.gradle.docstype": "javadoc",
"org.gradle.usage": "java-runtime"
},
"files": [
{
"name": "module-one-1.0.0-javadoc.jar",
"url": "module-one-1.0.0-javadoc.jar",
"size": 261,
"sha512": "2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00",
"sha256": "10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385",
"sha1": "8992b17455ce660da9c5fe47226b7ded9e872637",
"md5": "e84da489be91de821c95d41b8f0e0a0a"
}
]
},
{
"name": "sourcesElements",
"attributes": {
"org.gradle.category": "documentation",
"org.gradle.dependency.bundling": "external",
"org.gradle.docstype": "sources",
"org.gradle.usage": "java-runtime"
},
"files": [
{
"name": "module-one-1.0.0-sources.jar",
"url": "module-one-1.0.0-sources.jar",
"size": 261,
"sha512": "2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00",
"sha256": "10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385",
"sha1": "8992b17455ce660da9c5fe47226b7ded9e872637",
"md5": "e84da489be91de821c95d41b8f0e0a0a"
}
]
}
]
}
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT1HBQf/fCBHR+fpZjkcgonkAVWcGvRx5kRHlsCISs64XMw90++DTawoKxr9/TvY
fltQlq/xaf+2O2Xzh9HIymtZBeKp7a4fWQ2AHf/ygkGyIKvy8h+mu3MGDdmHZeA4
fn9FGjaE0a/wYJmCEHJ1qJ4GaNq47gzRTu76jzZNafnNRlq1rlyVu2txnlks6xDr
oE8EnRT86Y67Ku8YArjkhZSHhf/tzSSwdTAgBinh6eba5tW5ueRXfsheqgtpJMov
hiDIVxuAlJoHy2cQ8L9+8geg0OSXLwQ9BXrBsDCLvrDauU735/Hv/NGrWE95kemw
Ay9jCXhXFWKkzCw2ps3QHTTpTK4aVw==
=1QME
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT1SLQgApB6OWW9cgtaofOu3HwgsVxaxLYPsDf057m2O5pI6uV5Ikyt97B1txjTB
9EXcy4gsfu7rxwgRHIEPCQkQKhhZioscT1SPFN0yopCwsJEvxrJE018ojyaIem/L
KVcbtiBVMj3GZCbS0DHpwZNx2u7yblyBqUGhCMKLkYqVL7nUHJKtECECs5jbJnb9
xXGFe0xlZ/IbkHv5QXyStgUYCah7ayWQDvjN7UJrpJL1lmTD0rjWLilkeKsVu3/k
11cZb5YdOmrL9a+8ql1jXPkma3HPjoIPRC5LB2BnloduwEPsiiLGG7Cs8UFEJNjQ
m5w+l4dDd03y5ioaW8fI/meAKpBm4g==
=gwLM
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT2y5AgAlI4H5hwDIgVmXtRq/ri7kxEJnC9L9FOv8aE9YasHAruaU1YR5m17Jncl
4guJHc+gSd3BiSx1rsI6PNxLACabw4Vy56eCRpmiFWeIkoCETBUk8AN25Q/1tzgw
hHmIRgOkF9PzSBWDTUNsyx/7E9P2QSiJOkMAGGuMKGDpYTR9zmaluzwfY+BI/VoW
BbZpdzt02OGQosWmA7DlwkXUwip6iBjga79suUFIsyH0hmRW2q/nCeJ04ttzXUog
NTNkpEwMYpZAzQXE7ks7WJJlAPkVYPWy/j5YCV7xTFb9I/56ux+/wRUaGU5fumSR
lr3PNoYNToC/4GLX6Kc2OH0e1LXNTQ==
=s02D
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT1/vwgAhUTLKjxmry4W3cVdfX/D/vxDTLAp5OxwJy36CZmJwsVuN9TLjPo4tRqq
woiopR2oSTaJqld2pe98WlIeDJJRe4ta1Uwvg7k4Sf6YaZXm01Wufk4a835sFUwY
BTWmnFYX0+dp5mLyXZmZjrAr5Q2bowRuqZd2DAYiNY/E5MH2T7OAJE2hCOHUpCaB
JVeP7HcbaGYR3NX/mLq0t8+xjTPXQk/OHijuusuLQxfLZvZiaikDoOHUD6l0dlRw
xcLTghG5+jd1q7noKAbUVgoEOshstfomCHZpPMj11c7KIuG1+3wRMdm+F67lkcJ5
eDW2fmF+6LYr+WlEi33rDIyTk3GhlQ==
=mHUe
-----END PGP SIGNATURE-----
29b1bc06a150e4764826e35e2d2541933b0583ce823f5b00c02effad9f37f02f0d2eef1c81214d69eaf74220e1f77332c5e6a91eb413a3022b5a8a1d7914c4c3
\ No newline at end of file
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT0QLAf/ffTpTfH4IebklGJIKZC8ZjRt4CgwpR431qNeWkY25cHmWFj48x2u9dmS
ZpxN572d3PPjcMigT/9wM05omiU+4DHxGgHq/Xj6GXN1DNaENcu7uoye96thjKPv
jz98tPIRMC9hYr3m/K1CJ3+ZG0++7JorCZRpodH/MhklRWXOvNszs81VWtgvMnpd
h9r0PuoaYBl6bIl19o7E3JJU6dKgwfre4b+a1RSYI+A8bmJOKMgHytAKi+804r0P
4R2WuQT4q+dSmkMtgp65vJ9giv/xuFrd1bT4n+qcDkwE8pTcWvsB4w1RkDOKs4fK
/ta5xBQ1hiKAd6nJffke1b0MBrZOrA==
=ZMpE
-----END PGP SIGNATURE-----
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<!-- This module was also published with a richer model, Gradle metadata, -->
<!-- which should be used instead. Do not delete the following line which -->
<!-- is to indicate to Gradle or any Gradle module metadata file consumer -->
<!-- that they should prefer consuming it instead. -->
<!-- do_not_remove: published-with-gradle-metadata -->
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.example</groupId>
<artifactId>module-one</artifactId>
<version>1.0.0</version>
<name>module-one</name>
<description>Example module</description>
<url>https://spring.io/projects/spring-boot</url>
<organization>
<name>Pivotal Software, Inc.</name>
<url>https://spring.io</url>
</organization>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0</url>
</license>
</licenses>
<developers>
<developer>
<name>Pivotal</name>
<email>info@pivotal.io</email>
<organization>Pivotal Software, Inc.</organization>
<organizationUrl>https://www.spring.io</organizationUrl>
</developer>
</developers>
<scm>
<connection>
scm:git:git://github.com/spring-projects/spring-boot.git
</connection>
<developerConnection>
scm:git:ssh://git@github.com/spring-projects/spring-boot.git
</developerConnection>
<url>https://github.com/spring-projects/spring-boot</url>
</scm>
<issueManagement>
<system>GitHub</system>
<url>
https://github.com/spring-projects/spring-boot/issues
</url>
</issueManagement>
</project>
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT04rwgAwJHic8GGHFZ+UAJYLW/OxJOVyd0ebx4yT5zAyTjyvxnrlKmKZ6GP/NhZ
htJQnZez85lUKA0TsMvl/6H2iEhKOns6HgqY3PLFkKNRKOq601phtD9HCkxDibWB
UDT01I0q2xNOljD03lhfytefnSnZ96AaySol2v5DBIZsOKWGir0/8KJCpEQJHjCF
TwNk8lNF3moGlO4zUfoBbkSZ+J0J8Bq5QI3nIAWFYxHcrZ2YGsAZd48kux8x2V3C
c6QsYEonmztqxop76a7K8Gv+MDmo/u/vqM8z5C63/WpOoDtRG+F5vtPkhCrR6M5f
ygubQUy5TL+dWdHE8zgA2O9hZuoHEg==
=bkxG
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT0XEAf+O9a/29MIWBtj1oLxIT1LLdzTU68qt5+qW+58SNQmMxu0MaESW4GZOc3p
mTV0EJyxUkCLJyoqOY4/GhqBAm33mMZSY8BQtvUZPYxpbJwBo+pE8YfnH3n1v20P
4pS4oJKekXAhTqShpx5oFjCK4J3chaz+Xc8Ldm1DXakCRc1bc/YYZ+87sy2z+PXk
PmN3KPcc/XjH4GPjmVUR8vR1TGUjUMQGvbAdrgkjFyaCGNvyreuHLsAFWrFFbIOn
/mB++enkXhmjWbiyvmvWQvtU0QFA4sRGYww0Lup1GRQ+00IqHF1QRMskqujAwmok
+TuB3Zc9WuAERPre+Qr1DEevClNwAQ==
=3beu
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT2aVAf+MQhSBr1jzcQE5mo1bMOXa4owaRr+dRir5Q4Gr7Fz4NuGoQEIzoL7XP5r
0zIjebzworxCaw+JNyaIxniHNBeK3sPHTLeW8bCrdJLkhE9RtGdEHLyPYXwPuFin
xVw3VQHWiA0uPM+JaekgdPDtK5wGFQ/AK3pc6vR108oT0kV4zQEqgRnvLqV9Q5zZ
UPHBi5kypu1BmCW4upYL1dmjASWPn9Q8cNpHcX/NJPNJ9zW0yxAAtq4wLfh7PQml
3EaHEYllsf8v1vMv00+zZNhc6O4BBP1qrRiaYHDAJhJjn6ctV9GFhJ2Ttxh/NmSy
H679tlC2PeRjGMi8bOHBshcikn5KUw==
=4aJI
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT0nDQgAlfchq7/W/wubx3IR3tQs0tKiix3nZIc97zuH6sR8+r+CJe78wbmSE9Oo
/z96wfzeZYNIKh2v+dBLHF7OfcPGBE7tiX07jfCa6KzjjY3hFBhW+muMP/aBRb+4
itSs6F3lkZOPW2+hpSdFQ6U8Rm81cAlZv7Zk2XswwTQkJo8GcNL1w/5wAVpNK0yG
VinZr8YRMFs6OYQxLqGSypDLAmv9rOaJ7aCdaKnQwYES65kC7tbe0SRZGQoDe8n4
XLzpvC8rM9MXZDEN4qI+ZAANOJNVsXUmDZLDSe4ak48u/cTOokY8I6bR2k/XOhbu
L+D4W7oKAE9HmzlTMusosyjNOBQAmQ==
=Wjji
-----END PGP SIGNATURE-----
05bd8fd394a15b9dcc1bfaece0a63b0fdc2c3625a7e0aa5230fd3b5b75a8f8934a0af550b44437aa1486909058e84703e63fdec6f637d639d565b55bdaf1fa6c
\ No newline at end of file
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT19rwf/a6sZxSDNTxN72VvsrKsHq+wMes5UUcQ+L7e5QLjaCTx2ayW2FdHMBaNi
IDBBE9kxnxa/S6G6nSRARUjXowsEYZGUNLLvUjNZ4Z3g2R9XyGPaz3Ky9yWpRm36
E0lFqf8aaCLpzwV2z7cfeVNYsd2gnHakphK/UiZzXFz+GYzqby/0m5Kk8Zs7rK6V
/ji0bYWUi8t1jli8MfTHQtM8EUHG0nXRfEKilyoYkO3UsTEh/UN1VRpJ5DgcRC8L
Zbd2zPnV15MPUzZvz3kkycUulQdhOqTDjUod9P/WoASwjDuKCG2/kquwOvnoHXJ9
9Ju+ca0s9y0jbotIygYxJXZVev3EiA==
=oWIp
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT3qtgf8CeDvxzi7lPghlrZtmYTTjaic4FZsGRWTPky3H24i4wSRDhG0L5sj4uPK
eLLlITr5a9j26UCas9HRSthiC8+EgIMAhSN0X482SQhUZHAW67ErIvaHlwL+ixMD
0T5pmsW8PKN3lV1TFMhNYSEC2GRG/4GF+3yQA8LR+BgeEu/E5nmysIH8vuQMkOD6
3pKA8VKNBml591j6UTqxoHtPX+rThaziz3Hy3+ekf5iWslllTTGPd2SWqTvnj2Ae
GvRzsbli+FEM0Aj/v8jUQnQzOz891QSvWR+fMfCqZimiJMc+GBzJ9umbcyQsB5tY
e26mAoYd9KEpGXMKN4biHbJZNp1GGw==
=x/MY
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT1PSwf+InTdlh+BVVy3RFszEL5vN7waSPYMImXhgd5EZ1d4Lxd6EeOwtNgWNKpG
E+Ps/Kw0jZDvoD49WJlcUjzDHBNHcE7C/L3GAWHV6WwklhtQaJ4EegsynWdSXz6k
fqJY6r58aGKGjpKPutRWAjvfcdC170+ZRsc2oi9xrAgHCpvXzTjq4+O9Ah0t5jwW
jcZ/Xubcw4vjsw774OucHbtwGsvRN5SDJ3IONOH8WCwhUP5vEEKvA6MYX0KGoTdS
3wTCyZTzU3qtTWxcbTCpiJIWbYwRR7TzLB/uydWHlAMzuz6coIiBpYsGiO6wkmfg
W+QvcE7wyW2jtb22pCImLyObyZ21VA==
=VjDv
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT2HQgf+MTUEnwzXK4zi76VI7C5cchGan26lIA2Ebq4vtYGKDqNSISOAAdWs9+nT
U6ZA6OIFo5qdeD6F/45s91IoDbxbhMDMFEsSijKASqiuZN5TZM1U2h2kWFAl/sEl
EI1RTygn+xDw/ah4V3/duuMFC+jRgvJ/LgemIF4KBvECWaTQKNu0fu5d4dPXMpp+
jrxMEZPQZsivpOvklzV8O7wAkf/ZQhJdcB2m8uOfSPlJ91a4EEtXF9/GzzkXUi1P
bzt4NsmOag3227B3mO1Bc6yZdDBNu8wQ9apiJVCpqsxB9Dz0PCL4dHNa1u9g6Xo6
ElRgneV4HZp+LB125VoNabKuNH00bw==
=2yDl
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT0ScQf7Bip+quFq1CzDCTDxUhdTTOIpQcCfMKo1Jegpa2Hlm63XuK+6zVI9u6S+
dBXPdYeneMOqMPQUAaw3M06ZqohaDOt8Ti1EiQFGXCdOvbonTA52Lrd4EEZxwNnK
BdPuIh/8qCfozm5KbZe1bFyGVRAdNyf27KvzHgfBTirLtI+3MiOdL4bvNZbWRPfh
J84Ko+Ena8jPFgyz6nJv2Q2U/V3dCooLJAXs2vEG6owwk5J9zvSysWpHaJbXas5v
KXO9TOBBjf3+vxb1WVQa8ZYUU3+FIFes0RFVgOWghJXIooOcWrwOV2Q8z9qWXwoK
mMZ2oLS+z/7clXibK45KeRUeCX5DvQ==
=5oO1
-----END PGP SIGNATURE-----
2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00
\ No newline at end of file
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT3d+AgAwQvlwnKQLLuiZADGL+I922/YG317N2Re1EjC6WlMRZUKXH54fckRTyPm
4ZLyxVHy8LlUD2Q10g69opb7HRd/tV0miBJhn5OU1wIM3hqTgxNp9EFckK4md45k
osnhQJNDsFToxJL8zPP+KRs/aWPZs+FrRcH6k26lwLl2gTfyBDsaU11HFRVEN9yi
X41obVyKiVNlc9efSSvlLtRBSVt0VhAFhck+3t61H6D9H09QxaDGAqmduDua3Tg3
t5eqURuDfv3TfSztYgK3JBmG/6gVMsZodCgyC+8rhDDs6vSoDG30apx5Leg2rPbj
xuk2wi/WNzc94IgY9tVS3tAfT2k6yQ==
=6+Cv
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT3qtgf8CeDvxzi7lPghlrZtmYTTjaic4FZsGRWTPky3H24i4wSRDhG0L5sj4uPK
eLLlITr5a9j26UCas9HRSthiC8+EgIMAhSN0X482SQhUZHAW67ErIvaHlwL+ixMD
0T5pmsW8PKN3lV1TFMhNYSEC2GRG/4GF+3yQA8LR+BgeEu/E5nmysIH8vuQMkOD6
3pKA8VKNBml591j6UTqxoHtPX+rThaziz3Hy3+ekf5iWslllTTGPd2SWqTvnj2Ae
GvRzsbli+FEM0Aj/v8jUQnQzOz891QSvWR+fMfCqZimiJMc+GBzJ9umbcyQsB5tY
e26mAoYd9KEpGXMKN4biHbJZNp1GGw==
=x/MY
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT1PSwf+InTdlh+BVVy3RFszEL5vN7waSPYMImXhgd5EZ1d4Lxd6EeOwtNgWNKpG
E+Ps/Kw0jZDvoD49WJlcUjzDHBNHcE7C/L3GAWHV6WwklhtQaJ4EegsynWdSXz6k
fqJY6r58aGKGjpKPutRWAjvfcdC170+ZRsc2oi9xrAgHCpvXzTjq4+O9Ah0t5jwW
jcZ/Xubcw4vjsw774OucHbtwGsvRN5SDJ3IONOH8WCwhUP5vEEKvA6MYX0KGoTdS
3wTCyZTzU3qtTWxcbTCpiJIWbYwRR7TzLB/uydWHlAMzuz6coIiBpYsGiO6wkmfg
W+QvcE7wyW2jtb22pCImLyObyZ21VA==
=VjDv
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT2HQgf+MTUEnwzXK4zi76VI7C5cchGan26lIA2Ebq4vtYGKDqNSISOAAdWs9+nT
U6ZA6OIFo5qdeD6F/45s91IoDbxbhMDMFEsSijKASqiuZN5TZM1U2h2kWFAl/sEl
EI1RTygn+xDw/ah4V3/duuMFC+jRgvJ/LgemIF4KBvECWaTQKNu0fu5d4dPXMpp+
jrxMEZPQZsivpOvklzV8O7wAkf/ZQhJdcB2m8uOfSPlJ91a4EEtXF9/GzzkXUi1P
bzt4NsmOag3227B3mO1Bc6yZdDBNu8wQ9apiJVCpqsxB9Dz0PCL4dHNa1u9g6Xo6
ElRgneV4HZp+LB125VoNabKuNH00bw==
=2yDl
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT0ScQf7Bip+quFq1CzDCTDxUhdTTOIpQcCfMKo1Jegpa2Hlm63XuK+6zVI9u6S+
dBXPdYeneMOqMPQUAaw3M06ZqohaDOt8Ti1EiQFGXCdOvbonTA52Lrd4EEZxwNnK
BdPuIh/8qCfozm5KbZe1bFyGVRAdNyf27KvzHgfBTirLtI+3MiOdL4bvNZbWRPfh
J84Ko+Ena8jPFgyz6nJv2Q2U/V3dCooLJAXs2vEG6owwk5J9zvSysWpHaJbXas5v
KXO9TOBBjf3+vxb1WVQa8ZYUU3+FIFes0RFVgOWghJXIooOcWrwOV2Q8z9qWXwoK
mMZ2oLS+z/7clXibK45KeRUeCX5DvQ==
=5oO1
-----END PGP SIGNATURE-----
2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00
\ No newline at end of file
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT3d+AgAwQvlwnKQLLuiZADGL+I922/YG317N2Re1EjC6WlMRZUKXH54fckRTyPm
4ZLyxVHy8LlUD2Q10g69opb7HRd/tV0miBJhn5OU1wIM3hqTgxNp9EFckK4md45k
osnhQJNDsFToxJL8zPP+KRs/aWPZs+FrRcH6k26lwLl2gTfyBDsaU11HFRVEN9yi
X41obVyKiVNlc9efSSvlLtRBSVt0VhAFhck+3t61H6D9H09QxaDGAqmduDua3Tg3
t5eqURuDfv3TfSztYgK3JBmG/6gVMsZodCgyC+8rhDDs6vSoDG30apx5Leg2rPbj
xuk2wi/WNzc94IgY9tVS3tAfT2k6yQ==
=6+Cv
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT3qtgf8CeDvxzi7lPghlrZtmYTTjaic4FZsGRWTPky3H24i4wSRDhG0L5sj4uPK
eLLlITr5a9j26UCas9HRSthiC8+EgIMAhSN0X482SQhUZHAW67ErIvaHlwL+ixMD
0T5pmsW8PKN3lV1TFMhNYSEC2GRG/4GF+3yQA8LR+BgeEu/E5nmysIH8vuQMkOD6
3pKA8VKNBml591j6UTqxoHtPX+rThaziz3Hy3+ekf5iWslllTTGPd2SWqTvnj2Ae
GvRzsbli+FEM0Aj/v8jUQnQzOz891QSvWR+fMfCqZimiJMc+GBzJ9umbcyQsB5tY
e26mAoYd9KEpGXMKN4biHbJZNp1GGw==
=x/MY
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT1PSwf+InTdlh+BVVy3RFszEL5vN7waSPYMImXhgd5EZ1d4Lxd6EeOwtNgWNKpG
E+Ps/Kw0jZDvoD49WJlcUjzDHBNHcE7C/L3GAWHV6WwklhtQaJ4EegsynWdSXz6k
fqJY6r58aGKGjpKPutRWAjvfcdC170+ZRsc2oi9xrAgHCpvXzTjq4+O9Ah0t5jwW
jcZ/Xubcw4vjsw774OucHbtwGsvRN5SDJ3IONOH8WCwhUP5vEEKvA6MYX0KGoTdS
3wTCyZTzU3qtTWxcbTCpiJIWbYwRR7TzLB/uydWHlAMzuz6coIiBpYsGiO6wkmfg
W+QvcE7wyW2jtb22pCImLyObyZ21VA==
=VjDv
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT2HQgf+MTUEnwzXK4zi76VI7C5cchGan26lIA2Ebq4vtYGKDqNSISOAAdWs9+nT
U6ZA6OIFo5qdeD6F/45s91IoDbxbhMDMFEsSijKASqiuZN5TZM1U2h2kWFAl/sEl
EI1RTygn+xDw/ah4V3/duuMFC+jRgvJ/LgemIF4KBvECWaTQKNu0fu5d4dPXMpp+
jrxMEZPQZsivpOvklzV8O7wAkf/ZQhJdcB2m8uOfSPlJ91a4EEtXF9/GzzkXUi1P
bzt4NsmOag3227B3mO1Bc6yZdDBNu8wQ9apiJVCpqsxB9Dz0PCL4dHNa1u9g6Xo6
ElRgneV4HZp+LB125VoNabKuNH00bw==
=2yDl
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT0ScQf7Bip+quFq1CzDCTDxUhdTTOIpQcCfMKo1Jegpa2Hlm63XuK+6zVI9u6S+
dBXPdYeneMOqMPQUAaw3M06ZqohaDOt8Ti1EiQFGXCdOvbonTA52Lrd4EEZxwNnK
BdPuIh/8qCfozm5KbZe1bFyGVRAdNyf27KvzHgfBTirLtI+3MiOdL4bvNZbWRPfh
J84Ko+Ena8jPFgyz6nJv2Q2U/V3dCooLJAXs2vEG6owwk5J9zvSysWpHaJbXas5v
KXO9TOBBjf3+vxb1WVQa8ZYUU3+FIFes0RFVgOWghJXIooOcWrwOV2Q8z9qWXwoK
mMZ2oLS+z/7clXibK45KeRUeCX5DvQ==
=5oO1
-----END PGP SIGNATURE-----
2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00
\ No newline at end of file
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT3d+AgAwQvlwnKQLLuiZADGL+I922/YG317N2Re1EjC6WlMRZUKXH54fckRTyPm
4ZLyxVHy8LlUD2Q10g69opb7HRd/tV0miBJhn5OU1wIM3hqTgxNp9EFckK4md45k
osnhQJNDsFToxJL8zPP+KRs/aWPZs+FrRcH6k26lwLl2gTfyBDsaU11HFRVEN9yi
X41obVyKiVNlc9efSSvlLtRBSVt0VhAFhck+3t61H6D9H09QxaDGAqmduDua3Tg3
t5eqURuDfv3TfSztYgK3JBmG/6gVMsZodCgyC+8rhDDs6vSoDG30apx5Leg2rPbj
xuk2wi/WNzc94IgY9tVS3tAfT2k6yQ==
=6+Cv
-----END PGP SIGNATURE-----
{
"formatVersion": "1.1",
"component": {
"group": "org.springframework.example",
"module": "module-three",
"version": "1.0.0",
"attributes": {
"org.gradle.status": "release"
}
},
"createdBy": {
"gradle": {
"version": "6.5.1",
"buildId": "mvqepqsdqjcahjl7cii6b6ucoe"
}
},
"variants": [
{
"name": "apiElements",
"attributes": {
"org.gradle.category": "library",
"org.gradle.dependency.bundling": "external",
"org.gradle.jvm.version": 8,
"org.gradle.libraryelements": "jar",
"org.gradle.usage": "java-api"
},
"files": [
{
"name": "module-three-1.0.0.jar",
"url": "module-three-1.0.0.jar",
"size": 261,
"sha512": "2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00",
"sha256": "10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385",
"sha1": "8992b17455ce660da9c5fe47226b7ded9e872637",
"md5": "e84da489be91de821c95d41b8f0e0a0a"
}
]
},
{
"name": "runtimeElements",
"attributes": {
"org.gradle.category": "library",
"org.gradle.dependency.bundling": "external",
"org.gradle.jvm.version": 8,
"org.gradle.libraryelements": "jar",
"org.gradle.usage": "java-runtime"
},
"files": [
{
"name": "module-three-1.0.0.jar",
"url": "module-three-1.0.0.jar",
"size": 261,
"sha512": "2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00",
"sha256": "10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385",
"sha1": "8992b17455ce660da9c5fe47226b7ded9e872637",
"md5": "e84da489be91de821c95d41b8f0e0a0a"
}
]
},
{
"name": "javadocElements",
"attributes": {
"org.gradle.category": "documentation",
"org.gradle.dependency.bundling": "external",
"org.gradle.docstype": "javadoc",
"org.gradle.usage": "java-runtime"
},
"files": [
{
"name": "module-three-1.0.0-javadoc.jar",
"url": "module-three-1.0.0-javadoc.jar",
"size": 261,
"sha512": "2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00",
"sha256": "10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385",
"sha1": "8992b17455ce660da9c5fe47226b7ded9e872637",
"md5": "e84da489be91de821c95d41b8f0e0a0a"
}
]
},
{
"name": "sourcesElements",
"attributes": {
"org.gradle.category": "documentation",
"org.gradle.dependency.bundling": "external",
"org.gradle.docstype": "sources",
"org.gradle.usage": "java-runtime"
},
"files": [
{
"name": "module-three-1.0.0-sources.jar",
"url": "module-three-1.0.0-sources.jar",
"size": 261,
"sha512": "2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00",
"sha256": "10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385",
"sha1": "8992b17455ce660da9c5fe47226b7ded9e872637",
"md5": "e84da489be91de821c95d41b8f0e0a0a"
}
]
}
]
}
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT3OcAf+OJv0t0rhNnJcF656mem5qv3fvcJKkPqyKF9I0TiP33W61/ntrGezdaDX
tLde1MFRto3HS0/U0t6NqfMNTXYcQ5vH/qqnIRWP7Iv/t7f+mum6pOcYkxJhhXFT
1pH0l4iqVQOBUiAJhOpUh0utLNWdZcEv+DdxgtFbFyaEDmg46Cpy9YtAH6XKEh5d
ZZeiX/+XC+Ufx1bReDLHvFjUyQa/Lv8rEthX2eBmAXkoPwJG0LA9xF6X8leB0DI/
9a1KiNcmRSSUarLpqV/hE6oQggGeMLVoJ+51klunRAfiXw6h2m9gRlnWikLjC+23
/E2m+7Gb0Kc4izXIdHTqS2fYPMHsyw==
=h486
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT0xjQgAwJcUWVwcl3PI7FhRUoPaPfqaoG3bUPwLePYuf++qPCNUDOmnq0aXtqbr
Ul9SxQRDy9D7ygCWVCTVXRjg0HHZQT/ZYB7lhaDLxMEpV25q9acJAZ4qzbn8vRAG
FqlqYaSlIDducapPUGWAOF/xwhf5k8tIGO5p0hY4wdU3b+0YU1w5DavYOetTZ26Z
jwVagOj/6WFIHnu6PwXGkynqxui8dnsld23eamOZYsfR19weTNh0GT3ncl8y03eP
Wy6CkFxzN0kvSdze0nAfO9dygpRxh7nNbz/uJhTFP4pDwz5iE8FBiUXZLkxTZ8YJ
lvIKuS+JnUTEQcd00/nAL4cncMiJIQ==
=lQP9
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT3urQf/W31jKnVjCIckj7XFbeucazmVr0K73LNpg0eQwqqz877KKmBDV8qn8b3o
MTBDgUn/9LMJzUSWRFV+CkM0cgAG0s8vmzeymtH6RWv+ikHh/3Ky4sYxd9Pa3Ipo
zeeIqyJk1dysfcLLsP1ml6ayh8VM/DK+DDc4CU9wrEGAUDeVIFiTw7DrMIB7PcdG
ru7z6J/jcIA55RiJMDvuqhS+Obx/JUrmqDrrK8Npp9stRP+RpZpF1AKGgg1dfLo1
bKw+KYuMhK7Kq7nIg9GqZvhr46oOKko5NF2l+GfVR14Gdb330/88t/IxwJvsUiCC
sWQTrGJb062N5oHGtdoZ3mXLo7bnuw==
=+snH
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT14OggAlf9eyYFV3HRC7LoeM1Q9LrkYZUIDIUkukUxDxBTGPLf739qZtHgUl6lC
yUCQqGswhuuwR8s7ht2MDMp8isjs1j7inpAQA3kYgHOCUMjYlIyhPdIxHtQ8WD+S
CwW2nHtf7tXFrhKFecqolKyp+qZYWx1anMmbLggyaXWZmiIwhIHLxIogbyjVLdkD
9qUAKCUpEvyNqogyYYtAjJERRzw9RN4lwnpm/uEkKtFQVoxui2VQr/DEbzooXu8A
mqKkUBbgf9uxH5s+pUuUgbl+XZnPLGzJV6NcFe/jpsvEzHkUQzAsVnNnCWAPreY8
RTfj2eGleFWESIiMFUAp6U0an5GoOQ==
=T+Cv
-----END PGP SIGNATURE-----
ac3e2a0dfb3b8ddaa79468f85698ff97e9b88e401849e2e733073b711a1673ba2e37be61d224fd69ec2e1c59ed0342e6b3757bc3931c2e297d24bcae9370fa3b
\ No newline at end of file
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT1HGQgAuINmQJ5vpFWWmXbIrEVf5+fTKq72R7gXdJ9XHYgQdSyKoeUUy3FElqfI
55gyiLk1OMMy6Pd1HKi0bczOUOlz8K34uMXcT+ctm41Lp6243FfLm4iy1x/DWlHb
IWksIG1TRf7g0b//OiBbbaesjnc5QK1rft6T4KiEPD9NtOi/8ON7vVu0S9oERUGO
32Zwu/wGZeKztUoXVQ/zZk5UA9hYE/7C5bX3dRBS038luv7YZKe3313PfVj29vdx
bsfRIcH/qIe//WL3OTTbaOvSgOs8qvJHPN8NmdH70GbZ2W9jTe7KrIlb8FBEaPEj
BbLov9td9qxXlRxyBhLYRB7MN4rsKw==
=qIiQ
-----END PGP SIGNATURE-----
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<!-- This module was also published with a richer model, Gradle metadata, -->
<!-- which should be used instead. Do not delete the following line which -->
<!-- is to indicate to Gradle or any Gradle module metadata file consumer -->
<!-- that they should prefer consuming it instead. -->
<!-- do_not_remove: published-with-gradle-metadata -->
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.example</groupId>
<artifactId>module-three</artifactId>
<version>1.0.0</version>
<name>module-three</name>
<description>Example module</description>
<url>https://spring.io/projects/spring-boot</url>
<organization>
<name>Pivotal Software, Inc.</name>
<url>https://spring.io</url>
</organization>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0</url>
</license>
</licenses>
<developers>
<developer>
<name>Pivotal</name>
<email>info@pivotal.io</email>
<organization>Pivotal Software, Inc.</organization>
<organizationUrl>https://www.spring.io</organizationUrl>
</developer>
</developers>
<scm>
<connection>
scm:git:git://github.com/spring-projects/spring-boot.git
</connection>
<developerConnection>
scm:git:ssh://git@github.com/spring-projects/spring-boot.git
</developerConnection>
<url>https://github.com/spring-projects/spring-boot</url>
</scm>
<issueManagement>
<system>GitHub</system>
<url>
https://github.com/spring-projects/spring-boot/issues
</url>
</issueManagement>
</project>
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT0T5QgAgXfcX/6hkGNWRp4xIbpC0P9wi21WBmlWeM1l1vPjlogcPB5fIQ15tnxL
dyXVJjhdBXG70m5UkOtR5LbO+6Y7soEsocfuN/wdjNP/JUk2xW4HTj87F16r3EhV
s1nrydd/nZxsIemTY1irOrCk4yEOWlAO91VOGFI4UoGGE6oeMiTFje6vbNidGT3Y
RD1VrxbVasI38HHggQ+odrdp+rk8AwAUJq8g96KyRO5d+O6NQCf4cTe6S5+kJKG1
ETQ0yASHiD5pzcpQiEQu+wclgAunVAzr5Ql/SnOcZEjUoVOLix7Ttcv5KcXjZhY9
9VQyULZ1MzcrSEoRoOv8k8fT7swvLg==
=KgwJ
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT2Umgf+K57ogLEAGx/40dFOV0yiGmvCXwywMCVbnMXCA85Ceti0TGFY6T0EaJXy
wF7QQ0SW56svIxX/U58IVocWuaVRJA7tCZF1u9DCvafdYDJeM4iHHVu6GzM1Tng2
JFYV4q5MtT712rCrcf7ZH3MntYawsGjBiF9IHWwvSNUyf53W7L4VSWcpv0tfPLra
EeC7ztnnDXgi32FSpXvu27mDPbrQLibihUZBjoZ4uuRU2wB6HICJ90JjoYtK6JoC
ToEZY4jFLkEmQ8dy0KUa5rhUDWJ+Bq+bYHhwMXl9HQUKZuqjvlmHCRHIsJgdU+Cl
i5NPJkXhCZOs9tD3hf3NdeD9ef72kQ==
=PraK
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT2j8QgAiciViDfKLAv2rYMyBJbyQjK29fpG78NMsKw+j3zWwJEPlPuZhIT0/KWQ
3ipcYbtBoKYrKSG54uzPflGAQoostMYV+XtJ+0fjICsNDpKjfhuDWojaWkxnF1KD
NcWSiapNO6iX0s62yaL/netVVsHsE5fVr//IG6WDTrJK2GEkOQAoca2W8ixI3G0s
kTIJEmCMA9ZOUMKBwwtJ0NPEZPxe1N/R6SuGWGdkWlrqPRmA6lnY153zH3vJ6pqF
RM1Phwpt46l3o20D5wOhqU8jvV7b5HUZ50sHV0sJOMUbwvFyrhIOzJLMixk1WJhR
lnudbpWPssTJO3Fiv67b/iADaBaIbA==
=4CYs
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT3Pwwf+IikjoDdjeMQfmERTN4Gjirx9+fler+Gr5JYC78OxLrB8uq0tn11wEJMQ
ZDQe83OYjEkFQhPn6yQ5bc9edZTJztQJcGpVe7NkYffS4geo+ahuksaQWMF9opEc
OqBB5fTWVt18qGFTI2F3CEDIo38muTgkzndFuzmcbVhAcknF5ybcVDIFpZNlnqe/
xflD6lupXWXQC4nE4n2EVhNnmkiF9nKRJWwEJ1hoy1gwTxSYmnO/BLn8ob4tswnc
gHnj4Yp7qoV/SnRnfNDHMPQMfzAwq1jucjjPOt3xkw17UIGBnrP1zAxeZFmarCTO
YjJt5PUNwtqOVHlHzwgn3b3FGRJe3A==
=AU99
-----END PGP SIGNATURE-----
67549e36f07c9f4b83cfa9f00fa647705128eabd03dec3df0066177e89940652fc7e13b8c0b60652c811e167c3ffaba0ab95a4f315a10f69572c12e4e944b6e6
\ No newline at end of file
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT0bBgf+Nm9vooXNKE/z4cNeFqiHwLb+gMxvqlKnl/+03KbuFvlDUwdqSxfnHQZ3
qfCBtIe6MN/0I5FCNRCcFxiCjCPDqSMAcvRPU5UOG2M2W0uvcqMwKO6KRGBLd65J
MulDxoe7LrEk6KwNYfecxCtXBvLwzwQd3A10tcQOVl66neF/g+9jEc+MHJPa1xi5
4pDOo3TQ4EpGfB8eF9Z+7YKc9hPYBFsm+n3P6SYcVAiRUiNBE8gCOvvZE9mOTQAo
yC80AfDjXe/YBsd3a79hVW+ESQAKfK8S1RzO+c5GhIZCIJFdSJDOeww86O4U0n5g
6hIXRNWUFUEueAvQ2dYHZujQSBxNig==
=t5De
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX
xT1IkAf+N8ts9K88PC/dISZMWSrgRbSugd6VRG6pto6jsp9cJBzTzQ6C8psMkXcN
qX0fnLBn4Zn1dovxRIrA8QeT/vxHMl1X0Foe6SUTMjK34Ofq4V1FVlhuJIi0YZrP
L7B4cKTkv1ndwSVgE23zkynfaIPiPl1uZOwDmlpArokqnjSiUq9NndtKf87NwekW
hbf7brgfZddeDj9xhAn5hz2pHUhx/uH9tOX3JlZgE+yATZsGm6Z9BSf4Lur0W85P
hrJ+MfuYPzZ3n7okuaQdMT3QIqe3FO+dfGZKwakw2NWfgWP3LZsQ5rbyxBlyH7CG
JA/VAIIqe99ftHcBFRGB6C9hMn4FcA==
=prNt
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX
xT3jzQf8DUis2+v7616JWRpUziEnpnvms7+PkgovWttcpbO0RlLr1s0Uno0D0jAt
7KUGNN4//n8hKsojZ6ZI+7pzsk/0NathOQRNYcdb+AFf71T3yJhef1d5GmXHA7iV
wA6AfrFTEQ7SaimZXEGGpFXzb3rPsVnryOEbTOXno3B7nNjZTUpjkW/APkvJueUk
BIFCWH9rL1txRKWhKg8f6YT+l6HQFn+qu1Z3/MoqxCn6HvUxExA1mwNbzfvNaDTt
l04jNVG6NqZyGhivuDnpyonnmwKySryKVvGrWn6b5SfgPPQYQJTWK3c7npSPjKpO
ydzWOvISS55vBKLbB7g69g8ah3FHEw==
=fcEr
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX
xT0ovgf/aejEen2MEvJTF1Tjg24rK1jZAYqmgGi8H2b26h7ZEd/le2jWs6VpPmvv
DX3pyaKFyOZXpU8SOkoPgi021VIt9LLWPlxMgcWlb6EWg8xw70PXISbUFy3IcxRi
I14uAUXoFgIOT6jPt659kdXLNtYRsS3nQcBgJTIz6axHk2t5tD3TRf4xcLCyuVGv
/obkTwpLr2jdPBxgTe+oDPjCnOyI6YeN0dKq4aiGBI/xECNpitbzmYQA2FQ+WvsG
qq+1n/eAZAzAUWumxLna9ov1O0f6cY9d9hxWMTe2L4/a7B6KezF0CPnShFaC6pCV
98aamE5QxBmSeQtmRdI75WFHJ1h0Vg==
=0M4U
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX
xT104Qf+NG6IUBLo7eTPtPzWyNs9bShrJR759atbY9kBOrRfyoBM/hWrcr6pG4e5
BmBPULkuBWLVM+Ra52B2S96848oiaLLEuiWlMudWRsWCxyVknjm9EKlMHA3VdQtu
8grrPKS4mIwSvdzAEEeqR+mwtWXHlz+jc/R9NeQhNmmcNZv1nkyzCuoNCH/HMTl4
/ei6enpYrYNnMrNz9TOMQ67sCtZEm6TaxlqS+9h/V9TEnq6+1qXEt4c+AsqQdMWH
3BZREzXHFocQciSEXfL6m07pnNlnvCcjsM2SAqTeTqQupEmqFGkhL2blE1VMdplW
fDCC/ee5JYVyyUXpzydSjCYwFbO3NQ==
=gSEv
-----END PGP SIGNATURE-----
2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00
\ No newline at end of file
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX
xT0XdAf/feMRtW2BUz84x43aYLaERIEPx2TsqMUwVcov6sp5MWaSAJEX2vrscCRB
K/7x4N3dxnrckc3sBF1hs+zrRwySYU1FyGVIxQxdeURnUWxCva0uOWj91jcUOIkA
gpmOZZj51b4SkB6GZjtvN/Z3B4xzEPmTPfKFiBZPhuYW4HiC8FHM1JnlW6h2xoj6
Bja//qoj9ccfRjiMnnI0iPgZNiyR8n8+EJGi0ykizxkiT6cWI84kZ+JQYooDHbCf
NgPt2NjcGzd6SGrQW8/0td0N+xDRfLpTrbfTmlC5hikXmS0e79BV6W0eYcWcgDni
r8WjbDmomHHFDhT8p1R+tGtd8p2txg==
=BHvx
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX
xT1IkAf+N8ts9K88PC/dISZMWSrgRbSugd6VRG6pto6jsp9cJBzTzQ6C8psMkXcN
qX0fnLBn4Zn1dovxRIrA8QeT/vxHMl1X0Foe6SUTMjK34Ofq4V1FVlhuJIi0YZrP
L7B4cKTkv1ndwSVgE23zkynfaIPiPl1uZOwDmlpArokqnjSiUq9NndtKf87NwekW
hbf7brgfZddeDj9xhAn5hz2pHUhx/uH9tOX3JlZgE+yATZsGm6Z9BSf4Lur0W85P
hrJ+MfuYPzZ3n7okuaQdMT3QIqe3FO+dfGZKwakw2NWfgWP3LZsQ5rbyxBlyH7CG
JA/VAIIqe99ftHcBFRGB6C9hMn4FcA==
=prNt
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT1PSwf+InTdlh+BVVy3RFszEL5vN7waSPYMImXhgd5EZ1d4Lxd6EeOwtNgWNKpG
E+Ps/Kw0jZDvoD49WJlcUjzDHBNHcE7C/L3GAWHV6WwklhtQaJ4EegsynWdSXz6k
fqJY6r58aGKGjpKPutRWAjvfcdC170+ZRsc2oi9xrAgHCpvXzTjq4+O9Ah0t5jwW
jcZ/Xubcw4vjsw774OucHbtwGsvRN5SDJ3IONOH8WCwhUP5vEEKvA6MYX0KGoTdS
3wTCyZTzU3qtTWxcbTCpiJIWbYwRR7TzLB/uydWHlAMzuz6coIiBpYsGiO6wkmfg
W+QvcE7wyW2jtb22pCImLyObyZ21VA==
=VjDv
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT2HQgf+MTUEnwzXK4zi76VI7C5cchGan26lIA2Ebq4vtYGKDqNSISOAAdWs9+nT
U6ZA6OIFo5qdeD6F/45s91IoDbxbhMDMFEsSijKASqiuZN5TZM1U2h2kWFAl/sEl
EI1RTygn+xDw/ah4V3/duuMFC+jRgvJ/LgemIF4KBvECWaTQKNu0fu5d4dPXMpp+
jrxMEZPQZsivpOvklzV8O7wAkf/ZQhJdcB2m8uOfSPlJ91a4EEtXF9/GzzkXUi1P
bzt4NsmOag3227B3mO1Bc6yZdDBNu8wQ9apiJVCpqsxB9Dz0PCL4dHNa1u9g6Xo6
ElRgneV4HZp+LB125VoNabKuNH00bw==
=2yDl
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX
xT104Qf+NG6IUBLo7eTPtPzWyNs9bShrJR759atbY9kBOrRfyoBM/hWrcr6pG4e5
BmBPULkuBWLVM+Ra52B2S96848oiaLLEuiWlMudWRsWCxyVknjm9EKlMHA3VdQtu
8grrPKS4mIwSvdzAEEeqR+mwtWXHlz+jc/R9NeQhNmmcNZv1nkyzCuoNCH/HMTl4
/ei6enpYrYNnMrNz9TOMQ67sCtZEm6TaxlqS+9h/V9TEnq6+1qXEt4c+AsqQdMWH
3BZREzXHFocQciSEXfL6m07pnNlnvCcjsM2SAqTeTqQupEmqFGkhL2blE1VMdplW
fDCC/ee5JYVyyUXpzydSjCYwFbO3NQ==
=gSEv
-----END PGP SIGNATURE-----
2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00
\ No newline at end of file
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT3d+AgAwQvlwnKQLLuiZADGL+I922/YG317N2Re1EjC6WlMRZUKXH54fckRTyPm
4ZLyxVHy8LlUD2Q10g69opb7HRd/tV0miBJhn5OU1wIM3hqTgxNp9EFckK4md45k
osnhQJNDsFToxJL8zPP+KRs/aWPZs+FrRcH6k26lwLl2gTfyBDsaU11HFRVEN9yi
X41obVyKiVNlc9efSSvlLtRBSVt0VhAFhck+3t61H6D9H09QxaDGAqmduDua3Tg3
t5eqURuDfv3TfSztYgK3JBmG/6gVMsZodCgyC+8rhDDs6vSoDG30apx5Leg2rPbj
xuk2wi/WNzc94IgY9tVS3tAfT2k6yQ==
=6+Cv
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX
xT1IkAf+N8ts9K88PC/dISZMWSrgRbSugd6VRG6pto6jsp9cJBzTzQ6C8psMkXcN
qX0fnLBn4Zn1dovxRIrA8QeT/vxHMl1X0Foe6SUTMjK34Ofq4V1FVlhuJIi0YZrP
L7B4cKTkv1ndwSVgE23zkynfaIPiPl1uZOwDmlpArokqnjSiUq9NndtKf87NwekW
hbf7brgfZddeDj9xhAn5hz2pHUhx/uH9tOX3JlZgE+yATZsGm6Z9BSf4Lur0W85P
hrJ+MfuYPzZ3n7okuaQdMT3QIqe3FO+dfGZKwakw2NWfgWP3LZsQ5rbyxBlyH7CG
JA/VAIIqe99ftHcBFRGB6C9hMn4FcA==
=prNt
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX
xT3jzQf8DUis2+v7616JWRpUziEnpnvms7+PkgovWttcpbO0RlLr1s0Uno0D0jAt
7KUGNN4//n8hKsojZ6ZI+7pzsk/0NathOQRNYcdb+AFf71T3yJhef1d5GmXHA7iV
wA6AfrFTEQ7SaimZXEGGpFXzb3rPsVnryOEbTOXno3B7nNjZTUpjkW/APkvJueUk
BIFCWH9rL1txRKWhKg8f6YT+l6HQFn+qu1Z3/MoqxCn6HvUxExA1mwNbzfvNaDTt
l04jNVG6NqZyGhivuDnpyonnmwKySryKVvGrWn6b5SfgPPQYQJTWK3c7npSPjKpO
ydzWOvISS55vBKLbB7g69g8ah3FHEw==
=fcEr
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX
xT0ovgf/aejEen2MEvJTF1Tjg24rK1jZAYqmgGi8H2b26h7ZEd/le2jWs6VpPmvv
DX3pyaKFyOZXpU8SOkoPgi021VIt9LLWPlxMgcWlb6EWg8xw70PXISbUFy3IcxRi
I14uAUXoFgIOT6jPt659kdXLNtYRsS3nQcBgJTIz6axHk2t5tD3TRf4xcLCyuVGv
/obkTwpLr2jdPBxgTe+oDPjCnOyI6YeN0dKq4aiGBI/xECNpitbzmYQA2FQ+WvsG
qq+1n/eAZAzAUWumxLna9ov1O0f6cY9d9hxWMTe2L4/a7B6KezF0CPnShFaC6pCV
98aamE5QxBmSeQtmRdI75WFHJ1h0Vg==
=0M4U
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX
xT104Qf+NG6IUBLo7eTPtPzWyNs9bShrJR759atbY9kBOrRfyoBM/hWrcr6pG4e5
BmBPULkuBWLVM+Ra52B2S96848oiaLLEuiWlMudWRsWCxyVknjm9EKlMHA3VdQtu
8grrPKS4mIwSvdzAEEeqR+mwtWXHlz+jc/R9NeQhNmmcNZv1nkyzCuoNCH/HMTl4
/ei6enpYrYNnMrNz9TOMQ67sCtZEm6TaxlqS+9h/V9TEnq6+1qXEt4c+AsqQdMWH
3BZREzXHFocQciSEXfL6m07pnNlnvCcjsM2SAqTeTqQupEmqFGkhL2blE1VMdplW
fDCC/ee5JYVyyUXpzydSjCYwFbO3NQ==
=gSEv
-----END PGP SIGNATURE-----
2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00
\ No newline at end of file
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT3d+AgAwQvlwnKQLLuiZADGL+I922/YG317N2Re1EjC6WlMRZUKXH54fckRTyPm
4ZLyxVHy8LlUD2Q10g69opb7HRd/tV0miBJhn5OU1wIM3hqTgxNp9EFckK4md45k
osnhQJNDsFToxJL8zPP+KRs/aWPZs+FrRcH6k26lwLl2gTfyBDsaU11HFRVEN9yi
X41obVyKiVNlc9efSSvlLtRBSVt0VhAFhck+3t61H6D9H09QxaDGAqmduDua3Tg3
t5eqURuDfv3TfSztYgK3JBmG/6gVMsZodCgyC+8rhDDs6vSoDG30apx5Leg2rPbj
xuk2wi/WNzc94IgY9tVS3tAfT2k6yQ==
=6+Cv
-----END PGP SIGNATURE-----
{
"formatVersion": "1.1",
"component": {
"group": "org.springframework.example",
"module": "module-two",
"version": "1.0.0",
"attributes": {
"org.gradle.status": "release"
}
},
"createdBy": {
"gradle": {
"version": "6.5.1",
"buildId": "mvqepqsdqjcahjl7cii6b6ucoe"
}
},
"variants": [
{
"name": "apiElements",
"attributes": {
"org.gradle.category": "library",
"org.gradle.dependency.bundling": "external",
"org.gradle.jvm.version": 8,
"org.gradle.libraryelements": "jar",
"org.gradle.usage": "java-api"
},
"files": [
{
"name": "module-two-1.0.0.jar",
"url": "module-two-1.0.0.jar",
"size": 261,
"sha512": "2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00",
"sha256": "10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385",
"sha1": "8992b17455ce660da9c5fe47226b7ded9e872637",
"md5": "e84da489be91de821c95d41b8f0e0a0a"
}
]
},
{
"name": "runtimeElements",
"attributes": {
"org.gradle.category": "library",
"org.gradle.dependency.bundling": "external",
"org.gradle.jvm.version": 8,
"org.gradle.libraryelements": "jar",
"org.gradle.usage": "java-runtime"
},
"files": [
{
"name": "module-two-1.0.0.jar",
"url": "module-two-1.0.0.jar",
"size": 261,
"sha512": "2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00",
"sha256": "10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385",
"sha1": "8992b17455ce660da9c5fe47226b7ded9e872637",
"md5": "e84da489be91de821c95d41b8f0e0a0a"
}
]
},
{
"name": "javadocElements",
"attributes": {
"org.gradle.category": "documentation",
"org.gradle.dependency.bundling": "external",
"org.gradle.docstype": "javadoc",
"org.gradle.usage": "java-runtime"
},
"files": [
{
"name": "module-two-1.0.0-javadoc.jar",
"url": "module-two-1.0.0-javadoc.jar",
"size": 261,
"sha512": "2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00",
"sha256": "10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385",
"sha1": "8992b17455ce660da9c5fe47226b7ded9e872637",
"md5": "e84da489be91de821c95d41b8f0e0a0a"
}
]
},
{
"name": "sourcesElements",
"attributes": {
"org.gradle.category": "documentation",
"org.gradle.dependency.bundling": "external",
"org.gradle.docstype": "sources",
"org.gradle.usage": "java-runtime"
},
"files": [
{
"name": "module-two-1.0.0-sources.jar",
"url": "module-two-1.0.0-sources.jar",
"size": 261,
"sha512": "2730ac0859e1c2f450d54647c29ece43d095eb834e2130d4949dd1151317d013c072fa8f96f5f4b49836eff7c19a2eeeb5ca7483e5dac24c368bbcd522c27a00",
"sha256": "10ce8a82f7e53c67f2af8d4dc4b8cdb4d0630d0e1d21818da4d5b3ca2de08385",
"sha1": "8992b17455ce660da9c5fe47226b7ded9e872637",
"md5": "e84da489be91de821c95d41b8f0e0a0a"
}
]
}
]
}
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX
xT2SwAf/SDu2XlOfyL+oTidHItm3DmLsRY0xU2d5siCuasxxpzIPG+O5f3o7VeNS
pKUctb+Vbx7Za+tPYts4ztG+bqVUVZbtn9ERWCXAvuuAnbJMxIl4D7HXahZPJKtl
UrpKgA+45p2NLB9MK5B9QkmZInxF0ex3IUkc6e3MN8pmcefcjjDpoEvWKlc+ocEA
/ySwMcH38FRYB6XbwsAjdXm7jiLpA9ZA5MdfZzjmm3nRBDzujBjU/Pv1+PFPH4Lh
rfAS5+HOvWLQwt5kKyr8w3GzfbWT7FF7z024x0rT6mo0chOMe33Ng/AOYSGFzisJ
OrJieiEosNjdcFbfuvspQFsI0cRU/A==
=dgpO
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX
xT375Af+L1KawLLikLiiC6R2BCxufjcLHEzxh0RLjgDHpHXy8s/Bh5GgkTT4x/Hn
gGJNT3Yz18rzpZaLnvNR/J9wOFEzLxRsgl1rumvTrrwhbIjac774oj3Z8Zv+W1T8
KN1mtfUSLDZSRbmY0YByvPVtag1+FaIifxmIIFLny+xDzRVD1OZ38gOaxz91nqmd
pgjR2eVmeYLX3oAIApVopYdKWXNwOMzdBQbNroPRKCOesmTqQi0sjuvgN7r5JoxN
9vVzF1SFnAKnw/LQqL0KMrRzCBd+ncUk7A6D6RB0MM0V+TB9am9CsatxflRgQY+c
vzu/BHY8k6lh44TECvAuNuSr01CkPA==
=awJx
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX
xT1ubAgAtWXXzqRDIC+DhAaY3IfHyM/Dmlj58jLhTzta4xKQe9HykEjBlpkPSscp
9R+O2aL4xfBUmKtVLORKoGN3oUPhdU8a5vfgI2itdDPWLkOfFE64OJtIOZKp4ST4
i00Jsqd4GFryS3r+i5FL5MCCv+zG/OkylMIfcH1XVxynvwrJVY49Do+TmW4MOIFf
4uDOd29XmEc8vCJBd3VZu0epHqcXhdiQy0ekdl3NdUimzRuXAckNhGNMoLWYhKaw
voErlAtfDHMbYU1DebguEaiLi98N6IxX1aO0Lleg3JNveD7pLCjEEf7AW+7TYoz1
QBvHABpVHzZ7Rg5VhZNIIrQ38zyZdg==
=kz39
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX
xT3njggAl61MfkbtByxMKX8fQo5Jqp+vtvyZoaDZj+6FKr0xust5Wr4Fi+4+7fkj
KyXFCTZUTd0xokRNpC8bxeZhhVkeG0pq6DrTr5BTvJLMJwPWVvrtn+bzDN1FvMia
iZqcOlWtbwTdcosmBxXtxwI1gavwFhHUGudBzrBs85qkMkDz9BH8Egb+z/owFfPh
lB9NSzezj4axgr745Ov5gYCwZp44iDBTcLZDWSLGMTuC6VdrLTQVsNLxouGI/67E
0oqLmlaqfWZEJktTMk0LHG5ymy0g40Gm8r2kuxRnFEDVJwXSfJRiTvxifB+YoEHp
RAcpReQe8+iSStuGEKYmfwmyXTdXAA==
=759w
-----END PGP SIGNATURE-----
6294828fb9ce277d60e92c607d00b739e9266c3ebdca173fb32881c04f2d365d43bf9d77c0a07cf1dbcd09ad302f7458e938197f59ff5614a5d1d4e4146a0cf6
\ No newline at end of file
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX
xT1H5wgAnmHJRXH+f1cZLlLKqMv7JMbuSFha3XsDPeL/XIEXmD9zj/R8IE7EKmpS
uwkswZaOeIyc95J4FasxiZm5CExxzRSTQfXtK2lOl/7Gp84D+D6XXI28CUIRnOfo
SeOyCFk2U3a6uTsRgi1FSnJRvLCs+0tB+bByKuVgGbfQdF0mtQ9rCxlqKKVa/dz6
ertOXtz1A7fiLV44ovZG27GOciRJbogBmWfmNGPaQ+Ry8b8ICPf3SDdNSpp/nG2i
YZxzIyX9xabzPGg7M1d4ClhrpJQRjJD1hJRIHCkwC8f8Y544iQ/MuAwd3hNIfjWP
GJjgOl0iYjO8LPVaRdrHFkBUntVdHQ==
=tlE4
-----END PGP SIGNATURE-----
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<!-- This module was also published with a richer model, Gradle metadata, -->
<!-- which should be used instead. Do not delete the following line which -->
<!-- is to indicate to Gradle or any Gradle module metadata file consumer -->
<!-- that they should prefer consuming it instead. -->
<!-- do_not_remove: published-with-gradle-metadata -->
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.example</groupId>
<artifactId>module-two</artifactId>
<version>1.0.0</version>
<name>module-two</name>
<description>Example module</description>
<url>https://spring.io/projects/spring-boot</url>
<organization>
<name>Pivotal Software, Inc.</name>
<url>https://spring.io</url>
</organization>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0</url>
</license>
</licenses>
<developers>
<developer>
<name>Pivotal</name>
<email>info@pivotal.io</email>
<organization>Pivotal Software, Inc.</organization>
<organizationUrl>https://www.spring.io</organizationUrl>
</developer>
</developers>
<scm>
<connection>
scm:git:git://github.com/spring-projects/spring-boot.git
</connection>
<developerConnection>
scm:git:ssh://git@github.com/spring-projects/spring-boot.git
</developerConnection>
<url>https://github.com/spring-projects/spring-boot</url>
</scm>
<issueManagement>
<system>GitHub</system>
<url>
https://github.com/spring-projects/spring-boot/issues
</url>
</issueManagement>
</project>
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT2kvAgAoc2k0ljj7L3Pj4rPz73K1SO3pDHdf+6S6pU7E4ao9FEFZBcB7YJjEmmQ
U724HqU15PIkaJKI/v4Z612E1gMSMIQ8A0LnsFR9yQdvrsK1Ijv+CdPCdyvZsBfP
3MgmWaRUOToK3BAAVV5y0dfUNFUyeKKxHNclJd6H0HUK02of8I7LBn/5ULK4QRaQ
Lm3bUIT3PtjUfND+DK3QlczZ+YgOkIwTkLywYCYxblm9XJjWCRXaZI1MdUlA6SMs
uEqtglQ9zEJgyue/JtWsIkAlzUbdyjo34Cg5HEZJ6RNzboXlRNFm83fcKyPhSy7V
0xikP1INbKuZSU1ZE7/rRYIQ7ChK0g==
=96NH
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX
xT1eXQgAhIYkhiSoV4lFMPT9GuN4OjkoQC+v9ev8HkmIdq8lpoB+StIYzI35hKLU
kfm5d2aeVo7ifDdXh642p9oEXRfuDPfaLd9u8SZZBAdo4rolQZr4bl+JaUFzR79i
nRozXQeJF1UrDUKMi0+YGQxlosTbdx7Romj2UdfEmL2ACetxxR3rQExgZl4O/OUm
PHJtIrzO1xdbVxtKelILJ4D/PauqEqcqzC2gI5vObZJcRgxDU/wc2CVN9jruv07h
UW+8IEFV8vexoHo+Kq/F9xaTW2b7oXnvfOJgWRbh3zGpxSluJwVINDyX39/ym/Dr
FIO6BPWFKQPEUW2cY6C69jj7S+v8Ig==
=PYoG
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEyBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX
xT0bmAf4+WOg0xkoyRDXf/1hFOXlimKqyF1K7I6PXx1dFokRr+tvOtfFZucCOf+f
1hCvnBiPTQwwMPCgll0reTsH2nHfDVUcbugpxVDC3Yza0x3gHudBhPC7yv+osNIu
sVlnMRYbG1RQGjE6BxHoBk9pdOcwgN7zk2Y4LfAbOKMTo7dhAjZavRx3aShEUwHy
P9/kfxcWCL0tOSzWg5XpZuxFEdVMWNJvshFvP0j2/Nlr6ZL5o/AwtyZKMiZ8QcUb
0satLj501JYI6pM2cm8N17T94+jCsQXZic/hUCNteXA4XbRcDBR2wQLd08/Ht0U4
rHzZQNr5Ft5R5ScshTEVBwjcd/Kx
=drUj
-----END PGP SIGNATURE-----
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1oACgkQmix6mORX
xT3w3QgAvHS75cBMEEZGWiCQ+lbwDMWY7rjQBwkpO1Sj6WxpKcWD2F4YmNni1z1k
L71SDuLP0a+IbYcoDCkss7vxH6hx6Lm53e+2WwhaVGCO1A6N3a56rFyEFlATrn31
mcRjLrN4wzysqqbeamzSt+R0UoWj7yiihtBuz7tkhjGP+df2qCrXNeuetrhukFOz
P5RLd4PURYMMUMqqNZ8JNnRhdCVdSVUpfM+BDolNDaswDrvOI3jzjXD/6HCt0fcN
Pt484kFDqGEx0iXvv+7shiExs31gex+fsn2ta9yOGYluF/8Rc4z+X0/59MewoCgC
EelPT4oi4zirrIWzGp1bRF+jDi6feA==
=stbf
-----END PGP SIGNATURE-----
5b06587734aa146b452e78e34abffb38b8c820abf623975409ae003b5d90aded345fa8f92ed9d7a505a16db324c0649d2e16c7597bad8dc63f0a5c00123789e1
\ No newline at end of file
-----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEE4qywN5M83qq3v3fUmix6mORXxT0FAmAiY1sACgkQmix6mORX
xT3p3QgAp5doWrX6eMPD1I09NHMt29LI8wFa/xld7rof8OQLHDN55TLseqOvBU4V
E82s5cm4Uk0ndnth/VUHqsJK6SsNX8/0N2bvOtWgUWBYdClcy6ZBWXjQIDFfCdqX
LPqQN4nOT3ZZMrzTZhLsAJkbzvVaOzEtUYZWw1ZAIT8nPkud24stuuxKUtsAxfeD
QqcgKng/sPx6DS2+NSmmzyCF9eSL70NBcBF6+RJ+4X0YtZRtX+wsic2MnKnVAnyX
hPejxguJYhwWbn1yRdVWknCdffpiT09IC/7AS/yc8s1DdbS6XEae8uFl0OB5z5dx
nnaHUvlFrAjDGsrYeW5h1ZkM8VwBxA==
=2HWi
-----END PGP SIGNATURE-----
......@@ -10,6 +10,4 @@ branch: "2.3.x"
milestone: "2.3.x"
build-name: "spring-boot"
concourse-url: "https://ci.spring.io"
bintray-subject: "spring"
bintray-repo: "jars"
task-timeout: 2h00m
......@@ -21,14 +21,11 @@ anchors:
GITHUB_PASSWORD: ((github-ci-release-token))
GITHUB_USERNAME: ((github-username))
MILESTONE: ((milestone))
bintray-task-params: &bintray-task-params
BINTRAY_SUBJECT: ((bintray-subject))
BINTRAY_REPO: ((bintray-repo))
BINTRAY_USERNAME: ((bintray-username))
BINTRAY_API_KEY: ((bintray-api-key))
sontatype-task-params: &sonatype-task-params
SONATYPE_USER_TOKEN: ((sonatype-user-token))
SONATYPE_PASSWORD_TOKEN: ((sonatype-user-token-password))
SONATYPE_URL: ((sonatype-url))
SONATYPE_STAGING_PROFILE_ID: ((sonatype-staging-profile-id))
artifactory-task-params: &artifactory-task-params
ARTIFACTORY_SERVER: ((artifactory-server))
ARTIFACTORY_USERNAME: ((artifactory-username))
......@@ -571,7 +568,7 @@ jobs:
trigger: false
passed: [stage-release]
params:
download_artifacts: false
download_artifacts: true
save_build_info: true
- task: promote
image: ci-image
......@@ -579,8 +576,8 @@ jobs:
params:
RELEASE_TYPE: RELEASE
<<: *artifactory-task-params
<<: *bintray-task-params
- name: sync-to-maven-central
<<: *sonatype-task-params
- name: create-github-release
serial: true
plan:
- get: ci-image
......@@ -591,12 +588,6 @@ jobs:
params:
download_artifacts: false
save_build_info: true
- task: sync-to-maven-central
image: ci-image
file: git-repo/ci/tasks/sync-to-maven-central.yml
params:
<<: *bintray-task-params
<<: *sonatype-task-params
- task: generate-changelog
file: git-repo/ci/tasks/generate-changelog.yml
params:
......@@ -614,7 +605,7 @@ jobs:
- get: ci-image
- get: git-repo
- get: artifactory-repo
passed: [sync-to-maven-central]
passed: [create-github-release]
params:
download_artifacts: false
save_build_info: true
......@@ -633,7 +624,7 @@ jobs:
- get: git-repo
resource: homebrew-tap-repo
- get: artifactory-repo
passed: [sync-to-maven-central]
passed: [create-github-release]
params:
download_artifacts: false
save_build_info: true
......@@ -650,7 +641,7 @@ groups:
- name: "builds"
jobs: ["build", "jdk11-build", "jdk15-build", "windows-build"]
- name: "releases"
jobs: ["stage-milestone", "stage-rc", "stage-release", "promote-milestone", "promote-rc", "promote-release", "sync-to-maven-central", "publish-to-sdkman", "update-homebrew-tap"]
jobs: ["stage-milestone", "stage-rc", "stage-release", "promote-milestone", "promote-rc", "promote-release", "create-github-release", "publish-to-sdkman", "update-homebrew-tap"]
- name: "ci-images"
jobs: ["build-ci-images", "detect-docker-updates", "detect-jdk-updates", "detect-ubuntu-image-updates"]
- name: "pull-requests"
......
......@@ -5,11 +5,9 @@ source $(dirname $0)/common.sh
version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' )
export BUILD_INFO_LOCATION=$(pwd)/artifactory-repo/build-info.json
java -jar /spring-boot-release-scripts.jar promote $RELEASE_TYPE $BUILD_INFO_LOCATION || { exit 1; }
java -jar /spring-boot-release-scripts.jar distribute $RELEASE_TYPE $BUILD_INFO_LOCATION || { exit 1; }
java -jar /spring-boot-release-scripts.jar publishToCentral $RELEASE_TYPE $BUILD_INFO_LOCATION artifactory-repo || { exit 1; }
java -jar /spring-boot-release-scripts.jar publishGradlePlugin $RELEASE_TYPE $BUILD_INFO_LOCATION || { exit 1; }
java -jar /spring-boot-release-scripts.jar promote $RELEASE_TYPE $BUILD_INFO_LOCATION || { exit 1; }
echo "Promotion complete"
echo $version > version/version
#!/bin/bash
export BUILD_INFO_LOCATION=$(pwd)/artifactory-repo/build-info.json
version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' )
java -jar /spring-boot-release-scripts.jar syncToCentral "RELEASE" $BUILD_INFO_LOCATION || { exit 1; }
echo "Sync complete"
echo $version > version/version
......@@ -10,9 +10,8 @@ params:
ARTIFACTORY_SERVER:
ARTIFACTORY_USERNAME:
ARTIFACTORY_PASSWORD:
BINTRAY_SUBJECT:
BINTRAY_REPO:
BINTRAY_USERNAME:
BINTRAY_API_KEY:
SONATYPE_USER_TOKEN:
SONATYPE_PASSWORD_TOKEN:
SONATYPE_STAGING_PROFILE_ID:
run:
path: git-repo/ci/scripts/promote.sh
---
platform: linux
inputs:
- name: git-repo
- name: artifactory-repo
outputs:
- name: version
params:
BINTRAY_REPO:
BINTRAY_SUBJECT:
BINTRAY_USERNAME:
BINTRAY_API_KEY:
SONATYPE_USER_TOKEN:
SONATYPE_PASSWORD_TOKEN:
run:
path: git-repo/ci/scripts/sync-to-maven-central.sh
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment