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
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* 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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* 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 String name;
|
||||
private final Resource resource;
|
||||
|
||||
private String sha256;
|
||||
private final String path;
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
DeployableArtifact(Resource resource, String path) {
|
||||
this.resource = resource;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public String getSha256() {
|
||||
return this.sha256;
|
||||
Resource getResource() {
|
||||
return this.resource;
|
||||
}
|
||||
|
||||
}
|
||||
String getPath() {
|
||||
return this.path;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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
|
||||
Reference in New Issue
Block a user