move RPM plugin package, add new Sonatype Publish plugin

main
Jörg Prante 2 months ago
parent 9784dc72d7
commit 017df58da6

@ -1,16 +1,14 @@
group = 'org.xbib.gradle.plugin'
wrapper {
gradleVersion = libs.versions.gradle.get()
distributionType = Wrapper.DistributionType.BIN
}
ext {
user = 'jprante'
name = 'gradle-plugins'
name = rootProject.name
description = 'Gradle plugins'
inceptionYear = '2021'
user = 'jprante'
url = 'https://github.com/' + user + '/' + name
scmUrl = 'https://github.com/' + user + '/' + name
scmConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git'

@ -35,7 +35,7 @@ if (project.hasProperty('gradle.publish.key')) {
version = project.version
description = 'Java implementation for RPM packaging'
displayName = 'Java implementation for RPM packaging'
implementationClass = 'org.xbib.gradle.plugin.RpmPlugin'
implementationClass = 'org.xbib.gradle.plugin.rpm.RpmPlugin'
tags.set(['rpm'])
}
}

@ -1,4 +1,4 @@
package org.xbib.gradle.plugin
package org.xbib.gradle.plugin.rpm
import org.gradle.api.internal.file.copy.CopyAction
import org.gradle.api.tasks.Input

@ -1,4 +1,4 @@
package org.xbib.gradle.plugin
package org.xbib.gradle.plugin.rpm
import groovy.util.logging.Log
import org.gradle.api.Project

@ -1,4 +1,4 @@
package org.xbib.gradle.plugin
package org.xbib.gradle.plugin.rpm
import org.gradle.api.Plugin
import org.gradle.api.Project

@ -1,9 +1,10 @@
package org.xbib.gradle.plugin
package org.xbib.gradle.plugin.rpm.test
import org.gradle.api.Project
import org.gradle.testfixtures.ProjectBuilder
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.xbib.gradle.plugin.rpm.Rpm
import org.xbib.rpm.RpmReader
import org.xbib.rpm.RpmReaderResult
import org.xbib.rpm.format.Format

@ -1,4 +1,4 @@
package org.xbib.gradle.plugin
package org.xbib.gradle.plugin.rpm.test
import org.gradle.testkit.runner.BuildResult
import org.gradle.testkit.runner.GradleRunner

@ -1,9 +1,10 @@
package org.xbib.gradle.plugin
package org.xbib.gradle.plugin.rpm.test
import org.gradle.api.Project
import org.gradle.testfixtures.ProjectBuilder
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.xbib.gradle.plugin.rpm.Rpm
import org.xbib.rpm.RpmReader
import org.xbib.rpm.changelog.ChangelogParser
import org.xbib.rpm.format.Format

@ -1,9 +1,10 @@
package org.xbib.gradle.plugin
package org.xbib.gradle.plugin.rpm.test
import org.gradle.api.Project
import org.gradle.testfixtures.ProjectBuilder
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.xbib.gradle.plugin.rpm.Rpm
import org.xbib.rpm.RpmReader
import org.xbib.rpm.signature.SignatureTag
import java.nio.file.Paths

@ -1,17 +1,16 @@
package org.xbib.gradle.plugin
package org.xbib.gradle.plugin.rpm.test
import groovy.util.logging.Log
import org.gradle.api.Project
import org.gradle.testfixtures.ProjectBuilder
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.xbib.gradle.plugin.rpm.Rpm
import org.xbib.rpm.RpmReaderResult
import org.xbib.rpm.RpmReader
import org.xbib.rpm.format.Format
import java.nio.file.Paths
import java.util.logging.Level
import java.util.logging.Logger
import static org.hamcrest.MatcherAssert.assertThat
import static org.hamcrest.CoreMatchers.*

@ -0,0 +1,42 @@
plugins {
id 'java-gradle-plugin'
alias(libs.plugins.publish)
}
apply plugin: 'java-gradle-plugin'
apply plugin: 'com.gradle.plugin-publish'
apply from: rootProject.file('gradle/compile/java.gradle')
apply from: rootProject.file('gradle/test/junit5.gradle')
dependencies {
api gradleApi()
testImplementation gradleTestKit()
}
publishing {
repositories {
maven {
name = 'localRepository'
url = 'build/local-repository'
}
}
}
if (project.hasProperty('gradle.publish.key')) {
gradlePlugin {
website = 'https://xbib.org/joerg/gradle-plugins/src/branch/main/gradle-plugin-sonatype-publish'
vcsUrl = 'https://xbib.org/joerg/gradle-plugins'
plugins {
sonatypePublishPlugin {
id = 'org.xbib.gradle.plugin.sonatype.publish'
group = project.group
version = project.version
description = 'Sonatype publishing plugin'
displayName = 'Sonatype publishing plugin'
implementationClass = 'org.xbib.gradle.plugin.sonatype.publish.SonatypePublishPlugin'
tags.set(['sonatype', 'publish', 'mavencentral'])
}
}
}
}

@ -0,0 +1,2 @@
name = gradle-plugin-sonatype-publish
version = 1.0.0

@ -0,0 +1,62 @@
package org.xbib.gradle.plugin.sonatype.publish;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.provider.Property;
import static org.xbib.gradle.plugin.sonatype.publish.SonatypePublishHelper.STATUS_URL;
import static org.xbib.gradle.plugin.sonatype.publish.SonatypePublishHelper.UPLOAD_URL;
public class DefaultSonatypePublishExtension implements SonatypePublishExtension {
private static final Integer MAX_WAIT_SECONDS = 60;
private final Property<String> uploadUrl;
private final Property<String> publishingType;
private final Property<String> statusUrl;
private final Property<String> authToken;
private final DirectoryProperty repoDir;
private final Property<Integer> maxWait;
public DefaultSonatypePublishExtension(ObjectFactory objectFactory) {
uploadUrl = objectFactory.property(String.class).convention(UPLOAD_URL);
statusUrl = objectFactory.property(String.class).convention(STATUS_URL);
publishingType = objectFactory.property(String.class).convention(PublishingType.AUTOMATIC.name());
authToken = objectFactory.property(String.class);
repoDir = objectFactory.directoryProperty();
maxWait = objectFactory.property(Integer.class).convention(MAX_WAIT_SECONDS);
}
@Override
public Property<String> getUploadUrl() {
return uploadUrl;
}
@Override
public Property<String> getPublishingType() { return publishingType; }
@Override
public Property<String> getStatusUrl() {
return statusUrl;
}
@Override
public Property<String> getAuthToken() {
return authToken;
}
@Override
public DirectoryProperty getRepoDir() {
return repoDir;
}
@Override
public Property<Integer> getMaxWait() {
return maxWait;
}
}

@ -0,0 +1,5 @@
package org.xbib.gradle.plugin.sonatype.publish;
enum DeploymentStatus {
FAILED, PUBLISHING, PUBLISHED, PENDING, VALIDATED;
}

@ -0,0 +1,18 @@
package org.xbib.gradle.plugin.sonatype.publish;
/**
* Determines whether to publish the artifact immediately, or to hold it back behind Publish button
* in the Sonatype Central Portal panel.
*/
public enum PublishingType {
/**
* The Publish button will be pressed for you immediately after a successful validation. This is
* the default.
*/
AUTOMATIC,
/**
* The Publish button will be there for you to press in the Sonatype Central Portal panel.
*/
USER_MANAGED,
}

@ -0,0 +1,52 @@
package org.xbib.gradle.plugin.sonatype.publish;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.provider.Property;
public interface SonatypePublishExtension {
/**
* Upload URL for uploading a deployment bundle.
*
* @return URL string
*/
Property<String> getUploadUrl();
/**
* Whether to publish automatically or manually after a successful upload.
*
* @return Publishing type
*/
Property<String> getPublishingType();
/**
* The URL for retrieving status of a deployment.
*
* @return URL string
*/
Property<String> getStatusUrl();
/**
* The authorization toke for calling central portal APIs
*
* @return Token string
*/
Property<String> getAuthToken();
/**
* The repository directory for zipping the bundle.
* It is usually a local directory published by the Maven publish plugin.
*
* @return Repository directory
*/
DirectoryProperty getRepoDir();
/**
* Max wait time for status API to get 'PUBLISHING' or 'PUBLISHED' status when the
* publishing type is 'AUTOMATIC', or additionally 'VALIDATED' when the publishing type is
* 'USER_MANAGED'.
*
* @return Duration in seconds
*/
Property<Integer> getMaxWait();
}

@ -0,0 +1,100 @@
package org.xbib.gradle.plugin.sonatype.publish;
import groovy.json.JsonSlurper;
import org.gradle.api.GradleException;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublisher;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.nio.file.Path;
import java.util.Map;
import java.util.UUID;
import static java.net.http.HttpRequest.BodyPublishers.noBody;
import static java.net.http.HttpRequest.BodyPublishers.ofFile;
import static java.net.http.HttpRequest.BodyPublishers.ofString;
import static java.nio.charset.StandardCharsets.UTF_8;
public class SonatypePublishHelper {
private static final Logger logger = Logging.getLogger(SonatypePublishHelper.class);
private static final String CRLF = "\r\n";
static final String UPLOAD_URL = "https://central.sonatype.com/api/v1/publisher/upload";
static final String STATUS_URL = "https://central.sonatype.com/api/v1/publisher/status";
static final String CHECKING_URL = "https://central.sonatype.com/publishing/deployments";
private final HttpClient httpClient;
public SonatypePublishHelper() {
httpClient = HttpClient.newHttpClient();
}
public String uploadBundle(String url,
String publishingType,
String token,
Path uploadFile) throws IOException, InterruptedException {
String boundary = UUID.randomUUID().toString().replace("-", "");
BodyPublisher filePartPublisher = getFilePartPublisher(boundary, uploadFile);
HttpRequest request = HttpRequest.newBuilder(URI.create(url + "?publishingType=" + publishingType))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(filePartPublisher)
.build();
logger.info("request = " + request);
HttpResponse<String> response = httpClient.send(request, BodyHandlers.ofString(UTF_8));
int statusCode = response.statusCode();
String body = response.body();
if (statusCode == 201) {
logger.lifecycle("Upload success, response body: {}", body);
return body;
} else {
throw new GradleException(String.format("upload failed, status code: %d, response body: %s", statusCode, body));
}
}
public String getDeploymentStatus(String url,
String token,
String deploymentId) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder(URI.create(url + "?id=" + deploymentId))
.header("Authorization", "Bearer " + token)
.POST(noBody())
.build();
logger.info("request = " + request);
HttpResponse<String> response = httpClient.send(request, BodyHandlers.ofString(UTF_8));
int statusCode = response.statusCode();
String body = response.body();
if (statusCode == 200) {
logger.lifecycle("checking deployment status, response body: {}", body);
@SuppressWarnings("unchecked")
Map<String, Object> jsonObject = (Map<String, Object>) new JsonSlurper().parseText(body);
return (String) jsonObject.get("deploymentState");
} else {
throw new GradleException(String.format("status failed, status code: %d, response body: %s", statusCode, body));
}
}
private static BodyPublisher getFilePartPublisher(String boundary, Path file) throws FileNotFoundException {
String boundaryStart = CRLF + "--" + boundary + CRLF;
String boundaryEnd = CRLF + "--" + boundary + "--";
String partMeta = getFilePartMeta(boundaryStart, file.getFileName());
return HttpRequest.BodyPublishers.concat(ofString(partMeta), ofFile(file), ofString(boundaryEnd));
}
private static String getFilePartMeta(String boundaryStart, Path fileName) {
return boundaryStart +
"Content-Disposition: form-data; name=\"bundle\"; filename=\"" + fileName + "\"" + CRLF +
"Content-Type: application/octet-stream" + CRLF +
CRLF;
}
}

@ -0,0 +1,21 @@
package org.xbib.gradle.plugin.sonatype.publish;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.tasks.bundling.Zip;
public class SonatypePublishPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
SonatypePublishExtension extension = project.getExtensions()
.create(SonatypePublishExtension.class, "sonatypePublish", DefaultSonatypePublishExtension.class, project.getObjects());
Zip zipTask = project.getTasks().register("bundleForPublish", Zip.class).get();
zipTask.setGroup("publishing");
zipTask.from(extension.getRepoDir());
SonatypePublishTask sonatypePublishTask = project.getTasks().register("publishToSonatype", SonatypePublishTask.class).get();
sonatypePublishTask.setGroup("publishing");
sonatypePublishTask.dependsOn(zipTask);
sonatypePublishTask.setUploadFile(zipTask.getArchiveFile());
}
}

@ -0,0 +1,87 @@
package org.xbib.gradle.plugin.sonatype.publish;
import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.InvalidUserDataException;
import org.gradle.api.file.RegularFile;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.TaskAction;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import static org.xbib.gradle.plugin.sonatype.publish.DeploymentStatus.FAILED;
import static org.xbib.gradle.plugin.sonatype.publish.DeploymentStatus.PUBLISHED;
import static org.xbib.gradle.plugin.sonatype.publish.DeploymentStatus.PUBLISHING;
import static org.xbib.gradle.plugin.sonatype.publish.DeploymentStatus.VALIDATED;
import static org.xbib.gradle.plugin.sonatype.publish.PublishingType.USER_MANAGED;
import static org.xbib.gradle.plugin.sonatype.publish.SonatypePublishHelper.CHECKING_URL;
public abstract class SonatypePublishTask extends DefaultTask {
private final SonatypePublishExtension extension;
private final SonatypePublishHelper sonatypePublishHelper;
@SuppressWarnings("this-escape")
public SonatypePublishTask() {
this.sonatypePublishHelper = new SonatypePublishHelper();
SonatypePublishExtension extension = getProject().getExtensions().findByType(SonatypePublishExtension.class);
if (extension == null) {
extension = getProject().getExtensions().create(SonatypePublishExtension.class, "sonatypePublish",
DefaultSonatypePublishExtension.class, getProject().getObjects());
}
this.extension = Objects.requireNonNull(extension);
}
@TaskAction
public void executeTask() throws InterruptedException, IOException {
if (!extension.getAuthToken().isPresent()) {
throw new InvalidUserDataException("auth token is not provided for Sonatype publishing");
}
RegularFileProperty regularFileProperty = getProject().getObjects().fileProperty();
if (!regularFileProperty.isPresent()) {
throw new InvalidUserDataException("no upload file");
}
PublishingType type = PublishingType.valueOf(extension.getPublishingType().get().toUpperCase(Locale.ROOT));
Path deployment = regularFileProperty.get().getAsFile().toPath();
getLogger().lifecycle("Sonatype publishing deployment = {}", deployment);
String deploymentId = sonatypePublishHelper.uploadBundle(extension.getUploadUrl().get(),
extension.getPublishingType().get(), extension.getAuthToken().get(), deployment);
getLogger().lifecycle("Sonatype publishing deployment ID = {}", deploymentId);
int seconds = 10;
int count = 0;
int checkCount = extension.getMaxWait().get() / seconds;
checkCount = checkCount <= 0 ? 1 : checkCount;
while (count < checkCount) {
TimeUnit.SECONDS.sleep(seconds);
DeploymentStatus deploymentStatus = DeploymentStatus.valueOf(sonatypePublishHelper.getDeploymentStatus(extension.getStatusUrl().get(),
extension.getAuthToken().get(), deploymentId).toUpperCase(Locale.ROOT));
boolean success = (type.equals(USER_MANAGED) && VALIDATED.equals(deploymentStatus))
|| PUBLISHING.equals(deploymentStatus)
|| PUBLISHED.equals(deploymentStatus);
if (success) {
getLogger().lifecycle("Sonatype publishing successful. Status = {}", deploymentStatus);
return;
}
else if (FAILED.equals(deploymentStatus)) {
throw new GradleException(String.format("Deployment failed: %s, please visit %s and check your deployment",
deploymentStatus.name(), CHECKING_URL));
} else {
++count;
}
if (count == checkCount) {
throw new GradleException(String.format("Deployment timed out, status is: %s, please visit %s check your deployment",
deploymentStatus.name(), CHECKING_URL));
}
}
}
public void setUploadFile(Provider<RegularFile> regularFile) {
getProject().getObjects().fileProperty().value(regularFile);
}
}

@ -0,0 +1,77 @@
package org.xbib.gradle.plugin.sonatype.publish.test;
import org.gradle.testkit.runner.BuildResult;
import org.gradle.testkit.runner.BuildTask;
import org.gradle.testkit.runner.GradleRunner;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.gradle.testkit.runner.TaskOutcome.FAILED;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class SonatypePublishPluginIntegrationTest {
@TempDir
Path testProjectDir;
private Path settingsFile;
private Path buildFile;
@BeforeEach
public void setup() {
settingsFile = testProjectDir.resolve("settings.gradle");
buildFile = testProjectDir.resolve("build.gradle");
}
@Test
public void testSonatypePublish() throws IOException {
writeFile(settingsFile, "rootProject.name = 'sonatype-publish-test'");
String buildFileContent = """
plugins {
id 'org.xbib.gradle.plugin.sonatype.publish'
}
import org.xbib.gradle.plugin.sonatype.publish.SonatypePublishTask
apply plugin: 'java-library'
apply plugin: 'maven-publish'
apply plugin: 'signing'
apply plugin: 'org.xbib.gradle.plugin.sonatype.publish'
sonatypePublish {
uploadUrl = 'http://localhost'
repoDir = layout.buildDirectory.dir('repos/bundles')
authToken = 'dummy'
publishingType = 'AUTOMATIC'
maxWait = 10
}
""";
writeFile(buildFile, buildFileContent);
assertThrows(Exception.class, ()-> {
BuildResult buildResult = GradleRunner.create()
.withProjectDir(testProjectDir.toFile())
.withArguments(":publish", ":publishToSonatype")
.withPluginClasspath()
.forwardOutput()
.build();
BuildTask buildTask = buildResult.task(":publishToSonatype");
// "no upload file"
assertEquals(FAILED, buildTask.getOutcome());
});
}
private void writeFile(Path destination, String content) throws IOException {
try (BufferedWriter output = Files.newBufferedWriter(destination)) {
output.write(content);
}
}
}

@ -1,5 +1,3 @@
group = org.xbib.gradle.plugin
name = gradle-plugins
# version reflects gradle version plus a patch level version
version = 8.7.0

@ -13,6 +13,7 @@ test {
failFast = true
testLogging {
events 'STARTED', 'PASSED', 'FAILED', 'SKIPPED'
showStandardStreams = true
}
afterSuite { desc, result ->
if (!desc.parent) {

@ -9,7 +9,7 @@ dependencyResolutionManagement {
// Attention: it is impossible to develop a gradle plugin with groovy 4!
// The gradle plugin publish plugin enforces java-gradle-plugin,
// and java-gradle-plugin enforces the embedded groovy of gradle on the compile classpath.
// we keep this here as reference when Gradle switsches to Groovy 4+
// we keep this here as reference when Gradle switches to Groovy 4+
//library('groovy-bom', 'org.apache.groovy', 'groovy-bom').versionRef('groovy')
//library('groovy', 'org.apache.groovy', 'groovy').versionRef('groovy')
library('asm', 'org.ow2.asm', 'asm').versionRef('asm')
@ -57,3 +57,4 @@ include 'gradle-plugin-jlink'
include 'gradle-plugin-jpackage'
include 'gradle-plugin-rpm'
include 'gradle-plugin-shadow'
include 'gradle-plugin-sonatype-publish'

Loading…
Cancel
Save