From 531b21aabcae2ec89acf334e17767d5ee28b2cee Mon Sep 17 00:00:00 2001 From: pernyf Date: Wed, 23 Jul 2025 20:49:09 +0200 Subject: [PATCH 01/25] WIP: initial attempt. --- .../maven/plugins/deploy/BundleService.java | 101 ++++++++++++++++++ .../plugins/deploy/CentralBundleMojo.java | 50 +++++++++ .../plugins/deploy/CentralPortalClient.java | 96 +++++++++++++++++ .../plugins/deploy/CentralReleaseMojo.java | 86 +++++++++++++++ 4 files changed, 333 insertions(+) create mode 100644 src/main/java/org/apache/maven/plugins/deploy/BundleService.java create mode 100644 src/main/java/org/apache/maven/plugins/deploy/CentralBundleMojo.java create mode 100644 src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java create mode 100644 src/main/java/org/apache/maven/plugins/deploy/CentralReleaseMojo.java diff --git a/src/main/java/org/apache/maven/plugins/deploy/BundleService.java b/src/main/java/org/apache/maven/plugins/deploy/BundleService.java new file mode 100644 index 0000000..7e95a02 --- /dev/null +++ b/src/main/java/org/apache/maven/plugins/deploy/BundleService.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.plugins.deploy; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.security.DigestOutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.apache.maven.api.Project; +import org.apache.maven.api.Session; +import org.apache.maven.api.plugin.Log; + +public class BundleService { + + Project project; + Session session; + Log log; + + public BundleService(Project project, Session session, Log log) { + this.project = project; + this.session = session; + this.log = log; + } + + static final List CHECKSUM_ALGOS = List.of("MD5", "SHA-1", "SHA-256"); + + public void createZipBundle(File bundleFile) throws IOException, NoSuchAlgorithmException { + List projects = session.getProjects(); + try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(bundleFile))) { + for (Project subproject : projects) { + File artifactDir = new File(subproject.getBuild().getDirectory()); + File[] files = artifactDir.listFiles( + (dir, name) -> name.endsWith(".jar") || name.endsWith(".pom") || name.endsWith(".asc")); + + if (files != null) { + for (File file : files) { + zipOut.putNextEntry(new ZipEntry(subproject.getArtifactId() + "/" + file.getName())); + Files.copy(file.toPath(), zipOut); + zipOut.closeEntry(); + + for (String algo : CHECKSUM_ALGOS) { + File checksumFile = generateChecksum(file, algo); + zipOut.putNextEntry( + new ZipEntry(subproject.getArtifactId() + "/" + checksumFile.getName())); + Files.copy(checksumFile.toPath(), zipOut); + zipOut.closeEntry(); + } + } + } + } + } + log.info("Created bundle at: " + bundleFile.getAbsolutePath()); + } + + private File generateChecksum(File file, String algo) throws NoSuchAlgorithmException, IOException { + String extension = algo.toLowerCase().replace("-", ""); + File checksumFile = new File(file.getAbsolutePath() + "." + extension); + if (checksumFile.exists()) { + return checksumFile; + } + + MessageDigest digest = MessageDigest.getInstance(algo); + try (InputStream is = new FileInputStream(file); + DigestOutputStream dos = new DigestOutputStream(OutputStream.nullOutputStream(), digest)) { + is.transferTo(dos); + } + + StringBuilder sb = new StringBuilder(); + for (byte b : digest.digest()) { + sb.append(String.format("%02x", b)); + } + Files.writeString(checksumFile.toPath(), sb.toString()); + return checksumFile; + } +} diff --git a/src/main/java/org/apache/maven/plugins/deploy/CentralBundleMojo.java b/src/main/java/org/apache/maven/plugins/deploy/CentralBundleMojo.java new file mode 100644 index 0000000..a15742c --- /dev/null +++ b/src/main/java/org/apache/maven/plugins/deploy/CentralBundleMojo.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.plugins.deploy; + +import java.io.File; + +import org.apache.maven.api.Project; +import org.apache.maven.api.plugin.MojoException; +import org.apache.maven.api.plugin.annotations.Mojo; +import org.apache.maven.api.plugin.annotations.Parameter; + +/** + * mvn deploy:bundle + */ +@Mojo(name = "bundle", defaultPhase = "package") +public class CentralBundleMojo extends AbstractDeployMojo { + + @Parameter(defaultValue = "${project}", readonly = true) + private Project project; + + @Override + public void execute() throws MojoException { + File targetDir = new File(project.getBuild().getDirectory()); + File bundleFile = new File(targetDir, project.getArtifactId() + "-" + project.getVersion() + "-bundle.zip"); + + try { + BundleService bundleService = new BundleService(project, session, getLog()); + bundleService.createZipBundle(bundleFile); + getLog().info("Bundle created successfully: " + bundleFile); + } catch (Exception e) { + throw new MojoException("Failed to create bundle", e); + } + } +} diff --git a/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java b/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java new file mode 100644 index 0000000..2baf38e --- /dev/null +++ b/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.plugins.deploy; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.file.Files; +import java.util.Base64; + +public class CentralPortalClient { + + static final String CENTRAL_PORTAL_URL = "https://central.sonatype.com/api/v1"; + + private final String username; + private final String password; + private final String publishUrl; + + public CentralPortalClient(String username, String password, String publishUrl) { + this.username = username; + this.password = password; + this.publishUrl = (publishUrl != null && !publishUrl.isBlank()) ? publishUrl : CENTRAL_PORTAL_URL; + } + + public String upload(File bundle) throws IOException { + String boundary = "----MavenCentralBoundary" + System.currentTimeMillis(); + URL url = new URL(publishUrl + "/publisher/upload?publishingType=AUTOMATIC"); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setDoOutput(true); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Authorization", authHeader()); + conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary); + + try (OutputStream out = conn.getOutputStream(); + PrintWriter writer = new PrintWriter(new OutputStreamWriter(out))) { + writer.append("--").append(boundary).append("\r\n"); + writer.append("Content-Disposition: form-data; name=\"bundle\"; filename=\"") + .append(bundle.getName()) + .append("\"\r\n"); + writer.append("Content-Type: application/zip\r\n\r\n").flush(); + Files.copy(bundle.toPath(), out); + out.flush(); + writer.append("\r\n--").append(boundary).append("--\r\n").flush(); + } + + int status = conn.getResponseCode(); + if (status != HttpURLConnection.HTTP_OK) { + throw new IOException("Failed to upload: HTTP " + status); + } + + try (InputStream in = conn.getInputStream()) { + return new String(in.readAllBytes()); + } + } + + public String getStatus(String deploymentId) throws IOException { + URL url = new URL(publishUrl + "/publisher/status?id=" + deploymentId); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Authorization", authHeader()); + + int status = conn.getResponseCode(); + if (status != HttpURLConnection.HTTP_OK) { + throw new IOException("Failed to get status: HTTP " + status); + } + + try (InputStream in = conn.getInputStream()) { + return new String(in.readAllBytes()); + } + } + + private String authHeader() { + return "Bearer " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes()); + } +} diff --git a/src/main/java/org/apache/maven/plugins/deploy/CentralReleaseMojo.java b/src/main/java/org/apache/maven/plugins/deploy/CentralReleaseMojo.java new file mode 100644 index 0000000..7b8b431 --- /dev/null +++ b/src/main/java/org/apache/maven/plugins/deploy/CentralReleaseMojo.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.plugins.deploy; + +import java.io.File; +import java.util.List; + +import org.apache.maven.api.Project; +import org.apache.maven.api.plugin.MojoException; +import org.apache.maven.api.plugin.annotations.Mojo; +import org.apache.maven.api.plugin.annotations.Parameter; + +import static org.apache.maven.plugins.deploy.CentralPortalClient.CENTRAL_PORTAL_URL; + +/** + * mvn deploy:release + */ +@Mojo(name = "release", defaultPhase = "deploy") +public class CentralReleaseMojo extends AbstractDeployMojo { + + @Parameter(defaultValue = "${project}", readonly = true) + private Project project; + + @Parameter(property = "central.username") + private String username; + + @Parameter(property = "central.password") + private String password; + + @Parameter(property = "central.url", defaultValue = CENTRAL_PORTAL_URL) + private String centralUrl; + + private static final List CHECKSUM_ALGOS = List.of("MD5", "SHA-1"); + + @Override + public void execute() throws MojoException { + File targetDir = new File(project.getBuild().getDirectory()); + File bundleFile = new File(targetDir, project.getArtifactId() + "-" + project.getVersion() + "-bundle.zip"); + + try { + BundleService bundleService = new BundleService(project, session, getLog()); + bundleService.createZipBundle(bundleFile); + + CentralPortalClient client = new CentralPortalClient(username, password, centralUrl); + + String deploymentId = client.upload(bundleFile); + if (deploymentId == null) { + throw new MojoException("Failed to upload bundle"); + } + + getLog().info("Deployment ID: " + deploymentId); + String status = client.getStatus(deploymentId); + + int retries = 10; + while (!List.of("PUBLISHING", "PUBLISHED", "FAILED").contains(status) && retries-- > 0) { + getLog().info("Deploy status is " + status); + Thread.sleep(10000); + status = client.getStatus(deploymentId); + } + + switch (status) { + case "PUBLISHING" -> getLog().info("Published: Project is publishing on Central"); + case "PUBLISHED" -> getLog().info("Published successfully"); + default -> throw new MojoException("Release failed with status: " + status); + } + } catch (Exception e) { + throw new MojoException("Release process failed", e); + } + } +} From a1547e93e5a4e13b77ef6e7d2cdc5e3ff31f6103 Mon Sep 17 00:00:00 2001 From: per Date: Wed, 23 Jul 2025 21:11:22 +0200 Subject: [PATCH 02/25] adopt to java 8 --- .../maven/plugins/deploy/BundleService.java | 45 +++++++++++-------- .../plugins/deploy/CentralBundleMojo.java | 25 ++++++----- .../plugins/deploy/CentralPortalClient.java | 20 +++++++-- .../plugins/deploy/CentralReleaseMojo.java | 40 ++++++++++------- 4 files changed, 82 insertions(+), 48 deletions(-) diff --git a/src/main/java/org/apache/maven/plugins/deploy/BundleService.java b/src/main/java/org/apache/maven/plugins/deploy/BundleService.java index 7e95a02..7e63812 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/BundleService.java +++ b/src/main/java/org/apache/maven/plugins/deploy/BundleService.java @@ -19,41 +19,38 @@ package org.apache.maven.plugins.deploy; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.security.DigestOutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.Arrays; import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; -import org.apache.maven.api.Project; -import org.apache.maven.api.Session; -import org.apache.maven.api.plugin.Log; +import org.apache.maven.plugin.logging.Log; +import org.apache.maven.project.MavenProject; public class BundleService { - Project project; - Session session; + MavenProject project; Log log; - public BundleService(Project project, Session session, Log log) { + public BundleService(MavenProject project, Log log) { this.project = project; - this.session = session; this.log = log; } - static final List CHECKSUM_ALGOS = List.of("MD5", "SHA-1", "SHA-256"); + static final List CHECKSUM_ALGOS = Arrays.asList("MD5", "SHA-1", "SHA-256"); - public void createZipBundle(File bundleFile) throws IOException, NoSuchAlgorithmException { - List projects = session.getProjects(); - try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(bundleFile))) { - for (Project subproject : projects) { + public void createZipBundle(File bundleFile, List projects) + throws IOException, NoSuchAlgorithmException { + try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(bundleFile.toPath()))) { + for (MavenProject subproject : projects) { File artifactDir = new File(subproject.getBuild().getDirectory()); File[] files = artifactDir.listFiles( (dir, name) -> name.endsWith(".jar") || name.endsWith(".pom") || name.endsWith(".asc")); @@ -78,7 +75,7 @@ public void createZipBundle(File bundleFile) throws IOException, NoSuchAlgorithm log.info("Created bundle at: " + bundleFile.getAbsolutePath()); } - private File generateChecksum(File file, String algo) throws NoSuchAlgorithmException, IOException { + public File generateChecksum(File file, String algo) throws NoSuchAlgorithmException, IOException { String extension = algo.toLowerCase().replace("-", ""); File checksumFile = new File(file.getAbsolutePath() + "." + extension); if (checksumFile.exists()) { @@ -86,16 +83,26 @@ private File generateChecksum(File file, String algo) throws NoSuchAlgorithmExce } MessageDigest digest = MessageDigest.getInstance(algo); - try (InputStream is = new FileInputStream(file); - DigestOutputStream dos = new DigestOutputStream(OutputStream.nullOutputStream(), digest)) { - is.transferTo(dos); + try (InputStream is = Files.newInputStream(file.toPath()); + OutputStream nullOut = new OutputStream() { + @Override + public void write(int b) {} + }; + DigestOutputStream dos = new DigestOutputStream(nullOut, digest)) { + + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = is.read(buffer)) != -1) { + dos.write(buffer, 0, bytesRead); + } } StringBuilder sb = new StringBuilder(); for (byte b : digest.digest()) { sb.append(String.format("%02x", b)); } - Files.writeString(checksumFile.toPath(), sb.toString()); + + Files.write(checksumFile.toPath(), sb.toString().getBytes(StandardCharsets.UTF_8)); return checksumFile; } } diff --git a/src/main/java/org/apache/maven/plugins/deploy/CentralBundleMojo.java b/src/main/java/org/apache/maven/plugins/deploy/CentralBundleMojo.java index a15742c..e0ecdfa 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/CentralBundleMojo.java +++ b/src/main/java/org/apache/maven/plugins/deploy/CentralBundleMojo.java @@ -19,32 +19,37 @@ package org.apache.maven.plugins.deploy; import java.io.File; +import java.util.List; -import org.apache.maven.api.Project; -import org.apache.maven.api.plugin.MojoException; -import org.apache.maven.api.plugin.annotations.Mojo; -import org.apache.maven.api.plugin.annotations.Parameter; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; /** * mvn deploy:bundle */ -@Mojo(name = "bundle", defaultPhase = "package") +@Mojo(name = "bundle", defaultPhase = LifecyclePhase.DEPLOY) public class CentralBundleMojo extends AbstractDeployMojo { @Parameter(defaultValue = "${project}", readonly = true) - private Project project; + private MavenProject project; + + @Parameter(defaultValue = "${reactorProjects}", required = true, readonly = true) + private List reactorProjects; @Override - public void execute() throws MojoException { + public void execute() throws MojoExecutionException { File targetDir = new File(project.getBuild().getDirectory()); File bundleFile = new File(targetDir, project.getArtifactId() + "-" + project.getVersion() + "-bundle.zip"); try { - BundleService bundleService = new BundleService(project, session, getLog()); - bundleService.createZipBundle(bundleFile); + BundleService bundleService = new BundleService(project, getLog()); + bundleService.createZipBundle(bundleFile, reactorProjects); getLog().info("Bundle created successfully: " + bundleFile); } catch (Exception e) { - throw new MojoException("Failed to create bundle", e); + throw new MojoExecutionException("Failed to create bundle", e); } } } diff --git a/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java b/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java index 2baf38e..67c0913 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java +++ b/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java @@ -18,6 +18,7 @@ */ package org.apache.maven.plugins.deploy; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -40,7 +41,7 @@ public class CentralPortalClient { public CentralPortalClient(String username, String password, String publishUrl) { this.username = username; this.password = password; - this.publishUrl = (publishUrl != null && !publishUrl.isBlank()) ? publishUrl : CENTRAL_PORTAL_URL; + this.publishUrl = (publishUrl != null && !publishUrl.trim().isEmpty()) ? publishUrl : CENTRAL_PORTAL_URL; } public String upload(File bundle) throws IOException { @@ -54,13 +55,16 @@ public String upload(File bundle) throws IOException { try (OutputStream out = conn.getOutputStream(); PrintWriter writer = new PrintWriter(new OutputStreamWriter(out))) { + writer.append("--").append(boundary).append("\r\n"); writer.append("Content-Disposition: form-data; name=\"bundle\"; filename=\"") .append(bundle.getName()) .append("\"\r\n"); writer.append("Content-Type: application/zip\r\n\r\n").flush(); + Files.copy(bundle.toPath(), out); out.flush(); + writer.append("\r\n--").append(boundary).append("--\r\n").flush(); } @@ -70,7 +74,7 @@ public String upload(File bundle) throws IOException { } try (InputStream in = conn.getInputStream()) { - return new String(in.readAllBytes()); + return readFully(in); } } @@ -86,11 +90,21 @@ public String getStatus(String deploymentId) throws IOException { } try (InputStream in = conn.getInputStream()) { - return new String(in.readAllBytes()); + return readFully(in); } } private String authHeader() { return "Bearer " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes()); } + + private String readFully(InputStream in) throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + byte[] chunk = new byte[8192]; + int bytesRead; + while ((bytesRead = in.read(chunk)) != -1) { + buffer.write(chunk, 0, bytesRead); + } + return buffer.toString("UTF-8"); + } } diff --git a/src/main/java/org/apache/maven/plugins/deploy/CentralReleaseMojo.java b/src/main/java/org/apache/maven/plugins/deploy/CentralReleaseMojo.java index 7b8b431..8680f9a 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/CentralReleaseMojo.java +++ b/src/main/java/org/apache/maven/plugins/deploy/CentralReleaseMojo.java @@ -19,23 +19,25 @@ package org.apache.maven.plugins.deploy; import java.io.File; +import java.util.Arrays; import java.util.List; -import org.apache.maven.api.Project; -import org.apache.maven.api.plugin.MojoException; -import org.apache.maven.api.plugin.annotations.Mojo; -import org.apache.maven.api.plugin.annotations.Parameter; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.project.MavenProject; import static org.apache.maven.plugins.deploy.CentralPortalClient.CENTRAL_PORTAL_URL; /** * mvn deploy:release */ -@Mojo(name = "release", defaultPhase = "deploy") +@Mojo(name = "release", defaultPhase = LifecyclePhase.DEPLOY) public class CentralReleaseMojo extends AbstractDeployMojo { @Parameter(defaultValue = "${project}", readonly = true) - private Project project; + private MavenProject project; @Parameter(property = "central.username") private String username; @@ -46,41 +48,47 @@ public class CentralReleaseMojo extends AbstractDeployMojo { @Parameter(property = "central.url", defaultValue = CENTRAL_PORTAL_URL) private String centralUrl; - private static final List CHECKSUM_ALGOS = List.of("MD5", "SHA-1"); + @Parameter(defaultValue = "${reactorProjects}", required = true, readonly = true) + private List reactorProjects; @Override - public void execute() throws MojoException { + public void execute() throws MojoExecutionException { File targetDir = new File(project.getBuild().getDirectory()); File bundleFile = new File(targetDir, project.getArtifactId() + "-" + project.getVersion() + "-bundle.zip"); try { - BundleService bundleService = new BundleService(project, session, getLog()); - bundleService.createZipBundle(bundleFile); + BundleService bundleService = new BundleService(project, getLog()); + bundleService.createZipBundle(bundleFile, reactorProjects); CentralPortalClient client = new CentralPortalClient(username, password, centralUrl); String deploymentId = client.upload(bundleFile); if (deploymentId == null) { - throw new MojoException("Failed to upload bundle"); + throw new MojoExecutionException("Failed to upload bundle"); } getLog().info("Deployment ID: " + deploymentId); String status = client.getStatus(deploymentId); int retries = 10; - while (!List.of("PUBLISHING", "PUBLISHED", "FAILED").contains(status) && retries-- > 0) { + while (!Arrays.asList("PUBLISHING", "PUBLISHED", "FAILED").contains(status) && retries-- > 0) { getLog().info("Deploy status is " + status); Thread.sleep(10000); status = client.getStatus(deploymentId); } switch (status) { - case "PUBLISHING" -> getLog().info("Published: Project is publishing on Central"); - case "PUBLISHED" -> getLog().info("Published successfully"); - default -> throw new MojoException("Release failed with status: " + status); + case "PUBLISHING": + getLog().info("Published: Project is publishing on Central"); + break; + case "PUBLISHED": + getLog().info("Published successfully"); + break; + default: + throw new MojoExecutionException("Release failed with status: " + status); } } catch (Exception e) { - throw new MojoException("Release process failed", e); + throw new MojoExecutionException("Release process failed", e); } } } From a388f81b87a48e7a94a5a1e49ecdb7778dbde305 Mon Sep 17 00:00:00 2001 From: per Date: Fri, 25 Jul 2025 14:29:17 +0200 Subject: [PATCH 03/25] working version --- .../maven/plugins/deploy/BundleService.java | 80 ++++++++++++++----- .../plugins/deploy/CentralBundleMojo.java | 6 +- .../plugins/deploy/CentralReleaseMojo.java | 6 +- 3 files changed, 64 insertions(+), 28 deletions(-) diff --git a/src/main/java/org/apache/maven/plugins/deploy/BundleService.java b/src/main/java/org/apache/maven/plugins/deploy/BundleService.java index 7e63812..9f7dfcd 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/BundleService.java +++ b/src/main/java/org/apache/maven/plugins/deploy/BundleService.java @@ -32,6 +32,7 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; +import org.apache.maven.MavenExecutionException; import org.apache.maven.plugin.logging.Log; import org.apache.maven.project.MavenProject; @@ -47,34 +48,77 @@ public BundleService(MavenProject project, Log log) { static final List CHECKSUM_ALGOS = Arrays.asList("MD5", "SHA-1", "SHA-256"); - public void createZipBundle(File bundleFile, List projects) - throws IOException, NoSuchAlgorithmException { + /** + * This requires the "install" phase has been executed and gpg signing has been configured. + * e.g. mvn install deploy:bundle + * + * @param bundleFile the zip file to create + * @throws IOException if creating the zip file failed + * @throws NoSuchAlgorithmException if md5, sha-1 or sha-256 algorithms are not available in + * the environment. + */ + public void createZipBundle(File bundleFile) throws IOException, NoSuchAlgorithmException, MavenExecutionException { + bundleFile.getParentFile().mkdirs(); + bundleFile.createNewFile(); + String groupId = project.getGroupId(); + String artifactId = project.getArtifactId(); + String version = project.getVersion(); + String groupPath = groupId.replace('.', '/'); + String mavenPathPrefix = String.join("/", groupPath, artifactId, version) + "/"; try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(bundleFile.toPath()))) { - for (MavenProject subproject : projects) { - File artifactDir = new File(subproject.getBuild().getDirectory()); - File[] files = artifactDir.listFiles( - (dir, name) -> name.endsWith(".jar") || name.endsWith(".pom") || name.endsWith(".asc")); - if (files != null) { - for (File file : files) { - zipOut.putNextEntry(new ZipEntry(subproject.getArtifactId() + "/" + file.getName())); - Files.copy(file.toPath(), zipOut); - zipOut.closeEntry(); + File artifactDir = new File(project.getBuild().getDirectory()); + File[] files = artifactDir.listFiles( + (dir, name) -> name.endsWith(".jar") || name.endsWith(".pom") || name.endsWith(".asc")); - for (String algo : CHECKSUM_ALGOS) { - File checksumFile = generateChecksum(file, algo); - zipOut.putNextEntry( - new ZipEntry(subproject.getArtifactId() + "/" + checksumFile.getName())); - Files.copy(checksumFile.toPath(), zipOut); - zipOut.closeEntry(); - } + int ascCount = 0; + if (files != null) { + for (File file : files) { + zipOut.putNextEntry(new ZipEntry(mavenPathPrefix + file.getName())); + Files.copy(file.toPath(), zipOut); + zipOut.closeEntry(); + if (file.getName().endsWith(".asc")) { + ascCount++; + continue; // No checksums for asc files } + generateChecksumsAndAddToZip(file, mavenPathPrefix, zipOut); } } + // This is a bit crude, but there should be sign files for pom, jar, sourceJar, javadocJar + // unless the project is an aggregator + int expectedArtifactCount = 4; + if (project.getPackaging().equals("pom")) { + expectedArtifactCount = 1; + } + if (ascCount != expectedArtifactCount) { + log.warn("Expected " + expectedArtifactCount + " asc file(s) but found " + ascCount); + if (ascCount == 0) { + log.error("The artifacts were not signed!"); + } else { + if (expectedArtifactCount == 1) { + log.error("There should only be one asc file for the pom"); + } else { + log.error("There should be 4 signed artifacts (pom, jar, sourceJar, javadocJar)"); + } + } + log.error("This bundle will not be deployable!"); + throw new MavenExecutionException( + "Missing sign files (asc files) detected, bundle is NOT valid", project.getFile()); + } } log.info("Created bundle at: " + bundleFile.getAbsolutePath()); } + private void generateChecksumsAndAddToZip(File sourceFile, String prefix, ZipOutputStream zipOut) + throws NoSuchAlgorithmException, IOException { + for (String algo : CHECKSUM_ALGOS) { + File checksumFile = generateChecksum(sourceFile, algo); + zipOut.putNextEntry(new ZipEntry(prefix + checksumFile.getName())); + Files.copy(checksumFile.toPath(), zipOut); + zipOut.closeEntry(); + } + } + public File generateChecksum(File file, String algo) throws NoSuchAlgorithmException, IOException { String extension = algo.toLowerCase().replace("-", ""); File checksumFile = new File(file.getAbsolutePath() + "." + extension); diff --git a/src/main/java/org/apache/maven/plugins/deploy/CentralBundleMojo.java b/src/main/java/org/apache/maven/plugins/deploy/CentralBundleMojo.java index e0ecdfa..445199e 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/CentralBundleMojo.java +++ b/src/main/java/org/apache/maven/plugins/deploy/CentralBundleMojo.java @@ -19,7 +19,6 @@ package org.apache.maven.plugins.deploy; import java.io.File; -import java.util.List; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.LifecyclePhase; @@ -36,9 +35,6 @@ public class CentralBundleMojo extends AbstractDeployMojo { @Parameter(defaultValue = "${project}", readonly = true) private MavenProject project; - @Parameter(defaultValue = "${reactorProjects}", required = true, readonly = true) - private List reactorProjects; - @Override public void execute() throws MojoExecutionException { File targetDir = new File(project.getBuild().getDirectory()); @@ -46,7 +42,7 @@ public void execute() throws MojoExecutionException { try { BundleService bundleService = new BundleService(project, getLog()); - bundleService.createZipBundle(bundleFile, reactorProjects); + bundleService.createZipBundle(bundleFile); getLog().info("Bundle created successfully: " + bundleFile); } catch (Exception e) { throw new MojoExecutionException("Failed to create bundle", e); diff --git a/src/main/java/org/apache/maven/plugins/deploy/CentralReleaseMojo.java b/src/main/java/org/apache/maven/plugins/deploy/CentralReleaseMojo.java index 8680f9a..4504957 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/CentralReleaseMojo.java +++ b/src/main/java/org/apache/maven/plugins/deploy/CentralReleaseMojo.java @@ -20,7 +20,6 @@ import java.io.File; import java.util.Arrays; -import java.util.List; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.LifecyclePhase; @@ -48,9 +47,6 @@ public class CentralReleaseMojo extends AbstractDeployMojo { @Parameter(property = "central.url", defaultValue = CENTRAL_PORTAL_URL) private String centralUrl; - @Parameter(defaultValue = "${reactorProjects}", required = true, readonly = true) - private List reactorProjects; - @Override public void execute() throws MojoExecutionException { File targetDir = new File(project.getBuild().getDirectory()); @@ -58,7 +54,7 @@ public void execute() throws MojoExecutionException { try { BundleService bundleService = new BundleService(project, getLog()); - bundleService.createZipBundle(bundleFile, reactorProjects); + bundleService.createZipBundle(bundleFile); CentralPortalClient client = new CentralPortalClient(username, password, centralUrl); From 2263a1373db8eaf26a12d01af0c96555dab6a8d8 Mon Sep 17 00:00:00 2001 From: per Date: Fri, 25 Jul 2025 23:42:11 +0200 Subject: [PATCH 04/25] improve the publish artifacts retrieval. --- .../maven/plugins/deploy/BundleService.java | 81 ++++++++++--------- .../plugins/deploy/CentralBundleMojo.java | 3 +- .../plugins/deploy/CentralPortalClient.java | 44 ++++++++-- .../plugins/deploy/CentralReleaseMojo.java | 21 +++-- 4 files changed, 97 insertions(+), 52 deletions(-) diff --git a/src/main/java/org/apache/maven/plugins/deploy/BundleService.java b/src/main/java/org/apache/maven/plugins/deploy/BundleService.java index 9f7dfcd..901484c 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/BundleService.java +++ b/src/main/java/org/apache/maven/plugins/deploy/BundleService.java @@ -27,12 +27,14 @@ import java.security.DigestOutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; -import org.apache.maven.MavenExecutionException; +import org.apache.maven.artifact.Artifact; +import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.logging.Log; import org.apache.maven.project.MavenProject; @@ -49,7 +51,7 @@ public BundleService(MavenProject project, Log log) { static final List CHECKSUM_ALGOS = Arrays.asList("MD5", "SHA-1", "SHA-256"); /** - * This requires the "install" phase has been executed and gpg signing has been configured. + * This method requires that the "verify" phase has been executed and gpg signing has been configured. * e.g. mvn install deploy:bundle * * @param bundleFile the zip file to create @@ -57,7 +59,7 @@ public BundleService(MavenProject project, Log log) { * @throws NoSuchAlgorithmException if md5, sha-1 or sha-256 algorithms are not available in * the environment. */ - public void createZipBundle(File bundleFile) throws IOException, NoSuchAlgorithmException, MavenExecutionException { + public void createZipBundle(File bundleFile) throws IOException, NoSuchAlgorithmException, MojoExecutionException { bundleFile.getParentFile().mkdirs(); bundleFile.createNewFile(); String groupId = project.getGroupId(); @@ -65,45 +67,46 @@ public void createZipBundle(File bundleFile) throws IOException, NoSuchAlgorithm String version = project.getVersion(); String groupPath = groupId.replace('.', '/'); String mavenPathPrefix = String.join("/", groupPath, artifactId, version) + "/"; + + List artifactFiles = new ArrayList<>(); + File artifactFile = project.getArtifact().getFile(); + // Will be null for e.g., an aggregator project + if (artifactFile != null && artifactFile.exists()) { + artifactFiles.add(artifactFile); + } + + // pom is not in getAttachedArtifacts so add it explicitly + File pomFile = new File(project.getBuild().getDirectory(), String.join("-", artifactId, version) + ".pom"); + if (pomFile.exists()) { + artifactFiles.add(pomFile); + } else { + log.error("POM file " + pomFile + " does not exist (verify phase not reached)!"); + // throw new MojoExecutionException("POM file " + pomFile + " does not exist!"); + } + for (Artifact artifact : project.getAttachedArtifacts()) { + File file = artifact.getFile(); + if (file.exists()) { + artifactFiles.add(artifact.getFile()); + } else { + log.error("Artifact " + artifact.getId() + " does not exist!"); + } + } + try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(bundleFile.toPath()))) { - File artifactDir = new File(project.getBuild().getDirectory()); - File[] files = artifactDir.listFiles( - (dir, name) -> name.endsWith(".jar") || name.endsWith(".pom") || name.endsWith(".asc")); - - int ascCount = 0; - if (files != null) { - for (File file : files) { - zipOut.putNextEntry(new ZipEntry(mavenPathPrefix + file.getName())); - Files.copy(file.toPath(), zipOut); - zipOut.closeEntry(); - if (file.getName().endsWith(".asc")) { - ascCount++; - continue; // No checksums for asc files - } - generateChecksumsAndAddToZip(file, mavenPathPrefix, zipOut); + for (File file : artifactFiles) { + zipOut.putNextEntry(new ZipEntry(mavenPathPrefix + file.getName())); + Files.copy(file.toPath(), zipOut); + zipOut.closeEntry(); + if (file.getName().endsWith(".asc")) { + continue; // asc files has no checksums } - } - // This is a bit crude, but there should be sign files for pom, jar, sourceJar, javadocJar - // unless the project is an aggregator - int expectedArtifactCount = 4; - if (project.getPackaging().equals("pom")) { - expectedArtifactCount = 1; - } - if (ascCount != expectedArtifactCount) { - log.warn("Expected " + expectedArtifactCount + " asc file(s) but found " + ascCount); - if (ascCount == 0) { - log.error("The artifacts were not signed!"); - } else { - if (expectedArtifactCount == 1) { - log.error("There should only be one asc file for the pom"); - } else { - log.error("There should be 4 signed artifacts (pom, jar, sourceJar, javadocJar)"); - } + File signFile = new File(file.getAbsolutePath() + ".asc"); + if (!signFile.exists()) { + throw new MojoExecutionException( + "The artifact " + file + " was not signed! " + signFile + " does not exists"); } - log.error("This bundle will not be deployable!"); - throw new MavenExecutionException( - "Missing sign files (asc files) detected, bundle is NOT valid", project.getFile()); + generateChecksumsAndAddToZip(file, mavenPathPrefix, zipOut); } } log.info("Created bundle at: " + bundleFile.getAbsolutePath()); @@ -122,10 +125,12 @@ private void generateChecksumsAndAddToZip(File sourceFile, String prefix, ZipOut public File generateChecksum(File file, String algo) throws NoSuchAlgorithmException, IOException { String extension = algo.toLowerCase().replace("-", ""); File checksumFile = new File(file.getAbsolutePath() + "." + extension); + // It might have been generated externally. In that case, use that. if (checksumFile.exists()) { return checksumFile; } + // Create the checksum file MessageDigest digest = MessageDigest.getInstance(algo); try (InputStream is = Files.newInputStream(file.toPath()); OutputStream nullOut = new OutputStream() { diff --git a/src/main/java/org/apache/maven/plugins/deploy/CentralBundleMojo.java b/src/main/java/org/apache/maven/plugins/deploy/CentralBundleMojo.java index 445199e..3c22a93 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/CentralBundleMojo.java +++ b/src/main/java/org/apache/maven/plugins/deploy/CentralBundleMojo.java @@ -27,7 +27,8 @@ import org.apache.maven.project.MavenProject; /** - * mvn deploy:bundle + * Can be reached with + * mvn verify deploy:bundle */ @Mojo(name = "bundle", defaultPhase = LifecyclePhase.DEPLOY) public class CentralBundleMojo extends AbstractDeployMojo { diff --git a/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java b/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java index 67c0913..d6ebcd7 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java +++ b/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java @@ -29,6 +29,8 @@ import java.net.URL; import java.nio.file.Files; import java.util.Base64; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class CentralPortalClient { @@ -44,9 +46,15 @@ public CentralPortalClient(String username, String password, String publishUrl) this.publishUrl = (publishUrl != null && !publishUrl.trim().isEmpty()) ? publishUrl : CENTRAL_PORTAL_URL; } - public String upload(File bundle) throws IOException { + public String upload(File bundle, Boolean autoDeploy) throws IOException { String boundary = "----MavenCentralBoundary" + System.currentTimeMillis(); - URL url = new URL(publishUrl + "/publisher/upload?publishingType=AUTOMATIC"); + String deployUrl = publishUrl + "/publisher/upload?name=" + bundle.getName(); + if (autoDeploy) { + deployUrl += "&publishingType=AUTOMATIC"; + } else { + deployUrl += "&publishingType=USER_MANAGED"; + } + URL url = new URL(deployUrl); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setDoOutput(true); conn.setRequestMethod("POST"); @@ -78,19 +86,39 @@ public String upload(File bundle) throws IOException { } } + /** + * Query central for the deployment status of the deployment identified with the deploymentId + * that was sent when the bundle was uploaded. + * Example response: + *
{@code
+     * {
+     *   "deploymentId": "28570f16-da32-4c14-bd2e-c1acc0782365",
+     *   "deploymentName": "central-bundle.zip",
+     *   "deploymentState": "PUBLISHED",
+     *   "purls": [
+     *     "pkg:maven/com.sonatype.central.example/example_java_project@0.0.7"
+     *   ]
+     * }
+     * }
+ * @param deploymentId the identifier from the upload step + * @return the deploymentState part of the response + * @throws IOException if the connection could not be established + */ public String getStatus(String deploymentId) throws IOException { URL url = new URL(publishUrl + "/publisher/status?id=" + deploymentId); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setRequestProperty("Authorization", authHeader()); - int status = conn.getResponseCode(); - if (status != HttpURLConnection.HTTP_OK) { - throw new IOException("Failed to get status: HTTP " + status); - } - try (InputStream in = conn.getInputStream()) { - return readFully(in); + String responseBody = readFully(in); + Pattern pattern = Pattern.compile("\"deploymentState\"\\s*:\\s*\"([^\"]+)\""); + Matcher matcher = pattern.matcher(responseBody); + if (matcher.find()) { + return matcher.group(1); + } else { + return "deploymentState not found"; + } } } diff --git a/src/main/java/org/apache/maven/plugins/deploy/CentralReleaseMojo.java b/src/main/java/org/apache/maven/plugins/deploy/CentralReleaseMojo.java index 4504957..cb8f9fa 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/CentralReleaseMojo.java +++ b/src/main/java/org/apache/maven/plugins/deploy/CentralReleaseMojo.java @@ -19,6 +19,7 @@ package org.apache.maven.plugins.deploy; import java.io.File; +import java.io.IOException; import java.util.Arrays; import org.apache.maven.plugin.MojoExecutionException; @@ -30,7 +31,8 @@ import static org.apache.maven.plugins.deploy.CentralPortalClient.CENTRAL_PORTAL_URL; /** - * mvn deploy:release + * Can be reached with + * mvn verify deploy:release */ @Mojo(name = "release", defaultPhase = LifecyclePhase.DEPLOY) public class CentralReleaseMojo extends AbstractDeployMojo { @@ -47,6 +49,9 @@ public class CentralReleaseMojo extends AbstractDeployMojo { @Parameter(property = "central.url", defaultValue = CENTRAL_PORTAL_URL) private String centralUrl; + @Parameter(defaultValue = "true", property = "autoDeploy") + private boolean autoDeploy; + @Override public void execute() throws MojoExecutionException { File targetDir = new File(project.getBuild().getDirectory()); @@ -58,18 +63,24 @@ public void execute() throws MojoExecutionException { CentralPortalClient client = new CentralPortalClient(username, password, centralUrl); - String deploymentId = client.upload(bundleFile); - if (deploymentId == null) { - throw new MojoExecutionException("Failed to upload bundle"); + String deploymentId; + try { + deploymentId = client.upload(bundleFile, autoDeploy); + if (deploymentId == null) { + throw new MojoExecutionException("Failed to upload bundle, no deployment id found"); + } + } catch (IOException e) { + throw new MojoExecutionException("Failed to upload bundle", e); } getLog().info("Deployment ID: " + deploymentId); + Thread.sleep(5000); String status = client.getStatus(deploymentId); int retries = 10; while (!Arrays.asList("PUBLISHING", "PUBLISHED", "FAILED").contains(status) && retries-- > 0) { getLog().info("Deploy status is " + status); - Thread.sleep(10000); + Thread.sleep(5000); status = client.getStatus(deploymentId); } From 09f641a7eff1b9b71966cb1aa569ef1f66b9ed6a Mon Sep 17 00:00:00 2001 From: per Date: Mon, 28 Jul 2025 17:59:59 +0200 Subject: [PATCH 05/25] publish now works --- .../maven/plugins/deploy/BundleService.java | 55 +++++++++++++++- .../plugins/deploy/CentralPortalClient.java | 10 ++- .../plugins/deploy/CentralReleaseMojo.java | 62 +++++++++++++++++-- .../maven/plugins/deploy/DeployMojo.java | 1 + 4 files changed, 118 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/apache/maven/plugins/deploy/BundleService.java b/src/main/java/org/apache/maven/plugins/deploy/BundleService.java index 901484c..70cbbae 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/BundleService.java +++ b/src/main/java/org/apache/maven/plugins/deploy/BundleService.java @@ -19,9 +19,11 @@ package org.apache.maven.plugins.deploy; import java.io.File; +import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.io.Reader; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.security.DigestOutputStream; @@ -34,9 +36,12 @@ import java.util.zip.ZipOutputStream; import org.apache.maven.artifact.Artifact; +import org.apache.maven.model.Model; +import org.apache.maven.model.io.xpp3.MavenXpp3Reader; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.logging.Log; import org.apache.maven.project.MavenProject; +import org.codehaus.plexus.util.xml.pull.XmlPullParserException; public class BundleService { @@ -52,7 +57,7 @@ public BundleService(MavenProject project, Log log) { /** * This method requires that the "verify" phase has been executed and gpg signing has been configured. - * e.g. mvn install deploy:bundle + * e.g. mvn verify deploy:bundle * * @param bundleFile the zip file to create * @throws IOException if creating the zip file failed @@ -78,10 +83,13 @@ public void createZipBundle(File bundleFile) throws IOException, NoSuchAlgorithm // pom is not in getAttachedArtifacts so add it explicitly File pomFile = new File(project.getBuild().getDirectory(), String.join("-", artifactId, version) + ".pom"); if (pomFile.exists()) { + // Since it is the "raw" pom file that is published, not the effective pom, we must check the file, + // not the project. Also since the pom file is the signed one, we cannot change it to the effective pom. + validateForPublishing(pomFile); artifactFiles.add(pomFile); } else { log.error("POM file " + pomFile + " does not exist (verify phase not reached)!"); - // throw new MojoExecutionException("POM file " + pomFile + " does not exist!"); + throw new MojoExecutionException("POM file " + pomFile + " does not exist!"); } for (Artifact artifact : project.getAttachedArtifacts()) { File file = artifact.getFile(); @@ -112,6 +120,38 @@ public void createZipBundle(File bundleFile) throws IOException, NoSuchAlgorithm log.info("Created bundle at: " + bundleFile.getAbsolutePath()); } + /** + * Validates that the following elements are present (required for publishing to central): + *
    + *
  • Project description
  • + *
  • License information
  • + *
  • SCM URL
  • + *
  • Developers information
  • + *
+ */ + private void validateForPublishing(File pomFile) throws MojoExecutionException { + Model model = readPomFile(pomFile); + List errs = new ArrayList<>(); + if (model.getDescription() == null || model.getDescription().trim().isEmpty()) { + errs.add("description is missing"); + } + if (model.getLicenses() == null || model.getLicenses().isEmpty()) { + errs.add("license is missing"); + } + if (model.getScm() == null) { + errs.add("scm is missing"); + } else if (model.getScm().getUrl() == null) { + errs.add("scm url is missing"); + } + if (model.getDevelopers() == null || model.getDevelopers().isEmpty()) { + errs.add("developers is missing"); + } + + if (!errs.isEmpty()) { + throw new MojoExecutionException(pomFile + " is not valid for publishing: " + String.join(", ", errs)); + } + } + private void generateChecksumsAndAddToZip(File sourceFile, String prefix, ZipOutputStream zipOut) throws NoSuchAlgorithmException, IOException { for (String algo : CHECKSUM_ALGOS) { @@ -154,4 +194,15 @@ public void write(int b) {} Files.write(checksumFile.toPath(), sb.toString().getBytes(StandardCharsets.UTF_8)); return checksumFile; } + + Model readPomFile(File pomFile) throws MojoExecutionException { + try { + MavenXpp3Reader reader = new MavenXpp3Reader(); + try (Reader fileReader = new FileReader(pomFile)) { + return reader.read(fileReader); + } + } catch (XmlPullParserException | IOException e) { + throw new MojoExecutionException("Failed to parse POM file.", e); + } + } } diff --git a/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java b/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java index d6ebcd7..ff33ec1 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java +++ b/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java @@ -44,6 +44,7 @@ public CentralPortalClient(String username, String password, String publishUrl) this.username = username; this.password = password; this.publishUrl = (publishUrl != null && !publishUrl.trim().isEmpty()) ? publishUrl : CENTRAL_PORTAL_URL; + // System.out.println("Publish to Central Portal using url: " + publishUrl); } public String upload(File bundle, Boolean autoDeploy) throws IOException { @@ -57,6 +58,7 @@ public String upload(File bundle, Boolean autoDeploy) throws IOException { URL url = new URL(deployUrl); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setDoOutput(true); + conn.setInstanceFollowRedirects(true); conn.setRequestMethod("POST"); conn.setRequestProperty("Authorization", authHeader()); conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary); @@ -77,7 +79,7 @@ public String upload(File bundle, Boolean autoDeploy) throws IOException { } int status = conn.getResponseCode(); - if (status != HttpURLConnection.HTTP_OK) { + if (status >= 400) { throw new IOException("Failed to upload: HTTP " + status); } @@ -107,9 +109,13 @@ public String upload(File bundle, Boolean autoDeploy) throws IOException { public String getStatus(String deploymentId) throws IOException { URL url = new URL(publishUrl + "/publisher/status?id=" + deploymentId); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setInstanceFollowRedirects(true); conn.setRequestMethod("POST"); conn.setRequestProperty("Authorization", authHeader()); - + int status = conn.getResponseCode(); + if (status >= 400) { + throw new IOException("Failed to get status: HTTP " + status); + } try (InputStream in = conn.getInputStream()) { String responseBody = readFully(in); Pattern pattern = Pattern.compile("\"deploymentState\"\\s*:\\s*\"([^\"]+)\""); diff --git a/src/main/java/org/apache/maven/plugins/deploy/CentralReleaseMojo.java b/src/main/java/org/apache/maven/plugins/deploy/CentralReleaseMojo.java index cb8f9fa..878a2d9 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/CentralReleaseMojo.java +++ b/src/main/java/org/apache/maven/plugins/deploy/CentralReleaseMojo.java @@ -18,6 +18,8 @@ */ package org.apache.maven.plugins.deploy; +import javax.inject.Inject; + import java.io.File; import java.io.IOException; import java.util.Arrays; @@ -27,14 +29,20 @@ import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.project.MavenProject; +import org.apache.maven.settings.Server; +import org.apache.maven.settings.Settings; +import org.apache.maven.settings.crypto.DefaultSettingsDecryptionRequest; +import org.apache.maven.settings.crypto.SettingsDecrypter; +import org.apache.maven.settings.crypto.SettingsDecryptionRequest; +import org.apache.maven.settings.crypto.SettingsDecryptionResult; import static org.apache.maven.plugins.deploy.CentralPortalClient.CENTRAL_PORTAL_URL; /** * Can be reached with - * mvn verify deploy:release + * mvn verify deploy:publish */ -@Mojo(name = "release", defaultPhase = LifecyclePhase.DEPLOY) +@Mojo(name = "publish", defaultPhase = LifecyclePhase.DEPLOY) public class CentralReleaseMojo extends AbstractDeployMojo { @Parameter(defaultValue = "${project}", readonly = true) @@ -52,8 +60,15 @@ public class CentralReleaseMojo extends AbstractDeployMojo { @Parameter(defaultValue = "true", property = "autoDeploy") private boolean autoDeploy; + @Parameter(defaultValue = "${settings}", readonly = true) + private Settings settings; + + @Inject + private SettingsDecrypter settingsDecrypter; + @Override public void execute() throws MojoExecutionException { + getLog().info("Executing publish mojo, autoDeploy = " + autoDeploy); File targetDir = new File(project.getBuild().getDirectory()); File bundleFile = new File(targetDir, project.getArtifactId() + "-" + project.getVersion() + "-bundle.zip"); @@ -61,6 +76,15 @@ public void execute() throws MojoExecutionException { BundleService bundleService = new BundleService(project, getLog()); bundleService.createZipBundle(bundleFile); + if (username == null && password == null) { + String[] credentials = resolveCredentials( + project.getDistributionManagement().getRepository().getId()); + username = credentials[0]; + password = credentials[1]; + } + if (username == null && password == null) { + throw new MojoExecutionException("Username and password are not set"); + } CentralPortalClient client = new CentralPortalClient(username, password, centralUrl); String deploymentId; @@ -74,22 +98,28 @@ public void execute() throws MojoExecutionException { } getLog().info("Deployment ID: " + deploymentId); - Thread.sleep(5000); + getLog().info("Waiting 10 seconds before checking status..."); + Thread.sleep(10000); String status = client.getStatus(deploymentId); int retries = 10; - while (!Arrays.asList("PUBLISHING", "PUBLISHED", "FAILED").contains(status) && retries-- > 0) { + while (!Arrays.asList("VALIDATED", "PUBLISHING", "PUBLISHED", "FAILED") + .contains(status) + && retries-- > 0) { getLog().info("Deploy status is " + status); Thread.sleep(5000); status = client.getStatus(deploymentId); } switch (status) { + case "VALIDATED": + getLog().info("Validated: the project is ready for publishing!"); + getLog().info("See https://central.sonatype.com/publishing/deployments for more info"); case "PUBLISHING": - getLog().info("Published: Project is publishing on Central"); + getLog().info("Published: Project is publishing on Central!"); break; case "PUBLISHED": - getLog().info("Published successfully"); + getLog().info("Published successfully!"); break; default: throw new MojoExecutionException("Release failed with status: " + status); @@ -98,4 +128,24 @@ public void execute() throws MojoExecutionException { throw new MojoExecutionException("Release process failed", e); } } + + private String[] resolveCredentials(String serverId) throws MojoExecutionException { + Server server = settings.getServer(serverId); + if (server == null) { + throw new MojoExecutionException("No entry with id '" + serverId + "' in settings.xml"); + } + + SettingsDecryptionRequest decryptRequest = new DefaultSettingsDecryptionRequest(server); + SettingsDecryptionResult decryptResult = settingsDecrypter.decrypt(decryptRequest); + Server decryptedServer = decryptResult.getServer(); + + String username = decryptedServer.getUsername(); + String password = decryptedServer.getPassword(); + + if (username == null || password == null) { + throw new MojoExecutionException("Missing credentials for server '" + serverId + "' in settings.xml"); + } + + return new String[] {username, password}; + } } diff --git a/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java b/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java index 7cf7a7e..c39d638 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java +++ b/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java @@ -182,6 +182,7 @@ private boolean hasState(MavenProject project) { @Override public void execute() throws MojoExecutionException, MojoFailureException { + getLog().info("Executing deploy mojo"); State state; if (Boolean.parseBoolean(skip) || ("releases".equals(skip) && !ArtifactUtils.isSnapshot(project.getVersion())) From e25db126d02a594e027562b8cbd9ecdc65f7e87d Mon Sep 17 00:00:00 2001 From: per Date: Thu, 31 Jul 2025 22:43:58 +0200 Subject: [PATCH 06/25] Integrate tighter with the DeployMojo. Add support for publishing a mega bundle. --- .../maven/plugins/deploy/BundleService.java | 79 +++++++++++++++ .../plugins/deploy/CentralPortalClient.java | 12 +++ .../maven/plugins/deploy/DeployMojo.java | 95 ++++++++++++++++++- 3 files changed, 182 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/apache/maven/plugins/deploy/BundleService.java b/src/main/java/org/apache/maven/plugins/deploy/BundleService.java index 70cbbae..9c50c54 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/BundleService.java +++ b/src/main/java/org/apache/maven/plugins/deploy/BundleService.java @@ -31,7 +31,10 @@ import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -56,6 +59,78 @@ public BundleService(MavenProject project, Log log) { static final List CHECKSUM_ALGOS = Arrays.asList("MD5", "SHA-1", "SHA-256"); /** + * Create a mega bundle zip with the artifacts for all projects in this build.. + * This method requires that the "verify" phase has been executed and gpg signing has been configured. + * + * @param bundleFile the zip file to create + * @throws IOException if creating the zip file failed + * @throws NoSuchAlgorithmException if md5, sha-1 or sha-256 algorithms are not available in + * the environment. + */ + public void createZipBundle(File bundleFile, List allProjectsUsingPlugin) + throws IOException, NoSuchAlgorithmException, MojoExecutionException { + bundleFile.getParentFile().mkdirs(); + bundleFile.createNewFile(); + log.info("Creating zip bundle at " + bundleFile.getAbsolutePath()); + + Map> artifactFiles = new HashMap<>(); + for (MavenProject project : allProjectsUsingPlugin) { + String groupId = project.getGroupId(); + String artifactId = project.getArtifactId(); + String version = project.getVersion(); + String groupPath = groupId.replace('.', '/'); + String mavenPathPrefix = String.join("/", groupPath, artifactId, version) + "/"; + artifactFiles.computeIfAbsent(mavenPathPrefix, k -> new ArrayList<>()); + File artifactFile = project.getArtifact().getFile(); + // Will be null for e.g., an aggregator project + if (artifactFile != null && artifactFile.exists()) { + artifactFiles.get(mavenPathPrefix).add(artifactFile); + } + // pom is not in getAttachedArtifacts so add it explicitly + File pomFile = new File(project.getBuild().getDirectory(), String.join("-", artifactId, version) + ".pom"); + if (pomFile.exists()) { + // Since it is the "raw" pom file that is published, not the effective pom, we must check the file, + // not the project. Also since the pom file is the signed one, we cannot change it to the effective pom. + validateForPublishing(pomFile); + artifactFiles.get(mavenPathPrefix).add(pomFile); + } else { + log.error("POM file " + pomFile + " does not exist (verify phase not reached)!"); + throw new MojoExecutionException("POM file " + pomFile + " does not exist!"); + } + for (Artifact artifact : project.getAttachedArtifacts()) { + File file = artifact.getFile(); + if (file.exists()) { + artifactFiles.get(mavenPathPrefix).add(artifact.getFile()); + } else { + log.error("Artifact " + artifact.getId() + " does not exist!"); + } + } + } + + try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(bundleFile.toPath()))) { + + for (Map.Entry> entry : artifactFiles.entrySet()) { + String mavenPathPrefix = entry.getKey(); + for (File file : entry.getValue()) { + zipOut.putNextEntry(new ZipEntry(mavenPathPrefix + file.getName())); + Files.copy(file.toPath(), zipOut); + zipOut.closeEntry(); + if (file.getName().endsWith(".asc")) { + continue; // asc files has no checksums + } + File signFile = new File(file.getAbsolutePath() + ".asc"); + if (!signFile.exists()) { + throw new MojoExecutionException( + "The artifact " + file + " was not signed! " + signFile + " does not exists"); + } + generateChecksumsAndAddToZip(file, mavenPathPrefix, zipOut); + } + } + } + } + + /** + * Create a bundle zip with the artifacts for the current project only. * This method requires that the "verify" phase has been executed and gpg signing has been configured. * e.g. mvn verify deploy:bundle * @@ -65,6 +140,8 @@ public BundleService(MavenProject project, Log log) { * the environment. */ public void createZipBundle(File bundleFile) throws IOException, NoSuchAlgorithmException, MojoExecutionException { + createZipBundle(bundleFile, Collections.singletonList(project)); + /* bundleFile.getParentFile().mkdirs(); bundleFile.createNewFile(); String groupId = project.getGroupId(); @@ -118,6 +195,8 @@ public void createZipBundle(File bundleFile) throws IOException, NoSuchAlgorithm } } log.info("Created bundle at: " + bundleFile.getAbsolutePath()); + + */ } /** diff --git a/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java b/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java index ff33ec1..ca800ec 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java +++ b/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java @@ -141,4 +141,16 @@ private String readFully(InputStream in) throws IOException { } return buffer.toString("UTF-8"); } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public String getPublishUrl() { + return publishUrl; + } } diff --git a/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java b/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java index c39d638..110f794 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java +++ b/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java @@ -18,11 +18,15 @@ */ package org.apache.maven.plugins.deploy; +import javax.inject.Inject; + import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -38,6 +42,11 @@ import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.project.MavenProject; import org.apache.maven.project.artifact.ProjectArtifact; +import org.apache.maven.settings.Server; +import org.apache.maven.settings.crypto.DefaultSettingsDecryptionRequest; +import org.apache.maven.settings.crypto.SettingsDecrypter; +import org.apache.maven.settings.crypto.SettingsDecryptionRequest; +import org.apache.maven.settings.crypto.SettingsDecryptionResult; import org.eclipse.aether.artifact.Artifact; import org.eclipse.aether.deployment.DeployRequest; import org.eclipse.aether.repository.RemoteRepository; @@ -70,7 +79,7 @@ public class DeployMojo extends AbstractDeployMojo { * * @since 2.8 */ - @Parameter(defaultValue = "false", property = "deployAtEnd") + @Parameter(defaultValue = "true", property = "deployAtEnd") private boolean deployAtEnd; /** @@ -128,6 +137,15 @@ public class DeployMojo extends AbstractDeployMojo { @Parameter(property = "maven.deploy.skip", defaultValue = "false") private String skip = Boolean.FALSE.toString(); + @Parameter(property = "useCentralPortalApi", defaultValue = "true") + private boolean useCentralPortalApi; + + @Parameter(defaultValue = "true", property = "autoDeploy") + private boolean autoDeploy; + + @Inject + private SettingsDecrypter settingsDecrypter; + /** * Set this to true to allow incomplete project processing. By default, such projects are forbidden * and Mojo will fail to process them. Incomplete project is a Maven Project that has any other packaging than @@ -250,9 +268,14 @@ private void deployAllAtOnce(List allProjectsUsingPlugin) throws M processProject(reactorProject, request); } } - // finally execute all deployments request, lets resolver to optimize deployment - for (DeployRequest request : requests.values()) { - deploy(request); + if (useCentralPortalApi) { + File zipBundle = createBundle(allProjectsUsingPlugin); + deployBundle(requests.keySet(), zipBundle); + } else { + // finally execute all deployments request, lets resolver to optimize deployment + for (DeployRequest request : requests.values()) { + deploy(request); + } } } @@ -415,4 +438,68 @@ RemoteRepository getDeploymentRepository( return repo; } + + protected File createBundle(List allProjectsUsingPlugin) throws MojoExecutionException { + if (allProjectsUsingPlugin.isEmpty()) { + throw new MojoExecutionException("There are no deployments to process"); + } + // We need the root project, project here will be the last submodule built. + MavenProject rootProject = project; + while (rootProject.getParent() != null) { + if (rootProject.getParent().getBasedir().exists()) { + rootProject = rootProject.getParent(); + } + } + File targetDir = new File(rootProject.getBuild().getDirectory()); + File bundleFile = + new File(targetDir, rootProject.getGroupId() + "-" + rootProject.getVersion() + "-bundle.zip"); + + try { + BundleService bundleService = new BundleService(rootProject, getLog()); + bundleService.createZipBundle(bundleFile, allProjectsUsingPlugin); + getLog().info("Bundle created successfully: " + bundleFile); + } catch (Exception e) { + throw new MojoExecutionException("Failed to create bundle", e); + } + return bundleFile; + } + + protected void deployBundle(Set repos, File zipBundle) throws MojoExecutionException { + + for (RemoteRepository repo : repos) { + String[] credentials = resolveCredentials( + project.getDistributionManagement().getRepository().getId()); + String username = credentials[0]; + String password = credentials[1]; + String deployUrl = repo.getUrl(); + CentralPortalClient centralPortalClient = new CentralPortalClient(username, password, deployUrl); + getLog().info("Deploying " + zipBundle + " to " + centralPortalClient.getPublishUrl()); + try { + centralPortalClient.upload(zipBundle, autoDeploy); + } catch (IOException e) { + // todo: should we retry? + throw new MojoExecutionException("Failed to deploy bundle to " + deployUrl, e); + } + } + } + + private String[] resolveCredentials(String serverId) throws MojoExecutionException { + Server server = session.getSettings().getServer(serverId); + if (server == null) { + throw new MojoExecutionException("No entry with id '" + serverId + "' in settings.xml"); + } + + SettingsDecryptionRequest decryptRequest = new DefaultSettingsDecryptionRequest(server); + SettingsDecryptionResult decryptResult = settingsDecrypter.decrypt(decryptRequest); + Server decryptedServer = decryptResult.getServer(); + + String username = decryptedServer.getUsername(); + String password = decryptedServer.getPassword(); + + if (username == null || password == null) { + throw new MojoExecutionException("Missing credentials for server '" + serverId + "' in settings.xml"); + } + + return new String[] {username, password}; + } } From b9a6dd6dc681c024930d3393745658e08d2c70c1 Mon Sep 17 00:00:00 2001 From: per Date: Fri, 1 Aug 2025 10:32:41 +0200 Subject: [PATCH 07/25] modify deploy to central to include status check afterwards. Remove CentralReleaseMojo.java as the functionality is now fully integrated with the DeployMojo. --- .../plugins/deploy/CentralBundleMojo.java | 3 +- .../plugins/deploy/CentralPortalClient.java | 52 +++++- .../plugins/deploy/CentralReleaseMojo.java | 151 ------------------ .../maven/plugins/deploy/DeployMojo.java | 46 ++++-- 4 files changed, 83 insertions(+), 169 deletions(-) delete mode 100644 src/main/java/org/apache/maven/plugins/deploy/CentralReleaseMojo.java diff --git a/src/main/java/org/apache/maven/plugins/deploy/CentralBundleMojo.java b/src/main/java/org/apache/maven/plugins/deploy/CentralBundleMojo.java index 3c22a93..20d1eda 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/CentralBundleMojo.java +++ b/src/main/java/org/apache/maven/plugins/deploy/CentralBundleMojo.java @@ -27,7 +27,8 @@ import org.apache.maven.project.MavenProject; /** - * Can be reached with + * This is useful for manual upload of the bundle file to central. + * It can be reached with * mvn verify deploy:bundle */ @Mojo(name = "bundle", defaultPhase = LifecyclePhase.DEPLOY) diff --git a/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java b/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java index ca800ec..7301d34 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java +++ b/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java @@ -28,10 +28,14 @@ import java.net.HttpURLConnection; import java.net.URL; import java.nio.file.Files; +import java.util.Arrays; import java.util.Base64; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.logging.Log; + public class CentralPortalClient { static final String CENTRAL_PORTAL_URL = "https://central.sonatype.com/api/v1"; @@ -39,12 +43,13 @@ public class CentralPortalClient { private final String username; private final String password; private final String publishUrl; + private final Log log; - public CentralPortalClient(String username, String password, String publishUrl) { + public CentralPortalClient(String username, String password, String publishUrl, Log log) { this.username = username; this.password = password; this.publishUrl = (publishUrl != null && !publishUrl.trim().isEmpty()) ? publishUrl : CENTRAL_PORTAL_URL; - // System.out.println("Publish to Central Portal using url: " + publishUrl); + this.log = log; } public String upload(File bundle, Boolean autoDeploy) throws IOException { @@ -153,4 +158,47 @@ public String getPassword() { public String getPublishUrl() { return publishUrl; } + + public void uploadAndCheck(File zipBundle, boolean autoDeploy) throws MojoExecutionException { + String deploymentId; + try { + deploymentId = upload(zipBundle, autoDeploy); + if (deploymentId == null) { + throw new MojoExecutionException("Failed to upload bundle, no deployment id found"); + } + } catch (IOException e) { + throw new MojoExecutionException("Failed to upload bundle", e); + } + + log.info("Deployment ID: " + deploymentId); + log.info("Waiting 10 seconds before checking status..."); + try { + Thread.sleep(10000); + String status = getStatus(deploymentId); + + int retries = 10; + while (!Arrays.asList("VALIDATED", "PUBLISHING", "PUBLISHED", "FAILED") + .contains(status) + && retries-- > 0) { + log.info("Deploy status is " + status); + Thread.sleep(5000); + status = getStatus(deploymentId); + } + switch (status) { + case "VALIDATED": + log.info("Validated: the project is ready for publishing!"); + log.info("See https://central.sonatype.com/publishing/deployments for more info"); + case "PUBLISHING": + log.info("Published: Project is publishing on Central!"); + break; + case "PUBLISHED": + log.info("Published successfully!"); + break; + default: + throw new MojoExecutionException("Release failed with status: " + status); + } + } catch (InterruptedException | IOException e) { + throw new MojoExecutionException("Failed to check status", e); + } + } } diff --git a/src/main/java/org/apache/maven/plugins/deploy/CentralReleaseMojo.java b/src/main/java/org/apache/maven/plugins/deploy/CentralReleaseMojo.java deleted file mode 100644 index 878a2d9..0000000 --- a/src/main/java/org/apache/maven/plugins/deploy/CentralReleaseMojo.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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 - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.maven.plugins.deploy; - -import javax.inject.Inject; - -import java.io.File; -import java.io.IOException; -import java.util.Arrays; - -import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugins.annotations.LifecyclePhase; -import org.apache.maven.plugins.annotations.Mojo; -import org.apache.maven.plugins.annotations.Parameter; -import org.apache.maven.project.MavenProject; -import org.apache.maven.settings.Server; -import org.apache.maven.settings.Settings; -import org.apache.maven.settings.crypto.DefaultSettingsDecryptionRequest; -import org.apache.maven.settings.crypto.SettingsDecrypter; -import org.apache.maven.settings.crypto.SettingsDecryptionRequest; -import org.apache.maven.settings.crypto.SettingsDecryptionResult; - -import static org.apache.maven.plugins.deploy.CentralPortalClient.CENTRAL_PORTAL_URL; - -/** - * Can be reached with - * mvn verify deploy:publish - */ -@Mojo(name = "publish", defaultPhase = LifecyclePhase.DEPLOY) -public class CentralReleaseMojo extends AbstractDeployMojo { - - @Parameter(defaultValue = "${project}", readonly = true) - private MavenProject project; - - @Parameter(property = "central.username") - private String username; - - @Parameter(property = "central.password") - private String password; - - @Parameter(property = "central.url", defaultValue = CENTRAL_PORTAL_URL) - private String centralUrl; - - @Parameter(defaultValue = "true", property = "autoDeploy") - private boolean autoDeploy; - - @Parameter(defaultValue = "${settings}", readonly = true) - private Settings settings; - - @Inject - private SettingsDecrypter settingsDecrypter; - - @Override - public void execute() throws MojoExecutionException { - getLog().info("Executing publish mojo, autoDeploy = " + autoDeploy); - File targetDir = new File(project.getBuild().getDirectory()); - File bundleFile = new File(targetDir, project.getArtifactId() + "-" + project.getVersion() + "-bundle.zip"); - - try { - BundleService bundleService = new BundleService(project, getLog()); - bundleService.createZipBundle(bundleFile); - - if (username == null && password == null) { - String[] credentials = resolveCredentials( - project.getDistributionManagement().getRepository().getId()); - username = credentials[0]; - password = credentials[1]; - } - if (username == null && password == null) { - throw new MojoExecutionException("Username and password are not set"); - } - CentralPortalClient client = new CentralPortalClient(username, password, centralUrl); - - String deploymentId; - try { - deploymentId = client.upload(bundleFile, autoDeploy); - if (deploymentId == null) { - throw new MojoExecutionException("Failed to upload bundle, no deployment id found"); - } - } catch (IOException e) { - throw new MojoExecutionException("Failed to upload bundle", e); - } - - getLog().info("Deployment ID: " + deploymentId); - getLog().info("Waiting 10 seconds before checking status..."); - Thread.sleep(10000); - String status = client.getStatus(deploymentId); - - int retries = 10; - while (!Arrays.asList("VALIDATED", "PUBLISHING", "PUBLISHED", "FAILED") - .contains(status) - && retries-- > 0) { - getLog().info("Deploy status is " + status); - Thread.sleep(5000); - status = client.getStatus(deploymentId); - } - - switch (status) { - case "VALIDATED": - getLog().info("Validated: the project is ready for publishing!"); - getLog().info("See https://central.sonatype.com/publishing/deployments for more info"); - case "PUBLISHING": - getLog().info("Published: Project is publishing on Central!"); - break; - case "PUBLISHED": - getLog().info("Published successfully!"); - break; - default: - throw new MojoExecutionException("Release failed with status: " + status); - } - } catch (Exception e) { - throw new MojoExecutionException("Release process failed", e); - } - } - - private String[] resolveCredentials(String serverId) throws MojoExecutionException { - Server server = settings.getServer(serverId); - if (server == null) { - throw new MojoExecutionException("No entry with id '" + serverId + "' in settings.xml"); - } - - SettingsDecryptionRequest decryptRequest = new DefaultSettingsDecryptionRequest(server); - SettingsDecryptionResult decryptResult = settingsDecrypter.decrypt(decryptRequest); - Server decryptedServer = decryptResult.getServer(); - - String username = decryptedServer.getUsername(); - String password = decryptedServer.getPassword(); - - if (username == null || password == null) { - throw new MojoExecutionException("Missing credentials for server '" + serverId + "' in settings.xml"); - } - - return new String[] {username, password}; - } -} diff --git a/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java b/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java index 110f794..98e6754 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java +++ b/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java @@ -22,7 +22,9 @@ import java.io.File; import java.io.IOException; +import java.security.NoSuchAlgorithmException; import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -219,10 +221,14 @@ public void execute() throws MojoExecutionException, MojoFailureException { altReleaseDeploymentRepository, altDeploymentRepository); - DeployRequest request = new DeployRequest(); - request.setRepository(deploymentRepository); - processProject(project, request); - deploy(request); + if (useCentralPortalApi) { + createAndDeploySingleProjectBundle(deploymentRepository); + } else { + DeployRequest request = new DeployRequest(); + request.setRepository(deploymentRepository); + processProject(project, request); + deploy(request); + } state = State.DEPLOYED; } else { putPluginContextValue(DEPLOY_ALT_SNAPSHOT_DEPLOYMENT_REPOSITORY, altSnapshotDeploymentRepository); @@ -450,9 +456,8 @@ protected File createBundle(List allProjectsUsingPlugin) throws Mo rootProject = rootProject.getParent(); } } - File targetDir = new File(rootProject.getBuild().getDirectory()); - File bundleFile = - new File(targetDir, rootProject.getGroupId() + "-" + rootProject.getVersion() + "-bundle.zip"); + + File bundleFile = createBundleFile(rootProject); try { BundleService bundleService = new BundleService(rootProject, getLog()); @@ -464,22 +469,33 @@ protected File createBundle(List allProjectsUsingPlugin) throws Mo return bundleFile; } - protected void deployBundle(Set repos, File zipBundle) throws MojoExecutionException { + File createBundleFile(MavenProject project) { + File targetDir = new File(project.getBuild().getDirectory()); + return new File(targetDir, project.getGroupId() + "-" + project.getVersion() + "-bundle.zip"); + } + private void createAndDeploySingleProjectBundle(RemoteRepository deploymentRepository) + throws MojoExecutionException { + BundleService bundleService = new BundleService(project, getLog()); + File bundleFile = createBundleFile(project); + try { + bundleService.createZipBundle(bundleFile); + } catch (IOException | NoSuchAlgorithmException e) { + throw new MojoExecutionException("Failed to create zip bundle", e); + } + deployBundle(Collections.singleton(deploymentRepository), bundleFile); + } + + protected void deployBundle(Set repos, File zipBundle) throws MojoExecutionException { for (RemoteRepository repo : repos) { String[] credentials = resolveCredentials( project.getDistributionManagement().getRepository().getId()); String username = credentials[0]; String password = credentials[1]; String deployUrl = repo.getUrl(); - CentralPortalClient centralPortalClient = new CentralPortalClient(username, password, deployUrl); + CentralPortalClient centralPortalClient = new CentralPortalClient(username, password, deployUrl, getLog()); getLog().info("Deploying " + zipBundle + " to " + centralPortalClient.getPublishUrl()); - try { - centralPortalClient.upload(zipBundle, autoDeploy); - } catch (IOException e) { - // todo: should we retry? - throw new MojoExecutionException("Failed to deploy bundle to " + deployUrl, e); - } + centralPortalClient.uploadAndCheck(zipBundle, autoDeploy); } } From bc286917094dd407b0322a7a6369aa3963bb6e1f Mon Sep 17 00:00:00 2001 From: per Date: Fri, 1 Aug 2025 17:09:34 +0200 Subject: [PATCH 08/25] change default of useCentralPortalApi to false to not break any existing tests. --- src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java b/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java index 98e6754..5ddf14f 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java +++ b/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java @@ -139,7 +139,7 @@ public class DeployMojo extends AbstractDeployMojo { @Parameter(property = "maven.deploy.skip", defaultValue = "false") private String skip = Boolean.FALSE.toString(); - @Parameter(property = "useCentralPortalApi", defaultValue = "true") + @Parameter(property = "useCentralPortalApi", defaultValue = "false") private boolean useCentralPortalApi; @Parameter(defaultValue = "true", property = "autoDeploy") From 65f192713d8c7266ba0234cad7a70f8521078d29 Mon Sep 17 00:00:00 2001 From: per Date: Sat, 2 Aug 2025 22:46:54 +0200 Subject: [PATCH 09/25] WIP: adding integration tests for bundle and central deploy --- src/it/central-deploy-bundles/README.md | 36 +++++ src/it/central-deploy-bundles/common/pom.xml | 63 ++++++++ .../central-deploy-bundles/common/readme.md | 19 +++ .../java/multimodule/common/Greeting.java | 40 +++++ src/it/central-deploy-bundles/pom.xml | 143 ++++++++++++++++++ src/it/central-deploy-bundles/subA/pom.xml | 71 +++++++++ src/it/central-deploy-bundles/subA/readme.md | 19 +++ .../src/main/java/multimodule/suba/Hello.java | 37 +++++ src/it/central-deploy-megabundle/README.md | 34 +++++ .../central-deploy-megabundle/common/pom.xml | 63 ++++++++ .../common/readme.md | 17 +++ .../java/multimodule/common/Greeting.java | 40 +++++ src/it/central-deploy-megabundle/pom.xml | 143 ++++++++++++++++++ src/it/central-deploy-megabundle/subA/pom.xml | 71 +++++++++ .../central-deploy-megabundle/subA/readme.md | 17 +++ .../src/main/java/multimodule/suba/Hello.java | 37 +++++ src/it/central-zip-bundles/README.md | 53 +++++++ src/it/central-zip-bundles/common/pom.xml | 63 ++++++++ src/it/central-zip-bundles/common/readme.md | 17 +++ .../java/multimodule/common/Greeting.java | 40 +++++ src/it/central-zip-bundles/invoker.properties | 19 +++ src/it/central-zip-bundles/pom.xml | 128 ++++++++++++++++ src/it/central-zip-bundles/setup.groovy | 81 ++++++++++ src/it/central-zip-bundles/subA/pom.xml | 71 +++++++++ src/it/central-zip-bundles/subA/readme.md | 17 +++ .../src/main/java/multimodule/suba/Hello.java | 37 +++++ src/it/central-zip-bundles/verfy.groovy | 21 +++ src/it/central-zip-megabundle/README.md | 34 +++++ src/it/central-zip-megabundle/common/pom.xml | 63 ++++++++ .../central-zip-megabundle/common/readme.md | 17 +++ .../java/multimodule/common/Greeting.java | 40 +++++ src/it/central-zip-megabundle/pom.xml | 143 ++++++++++++++++++ src/it/central-zip-megabundle/subA/pom.xml | 71 +++++++++ src/it/central-zip-megabundle/subA/readme.md | 17 +++ .../src/main/java/multimodule/suba/Hello.java | 37 +++++ src/site/apt/examples/deploy-central.apt | 29 ++++ src/site/apt/index.apt.vm | 7 +- 37 files changed, 1854 insertions(+), 1 deletion(-) create mode 100644 src/it/central-deploy-bundles/README.md create mode 100644 src/it/central-deploy-bundles/common/pom.xml create mode 100644 src/it/central-deploy-bundles/common/readme.md create mode 100644 src/it/central-deploy-bundles/common/src/main/java/multimodule/common/Greeting.java create mode 100644 src/it/central-deploy-bundles/pom.xml create mode 100644 src/it/central-deploy-bundles/subA/pom.xml create mode 100644 src/it/central-deploy-bundles/subA/readme.md create mode 100644 src/it/central-deploy-bundles/subA/src/main/java/multimodule/suba/Hello.java create mode 100644 src/it/central-deploy-megabundle/README.md create mode 100644 src/it/central-deploy-megabundle/common/pom.xml create mode 100644 src/it/central-deploy-megabundle/common/readme.md create mode 100644 src/it/central-deploy-megabundle/common/src/main/java/multimodule/common/Greeting.java create mode 100644 src/it/central-deploy-megabundle/pom.xml create mode 100644 src/it/central-deploy-megabundle/subA/pom.xml create mode 100644 src/it/central-deploy-megabundle/subA/readme.md create mode 100644 src/it/central-deploy-megabundle/subA/src/main/java/multimodule/suba/Hello.java create mode 100644 src/it/central-zip-bundles/README.md create mode 100644 src/it/central-zip-bundles/common/pom.xml create mode 100644 src/it/central-zip-bundles/common/readme.md create mode 100644 src/it/central-zip-bundles/common/src/main/java/multimodule/common/Greeting.java create mode 100644 src/it/central-zip-bundles/invoker.properties create mode 100644 src/it/central-zip-bundles/pom.xml create mode 100644 src/it/central-zip-bundles/setup.groovy create mode 100644 src/it/central-zip-bundles/subA/pom.xml create mode 100644 src/it/central-zip-bundles/subA/readme.md create mode 100644 src/it/central-zip-bundles/subA/src/main/java/multimodule/suba/Hello.java create mode 100644 src/it/central-zip-bundles/verfy.groovy create mode 100644 src/it/central-zip-megabundle/README.md create mode 100644 src/it/central-zip-megabundle/common/pom.xml create mode 100644 src/it/central-zip-megabundle/common/readme.md create mode 100644 src/it/central-zip-megabundle/common/src/main/java/multimodule/common/Greeting.java create mode 100644 src/it/central-zip-megabundle/pom.xml create mode 100644 src/it/central-zip-megabundle/subA/pom.xml create mode 100644 src/it/central-zip-megabundle/subA/readme.md create mode 100644 src/it/central-zip-megabundle/subA/src/main/java/multimodule/suba/Hello.java create mode 100644 src/site/apt/examples/deploy-central.apt diff --git a/src/it/central-deploy-bundles/README.md b/src/it/central-deploy-bundles/README.md new file mode 100644 index 0000000..9003f79 --- /dev/null +++ b/src/it/central-deploy-bundles/README.md @@ -0,0 +1,36 @@ + +# maven-central-publishing-example + +This is a multimodule example showing deployment to maven central +using the new central publishing rest api. + +The maven-deploy-plugin used is a [modified fork](https://github.com/perNyfelt/maven-deploy-plugin/tree/add_central_support) of the apache maven deploy plugin. + +The project consist of +- an aggregator +- a common sub module +- two sub modules that each depends on the common submodule + +Each module (including the main aggregator) will be deployed separately. +I.e. when deploying the whole project, 4 zip files will be created and uploaded to central: +1. The aggregator pom (+ asc, md5 and sha1 files) +2. common, contains the pom, jar, javadoc and sources (all signed and with md5 and sha1 files) +3. subA, contains the pom, jar, javadoc and sources (all signed and with md5 and sha1 files) +4. subB, contains the pom, jar, javadoc and sources (all signed and with md5 and sha1 files) \ No newline at end of file diff --git a/src/it/central-deploy-bundles/common/pom.xml b/src/it/central-deploy-bundles/common/pom.xml new file mode 100644 index 0000000..321a88a --- /dev/null +++ b/src/it/central-deploy-bundles/common/pom.xml @@ -0,0 +1,63 @@ + + + + + 4.0.0 + + se.alipsa.maven.example + publishing-example-parent + 1.0.0 + + publishing-example-common + Maven Central Publishing Example, Common + https://github.com/perNyfelt/maven-central-publishing-example + jar + + Maven Central Publishing Example, Common + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + + + pnyfelt + Per Nyfelt + + + + https://github.com/perNyfelt/maven-central-publishing-example/tree/master + scm:git:https://github.com/perNyfelt/maven-central-publishing-example.git + scm:git:https://github.com/perNyfelt/maven-central-publishing-example.git + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + org.apache.maven.plugins + maven-source-plugin + + + + \ No newline at end of file diff --git a/src/it/central-deploy-bundles/common/readme.md b/src/it/central-deploy-bundles/common/readme.md new file mode 100644 index 0000000..a1c3e8e --- /dev/null +++ b/src/it/central-deploy-bundles/common/readme.md @@ -0,0 +1,19 @@ + +# This is the common module for the multimodule project \ No newline at end of file diff --git a/src/it/central-deploy-bundles/common/src/main/java/multimodule/common/Greeting.java b/src/it/central-deploy-bundles/common/src/main/java/multimodule/common/Greeting.java new file mode 100644 index 0000000..0a9be56 --- /dev/null +++ b/src/it/central-deploy-bundles/common/src/main/java/multimodule/common/Greeting.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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 multimodule.common; + +/** + * Common interface to define a greeting. + */ +public interface Greeting { + + /** + * Hello {name}. + * + * @param name the name to greet + * @return Hello {name} + */ + String greet(String name); + + /** + * Prints some info. + */ + default void info() { + System.out.println(this.getClass()); + } +} \ No newline at end of file diff --git a/src/it/central-deploy-bundles/pom.xml b/src/it/central-deploy-bundles/pom.xml new file mode 100644 index 0000000..44cc4f5 --- /dev/null +++ b/src/it/central-deploy-bundles/pom.xml @@ -0,0 +1,143 @@ + + + + 4.0.0 + se.alipsa.maven.example + publishing-example-parent + 1.0.0 + + Maven Central Publishing Example, Aggregator + https://github.com/perNyfelt/maven-central-publishing-example + pom + + + common + subA + + + Maven Central Publishing Example, Aggregator + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + + + pnyfelt + Per Nyfelt + + + + https://github.com/perNyfelt/maven-central-publishing-example/tree/master + scm:git:https://github.com/perNyfelt/maven-central-publishing-example.git + scm:git:https://github.com/perNyfelt/maven-central-publishing-example.git + + + UTF-8 + UTF-8 + 1.8 + 1.8 + + + + + central + Central Repository + https://central.sonatype.com/api/v1 + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.0 + + ${maven.compiler.source} + ${maven.compiler.target} + + -Xlint:-options + + + + + org.apache.maven.plugins + maven-deploy-plugin + 3.1.5-SNAPSHOT + + true + false + false + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.11.2 + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.3.1 + + + attach-sources + + jar + + + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.8 + + + sign-artifacts + verify + + sign + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + + + \ No newline at end of file diff --git a/src/it/central-deploy-bundles/subA/pom.xml b/src/it/central-deploy-bundles/subA/pom.xml new file mode 100644 index 0000000..04244db --- /dev/null +++ b/src/it/central-deploy-bundles/subA/pom.xml @@ -0,0 +1,71 @@ + + + + + 4.0.0 + + se.alipsa.maven.example + publishing-example-parent + 1.0.0 + + publishing-example-subA + Maven Central Publishing Example, SubA + https://github.com/perNyfelt/maven-central-publishing-example + jar + + Maven Central Publishing Example, SubA + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + + + pnyfelt + Per Nyfelt + + + + https://github.com/perNyfelt/maven-central-publishing-example/tree/master + scm:git:https://github.com/perNyfelt/maven-central-publishing-example.git + scm:git:https://github.com/perNyfelt/maven-central-publishing-example.git + + + + se.alipsa.maven.example + publishing-example-common + 1.0.0 + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + org.apache.maven.plugins + maven-source-plugin + + + + \ No newline at end of file diff --git a/src/it/central-deploy-bundles/subA/readme.md b/src/it/central-deploy-bundles/subA/readme.md new file mode 100644 index 0000000..f37eba0 --- /dev/null +++ b/src/it/central-deploy-bundles/subA/readme.md @@ -0,0 +1,19 @@ + +# The subA sub projects of the multmodule project \ No newline at end of file diff --git a/src/it/central-deploy-bundles/subA/src/main/java/multimodule/suba/Hello.java b/src/it/central-deploy-bundles/subA/src/main/java/multimodule/suba/Hello.java new file mode 100644 index 0000000..7593984 --- /dev/null +++ b/src/it/central-deploy-bundles/subA/src/main/java/multimodule/suba/Hello.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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 multimodule.suba; + +import multimodule.common.Greeting; + +/** + * This is the Greeting implementation in subA + */ +public class Hello implements Greeting { + + /** + * Default constructor. + */ + public Hello() {} + + @Override + public String greet(String name) { + return "Hello $name from suba"; + } +} diff --git a/src/it/central-deploy-megabundle/README.md b/src/it/central-deploy-megabundle/README.md new file mode 100644 index 0000000..e8067e6 --- /dev/null +++ b/src/it/central-deploy-megabundle/README.md @@ -0,0 +1,34 @@ + +# maven-central-publishing-example + +This is a multimodule example showing deployment to maven central +using the new central publishing rest api. + +The maven-deploy-plugin used is a [modified fork](https://github.com/perNyfelt/maven-deploy-plugin/tree/add_central_support) of the apache maven deploy plugin. + +The project consist of +- an aggregator +- a common sub module +- two sub modules that each depends on the common submodule + +Each module (including the main aggregator) will be deployed separately. +I.e. when deploying the whole project, 4 zip files will be created and uploaded to central: +1. The aggregator pom (+ asc, md5 and sha1 files) +2. common, contains the pom, jar, javadoc and sources (all signed and with md5 and sha1 files) +3. subA, contains the pom, jar, javadoc and sources (all signed and with md5 and sha1 files) +4. subB, contains the pom, jar, javadoc and sources (all signed and with md5 and sha1 files) \ No newline at end of file diff --git a/src/it/central-deploy-megabundle/common/pom.xml b/src/it/central-deploy-megabundle/common/pom.xml new file mode 100644 index 0000000..321a88a --- /dev/null +++ b/src/it/central-deploy-megabundle/common/pom.xml @@ -0,0 +1,63 @@ + + + + + 4.0.0 + + se.alipsa.maven.example + publishing-example-parent + 1.0.0 + + publishing-example-common + Maven Central Publishing Example, Common + https://github.com/perNyfelt/maven-central-publishing-example + jar + + Maven Central Publishing Example, Common + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + + + pnyfelt + Per Nyfelt + + + + https://github.com/perNyfelt/maven-central-publishing-example/tree/master + scm:git:https://github.com/perNyfelt/maven-central-publishing-example.git + scm:git:https://github.com/perNyfelt/maven-central-publishing-example.git + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + org.apache.maven.plugins + maven-source-plugin + + + + \ No newline at end of file diff --git a/src/it/central-deploy-megabundle/common/readme.md b/src/it/central-deploy-megabundle/common/readme.md new file mode 100644 index 0000000..32cc86d --- /dev/null +++ b/src/it/central-deploy-megabundle/common/readme.md @@ -0,0 +1,17 @@ + +# This is the common module for the multimodule project \ No newline at end of file diff --git a/src/it/central-deploy-megabundle/common/src/main/java/multimodule/common/Greeting.java b/src/it/central-deploy-megabundle/common/src/main/java/multimodule/common/Greeting.java new file mode 100644 index 0000000..0a9be56 --- /dev/null +++ b/src/it/central-deploy-megabundle/common/src/main/java/multimodule/common/Greeting.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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 multimodule.common; + +/** + * Common interface to define a greeting. + */ +public interface Greeting { + + /** + * Hello {name}. + * + * @param name the name to greet + * @return Hello {name} + */ + String greet(String name); + + /** + * Prints some info. + */ + default void info() { + System.out.println(this.getClass()); + } +} \ No newline at end of file diff --git a/src/it/central-deploy-megabundle/pom.xml b/src/it/central-deploy-megabundle/pom.xml new file mode 100644 index 0000000..90b4a29 --- /dev/null +++ b/src/it/central-deploy-megabundle/pom.xml @@ -0,0 +1,143 @@ + + + + 4.0.0 + se.alipsa.maven.example + publishing-example-parent + 1.0.0 + + Maven Central Publishing Example, Aggregator + https://github.com/perNyfelt/maven-central-publishing-example + pom + + + common + subA + + + Maven Central Publishing Example, Aggregator + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + + + pnyfelt + Per Nyfelt + + + + https://github.com/perNyfelt/maven-central-publishing-example/tree/master + scm:git:https://github.com/perNyfelt/maven-central-publishing-example.git + scm:git:https://github.com/perNyfelt/maven-central-publishing-example.git + + + UTF-8 + UTF-8 + 1.8 + 1.8 + + + + + central + Central Repository + https://central.sonatype.com/api/v1 + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.0 + + ${maven.compiler.source} + ${maven.compiler.target} + + -Xlint:-options + + + + + org.apache.maven.plugins + maven-deploy-plugin + 3.1.5-SNAPSHOT + + true + true + false + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.11.2 + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.3.1 + + + attach-sources + + jar + + + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.8 + + + sign-artifacts + verify + + sign + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + + + \ No newline at end of file diff --git a/src/it/central-deploy-megabundle/subA/pom.xml b/src/it/central-deploy-megabundle/subA/pom.xml new file mode 100644 index 0000000..04244db --- /dev/null +++ b/src/it/central-deploy-megabundle/subA/pom.xml @@ -0,0 +1,71 @@ + + + + + 4.0.0 + + se.alipsa.maven.example + publishing-example-parent + 1.0.0 + + publishing-example-subA + Maven Central Publishing Example, SubA + https://github.com/perNyfelt/maven-central-publishing-example + jar + + Maven Central Publishing Example, SubA + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + + + pnyfelt + Per Nyfelt + + + + https://github.com/perNyfelt/maven-central-publishing-example/tree/master + scm:git:https://github.com/perNyfelt/maven-central-publishing-example.git + scm:git:https://github.com/perNyfelt/maven-central-publishing-example.git + + + + se.alipsa.maven.example + publishing-example-common + 1.0.0 + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + org.apache.maven.plugins + maven-source-plugin + + + + \ No newline at end of file diff --git a/src/it/central-deploy-megabundle/subA/readme.md b/src/it/central-deploy-megabundle/subA/readme.md new file mode 100644 index 0000000..31f35ed --- /dev/null +++ b/src/it/central-deploy-megabundle/subA/readme.md @@ -0,0 +1,17 @@ + +# The subA sub projects of the multmodule project \ No newline at end of file diff --git a/src/it/central-deploy-megabundle/subA/src/main/java/multimodule/suba/Hello.java b/src/it/central-deploy-megabundle/subA/src/main/java/multimodule/suba/Hello.java new file mode 100644 index 0000000..7593984 --- /dev/null +++ b/src/it/central-deploy-megabundle/subA/src/main/java/multimodule/suba/Hello.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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 multimodule.suba; + +import multimodule.common.Greeting; + +/** + * This is the Greeting implementation in subA + */ +public class Hello implements Greeting { + + /** + * Default constructor. + */ + public Hello() {} + + @Override + public String greet(String name) { + return "Hello $name from suba"; + } +} diff --git a/src/it/central-zip-bundles/README.md b/src/it/central-zip-bundles/README.md new file mode 100644 index 0000000..1249e45 --- /dev/null +++ b/src/it/central-zip-bundles/README.md @@ -0,0 +1,53 @@ + +# maven-central-publishing-example + +This is a multimodule example showing deployment to maven central +using the new central publishing rest api. + +The maven-deploy-plugin used is a [modified fork](https://github.com/perNyfelt/maven-deploy-plugin/tree/add_central_support) of the apache maven deploy plugin. + +The project consist of +- an aggregator +- a common sub module +- two sub modules that each depends on the common submodule + +Each module (including the main aggregator) will be deployed separately. +I.e. when deploying the whole project, 4 zip files will be created and uploaded to central: +1. The aggregator pom (+ asc, md5 and sha1 files) +2. common, contains the pom, jar, javadoc and sources (all signed and with md5 and sha1 files) +3. subA, contains the pom, jar, javadoc and sources (all signed and with md5 and sha1 files) +4. subB, contains the pom, jar, javadoc and sources (all signed and with md5 and sha1 files) + +Note, normally we would have the gpg plugin configured in the build, e.g: +```xml + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.8 + + + sign-artifacts + verify + + sign + + + + +``` +But this requires some external setup so we just create fake asc files in this test. \ No newline at end of file diff --git a/src/it/central-zip-bundles/common/pom.xml b/src/it/central-zip-bundles/common/pom.xml new file mode 100644 index 0000000..321a88a --- /dev/null +++ b/src/it/central-zip-bundles/common/pom.xml @@ -0,0 +1,63 @@ + + + + + 4.0.0 + + se.alipsa.maven.example + publishing-example-parent + 1.0.0 + + publishing-example-common + Maven Central Publishing Example, Common + https://github.com/perNyfelt/maven-central-publishing-example + jar + + Maven Central Publishing Example, Common + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + + + pnyfelt + Per Nyfelt + + + + https://github.com/perNyfelt/maven-central-publishing-example/tree/master + scm:git:https://github.com/perNyfelt/maven-central-publishing-example.git + scm:git:https://github.com/perNyfelt/maven-central-publishing-example.git + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + org.apache.maven.plugins + maven-source-plugin + + + + \ No newline at end of file diff --git a/src/it/central-zip-bundles/common/readme.md b/src/it/central-zip-bundles/common/readme.md new file mode 100644 index 0000000..32cc86d --- /dev/null +++ b/src/it/central-zip-bundles/common/readme.md @@ -0,0 +1,17 @@ + +# This is the common module for the multimodule project \ No newline at end of file diff --git a/src/it/central-zip-bundles/common/src/main/java/multimodule/common/Greeting.java b/src/it/central-zip-bundles/common/src/main/java/multimodule/common/Greeting.java new file mode 100644 index 0000000..0a9be56 --- /dev/null +++ b/src/it/central-zip-bundles/common/src/main/java/multimodule/common/Greeting.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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 multimodule.common; + +/** + * Common interface to define a greeting. + */ +public interface Greeting { + + /** + * Hello {name}. + * + * @param name the name to greet + * @return Hello {name} + */ + String greet(String name); + + /** + * Prints some info. + */ + default void info() { + System.out.println(this.getClass()); + } +} \ No newline at end of file diff --git a/src/it/central-zip-bundles/invoker.properties b/src/it/central-zip-bundles/invoker.properties new file mode 100644 index 0000000..18034aa --- /dev/null +++ b/src/it/central-zip-bundles/invoker.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. + +# Clean build of the jars +invoker.goals = ${project.groupId}:${project.artifactId}:${project.version}:bundle diff --git a/src/it/central-zip-bundles/pom.xml b/src/it/central-zip-bundles/pom.xml new file mode 100644 index 0000000..e5b45ef --- /dev/null +++ b/src/it/central-zip-bundles/pom.xml @@ -0,0 +1,128 @@ + + + + 4.0.0 + se.alipsa.maven.example + publishing-example-parent + 1.0.0 + + Maven Central Publishing Example, Aggregator + https://github.com/perNyfelt/maven-central-publishing-example + pom + + + common + subA + + + Maven Central Publishing Example, Aggregator + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + + + pnyfelt + Per Nyfelt + + + + https://github.com/perNyfelt/maven-central-publishing-example/tree/master + scm:git:https://github.com/perNyfelt/maven-central-publishing-example.git + scm:git:https://github.com/perNyfelt/maven-central-publishing-example.git + + + UTF-8 + UTF-8 + 1.8 + 1.8 + + + + + central + Central Repository + https://central.sonatype.com/api/v1 + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.0 + + ${maven.compiler.source} + ${maven.compiler.target} + + -Xlint:-options + + + + + org.apache.maven.plugins + maven-deploy-plugin + 3.1.5-SNAPSHOT + + true + false + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.11.2 + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.3.1 + + + attach-sources + + jar + + + + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + + + \ No newline at end of file diff --git a/src/it/central-zip-bundles/setup.groovy b/src/it/central-zip-bundles/setup.groovy new file mode 100644 index 0000000..042406c --- /dev/null +++ b/src/it/central-zip-bundles/setup.groovy @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ +import org.apache.maven.model.Model +import org.apache.maven.model.io.xpp3.MavenXpp3Reader +// Generate jars +"mvn verify".execute().waitFor() + +// Since we do not want to mess with external gpg config, we just create fake asc files +signFile(copyPom("pom.xml", "target")) +signFile(copyPom("common/pom.xml", "common/target")) +signJarFiles("common/target") +signFile(copyPom("subA/pom.xml", "subA/target")) +signJarFiles("subA/target") + +// returning null is required by the invoker plugin +return null + +def signJarFiles(String dir) { + def targetDir = new File(getTestDir(), dir) + targetDir.listFiles({ file -> file.name.endsWith(".jar") } as FileFilter).each { + signFile(it) + } +} + +static def signFile(File file) { + def ascFile = new File(file.getParentFile(), "${file.getName()}.asc") + ascFile << "fake signed" +} + +def copyPom(String from, String toDir) { + File testDir = getTestDir() + def src = new File(testDir, from) + def pomInfo = readPomInfo(src) + def dstDir = new File(testDir, toDir) + dstDir.mkdirs() + def toFile = new File(dstDir, "$pomInfo.artifactId-${pomInfo.version}.pom") + toFile.text = src.text + return toFile +} + +static def readPomInfo(File pomFile) { + MavenXpp3Reader reader = new MavenXpp3Reader() + pomFile.withReader { r -> + Model model = reader.read(r) + + // Inherit groupId/version from parent if missing + if (!model.groupId && model.parent) { + model.groupId = model.parent.groupId + } + if (!model.version && model.parent) { + model.version = model.parent.version + } + + return [ + groupId : model.groupId, + artifactId : model.artifactId, + version : model.version + ] + } +} + +def getTestDir() { + new File("${basedir}") +} + diff --git a/src/it/central-zip-bundles/subA/pom.xml b/src/it/central-zip-bundles/subA/pom.xml new file mode 100644 index 0000000..04244db --- /dev/null +++ b/src/it/central-zip-bundles/subA/pom.xml @@ -0,0 +1,71 @@ + + + + + 4.0.0 + + se.alipsa.maven.example + publishing-example-parent + 1.0.0 + + publishing-example-subA + Maven Central Publishing Example, SubA + https://github.com/perNyfelt/maven-central-publishing-example + jar + + Maven Central Publishing Example, SubA + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + + + pnyfelt + Per Nyfelt + + + + https://github.com/perNyfelt/maven-central-publishing-example/tree/master + scm:git:https://github.com/perNyfelt/maven-central-publishing-example.git + scm:git:https://github.com/perNyfelt/maven-central-publishing-example.git + + + + se.alipsa.maven.example + publishing-example-common + 1.0.0 + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + org.apache.maven.plugins + maven-source-plugin + + + + \ No newline at end of file diff --git a/src/it/central-zip-bundles/subA/readme.md b/src/it/central-zip-bundles/subA/readme.md new file mode 100644 index 0000000..31f35ed --- /dev/null +++ b/src/it/central-zip-bundles/subA/readme.md @@ -0,0 +1,17 @@ + +# The subA sub projects of the multmodule project \ No newline at end of file diff --git a/src/it/central-zip-bundles/subA/src/main/java/multimodule/suba/Hello.java b/src/it/central-zip-bundles/subA/src/main/java/multimodule/suba/Hello.java new file mode 100644 index 0000000..7593984 --- /dev/null +++ b/src/it/central-zip-bundles/subA/src/main/java/multimodule/suba/Hello.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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 multimodule.suba; + +import multimodule.common.Greeting; + +/** + * This is the Greeting implementation in subA + */ +public class Hello implements Greeting { + + /** + * Default constructor. + */ + public Hello() {} + + @Override + public String greet(String name) { + return "Hello $name from suba"; + } +} diff --git a/src/it/central-zip-bundles/verfy.groovy b/src/it/central-zip-bundles/verfy.groovy new file mode 100644 index 0000000..222fd9b --- /dev/null +++ b/src/it/central-zip-bundles/verfy.groovy @@ -0,0 +1,21 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ +// Verify that bundles exists + +// Check the content of each file \ No newline at end of file diff --git a/src/it/central-zip-megabundle/README.md b/src/it/central-zip-megabundle/README.md new file mode 100644 index 0000000..e8067e6 --- /dev/null +++ b/src/it/central-zip-megabundle/README.md @@ -0,0 +1,34 @@ + +# maven-central-publishing-example + +This is a multimodule example showing deployment to maven central +using the new central publishing rest api. + +The maven-deploy-plugin used is a [modified fork](https://github.com/perNyfelt/maven-deploy-plugin/tree/add_central_support) of the apache maven deploy plugin. + +The project consist of +- an aggregator +- a common sub module +- two sub modules that each depends on the common submodule + +Each module (including the main aggregator) will be deployed separately. +I.e. when deploying the whole project, 4 zip files will be created and uploaded to central: +1. The aggregator pom (+ asc, md5 and sha1 files) +2. common, contains the pom, jar, javadoc and sources (all signed and with md5 and sha1 files) +3. subA, contains the pom, jar, javadoc and sources (all signed and with md5 and sha1 files) +4. subB, contains the pom, jar, javadoc and sources (all signed and with md5 and sha1 files) \ No newline at end of file diff --git a/src/it/central-zip-megabundle/common/pom.xml b/src/it/central-zip-megabundle/common/pom.xml new file mode 100644 index 0000000..321a88a --- /dev/null +++ b/src/it/central-zip-megabundle/common/pom.xml @@ -0,0 +1,63 @@ + + + + + 4.0.0 + + se.alipsa.maven.example + publishing-example-parent + 1.0.0 + + publishing-example-common + Maven Central Publishing Example, Common + https://github.com/perNyfelt/maven-central-publishing-example + jar + + Maven Central Publishing Example, Common + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + + + pnyfelt + Per Nyfelt + + + + https://github.com/perNyfelt/maven-central-publishing-example/tree/master + scm:git:https://github.com/perNyfelt/maven-central-publishing-example.git + scm:git:https://github.com/perNyfelt/maven-central-publishing-example.git + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + org.apache.maven.plugins + maven-source-plugin + + + + \ No newline at end of file diff --git a/src/it/central-zip-megabundle/common/readme.md b/src/it/central-zip-megabundle/common/readme.md new file mode 100644 index 0000000..32cc86d --- /dev/null +++ b/src/it/central-zip-megabundle/common/readme.md @@ -0,0 +1,17 @@ + +# This is the common module for the multimodule project \ No newline at end of file diff --git a/src/it/central-zip-megabundle/common/src/main/java/multimodule/common/Greeting.java b/src/it/central-zip-megabundle/common/src/main/java/multimodule/common/Greeting.java new file mode 100644 index 0000000..0a9be56 --- /dev/null +++ b/src/it/central-zip-megabundle/common/src/main/java/multimodule/common/Greeting.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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 multimodule.common; + +/** + * Common interface to define a greeting. + */ +public interface Greeting { + + /** + * Hello {name}. + * + * @param name the name to greet + * @return Hello {name} + */ + String greet(String name); + + /** + * Prints some info. + */ + default void info() { + System.out.println(this.getClass()); + } +} \ No newline at end of file diff --git a/src/it/central-zip-megabundle/pom.xml b/src/it/central-zip-megabundle/pom.xml new file mode 100644 index 0000000..d6f43f7 --- /dev/null +++ b/src/it/central-zip-megabundle/pom.xml @@ -0,0 +1,143 @@ + + + + 4.0.0 + se.alipsa.maven.example + publishing-example-parent + 1.0.0 + + Maven Central Publishing Example, Aggregator + https://github.com/perNyfelt/maven-central-publishing-example + pom + + + common + subA + subB + + + Maven Central Publishing Example, Aggregator + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + + + pnyfelt + Per Nyfelt + + + + https://github.com/perNyfelt/maven-central-publishing-example/tree/master + scm:git:https://github.com/perNyfelt/maven-central-publishing-example.git + scm:git:https://github.com/perNyfelt/maven-central-publishing-example.git + + + UTF-8 + UTF-8 + 1.8 + 1.8 + + + + + central + Central Repository + https://central.sonatype.com/api/v1 + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.0 + + ${maven.compiler.source} + ${maven.compiler.target} + + -Xlint:-options + + + + + org.apache.maven.plugins + maven-deploy-plugin + 3.1.5-SNAPSHOT + + true + false + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.11.2 + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.3.1 + + + attach-sources + + jar + + + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.8 + + + sign-artifacts + verify + + sign + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + + + \ No newline at end of file diff --git a/src/it/central-zip-megabundle/subA/pom.xml b/src/it/central-zip-megabundle/subA/pom.xml new file mode 100644 index 0000000..04244db --- /dev/null +++ b/src/it/central-zip-megabundle/subA/pom.xml @@ -0,0 +1,71 @@ + + + + + 4.0.0 + + se.alipsa.maven.example + publishing-example-parent + 1.0.0 + + publishing-example-subA + Maven Central Publishing Example, SubA + https://github.com/perNyfelt/maven-central-publishing-example + jar + + Maven Central Publishing Example, SubA + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + + + pnyfelt + Per Nyfelt + + + + https://github.com/perNyfelt/maven-central-publishing-example/tree/master + scm:git:https://github.com/perNyfelt/maven-central-publishing-example.git + scm:git:https://github.com/perNyfelt/maven-central-publishing-example.git + + + + se.alipsa.maven.example + publishing-example-common + 1.0.0 + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + org.apache.maven.plugins + maven-source-plugin + + + + \ No newline at end of file diff --git a/src/it/central-zip-megabundle/subA/readme.md b/src/it/central-zip-megabundle/subA/readme.md new file mode 100644 index 0000000..31f35ed --- /dev/null +++ b/src/it/central-zip-megabundle/subA/readme.md @@ -0,0 +1,17 @@ + +# The subA sub projects of the multmodule project \ No newline at end of file diff --git a/src/it/central-zip-megabundle/subA/src/main/java/multimodule/suba/Hello.java b/src/it/central-zip-megabundle/subA/src/main/java/multimodule/suba/Hello.java new file mode 100644 index 0000000..7593984 --- /dev/null +++ b/src/it/central-zip-megabundle/subA/src/main/java/multimodule/suba/Hello.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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 multimodule.suba; + +import multimodule.common.Greeting; + +/** + * This is the Greeting implementation in subA + */ +public class Hello implements Greeting { + + /** + * Default constructor. + */ + public Hello() {} + + @Override + public String greet(String name) { + return "Hello $name from suba"; + } +} diff --git a/src/site/apt/examples/deploy-central.apt b/src/site/apt/examples/deploy-central.apt new file mode 100644 index 0000000..d8852d5 --- /dev/null +++ b/src/site/apt/examples/deploy-central.apt @@ -0,0 +1,29 @@ + ------ + Deployment of artifacts to Maven Central using the Central Portal API + ------ + Per Nyfelt + ------ + 2025-08-01 + ------ + +~~ Licensed to the Apache Software Foundation (ASF) under one +~~ or more contributor license agreements. See the NOTICE file +~~ distributed with this work for additional information +~~ regarding copyright ownership. The ASF licenses this file +~~ to you 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 +~~ +~~ http://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. + +~~ NOTE: For help with the syntax of this file, see: +~~ http://maven.apache.org/doxia/references/apt-format.html + +Deployment of artifacts to Maven Central using the Central Portal API diff --git a/src/site/apt/index.apt.vm b/src/site/apt/index.apt.vm index 1ad2e42..34e030f 100644 --- a/src/site/apt/index.apt.vm +++ b/src/site/apt/index.apt.vm @@ -61,7 +61,7 @@ ${project.name} * Goals Overview - The deploy plugin has 2 goals: + The deploy plugin has 3 goals: * {{{./deploy-mojo.html}deploy:deploy}} is used to automatically install the artifact, its pom and the attached artifacts produced by a particular @@ -73,6 +73,9 @@ ${project.name} an optionally specified pomFile, but can be completed/overriden using the command line. + * {{{./deploy-bundle-mojo.html}deploy:bundle}} is used when you just want to create a + zip bundle that you want to inspect and then manually upload and publish on Maven Central. + [] * Usage @@ -106,6 +109,8 @@ ${project.name} * {{{./examples/deploy-network-issues.html}Workarounds when there are network issues}} + * {{{./examples/deploy-central.html}Deployment using the Central Portal API}} + [] From bc853db68417defba55956f603388819ab39093b Mon Sep 17 00:00:00 2001 From: per Date: Sun, 3 Aug 2025 23:11:29 +0200 Subject: [PATCH 10/25] First integration test (central-zip-bundles) now works. --- pom.xml | 2 +- src/it/central-zip-bundles/invoker.properties | 3 +- src/it/central-zip-bundles/pom.xml | 1 + src/it/central-zip-bundles/setup.groovy | 11 ++- src/it/central-zip-bundles/verfy.groovy | 21 ----- src/it/central-zip-bundles/verify.groovy | 93 +++++++++++++++++++ .../maven/plugins/deploy/BundleService.java | 72 +++++++++++--- .../plugins/deploy/CentralBundleMojo.java | 13 ++- 8 files changed, 174 insertions(+), 42 deletions(-) delete mode 100644 src/it/central-zip-bundles/verfy.groovy create mode 100644 src/it/central-zip-bundles/verify.groovy diff --git a/pom.xml b/pom.xml index 34d3f13..4100a77 100644 --- a/pom.xml +++ b/pom.xml @@ -224,7 +224,7 @@ under the License. true true ${project.build.directory}/it - true + false */pom.xml */non-default-pom.xml diff --git a/src/it/central-zip-bundles/invoker.properties b/src/it/central-zip-bundles/invoker.properties index 18034aa..2af6e87 100644 --- a/src/it/central-zip-bundles/invoker.properties +++ b/src/it/central-zip-bundles/invoker.properties @@ -15,5 +15,4 @@ # specific language governing permissions and limitations # under the License. -# Clean build of the jars -invoker.goals = ${project.groupId}:${project.artifactId}:${project.version}:bundle +invoker.goals = verify ${project.groupId}:${project.artifactId}:${project.version}:bundle diff --git a/src/it/central-zip-bundles/pom.xml b/src/it/central-zip-bundles/pom.xml index e5b45ef..5f478ec 100644 --- a/src/it/central-zip-bundles/pom.xml +++ b/src/it/central-zip-bundles/pom.xml @@ -88,6 +88,7 @@ true false + false diff --git a/src/it/central-zip-bundles/setup.groovy b/src/it/central-zip-bundles/setup.groovy index 042406c..07f7010 100644 --- a/src/it/central-zip-bundles/setup.groovy +++ b/src/it/central-zip-bundles/setup.groovy @@ -19,7 +19,7 @@ import org.apache.maven.model.Model import org.apache.maven.model.io.xpp3.MavenXpp3Reader // Generate jars -"mvn verify".execute().waitFor() +"mvn package".execute().waitFor() // Since we do not want to mess with external gpg config, we just create fake asc files signFile(copyPom("pom.xml", "target")) @@ -40,6 +40,7 @@ def signJarFiles(String dir) { static def signFile(File file) { def ascFile = new File(file.getParentFile(), "${file.getName()}.asc") + println "Setup: creating signing file: $ascFile" ascFile << "fake signed" } @@ -76,6 +77,12 @@ static def readPomInfo(File pomFile) { } def getTestDir() { - new File("${basedir}") + def bd + if (binding.hasVariable('basedir')) { + bd = binding.getVariable('basedir') + } else { + bd = System.getProperty("basedir") + } + bd instanceof File ? bd : new File(bd) } diff --git a/src/it/central-zip-bundles/verfy.groovy b/src/it/central-zip-bundles/verfy.groovy deleted file mode 100644 index 222fd9b..0000000 --- a/src/it/central-zip-bundles/verfy.groovy +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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 - * - * http://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. - */ -// Verify that bundles exists - -// Check the content of each file \ No newline at end of file diff --git a/src/it/central-zip-bundles/verify.groovy b/src/it/central-zip-bundles/verify.groovy new file mode 100644 index 0000000..a0cc77d --- /dev/null +++ b/src/it/central-zip-bundles/verify.groovy @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ +import java.util.zip.ZipFile + +// Verify that bundles exists +def aggregatorZip = new File(getBaseDir(), "target/publishing-example-parent-1.0.0-bundle.zip") +def commonZip = new File(getBaseDir(), "common/target/publishing-example-common-1.0.0-bundle.zip") +def subAZip = new File(getBaseDir(), "subA/target/publishing-example-subA-1.0.0-bundle.zip") +assert aggregatorZip.exists() +assert commonZip.exists() +assert subAZip.exists() + +// Check the content of each file +String groupId = "se.alipsa.maven.example" +def expectedAggregatorEntries = { String artifactId, String version -> + String basePath = "${groupId.replace('.', '/')}/${artifactId}/${version}/" + + String artifactPath = basePath + artifactId + '-' + version + [ + artifactPath + '.pom', + artifactPath +'.pom.asc', + artifactPath +'.pom.md5', + artifactPath +'.pom.sha1', + artifactPath +'.pom.sha256' + ] +} + +def expectedFullEntries = {String artifactId, String version -> + String basePath = "${groupId.replace('.', '/')}/${artifactId}/${version}/" + String artifactPath = basePath + artifactId + '-' + version + [ + artifactPath + '.jar', + artifactPath + '.jar.asc', + artifactPath + '.jar.md5', + artifactPath + '.jar.sha1', + artifactPath + '.jar.sha256', + artifactPath + '-sources.jar', + artifactPath + '-sources.jar.asc', + artifactPath + '-sources.jar.md5', + artifactPath + '-sources.jar.sha1', + artifactPath + '-sources.jar.sha256', + artifactPath + '-javadoc.jar', + artifactPath + '-javadoc.jar.asc', + artifactPath + '-javadoc.jar.md5', + artifactPath + '-javadoc.jar.sha1', + artifactPath + '-javadoc.jar.sha256' + ] + expectedAggregatorEntries(artifactId, version) +} +checkZipContent(aggregatorZip, "publishing-example-parent", "1.0.0", expectedAggregatorEntries) +checkZipContent(commonZip, "publishing-example-common", "1.0.0", expectedFullEntries) +checkZipContent(subAZip, "publishing-example-subA", "1.0.0", expectedFullEntries) + +static def checkZipContent(File zipFile, String artifactId, String version, Closure expectedMethod) { + + println "Checking content of $zipFile" + List expectedEntries = expectedMethod(artifactId, version) + List actualEntries + try (ZipFile zip = new ZipFile(zipFile)) { + actualEntries = zip.entries().collect { it.name } + } + expectedEntries.each { + println " - checking $it" + assert actualEntries.contains(it) : "Expected entry not found in ZIP: $it" + } + assert expectedEntries.size() == actualEntries.size() : "Mismatch in number of entries in ZIP" + +} + +def getBaseDir() { + def bd + if (binding.hasVariable('basedir')) { + bd = binding.getVariable('basedir') + } else { + bd = System.getProperty("basedir") + } + bd instanceof File ? bd : new File(bd) +} \ No newline at end of file diff --git a/src/main/java/org/apache/maven/plugins/deploy/BundleService.java b/src/main/java/org/apache/maven/plugins/deploy/BundleService.java index 9c50c54..1817c42 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/BundleService.java +++ b/src/main/java/org/apache/maven/plugins/deploy/BundleService.java @@ -31,7 +31,6 @@ import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -85,6 +84,11 @@ public void createZipBundle(File bundleFile, List allProjectsUsing // Will be null for e.g., an aggregator project if (artifactFile != null && artifactFile.exists()) { artifactFiles.get(mavenPathPrefix).add(artifactFile); + File signFile = new File(artifactFile.getAbsolutePath() + ".asc"); + if (!signFile.exists()) { + throw new MojoExecutionException(artifactFile + " is not signed, " + signFile + " is missing"); + } + artifactFiles.get(mavenPathPrefix).add(signFile); } // pom is not in getAttachedArtifacts so add it explicitly File pomFile = new File(project.getBuild().getDirectory(), String.join("-", artifactId, version) + ".pom"); @@ -92,11 +96,29 @@ public void createZipBundle(File bundleFile, List allProjectsUsing // Since it is the "raw" pom file that is published, not the effective pom, we must check the file, // not the project. Also since the pom file is the signed one, we cannot change it to the effective pom. validateForPublishing(pomFile); + artifactFiles.get(mavenPathPrefix).add(pomFile); + File signFile = new File(pomFile.getAbsolutePath() + ".asc"); + if (!signFile.exists()) { + throw new MojoExecutionException( + "POM file " + pomFile + " is not signed, " + signFile + " is missing"); + } + artifactFiles.get(mavenPathPrefix).add(signFile); + } else { log.error("POM file " + pomFile + " does not exist (verify phase not reached)!"); throw new MojoExecutionException("POM file " + pomFile + " does not exist!"); } + log.info("**********************************"); + log.info("Artifacts are:"); + project.getArtifacts().forEach(artifact -> { + log.info(artifact.getFile().getAbsolutePath()); + }); + log.info("Attached artifacts are:"); + project.getAttachedArtifacts().forEach(artifact -> { + log.info(artifact.getFile().getAbsolutePath()); + }); + log.info("**********************************"); for (Artifact artifact : project.getAttachedArtifacts()) { File file = artifact.getFile(); if (file.exists()) { @@ -104,20 +126,26 @@ public void createZipBundle(File bundleFile, List allProjectsUsing } else { log.error("Artifact " + artifact.getId() + " does not exist!"); } + File signFile = new File(file.getAbsolutePath() + ".asc"); + if (!signFile.exists()) { + throw new MojoExecutionException("File " + file + " is not signed, " + signFile + " is missing"); + } + artifactFiles.get(mavenPathPrefix).add(signFile); } } - try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(bundleFile.toPath()))) { + log.info("Adding the following entries to the zip file"); + log.info("********************************************"); + try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(bundleFile.toPath()))) { for (Map.Entry> entry : artifactFiles.entrySet()) { String mavenPathPrefix = entry.getKey(); for (File file : entry.getValue()) { - zipOut.putNextEntry(new ZipEntry(mavenPathPrefix + file.getName())); - Files.copy(file.toPath(), zipOut); - zipOut.closeEntry(); + addToZip(file, mavenPathPrefix, zipOut); if (file.getName().endsWith(".asc")) { continue; // asc files has no checksums } + // Ensure the artifact is signed before continuing to create and add checksums File signFile = new File(file.getAbsolutePath() + ".asc"); if (!signFile.exists()) { throw new MojoExecutionException( @@ -140,8 +168,6 @@ public void createZipBundle(File bundleFile, List allProjectsUsing * the environment. */ public void createZipBundle(File bundleFile) throws IOException, NoSuchAlgorithmException, MojoExecutionException { - createZipBundle(bundleFile, Collections.singletonList(project)); - /* bundleFile.getParentFile().mkdirs(); bundleFile.createNewFile(); String groupId = project.getGroupId(); @@ -155,6 +181,11 @@ public void createZipBundle(File bundleFile) throws IOException, NoSuchAlgorithm // Will be null for e.g., an aggregator project if (artifactFile != null && artifactFile.exists()) { artifactFiles.add(artifactFile); + File signFile = new File(artifactFile.getAbsolutePath() + ".asc"); + if (!signFile.exists()) { + throw new MojoExecutionException(artifactFile + " is not signed, " + signFile + " is missing"); + } + artifactFiles.add(signFile); } // pom is not in getAttachedArtifacts so add it explicitly @@ -164,6 +195,11 @@ public void createZipBundle(File bundleFile) throws IOException, NoSuchAlgorithm // not the project. Also since the pom file is the signed one, we cannot change it to the effective pom. validateForPublishing(pomFile); artifactFiles.add(pomFile); + File signFile = new File(pomFile.getAbsolutePath() + ".asc"); + if (!signFile.exists()) { + throw new MojoExecutionException("POM file " + pomFile + " is not signed, " + signFile + " is missing"); + } + artifactFiles.add(signFile); } else { log.error("POM file " + pomFile + " does not exist (verify phase not reached)!"); throw new MojoExecutionException("POM file " + pomFile + " does not exist!"); @@ -172,6 +208,11 @@ public void createZipBundle(File bundleFile) throws IOException, NoSuchAlgorithm File file = artifact.getFile(); if (file.exists()) { artifactFiles.add(artifact.getFile()); + File signFile = new File(file.getAbsolutePath() + ".asc"); + if (!signFile.exists()) { + throw new MojoExecutionException(file + " is not signed, " + signFile + " is missing"); + } + artifactFiles.add(signFile); } else { log.error("Artifact " + artifact.getId() + " does not exist!"); } @@ -180,9 +221,7 @@ public void createZipBundle(File bundleFile) throws IOException, NoSuchAlgorithm try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(bundleFile.toPath()))) { for (File file : artifactFiles) { - zipOut.putNextEntry(new ZipEntry(mavenPathPrefix + file.getName())); - Files.copy(file.toPath(), zipOut); - zipOut.closeEntry(); + addToZip(file, mavenPathPrefix, zipOut); if (file.getName().endsWith(".asc")) { continue; // asc files has no checksums } @@ -195,8 +234,6 @@ public void createZipBundle(File bundleFile) throws IOException, NoSuchAlgorithm } } log.info("Created bundle at: " + bundleFile.getAbsolutePath()); - - */ } /** @@ -231,13 +268,18 @@ private void validateForPublishing(File pomFile) throws MojoExecutionException { } } + private void addToZip(File file, String prefix, ZipOutputStream zipOut) throws IOException { + log.info("addToZip - " + file.getAbsolutePath()); + zipOut.putNextEntry(new ZipEntry(prefix + file.getName())); + Files.copy(file.toPath(), zipOut); + zipOut.closeEntry(); + } + private void generateChecksumsAndAddToZip(File sourceFile, String prefix, ZipOutputStream zipOut) throws NoSuchAlgorithmException, IOException { for (String algo : CHECKSUM_ALGOS) { File checksumFile = generateChecksum(sourceFile, algo); - zipOut.putNextEntry(new ZipEntry(prefix + checksumFile.getName())); - Files.copy(checksumFile.toPath(), zipOut); - zipOut.closeEntry(); + addToZip(checksumFile, prefix, zipOut); } } diff --git a/src/main/java/org/apache/maven/plugins/deploy/CentralBundleMojo.java b/src/main/java/org/apache/maven/plugins/deploy/CentralBundleMojo.java index 20d1eda..fa20463 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/CentralBundleMojo.java +++ b/src/main/java/org/apache/maven/plugins/deploy/CentralBundleMojo.java @@ -19,6 +19,7 @@ package org.apache.maven.plugins.deploy; import java.io.File; +import java.util.List; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.LifecyclePhase; @@ -37,6 +38,12 @@ public class CentralBundleMojo extends AbstractDeployMojo { @Parameter(defaultValue = "${project}", readonly = true) private MavenProject project; + @Parameter(defaultValue = "${reactorProjects}", required = true, readonly = true) + private List reactorProjects; + + @Parameter(defaultValue = "true", property = "deployAtEnd") + private boolean deployAtEnd; + @Override public void execute() throws MojoExecutionException { File targetDir = new File(project.getBuild().getDirectory()); @@ -44,7 +51,11 @@ public void execute() throws MojoExecutionException { try { BundleService bundleService = new BundleService(project, getLog()); - bundleService.createZipBundle(bundleFile); + if (deployAtEnd) { + bundleService.createZipBundle(bundleFile, reactorProjects); + } else { + bundleService.createZipBundle(bundleFile); + } getLog().info("Bundle created successfully: " + bundleFile); } catch (Exception e) { throw new MojoExecutionException("Failed to create bundle", e); From 33cfe58b18506236af25323654a7091239b356bd Mon Sep 17 00:00:00 2001 From: pernyf Date: Mon, 4 Aug 2025 13:28:40 +0200 Subject: [PATCH 11/25] move setup logic to inside the build script to mock what the gpg plugin would do. --- pom.xml | 2 +- src/it/central-zip-bundles/README.md | 20 ++++++++++- .../{setup.groovy => fakeSign.groovy} | 20 ++--------- src/it/central-zip-bundles/pom.xml | 36 +++++++++++++++++-- 4 files changed, 57 insertions(+), 21 deletions(-) rename src/it/central-zip-bundles/{setup.groovy => fakeSign.groovy} (82%) diff --git a/pom.xml b/pom.xml index 4100a77..34d3f13 100644 --- a/pom.xml +++ b/pom.xml @@ -224,7 +224,7 @@ under the License. true true ${project.build.directory}/it - false + true */pom.xml */non-default-pom.xml diff --git a/src/it/central-zip-bundles/README.md b/src/it/central-zip-bundles/README.md index 1249e45..6d3377d 100644 --- a/src/it/central-zip-bundles/README.md +++ b/src/it/central-zip-bundles/README.md @@ -50,4 +50,22 @@ Note, normally we would have the gpg plugin configured in the build, e.g: ``` -But this requires some external setup so we just create fake asc files in this test. \ No newline at end of file +But this requires some external setup so we just create fake asc files in this test. +We cannot do it in setup.groovy as the invoker plugin uses true so +instead we mimic what the sign plugin would do in a groovy script (fakeSign.groovy) that we +call from the pom (and hence it is part of the build, which is also closer to reality). + +## Running only this test +```shell +mvn -Prun-its verify -Dinvoker.test=central-zip-bundles +``` + +## Running the test manually +```shell +CLASSPATH=$(find "$MAVEN_HOME/lib" -name "*.jar" | tr '\n' ':' | sed 's/:$//') +groovy -cp $CLASSPATH -Dbasedir=$PWD setup.groovy +mvn verify deploy:bundle +groovy -cp $CLASSPATH -Dbasedir=$PWD verify.groovy +``` + + diff --git a/src/it/central-zip-bundles/setup.groovy b/src/it/central-zip-bundles/fakeSign.groovy similarity index 82% rename from src/it/central-zip-bundles/setup.groovy rename to src/it/central-zip-bundles/fakeSign.groovy index 07f7010..d75cd9d 100644 --- a/src/it/central-zip-bundles/setup.groovy +++ b/src/it/central-zip-bundles/fakeSign.groovy @@ -18,18 +18,10 @@ */ import org.apache.maven.model.Model import org.apache.maven.model.io.xpp3.MavenXpp3Reader -// Generate jars -"mvn package".execute().waitFor() // Since we do not want to mess with external gpg config, we just create fake asc files signFile(copyPom("pom.xml", "target")) -signFile(copyPom("common/pom.xml", "common/target")) -signJarFiles("common/target") -signFile(copyPom("subA/pom.xml", "subA/target")) -signJarFiles("subA/target") - -// returning null is required by the invoker plugin -return null +signJarFiles("target") def signJarFiles(String dir) { def targetDir = new File(getTestDir(), dir) @@ -76,13 +68,7 @@ static def readPomInfo(File pomFile) { } } -def getTestDir() { - def bd - if (binding.hasVariable('basedir')) { - bd = binding.getVariable('basedir') - } else { - bd = System.getProperty("basedir") - } - bd instanceof File ? bd : new File(bd) +File getTestDir() { + return project.basedir as File } diff --git a/src/it/central-zip-bundles/pom.xml b/src/it/central-zip-bundles/pom.xml index 5f478ec..b9c7898 100644 --- a/src/it/central-zip-bundles/pom.xml +++ b/src/it/central-zip-bundles/pom.xml @@ -42,8 +42,8 @@ - pnyfelt - Per Nyfelt + pnyfelt + Per Nyfelt @@ -120,9 +120,41 @@ + + org.codehaus.gmavenplus + gmavenplus-plugin + 1.13.1 + + + simulate-signing + verify + + execute + + + + fakeSign.groovy + + + + + + + org.apache.groovy + groovy + 4.0.27 + + + org.apache.groovy + groovy-ant + 4.0.27 + + + org.apache.maven.plugins maven-deploy-plugin + @project.version@ From a5b42cd5db72845a42e30d3aa49f567fe0a327ab Mon Sep 17 00:00:00 2001 From: pernyf Date: Mon, 4 Aug 2025 14:23:50 +0200 Subject: [PATCH 12/25] use variables when available. Improve support for invokation from different env. --- src/it/central-zip-bundles/fakeSign.groovy | 14 ++- src/it/central-zip-bundles/pom.xml | 10 +- src/it/central-zip-bundles/verify.groovy | 9 +- src/it/central-zip-megabundle/fakeSign.groovy | 86 ++++++++++++++ .../central-zip-megabundle/invoker.properties | 18 +++ src/it/central-zip-megabundle/pom.xml | 38 +++++-- src/it/central-zip-megabundle/verify.groovy | 107 ++++++++++++++++++ 7 files changed, 265 insertions(+), 17 deletions(-) create mode 100644 src/it/central-zip-megabundle/fakeSign.groovy create mode 100644 src/it/central-zip-megabundle/invoker.properties create mode 100644 src/it/central-zip-megabundle/verify.groovy diff --git a/src/it/central-zip-bundles/fakeSign.groovy b/src/it/central-zip-bundles/fakeSign.groovy index d75cd9d..b432045 100644 --- a/src/it/central-zip-bundles/fakeSign.groovy +++ b/src/it/central-zip-bundles/fakeSign.groovy @@ -69,6 +69,18 @@ static def readPomInfo(File pomFile) { } File getTestDir() { - return project.basedir as File + if (binding.hasVariable('project')) { + // from gmavenplus + return binding.getVariable('project').basedir as File + } + def bd + if (binding.hasVariable('basedir')) { + // from the invoker plugin + bd = binding.getVariable('basedir') + } else { + // from command line (e.g. with -Dbasedir=$PWD) + bd = System.getProperty("basedir") + } + bd instanceof File ? bd : new File(bd) } diff --git a/src/it/central-zip-bundles/pom.xml b/src/it/central-zip-bundles/pom.xml index b9c7898..f39748d 100644 --- a/src/it/central-zip-bundles/pom.xml +++ b/src/it/central-zip-bundles/pom.xml @@ -72,7 +72,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.14.0 + @mavenCompilerPluginVersion@ ${maven.compiler.source} ${maven.compiler.target} @@ -84,7 +84,7 @@ org.apache.maven.plugins maven-deploy-plugin - 3.1.5-SNAPSHOT + @project.version@ true false @@ -94,7 +94,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.11.2 + @mavenJavadocPluginVersion@ attach-javadocs @@ -107,7 +107,7 @@ org.apache.maven.plugins maven-source-plugin - 3.3.1 + @mavenSourcePluginVersion@ attach-sources @@ -121,6 +121,7 @@ + org.codehaus.gmavenplus gmavenplus-plugin 1.13.1 @@ -154,7 +155,6 @@ org.apache.maven.plugins maven-deploy-plugin - @project.version@ diff --git a/src/it/central-zip-bundles/verify.groovy b/src/it/central-zip-bundles/verify.groovy index a0cc77d..ecfffa5 100644 --- a/src/it/central-zip-bundles/verify.groovy +++ b/src/it/central-zip-bundles/verify.groovy @@ -82,11 +82,18 @@ static def checkZipContent(File zipFile, String artifactId, String version, Clos } -def getBaseDir() { +// make it possible to run this script from different locations (command line, gmavenplus, invoker) +File getBaseDir() { + if (binding.hasVariable('project')) { + // from gmavenplus + return binding.getVariable('project').basedir as File + } def bd if (binding.hasVariable('basedir')) { + // from the invoker plugin bd = binding.getVariable('basedir') } else { + // from command line (e.g. with -Dbasedir=$PWD) bd = System.getProperty("basedir") } bd instanceof File ? bd : new File(bd) diff --git a/src/it/central-zip-megabundle/fakeSign.groovy b/src/it/central-zip-megabundle/fakeSign.groovy new file mode 100644 index 0000000..b432045 --- /dev/null +++ b/src/it/central-zip-megabundle/fakeSign.groovy @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ +import org.apache.maven.model.Model +import org.apache.maven.model.io.xpp3.MavenXpp3Reader + +// Since we do not want to mess with external gpg config, we just create fake asc files +signFile(copyPom("pom.xml", "target")) +signJarFiles("target") + +def signJarFiles(String dir) { + def targetDir = new File(getTestDir(), dir) + targetDir.listFiles({ file -> file.name.endsWith(".jar") } as FileFilter).each { + signFile(it) + } +} + +static def signFile(File file) { + def ascFile = new File(file.getParentFile(), "${file.getName()}.asc") + println "Setup: creating signing file: $ascFile" + ascFile << "fake signed" +} + +def copyPom(String from, String toDir) { + File testDir = getTestDir() + def src = new File(testDir, from) + def pomInfo = readPomInfo(src) + def dstDir = new File(testDir, toDir) + dstDir.mkdirs() + def toFile = new File(dstDir, "$pomInfo.artifactId-${pomInfo.version}.pom") + toFile.text = src.text + return toFile +} + +static def readPomInfo(File pomFile) { + MavenXpp3Reader reader = new MavenXpp3Reader() + pomFile.withReader { r -> + Model model = reader.read(r) + + // Inherit groupId/version from parent if missing + if (!model.groupId && model.parent) { + model.groupId = model.parent.groupId + } + if (!model.version && model.parent) { + model.version = model.parent.version + } + + return [ + groupId : model.groupId, + artifactId : model.artifactId, + version : model.version + ] + } +} + +File getTestDir() { + if (binding.hasVariable('project')) { + // from gmavenplus + return binding.getVariable('project').basedir as File + } + def bd + if (binding.hasVariable('basedir')) { + // from the invoker plugin + bd = binding.getVariable('basedir') + } else { + // from command line (e.g. with -Dbasedir=$PWD) + bd = System.getProperty("basedir") + } + bd instanceof File ? bd : new File(bd) +} + diff --git a/src/it/central-zip-megabundle/invoker.properties b/src/it/central-zip-megabundle/invoker.properties new file mode 100644 index 0000000..2af6e87 --- /dev/null +++ b/src/it/central-zip-megabundle/invoker.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. + +invoker.goals = verify ${project.groupId}:${project.artifactId}:${project.version}:bundle diff --git a/src/it/central-zip-megabundle/pom.xml b/src/it/central-zip-megabundle/pom.xml index d6f43f7..bab714c 100644 --- a/src/it/central-zip-megabundle/pom.xml +++ b/src/it/central-zip-megabundle/pom.xml @@ -31,7 +31,6 @@ common subA - subB Maven Central Publishing Example, Aggregator @@ -73,7 +72,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.14.0 + @mavenCompilerPluginVersion@ ${maven.compiler.source} ${maven.compiler.target} @@ -85,16 +84,17 @@ org.apache.maven.plugins maven-deploy-plugin - 3.1.5-SNAPSHOT + @project.version@ true false + true org.apache.maven.plugins maven-javadoc-plugin - 3.11.2 + @mavenJavadocPluginVersion@ attach-javadocs @@ -107,7 +107,7 @@ org.apache.maven.plugins maven-source-plugin - 3.3.1 + @mavenSourcePluginVersion@ attach-sources @@ -121,18 +121,36 @@ - org.apache.maven.plugins - maven-gpg-plugin - 3.2.8 + + org.codehaus.gmavenplus + gmavenplus-plugin + 1.13.1 - sign-artifacts + simulate-signing verify - sign + execute + + + fakeSign.groovy + + + + + org.apache.groovy + groovy + 4.0.27 + + + org.apache.groovy + groovy-ant + 4.0.27 + + org.apache.maven.plugins diff --git a/src/it/central-zip-megabundle/verify.groovy b/src/it/central-zip-megabundle/verify.groovy new file mode 100644 index 0000000..a60b0db --- /dev/null +++ b/src/it/central-zip-megabundle/verify.groovy @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ +import java.util.zip.ZipFile + +// Verify that bundles exists +def aggregatorZip = new File(getBaseDir(), "target/publishing-example-parent-1.0.0-bundle.zip") +assert aggregatorZip.exists() +// We should NOT have any bundles for the sub modules +def commonZip = new File(getBaseDir(), "common/target/publishing-example-common-1.0.0-bundle.zip") +def subAZip = new File(getBaseDir(), "subA/target/publishing-example-subA-1.0.0-bundle.zip") +assert !commonZip.exists() +assert !subAZip.exists() + +// Check the content of the bundle +String groupId = "se.alipsa.maven.example" +def expectedAggregatorEntries = { String artifactId, String version -> + String basePath = "${groupId.replace('.', '/')}/${artifactId}/${version}/" + + String artifactPath = basePath + artifactId + '-' + version + [ + artifactPath + '.pom', + artifactPath +'.pom.asc', + artifactPath +'.pom.md5', + artifactPath +'.pom.sha1', + artifactPath +'.pom.sha256' + ] +} + +def expectedFullEntries = {String artifactId, String version -> + String basePath = "${groupId.replace('.', '/')}/${artifactId}/${version}/" + String artifactPath = basePath + artifactId + '-' + version + [ + artifactPath + '.pom', + artifactPath +'.pom.asc', + artifactPath +'.pom.md5', + artifactPath +'.pom.sha1', + artifactPath +'.pom.sha256', + artifactPath + '.jar', + artifactPath + '.jar.asc', + artifactPath + '.jar.md5', + artifactPath + '.jar.sha1', + artifactPath + '.jar.sha256', + artifactPath + '-sources.jar', + artifactPath + '-sources.jar.asc', + artifactPath + '-sources.jar.md5', + artifactPath + '-sources.jar.sha1', + artifactPath + '-sources.jar.sha256', + artifactPath + '-javadoc.jar', + artifactPath + '-javadoc.jar.asc', + artifactPath + '-javadoc.jar.md5', + artifactPath + '-javadoc.jar.sha1', + artifactPath + '-javadoc.jar.sha256' + ] + expectedAggregatorEntries(artifactId, version) +} +// Ensure aggregator, common and subA files exists +checkZipContent(aggregatorZip, "publishing-example-parent", "1.0.0", expectedAggregatorEntries) +checkZipContent(aggregatorZip, "publishing-example-common", "1.0.0", expectedFullEntries) +checkZipContent(aggregatorZip, "publishing-example-subA", "1.0.0", expectedFullEntries) + +static def checkZipContent(File zipFile, String artifactId, String version, Closure expectedMethod) { + + println "Checking content of $zipFile" + List expectedEntries = expectedMethod(artifactId, version) + List actualEntries + try (ZipFile zip = new ZipFile(zipFile)) { + actualEntries = zip.entries().collect { it.name } + } + expectedEntries.each { + println " - checking $it" + assert actualEntries.contains(it) : "Expected entry not found in ZIP: $it" + } + assert expectedEntries.size() == actualEntries.size() : "Mismatch in number of entries in ZIP" + +} + +// make it possible to run this script from different locations (command line, gmavenplus, invoker) +File getBaseDir() { + if (binding.hasVariable('project')) { + // from gmavenplus + return binding.getVariable('project').basedir as File + } + def bd + if (binding.hasVariable('basedir')) { + // from the invoker plugin + bd = binding.getVariable('basedir') + } else { + // from command line (e.g. with -Dbasedir=$PWD) + bd = System.getProperty("basedir") + } + bd instanceof File ? bd : new File(bd) +} \ No newline at end of file From 5483453508619215d0540d7817b84aee895ca73d Mon Sep 17 00:00:00 2001 From: pernyf Date: Tue, 5 Aug 2025 12:27:12 +0200 Subject: [PATCH 13/25] Rename BundleService to Bundler. Integrate functionality into the DeployMojo and remove CentralBundleMojo. Fix verify scripts in central-zip-bundles and central-zip-megabundle. --- src/it/central-zip-bundles/README.md | 1 - src/it/central-zip-bundles/common/pom.xml | 16 ++--- src/it/central-zip-bundles/invoker.properties | 2 +- src/it/central-zip-bundles/pom.xml | 2 +- src/it/central-zip-bundles/subA/pom.xml | 5 +- src/it/central-zip-bundles/verify.groovy | 1 + src/it/central-zip-megabundle/README.md | 19 +++++- .../central-zip-megabundle/invoker.properties | 2 +- src/it/central-zip-megabundle/pom.xml | 2 +- src/it/central-zip-megabundle/verify.groovy | 38 +++++++---- .../{BundleService.java => Bundler.java} | 25 +++---- .../plugins/deploy/CentralBundleMojo.java | 64 ------------------ .../maven/plugins/deploy/DeployMojo.java | 65 ++++++++++++++----- 13 files changed, 113 insertions(+), 129 deletions(-) rename src/main/java/org/apache/maven/plugins/deploy/{BundleService.java => Bundler.java} (94%) delete mode 100644 src/main/java/org/apache/maven/plugins/deploy/CentralBundleMojo.java diff --git a/src/it/central-zip-bundles/README.md b/src/it/central-zip-bundles/README.md index 6d3377d..d5dda02 100644 --- a/src/it/central-zip-bundles/README.md +++ b/src/it/central-zip-bundles/README.md @@ -63,7 +63,6 @@ mvn -Prun-its verify -Dinvoker.test=central-zip-bundles ## Running the test manually ```shell CLASSPATH=$(find "$MAVEN_HOME/lib" -name "*.jar" | tr '\n' ':' | sed 's/:$//') -groovy -cp $CLASSPATH -Dbasedir=$PWD setup.groovy mvn verify deploy:bundle groovy -cp $CLASSPATH -Dbasedir=$PWD verify.groovy ``` diff --git a/src/it/central-zip-bundles/common/pom.xml b/src/it/central-zip-bundles/common/pom.xml index 321a88a..f3f4316 100644 --- a/src/it/central-zip-bundles/common/pom.xml +++ b/src/it/central-zip-bundles/common/pom.xml @@ -50,14 +50,14 @@ under the License. - - org.apache.maven.plugins - maven-javadoc-plugin - - - org.apache.maven.plugins - maven-source-plugin - + + org.apache.maven.plugins + maven-source-plugin + + + org.apache.maven.plugins + maven-javadoc-plugin +
\ No newline at end of file diff --git a/src/it/central-zip-bundles/invoker.properties b/src/it/central-zip-bundles/invoker.properties index 2af6e87..1f3fd89 100644 --- a/src/it/central-zip-bundles/invoker.properties +++ b/src/it/central-zip-bundles/invoker.properties @@ -15,4 +15,4 @@ # specific language governing permissions and limitations # under the License. -invoker.goals = verify ${project.groupId}:${project.artifactId}:${project.version}:bundle +invoker.goals = deploy \ No newline at end of file diff --git a/src/it/central-zip-bundles/pom.xml b/src/it/central-zip-bundles/pom.xml index f39748d..e14ac93 100644 --- a/src/it/central-zip-bundles/pom.xml +++ b/src/it/central-zip-bundles/pom.xml @@ -87,8 +87,8 @@ @project.version@ true - false false + false diff --git a/src/it/central-zip-bundles/subA/pom.xml b/src/it/central-zip-bundles/subA/pom.xml index 04244db..5aa7e3a 100644 --- a/src/it/central-zip-bundles/subA/pom.xml +++ b/src/it/central-zip-bundles/subA/pom.xml @@ -55,16 +55,15 @@ under the License. 1.0.0 - org.apache.maven.plugins - maven-javadoc-plugin + maven-source-plugin org.apache.maven.plugins - maven-source-plugin + maven-javadoc-plugin diff --git a/src/it/central-zip-bundles/verify.groovy b/src/it/central-zip-bundles/verify.groovy index ecfffa5..d67afdd 100644 --- a/src/it/central-zip-bundles/verify.groovy +++ b/src/it/central-zip-bundles/verify.groovy @@ -96,5 +96,6 @@ File getBaseDir() { // from command line (e.g. with -Dbasedir=$PWD) bd = System.getProperty("basedir") } + if (bd == null) bd = '.' bd instanceof File ? bd : new File(bd) } \ No newline at end of file diff --git a/src/it/central-zip-megabundle/README.md b/src/it/central-zip-megabundle/README.md index e8067e6..fbd94e3 100644 --- a/src/it/central-zip-megabundle/README.md +++ b/src/it/central-zip-megabundle/README.md @@ -26,9 +26,22 @@ The project consist of - a common sub module - two sub modules that each depends on the common submodule -Each module (including the main aggregator) will be deployed separately. -I.e. when deploying the whole project, 4 zip files will be created and uploaded to central: +Each module (including the main aggregator) will be deployed together. +I.e. when deploying the whole project, 1 zip file will be created and uploaded to central: +This 1 zip will contain: 1. The aggregator pom (+ asc, md5 and sha1 files) 2. common, contains the pom, jar, javadoc and sources (all signed and with md5 and sha1 files) 3. subA, contains the pom, jar, javadoc and sources (all signed and with md5 and sha1 files) -4. subB, contains the pom, jar, javadoc and sources (all signed and with md5 and sha1 files) \ No newline at end of file +4. subB, contains the pom, jar, javadoc and sources (all signed and with md5 and sha1 files) + +## Running only this test +```shell +mvn -Prun-its verify -Dinvoker.test=central-zip-megabundle +``` + +## Running the test manually +```shell +CLASSPATH=$(find "$MAVEN_HOME/lib" -name "*.jar" | tr '\n' ':' | sed 's/:$//') +mvn verify deploy:bundle +groovy -cp $CLASSPATH -Dbasedir=$PWD verify.groovy +``` \ No newline at end of file diff --git a/src/it/central-zip-megabundle/invoker.properties b/src/it/central-zip-megabundle/invoker.properties index 2af6e87..a8c0237 100644 --- a/src/it/central-zip-megabundle/invoker.properties +++ b/src/it/central-zip-megabundle/invoker.properties @@ -15,4 +15,4 @@ # specific language governing permissions and limitations # under the License. -invoker.goals = verify ${project.groupId}:${project.artifactId}:${project.version}:bundle +invoker.goals = deploy diff --git a/src/it/central-zip-megabundle/pom.xml b/src/it/central-zip-megabundle/pom.xml index bab714c..996ced9 100644 --- a/src/it/central-zip-megabundle/pom.xml +++ b/src/it/central-zip-megabundle/pom.xml @@ -87,8 +87,8 @@ @project.version@ true - false true + false diff --git a/src/it/central-zip-megabundle/verify.groovy b/src/it/central-zip-megabundle/verify.groovy index a60b0db..b8e7aad 100644 --- a/src/it/central-zip-megabundle/verify.groovy +++ b/src/it/central-zip-megabundle/verify.groovy @@ -19,11 +19,13 @@ import java.util.zip.ZipFile // Verify that bundles exists -def aggregatorZip = new File(getBaseDir(), "target/publishing-example-parent-1.0.0-bundle.zip") -assert aggregatorZip.exists() +def megaZip = new File(getBaseDir(), "target/se.alipsa.maven.example-1.0.0-bundle.zip") +assert megaZip.exists() // We should NOT have any bundles for the sub modules +def aggregatorZip = new File(getBaseDir(), "target/publishing-example-parent-1.0.0-bundle.zip") def commonZip = new File(getBaseDir(), "common/target/publishing-example-common-1.0.0-bundle.zip") def subAZip = new File(getBaseDir(), "subA/target/publishing-example-subA-1.0.0-bundle.zip") +assert !aggregatorZip.exists() assert !commonZip.exists() assert !subAZip.exists() @@ -46,11 +48,6 @@ def expectedFullEntries = {String artifactId, String version -> String basePath = "${groupId.replace('.', '/')}/${artifactId}/${version}/" String artifactPath = basePath + artifactId + '-' + version [ - artifactPath + '.pom', - artifactPath +'.pom.asc', - artifactPath +'.pom.md5', - artifactPath +'.pom.sha1', - artifactPath +'.pom.sha256', artifactPath + '.jar', artifactPath + '.jar.asc', artifactPath + '.jar.md5', @@ -68,13 +65,23 @@ def expectedFullEntries = {String artifactId, String version -> artifactPath + '-javadoc.jar.sha256' ] + expectedAggregatorEntries(artifactId, version) } -// Ensure aggregator, common and subA files exists -checkZipContent(aggregatorZip, "publishing-example-parent", "1.0.0", expectedAggregatorEntries) -checkZipContent(aggregatorZip, "publishing-example-common", "1.0.0", expectedFullEntries) -checkZipContent(aggregatorZip, "publishing-example-subA", "1.0.0", expectedFullEntries) +// Ensure aggregator, common and subA files exists in the mega bundle -static def checkZipContent(File zipFile, String artifactId, String version, Closure expectedMethod) { +checkZipContent(megaZip, "publishing-example-parent", "1.0.0", expectedAggregatorEntries) +checkZipContent(megaZip, "publishing-example-common", "1.0.0", expectedFullEntries) +checkZipContent(megaZip, "publishing-example-subA", "1.0.0", expectedFullEntries) +List aggregatorEntries = expectedAggregatorEntries("publishing-example-parent", "1.0.0") +List commonEntries = expectedFullEntries("publishing-example-common", "1.0.0") +List subAEntries = expectedFullEntries("publishing-example-subA", "1.0.0") + +def actualEntries +try (ZipFile zip = new ZipFile(megaZip)) { + actualEntries = zip.entries().collect { it.name } +} +int expectedEntries = aggregatorEntries.size() + commonEntries.size() + subAEntries.size() +assert expectedEntries == actualEntries.size() : "Mismatch in number of entries in ZIP, expected $expectedEntries but was ${actualEntries.size()}" +static def checkZipContent(File zipFile, String artifactId, String version, Closure expectedMethod) { println "Checking content of $zipFile" List expectedEntries = expectedMethod(artifactId, version) List actualEntries @@ -85,8 +92,7 @@ static def checkZipContent(File zipFile, String artifactId, String version, Clos println " - checking $it" assert actualEntries.contains(it) : "Expected entry not found in ZIP: $it" } - assert expectedEntries.size() == actualEntries.size() : "Mismatch in number of entries in ZIP" - + return actualEntries } // make it possible to run this script from different locations (command line, gmavenplus, invoker) @@ -103,5 +109,9 @@ File getBaseDir() { // from command line (e.g. with -Dbasedir=$PWD) bd = System.getProperty("basedir") } + if (bd == null) { + // invoked from command line and forgot to set property, assume we are in the project root + bd = "." + } bd instanceof File ? bd : new File(bd) } \ No newline at end of file diff --git a/src/main/java/org/apache/maven/plugins/deploy/BundleService.java b/src/main/java/org/apache/maven/plugins/deploy/Bundler.java similarity index 94% rename from src/main/java/org/apache/maven/plugins/deploy/BundleService.java rename to src/main/java/org/apache/maven/plugins/deploy/Bundler.java index 1817c42..e9c5b4e 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/BundleService.java +++ b/src/main/java/org/apache/maven/plugins/deploy/Bundler.java @@ -31,6 +31,7 @@ import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -45,12 +46,12 @@ import org.apache.maven.project.MavenProject; import org.codehaus.plexus.util.xml.pull.XmlPullParserException; -public class BundleService { +public class Bundler { MavenProject project; Log log; - public BundleService(MavenProject project, Log log) { + public Bundler(MavenProject project, Log log) { this.project = project; this.log = log; } @@ -109,16 +110,7 @@ public void createZipBundle(File bundleFile, List allProjectsUsing log.error("POM file " + pomFile + " does not exist (verify phase not reached)!"); throw new MojoExecutionException("POM file " + pomFile + " does not exist!"); } - log.info("**********************************"); - log.info("Artifacts are:"); - project.getArtifacts().forEach(artifact -> { - log.info(artifact.getFile().getAbsolutePath()); - }); - log.info("Attached artifacts are:"); - project.getAttachedArtifacts().forEach(artifact -> { - log.info(artifact.getFile().getAbsolutePath()); - }); - log.info("**********************************"); + for (Artifact artifact : project.getAttachedArtifacts()) { File file = artifact.getFile(); if (file.exists()) { @@ -134,9 +126,6 @@ public void createZipBundle(File bundleFile, List allProjectsUsing } } - log.info("Adding the following entries to the zip file"); - log.info("********************************************"); - try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(bundleFile.toPath()))) { for (Map.Entry> entry : artifactFiles.entrySet()) { String mavenPathPrefix = entry.getKey(); @@ -168,6 +157,8 @@ public void createZipBundle(File bundleFile, List allProjectsUsing * the environment. */ public void createZipBundle(File bundleFile) throws IOException, NoSuchAlgorithmException, MojoExecutionException { + createZipBundle(bundleFile, Collections.singletonList(project)); + /* bundleFile.getParentFile().mkdirs(); bundleFile.createNewFile(); String groupId = project.getGroupId(); @@ -234,6 +225,8 @@ public void createZipBundle(File bundleFile) throws IOException, NoSuchAlgorithm } } log.info("Created bundle at: " + bundleFile.getAbsolutePath()); + + */ } /** @@ -269,7 +262,7 @@ private void validateForPublishing(File pomFile) throws MojoExecutionException { } private void addToZip(File file, String prefix, ZipOutputStream zipOut) throws IOException { - log.info("addToZip - " + file.getAbsolutePath()); + log.info("Create bundle, addToZip - " + file.getAbsolutePath()); zipOut.putNextEntry(new ZipEntry(prefix + file.getName())); Files.copy(file.toPath(), zipOut); zipOut.closeEntry(); diff --git a/src/main/java/org/apache/maven/plugins/deploy/CentralBundleMojo.java b/src/main/java/org/apache/maven/plugins/deploy/CentralBundleMojo.java deleted file mode 100644 index fa20463..0000000 --- a/src/main/java/org/apache/maven/plugins/deploy/CentralBundleMojo.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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 - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.maven.plugins.deploy; - -import java.io.File; -import java.util.List; - -import org.apache.maven.plugin.MojoExecutionException; -import org.apache.maven.plugins.annotations.LifecyclePhase; -import org.apache.maven.plugins.annotations.Mojo; -import org.apache.maven.plugins.annotations.Parameter; -import org.apache.maven.project.MavenProject; - -/** - * This is useful for manual upload of the bundle file to central. - * It can be reached with - * mvn verify deploy:bundle - */ -@Mojo(name = "bundle", defaultPhase = LifecyclePhase.DEPLOY) -public class CentralBundleMojo extends AbstractDeployMojo { - - @Parameter(defaultValue = "${project}", readonly = true) - private MavenProject project; - - @Parameter(defaultValue = "${reactorProjects}", required = true, readonly = true) - private List reactorProjects; - - @Parameter(defaultValue = "true", property = "deployAtEnd") - private boolean deployAtEnd; - - @Override - public void execute() throws MojoExecutionException { - File targetDir = new File(project.getBuild().getDirectory()); - File bundleFile = new File(targetDir, project.getArtifactId() + "-" + project.getVersion() + "-bundle.zip"); - - try { - BundleService bundleService = new BundleService(project, getLog()); - if (deployAtEnd) { - bundleService.createZipBundle(bundleFile, reactorProjects); - } else { - bundleService.createZipBundle(bundleFile); - } - getLog().info("Bundle created successfully: " + bundleFile); - } catch (Exception e) { - throw new MojoExecutionException("Failed to create bundle", e); - } - } -} diff --git a/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java b/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java index 5ddf14f..5eefb4f 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java +++ b/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java @@ -139,12 +139,35 @@ public class DeployMojo extends AbstractDeployMojo { @Parameter(property = "maven.deploy.skip", defaultValue = "false") private String skip = Boolean.FALSE.toString(); + /** + * If false, the deploy plugin will use the legacy deployment api. + * If true, the new central portal api will be used. + * Default is false. + * @since 3.1.5 + */ @Parameter(property = "useCentralPortalApi", defaultValue = "false") private boolean useCentralPortalApi; + /** + * If this is set to false, the bundle will be uploaded to central but not released (published). + * You can release it manually at + * central deployments. If true, the bundle will be uploaded, validated and then + * automatically released if it is a valid deployment bundle. + * Default is true i.e. upload and release automatically. + * @since 3.1.5 + */ @Parameter(defaultValue = "true", property = "autoDeploy") private boolean autoDeploy; + /** + * Set this to false to create the bundle but not upload it to central. + * This is useful if e.g. you want to check it and then manually upload the bundle. + * Default is true (i.e. upload it). + * @since 3.1.5 + */ + @Parameter(defaultValue = "true", property = "uploadToCentral") + private boolean uploadToCentral; + @Inject private SettingsDecrypter settingsDecrypter; @@ -202,7 +225,9 @@ private boolean hasState(MavenProject project) { @Override public void execute() throws MojoExecutionException, MojoFailureException { - getLog().info("Executing deploy mojo"); + getLog().info("Executing deploy mojo: \n deployAtEnd = " + deployAtEnd + + "\n skip = " + skip + "\n useCentralPortalApi = " + useCentralPortalApi + + "\n autoDeploy = " + autoDeploy); State state; if (Boolean.parseBoolean(skip) || ("releases".equals(skip) && !ArtifactUtils.isSnapshot(project.getVersion())) @@ -251,6 +276,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { } private void deployAllAtOnce(List allProjectsUsingPlugin) throws MojoExecutionException { + getLog().info("deployAllAtOnce"); Map requests = new LinkedHashMap<>(); // collect all arifacts from all modules to deploy @@ -274,9 +300,13 @@ private void deployAllAtOnce(List allProjectsUsingPlugin) throws M processProject(reactorProject, request); } } - if (useCentralPortalApi) { + if (useCentralPortalApi && deployAtEnd) { + getLog().info("deployAllAtOnce - create bundle"); File zipBundle = createBundle(allProjectsUsingPlugin); - deployBundle(requests.keySet(), zipBundle); + if (uploadToCentral) { + getLog().info("deployAllAtOnce - deploy to central portal"); + deployBundle(requests.keySet(), zipBundle); + } } else { // finally execute all deployments request, lets resolver to optimize deployment for (DeployRequest request : requests.values()) { @@ -446,6 +476,7 @@ RemoteRepository getDeploymentRepository( } protected File createBundle(List allProjectsUsingPlugin) throws MojoExecutionException { + getLog().info("createBundle"); if (allProjectsUsingPlugin.isEmpty()) { throw new MojoExecutionException("There are no deployments to process"); } @@ -456,12 +487,15 @@ protected File createBundle(List allProjectsUsingPlugin) throws Mo rootProject = rootProject.getParent(); } } - - File bundleFile = createBundleFile(rootProject); + // Locate the mega bundle in the top-level directory of the project + // If we use project, it will be the last module built which is semi-random. + File targetDir = new File(rootProject.getBuild().getDirectory()); + File bundleFile = + new File(targetDir, rootProject.getGroupId() + "-" + rootProject.getVersion() + "-bundle.zip"); try { - BundleService bundleService = new BundleService(rootProject, getLog()); - bundleService.createZipBundle(bundleFile, allProjectsUsingPlugin); + Bundler bundler = new Bundler(rootProject, getLog()); + bundler.createZipBundle(bundleFile, allProjectsUsingPlugin); getLog().info("Bundle created successfully: " + bundleFile); } catch (Exception e) { throw new MojoExecutionException("Failed to create bundle", e); @@ -469,21 +503,20 @@ protected File createBundle(List allProjectsUsingPlugin) throws Mo return bundleFile; } - File createBundleFile(MavenProject project) { - File targetDir = new File(project.getBuild().getDirectory()); - return new File(targetDir, project.getGroupId() + "-" + project.getVersion() + "-bundle.zip"); - } - private void createAndDeploySingleProjectBundle(RemoteRepository deploymentRepository) throws MojoExecutionException { - BundleService bundleService = new BundleService(project, getLog()); - File bundleFile = createBundleFile(project); + getLog().info("createAndDeploySingleProjectBundle"); + Bundler bundler = new Bundler(project, getLog()); + File targetDir = new File(project.getBuild().getDirectory()); + File bundleFile = new File(targetDir, project.getArtifactId() + "-" + project.getVersion() + "-bundle.zip"); try { - bundleService.createZipBundle(bundleFile); + bundler.createZipBundle(bundleFile); } catch (IOException | NoSuchAlgorithmException e) { throw new MojoExecutionException("Failed to create zip bundle", e); } - deployBundle(Collections.singleton(deploymentRepository), bundleFile); + if (uploadToCentral) { + deployBundle(Collections.singleton(deploymentRepository), bundleFile); + } } protected void deployBundle(Set repos, File zipBundle) throws MojoExecutionException { From 5ddb545df47c464f2ef93096ef70b59b4bd493fe Mon Sep 17 00:00:00 2001 From: pernyf Date: Tue, 5 Aug 2025 13:23:25 +0200 Subject: [PATCH 14/25] remove some overly verbose output. --- src/it/central-zip-bundles/verify.groovy | 4 +- src/it/central-zip-megabundle/verify.groovy | 4 +- .../apache/maven/plugins/deploy/Bundler.java | 70 ------------------- .../maven/plugins/deploy/DeployMojo.java | 17 ++--- 4 files changed, 9 insertions(+), 86 deletions(-) diff --git a/src/it/central-zip-bundles/verify.groovy b/src/it/central-zip-bundles/verify.groovy index d67afdd..d857636 100644 --- a/src/it/central-zip-bundles/verify.groovy +++ b/src/it/central-zip-bundles/verify.groovy @@ -75,8 +75,8 @@ static def checkZipContent(File zipFile, String artifactId, String version, Clos actualEntries = zip.entries().collect { it.name } } expectedEntries.each { - println " - checking $it" - assert actualEntries.contains(it) : "Expected entry not found in ZIP: $it" + //println " - checking $it" + assert actualEntries.contains(it) : "Expected entry not found in $zipFile.name: $it" } assert expectedEntries.size() == actualEntries.size() : "Mismatch in number of entries in ZIP" diff --git a/src/it/central-zip-megabundle/verify.groovy b/src/it/central-zip-megabundle/verify.groovy index b8e7aad..a13115e 100644 --- a/src/it/central-zip-megabundle/verify.groovy +++ b/src/it/central-zip-megabundle/verify.groovy @@ -89,8 +89,8 @@ static def checkZipContent(File zipFile, String artifactId, String version, Clos actualEntries = zip.entries().collect { it.name } } expectedEntries.each { - println " - checking $it" - assert actualEntries.contains(it) : "Expected entry not found in ZIP: $it" + //println " - checking $it" + assert actualEntries.contains(it) : "Expected entry not found in $zipFile.name: $it" } return actualEntries } diff --git a/src/main/java/org/apache/maven/plugins/deploy/Bundler.java b/src/main/java/org/apache/maven/plugins/deploy/Bundler.java index e9c5b4e..bb65fd4 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/Bundler.java +++ b/src/main/java/org/apache/maven/plugins/deploy/Bundler.java @@ -71,7 +71,6 @@ public void createZipBundle(File bundleFile, List allProjectsUsing throws IOException, NoSuchAlgorithmException, MojoExecutionException { bundleFile.getParentFile().mkdirs(); bundleFile.createNewFile(); - log.info("Creating zip bundle at " + bundleFile.getAbsolutePath()); Map> artifactFiles = new HashMap<>(); for (MavenProject project : allProjectsUsingPlugin) { @@ -158,75 +157,6 @@ public void createZipBundle(File bundleFile, List allProjectsUsing */ public void createZipBundle(File bundleFile) throws IOException, NoSuchAlgorithmException, MojoExecutionException { createZipBundle(bundleFile, Collections.singletonList(project)); - /* - bundleFile.getParentFile().mkdirs(); - bundleFile.createNewFile(); - String groupId = project.getGroupId(); - String artifactId = project.getArtifactId(); - String version = project.getVersion(); - String groupPath = groupId.replace('.', '/'); - String mavenPathPrefix = String.join("/", groupPath, artifactId, version) + "/"; - - List artifactFiles = new ArrayList<>(); - File artifactFile = project.getArtifact().getFile(); - // Will be null for e.g., an aggregator project - if (artifactFile != null && artifactFile.exists()) { - artifactFiles.add(artifactFile); - File signFile = new File(artifactFile.getAbsolutePath() + ".asc"); - if (!signFile.exists()) { - throw new MojoExecutionException(artifactFile + " is not signed, " + signFile + " is missing"); - } - artifactFiles.add(signFile); - } - - // pom is not in getAttachedArtifacts so add it explicitly - File pomFile = new File(project.getBuild().getDirectory(), String.join("-", artifactId, version) + ".pom"); - if (pomFile.exists()) { - // Since it is the "raw" pom file that is published, not the effective pom, we must check the file, - // not the project. Also since the pom file is the signed one, we cannot change it to the effective pom. - validateForPublishing(pomFile); - artifactFiles.add(pomFile); - File signFile = new File(pomFile.getAbsolutePath() + ".asc"); - if (!signFile.exists()) { - throw new MojoExecutionException("POM file " + pomFile + " is not signed, " + signFile + " is missing"); - } - artifactFiles.add(signFile); - } else { - log.error("POM file " + pomFile + " does not exist (verify phase not reached)!"); - throw new MojoExecutionException("POM file " + pomFile + " does not exist!"); - } - for (Artifact artifact : project.getAttachedArtifacts()) { - File file = artifact.getFile(); - if (file.exists()) { - artifactFiles.add(artifact.getFile()); - File signFile = new File(file.getAbsolutePath() + ".asc"); - if (!signFile.exists()) { - throw new MojoExecutionException(file + " is not signed, " + signFile + " is missing"); - } - artifactFiles.add(signFile); - } else { - log.error("Artifact " + artifact.getId() + " does not exist!"); - } - } - - try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(bundleFile.toPath()))) { - - for (File file : artifactFiles) { - addToZip(file, mavenPathPrefix, zipOut); - if (file.getName().endsWith(".asc")) { - continue; // asc files has no checksums - } - File signFile = new File(file.getAbsolutePath() + ".asc"); - if (!signFile.exists()) { - throw new MojoExecutionException( - "The artifact " + file + " was not signed! " + signFile + " does not exists"); - } - generateChecksumsAndAddToZip(file, mavenPathPrefix, zipOut); - } - } - log.info("Created bundle at: " + bundleFile.getAbsolutePath()); - - */ } /** diff --git a/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java b/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java index 5eefb4f..4d434d7 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java +++ b/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java @@ -225,9 +225,6 @@ private boolean hasState(MavenProject project) { @Override public void execute() throws MojoExecutionException, MojoFailureException { - getLog().info("Executing deploy mojo: \n deployAtEnd = " + deployAtEnd - + "\n skip = " + skip + "\n useCentralPortalApi = " + useCentralPortalApi - + "\n autoDeploy = " + autoDeploy); State state; if (Boolean.parseBoolean(skip) || ("releases".equals(skip) && !ArtifactUtils.isSnapshot(project.getVersion())) @@ -276,7 +273,6 @@ public void execute() throws MojoExecutionException, MojoFailureException { } private void deployAllAtOnce(List allProjectsUsingPlugin) throws MojoExecutionException { - getLog().info("deployAllAtOnce"); Map requests = new LinkedHashMap<>(); // collect all arifacts from all modules to deploy @@ -301,10 +297,8 @@ private void deployAllAtOnce(List allProjectsUsingPlugin) throws M } } if (useCentralPortalApi && deployAtEnd) { - getLog().info("deployAllAtOnce - create bundle"); File zipBundle = createBundle(allProjectsUsingPlugin); if (uploadToCentral) { - getLog().info("deployAllAtOnce - deploy to central portal"); deployBundle(requests.keySet(), zipBundle); } } else { @@ -476,19 +470,19 @@ RemoteRepository getDeploymentRepository( } protected File createBundle(List allProjectsUsingPlugin) throws MojoExecutionException { - getLog().info("createBundle"); if (allProjectsUsingPlugin.isEmpty()) { - throw new MojoExecutionException("There are no deployments to process"); + throw new MojoExecutionException("There are no deployments to process so no bundle to create"); } - // We need the root project, project here will be the last submodule built. + // Locate the mega bundle in the top-level directory of the project + // If we use project, it will be the last module built which is semi-random. MavenProject rootProject = project; while (rootProject.getParent() != null) { if (rootProject.getParent().getBasedir().exists()) { rootProject = rootProject.getParent(); } } - // Locate the mega bundle in the top-level directory of the project - // If we use project, it will be the last module built which is semi-random. + // Since it is a mega bundle (containing all sub projects), + // name the zip using groupId and version. File targetDir = new File(rootProject.getBuild().getDirectory()); File bundleFile = new File(targetDir, rootProject.getGroupId() + "-" + rootProject.getVersion() + "-bundle.zip"); @@ -505,7 +499,6 @@ protected File createBundle(List allProjectsUsingPlugin) throws Mo private void createAndDeploySingleProjectBundle(RemoteRepository deploymentRepository) throws MojoExecutionException { - getLog().info("createAndDeploySingleProjectBundle"); Bundler bundler = new Bundler(project, getLog()); File targetDir = new File(project.getBuild().getDirectory()); File bundleFile = new File(targetDir, project.getArtifactId() + "-" + project.getVersion() + "-bundle.zip"); From aa7cc2b3db7b196ddd2c48e3f0cd6c86ac8828f8 Mon Sep 17 00:00:00 2001 From: pernyf Date: Tue, 5 Aug 2025 19:52:55 +0200 Subject: [PATCH 15/25] improve error output. The IT test central-deploy-megabundle now works (but is not yet finished) --- pom.xml | 38 ++++ .../central-deploy-megabundle/fakeSign.groovy | 86 ++++++++ .../invoker.properties | 18 ++ src/it/central-deploy-megabundle/pom.xml | 38 +++- src/it/central-deploy-megabundle/setup.groovy | 189 ++++++++++++++++++ .../central-deploy-megabundle/verify.groovy | 53 +++++ src/it/settings.xml | 7 + .../plugins/deploy/CentralPortalClient.java | 23 ++- 8 files changed, 440 insertions(+), 12 deletions(-) create mode 100644 src/it/central-deploy-megabundle/fakeSign.groovy create mode 100644 src/it/central-deploy-megabundle/invoker.properties create mode 100644 src/it/central-deploy-megabundle/setup.groovy create mode 100644 src/it/central-deploy-megabundle/verify.groovy diff --git a/pom.xml b/pom.xml index 34d3f13..1941ced 100644 --- a/pom.xml +++ b/pom.xml @@ -243,6 +243,44 @@ under the License. deploy + + + org.eclipse.jetty + jetty-server + 9.4.53.v20231009 + runtime + + + org.eclipse.jetty + jetty-servlet + 9.4.53.v20231009 + runtime + + + javax.servlet + javax.servlet-api + 3.1.0 + runtime + + + commons-io + commons-io + 2.14.0 + runtime + + + com.fasterxml.jackson.core + jackson-databind + 2.13.5 + runtime + + + commons-fileupload + commons-fileupload + 1.4 + runtime + + diff --git a/src/it/central-deploy-megabundle/fakeSign.groovy b/src/it/central-deploy-megabundle/fakeSign.groovy new file mode 100644 index 0000000..b432045 --- /dev/null +++ b/src/it/central-deploy-megabundle/fakeSign.groovy @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ +import org.apache.maven.model.Model +import org.apache.maven.model.io.xpp3.MavenXpp3Reader + +// Since we do not want to mess with external gpg config, we just create fake asc files +signFile(copyPom("pom.xml", "target")) +signJarFiles("target") + +def signJarFiles(String dir) { + def targetDir = new File(getTestDir(), dir) + targetDir.listFiles({ file -> file.name.endsWith(".jar") } as FileFilter).each { + signFile(it) + } +} + +static def signFile(File file) { + def ascFile = new File(file.getParentFile(), "${file.getName()}.asc") + println "Setup: creating signing file: $ascFile" + ascFile << "fake signed" +} + +def copyPom(String from, String toDir) { + File testDir = getTestDir() + def src = new File(testDir, from) + def pomInfo = readPomInfo(src) + def dstDir = new File(testDir, toDir) + dstDir.mkdirs() + def toFile = new File(dstDir, "$pomInfo.artifactId-${pomInfo.version}.pom") + toFile.text = src.text + return toFile +} + +static def readPomInfo(File pomFile) { + MavenXpp3Reader reader = new MavenXpp3Reader() + pomFile.withReader { r -> + Model model = reader.read(r) + + // Inherit groupId/version from parent if missing + if (!model.groupId && model.parent) { + model.groupId = model.parent.groupId + } + if (!model.version && model.parent) { + model.version = model.parent.version + } + + return [ + groupId : model.groupId, + artifactId : model.artifactId, + version : model.version + ] + } +} + +File getTestDir() { + if (binding.hasVariable('project')) { + // from gmavenplus + return binding.getVariable('project').basedir as File + } + def bd + if (binding.hasVariable('basedir')) { + // from the invoker plugin + bd = binding.getVariable('basedir') + } else { + // from command line (e.g. with -Dbasedir=$PWD) + bd = System.getProperty("basedir") + } + bd instanceof File ? bd : new File(bd) +} + diff --git a/src/it/central-deploy-megabundle/invoker.properties b/src/it/central-deploy-megabundle/invoker.properties new file mode 100644 index 0000000..1f3fd89 --- /dev/null +++ b/src/it/central-deploy-megabundle/invoker.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. + +invoker.goals = deploy \ No newline at end of file diff --git a/src/it/central-deploy-megabundle/pom.xml b/src/it/central-deploy-megabundle/pom.xml index 90b4a29..dca2805 100644 --- a/src/it/central-deploy-megabundle/pom.xml +++ b/src/it/central-deploy-megabundle/pom.xml @@ -62,7 +62,7 @@ central Central Repository - https://central.sonatype.com/api/v1 + http://localhost:8088/api/v1 @@ -72,7 +72,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.14.0 + @mavenCompilerPluginVersion@ ${maven.compiler.source} ${maven.compiler.target} @@ -84,7 +84,7 @@ org.apache.maven.plugins maven-deploy-plugin - 3.1.5-SNAPSHOT + @project.version@ true true @@ -94,7 +94,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.11.2 + @mavenJavadocPluginVersion@ attach-javadocs @@ -107,7 +107,7 @@ org.apache.maven.plugins maven-source-plugin - 3.3.1 + @mavenSourcePluginVersion@ attach-sources @@ -121,18 +121,36 @@ - org.apache.maven.plugins - maven-gpg-plugin - 3.2.8 + + org.codehaus.gmavenplus + gmavenplus-plugin + 1.13.1 - sign-artifacts + simulate-signing verify - sign + execute + + + fakeSign.groovy + + + + + org.apache.groovy + groovy + 4.0.27 + + + org.apache.groovy + groovy-ant + 4.0.27 + + org.apache.maven.plugins diff --git a/src/it/central-deploy-megabundle/setup.groovy b/src/it/central-deploy-megabundle/setup.groovy new file mode 100644 index 0000000..74ad40f --- /dev/null +++ b/src/it/central-deploy-megabundle/setup.groovy @@ -0,0 +1,189 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.servlet.* +import javax.servlet.http.* +import javax.servlet.* +import java.util.concurrent.ConcurrentHashMap +import org.apache.commons.fileupload.servlet.ServletFileUpload +import org.apache.commons.io.IOUtils +import com.fasterxml.jackson.databind.ObjectMapper + +def port = 8088 +def server = new Server(port) +def context = new ServletContextHandler(ServletContextHandler.SESSIONS) +context.setContextPath("/") + +def bundles = new ConcurrentHashMap() +def mapper = new ObjectMapper() + +// /api/v1 - Upload endpoint +context.addServlet(new ServletHolder(new HttpServlet() { + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + try { + if (!authHeader().equals(req.getHeader("Authorization"))) { + resp.setStatus(401) + println "Bearer token not recognized, should be based on testUser:testPwd" + return + } + + if (!ServletFileUpload.isMultipartContent(req)) { + resp.setStatus(400) + return + } + + def multipartConfig = new MultipartConfigElement(System.getProperty("java.io.tmpdir")) + req.setAttribute("org.eclipse.jetty.multipartConfig", multipartConfig) + + def publishingType = req.getParameter("publishingType") + def name = req.getParameter("name") + def part = req.getPart("bundle") + def zipData = IOUtils.toByteArray(part.inputStream) + if (zipData == null || zipData.length == 0) { + resp.setStatus(400) + resp.setContentType("application/json") + resp.writer.write("bundle contains no data") + return + } + def deploymentId = UUID.randomUUID().toString() + + bundles.put(deploymentId, zipData) + println("Received bundle " + name) + + resp.setContentType("text/plain") + resp.writer.write(deploymentId) + + } catch (Exception e) { + // Log stack trace to console + e.printStackTrace() + + // Respond with HTTP 500 and JSON error details + resp.setStatus(500) + resp.setContentType("application/json") + def errorResponse = [ + error: e.getClass().name, + message: e.message, + stackTrace: e.stackTrace.collect { it.toString() } + ] + resp.writer.write(mapper.writeValueAsString(errorResponse)) + } + } +}), "/api/v1/publisher/upload") + +// /publisher/status - Status check +context.addServlet(new ServletHolder(new HttpServlet() { + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + try { + if (!authHeader().equals(req.getHeader("Authorization"))) { + resp.setStatus(401) + println "Bearer token not recognized, should be based on testUser:testPwd" + return + } + + def deploymentId = req.getParameter("id") + def data = bundles.get(deploymentId) + + if (data == null) { + resp.writer.write("Deployment $deploymentId not found") + resp.setStatus(404) + return + } + + resp.setContentType("application/json") + Map deployments = new HashMap<>() + deployments.put(deploymentId, [ + deploymentId: deploymentId, + deploymentState: "PUBLISHED", + purls: ["pkg:maven/com.sonatype.central.example/example_java_project@0.0.7"] + ]) + resp.writer.write(mapper.writeValueAsString(deployments)) + } catch (Exception e) { + // Log stack trace to console + e.printStackTrace() + + // Respond with HTTP 500 and JSON error details + resp.setStatus(500) + resp.setContentType("application/json") + def errorResponse = [ + error: e.getClass().name, + message: e.message, + stackTrace: e.stackTrace.collect { it.toString() } + ] + resp.writer.write(mapper.writeValueAsString(errorResponse)) + } + } +}), "/api/v1/publisher/status") + +// /getBundleIds - get a json list of all uploaded bundles +context.addServlet(new ServletHolder(new HttpServlet() { + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setContentType("application/json") + resp.writer.write(mapper.writeValueAsString(bundles.keys())) + } +}), "/getBundleIds") + +// /getBundle - Retrieve the uploaded bundle by deploymentId +context.addServlet(new ServletHolder(new HttpServlet() { + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + def id = req.getParameter("deploymentId") + def zip = bundles.get(id) + if (zip == null) { + resp.setStatus(404) + return + } + + resp.setContentType("application/zip") + resp.setHeader("Content-Disposition", "attachment; filename=\"${id}.zip\"") + resp.outputStream.write(zip) + } +}), "/getBundle") + +// /shutdown - Shut down the server gracefully +context.addServlet(new ServletHolder(new HttpServlet() { + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.writer.write("Shutting down...") + new Thread({ + Thread.sleep(1000) + server.stop() + }).start() + } +}), "/shutdown") + +context.addServlet(new ServletHolder(new HttpServlet() { + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // Log or print the unmatched request URI + println("Unhandled request to: " + req.getRequestURI()) + + resp.setStatus(404) + resp.setContentType("application/json") + resp.writer.write(mapper.writeValueAsString([ + error: "Unknown endpoint", + path: req.getRequestURI() + ])) + } +}), "/*") + +// Must match the user and pwd in settings.xml for the central server +private String authHeader() { + return "Bearer " + Base64.getEncoder().encodeToString(("testUser:testPwd").getBytes()); +} + +server.setHandler(context) +server.start() + diff --git a/src/it/central-deploy-megabundle/verify.groovy b/src/it/central-deploy-megabundle/verify.groovy new file mode 100644 index 0000000..abb903c --- /dev/null +++ b/src/it/central-deploy-megabundle/verify.groovy @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ +import groovy.json.JsonSlurper +String baseUrl = "http://localhost:8088" + +def conn = new URI("$baseUrl/getBundleIds").toURL().openConnection() +conn.setRequestMethod("GET") +conn.connect() +def json = conn.inputStream.text +def ids = new JsonSlurper().parseText(json) +println "verify: Received bundle IDs: $ids" + +// Check that exactly one bundle was uploaded +assert ids.size() == 1 : "Expected exactly one bundle, but got ${ids.size()}" +// TODO: maybe download and check the zip content + +try { + conn = new URI("$baseUrl/shutdown").toURL().openConnection() + conn.setRequestMethod("POST") + conn.connect() + println conn.inputStream.text +} catch (Exception e) { + println "Shutdown failed: ${e.message}" +} + + +/* +try { + def url = new URI("$baseUrl/shutdown").toURL() + def conn = url.openConnection() + conn.setRequestMethod("POST") + conn.doOutput = true + conn.connect() + println "Shutdown response: ${conn.inputStream.text}" +} catch (Exception e) { + println "Shutdown failed or server already stopped: ${e.message}" +}*/ \ No newline at end of file diff --git a/src/it/settings.xml b/src/it/settings.xml index c8f77f0..5fa6a3f 100644 --- a/src/it/settings.xml +++ b/src/it/settings.xml @@ -52,4 +52,11 @@ under the License. + + + central + testUser + testPwd + + diff --git a/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java b/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java index 7301d34..98dbb0d 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java +++ b/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java @@ -85,6 +85,13 @@ public String upload(File bundle, Boolean autoDeploy) throws IOException { int status = conn.getResponseCode(); if (status >= 400) { + log.error(deployUrl + " returned HTTP error code : " + status); + try (InputStream in = conn.getErrorStream()) { + String body = readFully(in); + log.error("Response body: " + body); + } catch (IOException e) { + log.error("Failed to read response body", e); + } throw new IOException("Failed to upload: HTTP " + status); } @@ -119,9 +126,21 @@ public String getStatus(String deploymentId) throws IOException { conn.setRequestProperty("Authorization", authHeader()); int status = conn.getResponseCode(); if (status >= 400) { + log.error(url + " returned HTTP error code : " + status); + try (InputStream in = conn.getErrorStream()) { + if (in != null) { + String body = readFully(in); + log.error("Response body: " + body); + } + } catch (IOException e) { + log.error("Failed to read response body", e); + } throw new IOException("Failed to get status: HTTP " + status); } try (InputStream in = conn.getInputStream()) { + if (in == null) { + return "no response body"; + } String responseBody = readFully(in); Pattern pattern = Pattern.compile("\"deploymentState\"\\s*:\\s*\"([^\"]+)\""); Matcher matcher = pattern.matcher(responseBody); @@ -134,7 +153,7 @@ public String getStatus(String deploymentId) throws IOException { } private String authHeader() { - return "Bearer " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes()); + return "Bearer " + Base64.getEncoder().encodeToString((getUsername() + ":" + getPassword()).getBytes()); } private String readFully(InputStream in) throws IOException { @@ -144,7 +163,7 @@ private String readFully(InputStream in) throws IOException { while ((bytesRead = in.read(chunk)) != -1) { buffer.write(chunk, 0, bytesRead); } - return buffer.toString("UTF-8"); + return buffer.toString("UTF-8").trim(); } public String getUsername() { From f4fef1b400f0be8d4ac6b3fcd7cb1a0c536a455a Mon Sep 17 00:00:00 2001 From: pernyf Date: Wed, 6 Aug 2025 09:38:10 +0200 Subject: [PATCH 16/25] add content check to central-deploy-megabundle --- .../central-deploy-megabundle/verify.groovy | 100 +++++++++++++++--- 1 file changed, 84 insertions(+), 16 deletions(-) diff --git a/src/it/central-deploy-megabundle/verify.groovy b/src/it/central-deploy-megabundle/verify.groovy index abb903c..7f8dc55 100644 --- a/src/it/central-deploy-megabundle/verify.groovy +++ b/src/it/central-deploy-megabundle/verify.groovy @@ -17,9 +17,13 @@ * under the License. */ import groovy.json.JsonSlurper + +import java.nio.file.Files +import java.util.zip.ZipFile + String baseUrl = "http://localhost:8088" -def conn = new URI("$baseUrl/getBundleIds").toURL().openConnection() +URLConnection conn = new URI("$baseUrl/getBundleIds").toURL().openConnection() conn.setRequestMethod("GET") conn.connect() def json = conn.inputStream.text @@ -28,26 +32,90 @@ println "verify: Received bundle IDs: $ids" // Check that exactly one bundle was uploaded assert ids.size() == 1 : "Expected exactly one bundle, but got ${ids.size()}" -// TODO: maybe download and check the zip content +def deploymentId = ids[0] -try { - conn = new URI("$baseUrl/shutdown").toURL().openConnection() - conn.setRequestMethod("POST") - conn.connect() - println conn.inputStream.text -} catch (Exception e) { - println "Shutdown failed: ${e.message}" +// download and check the zip content +conn = new URI("$baseUrl/getBundle?deploymentId=$deploymentId").toURL().openConnection() +conn.setRequestMethod("GET") +conn.connect() +def zip = Files.createTempFile("bundle", ".zip") +def megaZip = zip.toFile() +if (megaZip.exists()) { + megaZip.delete() } +Files.copy(conn.inputStream, zip) +String groupId = "se.alipsa.maven.example" +def expectedAggregatorEntries = { String artifactId, String version -> + String basePath = "${groupId.replace('.', '/')}/${artifactId}/${version}/" -/* + String artifactPath = basePath + artifactId + '-' + version + [ + artifactPath + '.pom', + artifactPath +'.pom.asc', + artifactPath +'.pom.md5', + artifactPath +'.pom.sha1', + artifactPath +'.pom.sha256' + ] +} + +def expectedFullEntries = {String artifactId, String version -> + String basePath = "${groupId.replace('.', '/')}/${artifactId}/${version}/" + String artifactPath = basePath + artifactId + '-' + version + [ + artifactPath + '.jar', + artifactPath + '.jar.asc', + artifactPath + '.jar.md5', + artifactPath + '.jar.sha1', + artifactPath + '.jar.sha256', + artifactPath + '-sources.jar', + artifactPath + '-sources.jar.asc', + artifactPath + '-sources.jar.md5', + artifactPath + '-sources.jar.sha1', + artifactPath + '-sources.jar.sha256', + artifactPath + '-javadoc.jar', + artifactPath + '-javadoc.jar.asc', + artifactPath + '-javadoc.jar.md5', + artifactPath + '-javadoc.jar.sha1', + artifactPath + '-javadoc.jar.sha256' + ] + expectedAggregatorEntries(artifactId, version) +} + +checkZipContent(megaZip, "publishing-example-parent", "1.0.0", expectedAggregatorEntries) +checkZipContent(megaZip, "publishing-example-common", "1.0.0", expectedFullEntries) +checkZipContent(megaZip, "publishing-example-subA", "1.0.0", expectedFullEntries) +List aggregatorEntries = expectedAggregatorEntries("publishing-example-parent", "1.0.0") +List commonEntries = expectedFullEntries("publishing-example-common", "1.0.0") +List subAEntries = expectedFullEntries("publishing-example-subA", "1.0.0") + +def actualEntries +try (ZipFile zipFile = new ZipFile(megaZip)) { + actualEntries = zipFile.entries().collect { it.name } +} +int expectedEntries = aggregatorEntries.size() + commonEntries.size() + subAEntries.size() +assert expectedEntries == actualEntries.size() : "Mismatch in number of entries in ZIP, expected $expectedEntries but was ${actualEntries.size()}" + +static def checkZipContent(File zipFile, String artifactId, String version, Closure expectedMethod) { + println "Checking content of $zipFile" + List expectedEntries = expectedMethod(artifactId, version) + List actualEntries + try (ZipFile zip = new ZipFile(zipFile)) { + actualEntries = zip.entries().collect { it.name } + } + expectedEntries.each { + //println " - checking $it" + assert actualEntries.contains(it) : "Expected entry not found in $zipFile.name: $it" + } + return actualEntries +} +// Cleanup +megaZip.deleteOnExit() +// Shut down the server try { - def url = new URI("$baseUrl/shutdown").toURL() - def conn = url.openConnection() + conn = new URI("$baseUrl/shutdown").toURL().openConnection() conn.setRequestMethod("POST") - conn.doOutput = true conn.connect() - println "Shutdown response: ${conn.inputStream.text}" + println conn.inputStream.text } catch (Exception e) { - println "Shutdown failed or server already stopped: ${e.message}" -}*/ \ No newline at end of file + println "Shutdown failed: ${e.message}" +} \ No newline at end of file From c1a14c1e8aba21d55a31939a916e5b7b0d5c6268 Mon Sep 17 00:00:00 2001 From: pernyf Date: Wed, 6 Aug 2025 11:34:23 +0200 Subject: [PATCH 17/25] central-deploy-bundles IT complete. Everything is now working. 'mvn -Prun-its verify' runs successfully. --- src/it/central-deploy-bundles/fakeSign.groovy | 86 +++++++ .../central-deploy-bundles/invoker.properties | 18 ++ src/it/central-deploy-bundles/pom.xml | 40 +++- src/it/central-deploy-bundles/setup.groovy | 211 ++++++++++++++++++ src/it/central-deploy-bundles/verify.groovy | 126 +++++++++++ src/it/central-deploy-megabundle/setup.groovy | 2 +- .../plugins/deploy/CentralPortalClient.java | 2 +- 7 files changed, 472 insertions(+), 13 deletions(-) create mode 100644 src/it/central-deploy-bundles/fakeSign.groovy create mode 100644 src/it/central-deploy-bundles/invoker.properties create mode 100644 src/it/central-deploy-bundles/setup.groovy create mode 100644 src/it/central-deploy-bundles/verify.groovy diff --git a/src/it/central-deploy-bundles/fakeSign.groovy b/src/it/central-deploy-bundles/fakeSign.groovy new file mode 100644 index 0000000..b432045 --- /dev/null +++ b/src/it/central-deploy-bundles/fakeSign.groovy @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ +import org.apache.maven.model.Model +import org.apache.maven.model.io.xpp3.MavenXpp3Reader + +// Since we do not want to mess with external gpg config, we just create fake asc files +signFile(copyPom("pom.xml", "target")) +signJarFiles("target") + +def signJarFiles(String dir) { + def targetDir = new File(getTestDir(), dir) + targetDir.listFiles({ file -> file.name.endsWith(".jar") } as FileFilter).each { + signFile(it) + } +} + +static def signFile(File file) { + def ascFile = new File(file.getParentFile(), "${file.getName()}.asc") + println "Setup: creating signing file: $ascFile" + ascFile << "fake signed" +} + +def copyPom(String from, String toDir) { + File testDir = getTestDir() + def src = new File(testDir, from) + def pomInfo = readPomInfo(src) + def dstDir = new File(testDir, toDir) + dstDir.mkdirs() + def toFile = new File(dstDir, "$pomInfo.artifactId-${pomInfo.version}.pom") + toFile.text = src.text + return toFile +} + +static def readPomInfo(File pomFile) { + MavenXpp3Reader reader = new MavenXpp3Reader() + pomFile.withReader { r -> + Model model = reader.read(r) + + // Inherit groupId/version from parent if missing + if (!model.groupId && model.parent) { + model.groupId = model.parent.groupId + } + if (!model.version && model.parent) { + model.version = model.parent.version + } + + return [ + groupId : model.groupId, + artifactId : model.artifactId, + version : model.version + ] + } +} + +File getTestDir() { + if (binding.hasVariable('project')) { + // from gmavenplus + return binding.getVariable('project').basedir as File + } + def bd + if (binding.hasVariable('basedir')) { + // from the invoker plugin + bd = binding.getVariable('basedir') + } else { + // from command line (e.g. with -Dbasedir=$PWD) + bd = System.getProperty("basedir") + } + bd instanceof File ? bd : new File(bd) +} + diff --git a/src/it/central-deploy-bundles/invoker.properties b/src/it/central-deploy-bundles/invoker.properties new file mode 100644 index 0000000..1f3fd89 --- /dev/null +++ b/src/it/central-deploy-bundles/invoker.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. + +invoker.goals = deploy \ No newline at end of file diff --git a/src/it/central-deploy-bundles/pom.xml b/src/it/central-deploy-bundles/pom.xml index 44cc4f5..2017010 100644 --- a/src/it/central-deploy-bundles/pom.xml +++ b/src/it/central-deploy-bundles/pom.xml @@ -62,7 +62,7 @@ central Central Repository - https://central.sonatype.com/api/v1 + http://localhost:8088/api/v1 @@ -72,7 +72,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.14.0 + @mavenCompilerPluginVersion@ ${maven.compiler.source} ${maven.compiler.target} @@ -84,17 +84,17 @@ org.apache.maven.plugins maven-deploy-plugin - 3.1.5-SNAPSHOT + @project.version@ true false - false + true org.apache.maven.plugins maven-javadoc-plugin - 3.11.2 + @mavenJavadocPluginVersion@ attach-javadocs @@ -107,7 +107,7 @@ org.apache.maven.plugins maven-source-plugin - 3.3.1 + @mavenSourcePluginVersion@ attach-sources @@ -121,18 +121,36 @@ - org.apache.maven.plugins - maven-gpg-plugin - 3.2.8 + + org.codehaus.gmavenplus + gmavenplus-plugin + 1.13.1 - sign-artifacts + simulate-signing verify - sign + execute + + + fakeSign.groovy + + + + + org.apache.groovy + groovy + 4.0.27 + + + org.apache.groovy + groovy-ant + 4.0.27 + + org.apache.maven.plugins diff --git a/src/it/central-deploy-bundles/setup.groovy b/src/it/central-deploy-bundles/setup.groovy new file mode 100644 index 0000000..62b3e6b --- /dev/null +++ b/src/it/central-deploy-bundles/setup.groovy @@ -0,0 +1,211 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.servlet.* +import javax.servlet.http.* +import javax.servlet.* +import java.util.concurrent.ConcurrentHashMap +import org.apache.commons.fileupload.servlet.ServletFileUpload +import org.apache.commons.io.IOUtils +import com.fasterxml.jackson.databind.ObjectMapper + +def port = 8088 +def server = new Server(port) +def context = new ServletContextHandler(ServletContextHandler.SESSIONS) +context.setContextPath("/") + +def bundles = new ConcurrentHashMap() +def mapper = new ObjectMapper() +class Bundle { + byte[] content + String name + String publishingType + String fileName +} + +// /api/v1/publisher/upload - Upload endpoint +context.addServlet(new ServletHolder(new HttpServlet() { + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + try { + if (!authHeader().equals(req.getHeader("Authorization"))) { + resp.setStatus(401) + println "Bearer token not recognized, should be based on testUser:testPwd" + return + } + + if (!ServletFileUpload.isMultipartContent(req)) { + resp.setStatus(400) + return + } + + def multipartConfig = new MultipartConfigElement(System.getProperty("java.io.tmpdir")) + req.setAttribute("org.eclipse.jetty.multipartConfig", multipartConfig) + + def publishingType = req.getParameter("publishingType") + def name = req.getParameter("name") + def part = req.getPart("bundle") + def zipData = IOUtils.toByteArray(part.inputStream) + if (zipData == null || zipData.length == 0) { + resp.setStatus(400) + resp.setContentType("application/json") + resp.writer.write("bundle contains no data") + return + } + def deploymentId = UUID.randomUUID().toString() + + Bundle bundle = new Bundle() + bundle.content = zipData + bundle.name = name + bundle.publishingType = publishingType + bundle.fileName = part.getSubmittedFileName() + + bundles.put(deploymentId, bundle) + println("Central Simulator received bundle $name, publishingType: $publishingType") + + resp.setContentType("text/plain") + resp.writer.write(deploymentId) + + } catch (Exception e) { + // Log stack trace to console + e.printStackTrace() + + // Respond with HTTP 500 and JSON error details + resp.setStatus(500) + resp.setContentType("application/json") + def errorResponse = [ + error: e.getClass().name, + message: e.message, + stackTrace: e.stackTrace.collect { it.toString() } + ] + resp.writer.write(mapper.writeValueAsString(errorResponse)) + } + } +}), "/api/v1/publisher/upload") + +// /api/v1/publisher/status - Status check +context.addServlet(new ServletHolder(new HttpServlet() { + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + try { + if (!authHeader().equals(req.getHeader("Authorization"))) { + resp.setStatus(401) + println "Bearer token not recognized, should be based on testUser:testPwd" + return + } + + def deploymentId = req.getParameter("id") + def bundleInfo = bundles.get(deploymentId) + + if (bundleInfo == null) { + resp.writer.write("Deployment $deploymentId not found") + resp.setStatus(404) + return + } + String deployState = bundleInfo.publishingType == "AUTOMATIC" ? "PUBLISHING" : "VALIDATED" + + resp.setContentType("application/json") + Map deployments = new HashMap<>() + deployments.put(deploymentId, [ + deploymentId: deploymentId, + deploymentState: deployState, + purls: ["pkg:maven/se.alipsa.maven.example/example_java_project@0.0.7"] + ]) + resp.writer.write(mapper.writeValueAsString(deployments)) + } catch (Exception e) { + // Log stack trace to console + e.printStackTrace() + + // Respond with HTTP 500 and JSON error details + resp.setStatus(500) + resp.setContentType("application/json") + def errorResponse = [ + error: e.getClass().name, + message: e.message, + stackTrace: e.stackTrace.collect { it.toString() } + ] + resp.writer.write(mapper.writeValueAsString(errorResponse)) + } + } +}), "/api/v1/publisher/status") + +// /getBundleIds - get a json list of all uploaded bundles (test only api, not part of central api) +context.addServlet(new ServletHolder(new HttpServlet() { + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setContentType("application/json") + List> bundleInfo = new ArrayList() + for (Map.Entry entry : bundles.entrySet()) { + bundleInfo << [ + deploymentId: entry.key, + name: entry.value.name, + publishingType: entry.value.publishingType, + fileName: entry.value.fileName + ] + } + resp.writer.write(mapper.writeValueAsString(bundleInfo)) + } +}), "/getBundleInfo") + +// /getBundle - Retrieve the uploaded bundle by deploymentId (test only api, not part of central api) +context.addServlet(new ServletHolder(new HttpServlet() { + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + def id = req.getParameter("deploymentId") + def zip = bundles.get(id) + if (zip == null) { + resp.setStatus(404) + return + } + + resp.setContentType("application/zip") + resp.setHeader("Content-Disposition", "attachment; filename=\"${id}.zip\"") + resp.outputStream.write(zip.content) + } +}), "/getBundle") + +// /shutdown - Shut down the server gracefully (test only api, not part of central api) +context.addServlet(new ServletHolder(new HttpServlet() { + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.writer.write("Shutting down...") + new Thread({ + Thread.sleep(1000) + server.stop() + }).start() + } +}), "/shutdown") + +// catch all for everything else (test only api, not part of central api) +context.addServlet(new ServletHolder(new HttpServlet() { + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // Log or print the unmatched request URI + println("Unhandled request to: " + req.getRequestURI()) + + resp.setStatus(404) + resp.setContentType("application/json") + resp.writer.write(mapper.writeValueAsString([ + error: "Unknown endpoint", + path: req.getRequestURI() + ])) + } +}), "/*") + +// Must match the user and pwd in settings.xml for the central server +private String authHeader() { + return "Bearer " + Base64.getEncoder().encodeToString(("testUser:testPwd").getBytes()); +} + +server.setHandler(context) +server.start() diff --git a/src/it/central-deploy-bundles/verify.groovy b/src/it/central-deploy-bundles/verify.groovy new file mode 100644 index 0000000..1ef9cf6 --- /dev/null +++ b/src/it/central-deploy-bundles/verify.groovy @@ -0,0 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ +import groovy.json.JsonSlurper + +import java.nio.file.Files +import java.util.zip.ZipFile + +String getBaseUrl() { + "http://localhost:8088" +} + +URLConnection conn = new URI("$baseUrl/getBundleInfo").toURL().openConnection() +conn.setRequestMethod("GET") +conn.connect() +def json = conn.inputStream.text +List bundles = new JsonSlurper().parseText(json) +println "verify: Received bundles: $bundles" + +// Check that exactly 3 bundles was uploaded +assert bundles.size() == 3 : "Expected exactly 3 bundles, but got ${bundles.size()}" + +String groupId = "se.alipsa.maven.example" +def expectedAggregatorEntries = { String artifactId, String version -> + String basePath = "${groupId.replace('.', '/')}/${artifactId}/${version}/" + + String artifactPath = basePath + artifactId + '-' + version + [ + artifactPath + '.pom', + artifactPath +'.pom.asc', + artifactPath +'.pom.md5', + artifactPath +'.pom.sha1', + artifactPath +'.pom.sha256' + ] +} + +def expectedFullEntries = {String artifactId, String version -> + String basePath = "${groupId.replace('.', '/')}/${artifactId}/${version}/" + String artifactPath = basePath + artifactId + '-' + version + [ + artifactPath + '.jar', + artifactPath + '.jar.asc', + artifactPath + '.jar.md5', + artifactPath + '.jar.sha1', + artifactPath + '.jar.sha256', + artifactPath + '-sources.jar', + artifactPath + '-sources.jar.asc', + artifactPath + '-sources.jar.md5', + artifactPath + '-sources.jar.sha1', + artifactPath + '-sources.jar.sha256', + artifactPath + '-javadoc.jar', + artifactPath + '-javadoc.jar.asc', + artifactPath + '-javadoc.jar.md5', + artifactPath + '-javadoc.jar.sha1', + artifactPath + '-javadoc.jar.sha256' + ] + expectedAggregatorEntries(artifactId, version) +} + +bundles.each { Map info -> + File zipFile = download(info.deploymentId as String) + String fileName = info.fileName + if (fileName.contains('parent')) { + checkZipContent(zipFile, "publishing-example-parent", "1.0.0", expectedAggregatorEntries) + } else if (fileName.contains("common")) { + checkZipContent(zipFile, "publishing-example-common", "1.0.0", expectedFullEntries) + } else if(fileName.contains("subA")) { + checkZipContent(zipFile, "publishing-example-subA", "1.0.0", expectedFullEntries) + } else { + throw new Exception("Unexpected bundle name: ${info.fileName}") + } + zipFile.deleteOnExit() +} + +// download zip content +File download(String deploymentId) { + def conn = new URI("$baseUrl/getBundle?deploymentId=$deploymentId").toURL().openConnection() + conn.setRequestMethod("GET") + conn.connect() + def zip = Files.createTempFile("bundle", ".zip") + def zipFile = zip.toFile() + if (zipFile.exists()) { + zipFile.delete() + } + Files.copy(conn.inputStream, zip) + zipFile +} + +static def checkZipContent(File zipFile, String artifactId, String version, Closure expectedMethod) { + println "Checking content of $zipFile" + List expectedEntries = expectedMethod(artifactId, version) + List actualEntries + try (ZipFile zip = new ZipFile(zipFile)) { + actualEntries = zip.entries().collect { it.name } + } + expectedEntries.each { + //println " - checking $it" + assert actualEntries.contains(it) : "Expected entry not found in $zipFile.name: $it" + } + assert expectedEntries.size() == actualEntries.size() : "Mismatch in number of entries in ZIP" +} + + +// Shut down the server +try { + conn = new URI("$baseUrl/shutdown").toURL().openConnection() + conn.setRequestMethod("POST") + conn.connect() + println conn.inputStream.text +} catch (Exception e) { + println "Shutdown failed: ${e.message}" +} \ No newline at end of file diff --git a/src/it/central-deploy-megabundle/setup.groovy b/src/it/central-deploy-megabundle/setup.groovy index 74ad40f..a8ffbdc 100644 --- a/src/it/central-deploy-megabundle/setup.groovy +++ b/src/it/central-deploy-megabundle/setup.groovy @@ -109,7 +109,7 @@ context.addServlet(new ServletHolder(new HttpServlet() { Map deployments = new HashMap<>() deployments.put(deploymentId, [ deploymentId: deploymentId, - deploymentState: "PUBLISHED", + deploymentState: "VALIDATED", purls: ["pkg:maven/com.sonatype.central.example/example_java_project@0.0.7"] ]) resp.writer.write(mapper.writeValueAsString(deployments)) diff --git a/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java b/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java index 98dbb0d..a1e5938 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java +++ b/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java @@ -147,7 +147,7 @@ public String getStatus(String deploymentId) throws IOException { if (matcher.find()) { return matcher.group(1); } else { - return "deploymentState not found"; + return "deploymentState not found in $responseBody"; } } } From fd6f2fe65f608e261208e417a9a15613f3c1d7d8 Mon Sep 17 00:00:00 2001 From: pernyf Date: Thu, 7 Aug 2025 14:29:35 +0200 Subject: [PATCH 18/25] Add a unit test. Add an integration test for a simple project layout. --- src/it/central-deploy-bundles/verify.groovy | 1 + .../central-deploy-megabundle/verify.groovy | 1 + src/it/central-deploy-simplebundle/README.md | 42 +++ .../fakeSign.groovy | 86 +++++ .../invoker.properties | 18 ++ src/it/central-deploy-simplebundle/pom.xml | 164 ++++++++++ .../central-deploy-simplebundle/setup.groovy | 189 +++++++++++ .../src/main/java/simple/common/Greeting.java | 40 +++ .../src/main/java/simple/suba/Hello.java | 37 +++ .../central-deploy-simplebundle/verify.groovy | 110 +++++++ .../plugins/deploy/CentralPortalClient.java | 58 +++- .../maven/plugins/deploy/DeployMojo.java | 18 +- .../plugins/deploy/CentralDeployTest.java | 295 ++++++++++++++++++ .../deploy/stubs/MavenProjectBigStub.java | 117 +++++++ .../central-deploy-test/plugin-config.xml | 34 ++ 15 files changed, 1188 insertions(+), 22 deletions(-) create mode 100644 src/it/central-deploy-simplebundle/README.md create mode 100644 src/it/central-deploy-simplebundle/fakeSign.groovy create mode 100644 src/it/central-deploy-simplebundle/invoker.properties create mode 100644 src/it/central-deploy-simplebundle/pom.xml create mode 100644 src/it/central-deploy-simplebundle/setup.groovy create mode 100644 src/it/central-deploy-simplebundle/src/main/java/simple/common/Greeting.java create mode 100644 src/it/central-deploy-simplebundle/src/main/java/simple/suba/Hello.java create mode 100644 src/it/central-deploy-simplebundle/verify.groovy create mode 100644 src/test/java/org/apache/maven/plugins/deploy/CentralDeployTest.java create mode 100644 src/test/java/org/apache/maven/plugins/deploy/stubs/MavenProjectBigStub.java create mode 100644 src/test/resources/unit/central-deploy-test/plugin-config.xml diff --git a/src/it/central-deploy-bundles/verify.groovy b/src/it/central-deploy-bundles/verify.groovy index 1ef9cf6..8daad7d 100644 --- a/src/it/central-deploy-bundles/verify.groovy +++ b/src/it/central-deploy-bundles/verify.groovy @@ -121,6 +121,7 @@ try { conn.setRequestMethod("POST") conn.connect() println conn.inputStream.text + Thread.sleep(1000) } catch (Exception e) { println "Shutdown failed: ${e.message}" } \ No newline at end of file diff --git a/src/it/central-deploy-megabundle/verify.groovy b/src/it/central-deploy-megabundle/verify.groovy index 7f8dc55..e9e0726 100644 --- a/src/it/central-deploy-megabundle/verify.groovy +++ b/src/it/central-deploy-megabundle/verify.groovy @@ -116,6 +116,7 @@ try { conn.setRequestMethod("POST") conn.connect() println conn.inputStream.text + Thread.sleep(1000) } catch (Exception e) { println "Shutdown failed: ${e.message}" } \ No newline at end of file diff --git a/src/it/central-deploy-simplebundle/README.md b/src/it/central-deploy-simplebundle/README.md new file mode 100644 index 0000000..60ac684 --- /dev/null +++ b/src/it/central-deploy-simplebundle/README.md @@ -0,0 +1,42 @@ + +# maven-central-publishing-example + +This is a multimodule example showing deployment to maven central +using the new central publishing rest api. + +The maven-deploy-plugin used is a [modified fork](https://github.com/perNyfelt/maven-deploy-plugin/tree/add_central_support) of the apache maven deploy plugin. + +The project consist of +- an aggregator +- a common sub module +- two sub modules that each depends on the common submodule + +Each module (including the main aggregator) will be deployed separately. +I.e. when deploying the whole project, 1 zip file will be created and uploaded to central. + +## Running only this test +```shell +mvn -Prun-its verify -Dinvoker.test=central-deploy-simplebundle +``` + +## Running the test manually +```shell +CLASSPATH=$(find "$MAVEN_HOME/lib" -name "*.jar" | tr '\n' ':' | sed 's/:$//') +mvn verify deploy:bundle +groovy -cp $CLASSPATH -Dbasedir=$PWD verify.groovy +``` \ No newline at end of file diff --git a/src/it/central-deploy-simplebundle/fakeSign.groovy b/src/it/central-deploy-simplebundle/fakeSign.groovy new file mode 100644 index 0000000..b432045 --- /dev/null +++ b/src/it/central-deploy-simplebundle/fakeSign.groovy @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ +import org.apache.maven.model.Model +import org.apache.maven.model.io.xpp3.MavenXpp3Reader + +// Since we do not want to mess with external gpg config, we just create fake asc files +signFile(copyPom("pom.xml", "target")) +signJarFiles("target") + +def signJarFiles(String dir) { + def targetDir = new File(getTestDir(), dir) + targetDir.listFiles({ file -> file.name.endsWith(".jar") } as FileFilter).each { + signFile(it) + } +} + +static def signFile(File file) { + def ascFile = new File(file.getParentFile(), "${file.getName()}.asc") + println "Setup: creating signing file: $ascFile" + ascFile << "fake signed" +} + +def copyPom(String from, String toDir) { + File testDir = getTestDir() + def src = new File(testDir, from) + def pomInfo = readPomInfo(src) + def dstDir = new File(testDir, toDir) + dstDir.mkdirs() + def toFile = new File(dstDir, "$pomInfo.artifactId-${pomInfo.version}.pom") + toFile.text = src.text + return toFile +} + +static def readPomInfo(File pomFile) { + MavenXpp3Reader reader = new MavenXpp3Reader() + pomFile.withReader { r -> + Model model = reader.read(r) + + // Inherit groupId/version from parent if missing + if (!model.groupId && model.parent) { + model.groupId = model.parent.groupId + } + if (!model.version && model.parent) { + model.version = model.parent.version + } + + return [ + groupId : model.groupId, + artifactId : model.artifactId, + version : model.version + ] + } +} + +File getTestDir() { + if (binding.hasVariable('project')) { + // from gmavenplus + return binding.getVariable('project').basedir as File + } + def bd + if (binding.hasVariable('basedir')) { + // from the invoker plugin + bd = binding.getVariable('basedir') + } else { + // from command line (e.g. with -Dbasedir=$PWD) + bd = System.getProperty("basedir") + } + bd instanceof File ? bd : new File(bd) +} + diff --git a/src/it/central-deploy-simplebundle/invoker.properties b/src/it/central-deploy-simplebundle/invoker.properties new file mode 100644 index 0000000..1f3fd89 --- /dev/null +++ b/src/it/central-deploy-simplebundle/invoker.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. + +invoker.goals = deploy \ No newline at end of file diff --git a/src/it/central-deploy-simplebundle/pom.xml b/src/it/central-deploy-simplebundle/pom.xml new file mode 100644 index 0000000..4adbbce --- /dev/null +++ b/src/it/central-deploy-simplebundle/pom.xml @@ -0,0 +1,164 @@ + + + + 4.0.0 + se.alipsa.maven.example + publishing-example-simple + 1.0.0-SNAPSHOT + + Maven Central Publishing Example, Simple + https://github.com/perNyfelt/maven-central-publishing-example + jar + + Maven Central Publishing Example, simple + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + + + pnyfelt + Per Nyfelt + + + + https://github.com/perNyfelt/maven-central-publishing-example/tree/master + scm:git:https://github.com/perNyfelt/maven-central-publishing-example.git + scm:git:https://github.com/perNyfelt/maven-central-publishing-example.git + + + UTF-8 + UTF-8 + 1.8 + 1.8 + + + + + central + Central Snapshot Repository + http://localhost:8088/api/v1 + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + @mavenCompilerPluginVersion@ + + ${maven.compiler.source} + ${maven.compiler.target} + + -Xlint:-options + + + + + org.apache.maven.plugins + maven-deploy-plugin + @project.version@ + + true + true + false + + + + org.apache.maven.plugins + maven-javadoc-plugin + @mavenJavadocPluginVersion@ + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-source-plugin + @mavenSourcePluginVersion@ + + + attach-sources + + jar + + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + org.apache.maven.plugins + maven-source-plugin + + + + org.codehaus.gmavenplus + gmavenplus-plugin + 1.13.1 + + + simulate-signing + verify + + execute + + + + fakeSign.groovy + + + + + + + org.apache.groovy + groovy + 4.0.27 + + + org.apache.groovy + groovy-ant + 4.0.27 + + + + + org.apache.maven.plugins + maven-deploy-plugin + + + + \ No newline at end of file diff --git a/src/it/central-deploy-simplebundle/setup.groovy b/src/it/central-deploy-simplebundle/setup.groovy new file mode 100644 index 0000000..a8ffbdc --- /dev/null +++ b/src/it/central-deploy-simplebundle/setup.groovy @@ -0,0 +1,189 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.servlet.* +import javax.servlet.http.* +import javax.servlet.* +import java.util.concurrent.ConcurrentHashMap +import org.apache.commons.fileupload.servlet.ServletFileUpload +import org.apache.commons.io.IOUtils +import com.fasterxml.jackson.databind.ObjectMapper + +def port = 8088 +def server = new Server(port) +def context = new ServletContextHandler(ServletContextHandler.SESSIONS) +context.setContextPath("/") + +def bundles = new ConcurrentHashMap() +def mapper = new ObjectMapper() + +// /api/v1 - Upload endpoint +context.addServlet(new ServletHolder(new HttpServlet() { + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + try { + if (!authHeader().equals(req.getHeader("Authorization"))) { + resp.setStatus(401) + println "Bearer token not recognized, should be based on testUser:testPwd" + return + } + + if (!ServletFileUpload.isMultipartContent(req)) { + resp.setStatus(400) + return + } + + def multipartConfig = new MultipartConfigElement(System.getProperty("java.io.tmpdir")) + req.setAttribute("org.eclipse.jetty.multipartConfig", multipartConfig) + + def publishingType = req.getParameter("publishingType") + def name = req.getParameter("name") + def part = req.getPart("bundle") + def zipData = IOUtils.toByteArray(part.inputStream) + if (zipData == null || zipData.length == 0) { + resp.setStatus(400) + resp.setContentType("application/json") + resp.writer.write("bundle contains no data") + return + } + def deploymentId = UUID.randomUUID().toString() + + bundles.put(deploymentId, zipData) + println("Received bundle " + name) + + resp.setContentType("text/plain") + resp.writer.write(deploymentId) + + } catch (Exception e) { + // Log stack trace to console + e.printStackTrace() + + // Respond with HTTP 500 and JSON error details + resp.setStatus(500) + resp.setContentType("application/json") + def errorResponse = [ + error: e.getClass().name, + message: e.message, + stackTrace: e.stackTrace.collect { it.toString() } + ] + resp.writer.write(mapper.writeValueAsString(errorResponse)) + } + } +}), "/api/v1/publisher/upload") + +// /publisher/status - Status check +context.addServlet(new ServletHolder(new HttpServlet() { + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + try { + if (!authHeader().equals(req.getHeader("Authorization"))) { + resp.setStatus(401) + println "Bearer token not recognized, should be based on testUser:testPwd" + return + } + + def deploymentId = req.getParameter("id") + def data = bundles.get(deploymentId) + + if (data == null) { + resp.writer.write("Deployment $deploymentId not found") + resp.setStatus(404) + return + } + + resp.setContentType("application/json") + Map deployments = new HashMap<>() + deployments.put(deploymentId, [ + deploymentId: deploymentId, + deploymentState: "VALIDATED", + purls: ["pkg:maven/com.sonatype.central.example/example_java_project@0.0.7"] + ]) + resp.writer.write(mapper.writeValueAsString(deployments)) + } catch (Exception e) { + // Log stack trace to console + e.printStackTrace() + + // Respond with HTTP 500 and JSON error details + resp.setStatus(500) + resp.setContentType("application/json") + def errorResponse = [ + error: e.getClass().name, + message: e.message, + stackTrace: e.stackTrace.collect { it.toString() } + ] + resp.writer.write(mapper.writeValueAsString(errorResponse)) + } + } +}), "/api/v1/publisher/status") + +// /getBundleIds - get a json list of all uploaded bundles +context.addServlet(new ServletHolder(new HttpServlet() { + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setContentType("application/json") + resp.writer.write(mapper.writeValueAsString(bundles.keys())) + } +}), "/getBundleIds") + +// /getBundle - Retrieve the uploaded bundle by deploymentId +context.addServlet(new ServletHolder(new HttpServlet() { + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + def id = req.getParameter("deploymentId") + def zip = bundles.get(id) + if (zip == null) { + resp.setStatus(404) + return + } + + resp.setContentType("application/zip") + resp.setHeader("Content-Disposition", "attachment; filename=\"${id}.zip\"") + resp.outputStream.write(zip) + } +}), "/getBundle") + +// /shutdown - Shut down the server gracefully +context.addServlet(new ServletHolder(new HttpServlet() { + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.writer.write("Shutting down...") + new Thread({ + Thread.sleep(1000) + server.stop() + }).start() + } +}), "/shutdown") + +context.addServlet(new ServletHolder(new HttpServlet() { + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + // Log or print the unmatched request URI + println("Unhandled request to: " + req.getRequestURI()) + + resp.setStatus(404) + resp.setContentType("application/json") + resp.writer.write(mapper.writeValueAsString([ + error: "Unknown endpoint", + path: req.getRequestURI() + ])) + } +}), "/*") + +// Must match the user and pwd in settings.xml for the central server +private String authHeader() { + return "Bearer " + Base64.getEncoder().encodeToString(("testUser:testPwd").getBytes()); +} + +server.setHandler(context) +server.start() + diff --git a/src/it/central-deploy-simplebundle/src/main/java/simple/common/Greeting.java b/src/it/central-deploy-simplebundle/src/main/java/simple/common/Greeting.java new file mode 100644 index 0000000..1700671 --- /dev/null +++ b/src/it/central-deploy-simplebundle/src/main/java/simple/common/Greeting.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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 simple.common; + +/** + * Common interface to define a greeting. + */ +public interface Greeting { + + /** + * Hello {name}. + * + * @param name the name to greet + * @return Hello {name} + */ + String greet(String name); + + /** + * Prints some info. + */ + default void info() { + System.out.println(this.getClass()); + } +} \ No newline at end of file diff --git a/src/it/central-deploy-simplebundle/src/main/java/simple/suba/Hello.java b/src/it/central-deploy-simplebundle/src/main/java/simple/suba/Hello.java new file mode 100644 index 0000000..78df288 --- /dev/null +++ b/src/it/central-deploy-simplebundle/src/main/java/simple/suba/Hello.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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 simple.suba; + +import simple.common.Greeting; + +/** + * This is the Greeting implementation in subA + */ +public class Hello implements Greeting { + + /** + * Default constructor. + */ + public Hello() {} + + @Override + public String greet(String name) { + return "Hello $name from suba"; + } +} diff --git a/src/it/central-deploy-simplebundle/verify.groovy b/src/it/central-deploy-simplebundle/verify.groovy new file mode 100644 index 0000000..dd38a8e --- /dev/null +++ b/src/it/central-deploy-simplebundle/verify.groovy @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ +import groovy.json.JsonSlurper + +import java.nio.file.Files +import java.util.zip.ZipFile + +String baseUrl = "http://localhost:8088" + +URLConnection conn = new URI("$baseUrl/getBundleIds").toURL().openConnection() +conn.setRequestMethod("GET") +conn.connect() +def json = conn.inputStream.text +def ids = new JsonSlurper().parseText(json) +println "verify: Received bundle IDs: $ids" + +// Check that exactly one bundle was uploaded +assert ids.size() == 1 : "Expected exactly one bundle, but got ${ids.size()}" +def deploymentId = ids[0] + +// download and check the zip content +conn = new URI("$baseUrl/getBundle?deploymentId=$deploymentId").toURL().openConnection() +conn.setRequestMethod("GET") +conn.connect() +def zip = Files.createTempFile("bundle", ".zip") +def megaZip = zip.toFile() +if (megaZip.exists()) { + megaZip.delete() +} +Files.copy(conn.inputStream, zip) + +String groupId = "se.alipsa.maven.example" + +def expectedEntries = {String artifactId, String version -> + String basePath = "${groupId.replace('.', '/')}/${artifactId}/${version}/" + String artifactPath = basePath + artifactId + '-' + version + [ + artifactPath + '.pom', + artifactPath +'.pom.asc', + artifactPath +'.pom.md5', + artifactPath +'.pom.sha1', + artifactPath +'.pom.sha256', + artifactPath + '.jar', + artifactPath + '.jar.asc', + artifactPath + '.jar.md5', + artifactPath + '.jar.sha1', + artifactPath + '.jar.sha256', + artifactPath + '-sources.jar', + artifactPath + '-sources.jar.asc', + artifactPath + '-sources.jar.md5', + artifactPath + '-sources.jar.sha1', + artifactPath + '-sources.jar.sha256', + artifactPath + '-javadoc.jar', + artifactPath + '-javadoc.jar.asc', + artifactPath + '-javadoc.jar.md5', + artifactPath + '-javadoc.jar.sha1', + artifactPath + '-javadoc.jar.sha256' + ] +} + +checkZipContent(megaZip, "publishing-example-simple", "1.0.0-SNAPSHOT", expectedEntries) +List entries = expectedEntries("publishing-example-simple", "1.0.0-SNAPSHOT") + +def actualEntries +try (ZipFile zipFile = new ZipFile(megaZip)) { + actualEntries = zipFile.entries().collect { it.name } +} +assert entries.size() == actualEntries.size() : "Mismatch in number of entries in ZIP, expected $expectedEntries but was ${actualEntries.size()}" + +static def checkZipContent(File zipFile, String artifactId, String version, Closure expectedMethod) { + println "Checking content of $zipFile" + List expectedEntries = expectedMethod(artifactId, version) + List actualEntries + try (ZipFile zip = new ZipFile(zipFile)) { + actualEntries = zip.entries().collect { it.name } + } + expectedEntries.each { + //println " - checking $it" + assert actualEntries.contains(it) : "Expected entry not found in $zipFile.name: $it" + } + return actualEntries +} +// Cleanup +megaZip.deleteOnExit() +// Shut down the server +try { + conn = new URI("$baseUrl/shutdown").toURL().openConnection() + conn.setRequestMethod("POST") + conn.connect() + println conn.inputStream.text + Thread.sleep(1000) +} catch (Exception e) { + println "Shutdown failed: ${e.message}" +} \ No newline at end of file diff --git a/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java b/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java index a1e5938..2f73c35 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java +++ b/src/main/java/org/apache/maven/plugins/deploy/CentralPortalClient.java @@ -40,12 +40,16 @@ public class CentralPortalClient { static final String CENTRAL_PORTAL_URL = "https://central.sonatype.com/api/v1"; - private final String username; - private final String password; - private final String publishUrl; - private final Log log; + private String username; + private String password; + private String publishUrl; + private Log log; - public CentralPortalClient(String username, String password, String publishUrl, Log log) { + public CentralPortalClient() { + this.publishUrl = CENTRAL_PORTAL_URL; + } + + public void setVariables(String username, String password, String publishUrl, Log log) { this.username = username; this.password = password; this.publishUrl = (publishUrl != null && !publishUrl.trim().isEmpty()) ? publishUrl : CENTRAL_PORTAL_URL; @@ -166,18 +170,6 @@ private String readFully(InputStream in) throws IOException { return buffer.toString("UTF-8").trim(); } - public String getUsername() { - return username; - } - - public String getPassword() { - return password; - } - - public String getPublishUrl() { - return publishUrl; - } - public void uploadAndCheck(File zipBundle, boolean autoDeploy) throws MojoExecutionException { String deploymentId; try { @@ -220,4 +212,36 @@ public void uploadAndCheck(File zipBundle, boolean autoDeploy) throws MojoExecut throw new MojoExecutionException("Failed to check status", e); } } + + public Log getLog() { + return log; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public String getPublishUrl() { + return publishUrl; + } + + public void setUsername(String username) { + this.username = username; + } + + public void setPassword(String password) { + this.password = password; + } + + public void setPublishUrl(String publishUrl) { + this.publishUrl = publishUrl; + } + + public void setLog(Log log) { + this.log = log; + } } diff --git a/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java b/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java index 4d434d7..6f29ba0 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java +++ b/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java @@ -200,6 +200,9 @@ private enum State { private static final String DEPLOY_ALT_DEPLOYMENT_REPOSITORY = DeployMojo.class.getName() + ".altDeploymentRepository"; + // Make it a member variable to allow test to mock central portal client + private CentralPortalClient centralPortalClient = new CentralPortalClient(); + private void putState(State state) { getPluginContext().put(DEPLOY_PROCESSED_MARKER, state.name()); } @@ -275,7 +278,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { private void deployAllAtOnce(List allProjectsUsingPlugin) throws MojoExecutionException { Map requests = new LinkedHashMap<>(); - // collect all arifacts from all modules to deploy + // collect all artifacts from all modules to deploy // requests are grouped by used remote repository for (MavenProject reactorProject : allProjectsUsingPlugin) { Map pluginContext = session.getPluginContext(pluginDescriptor, reactorProject); @@ -514,13 +517,13 @@ private void createAndDeploySingleProjectBundle(RemoteRepository deploymentRepos protected void deployBundle(Set repos, File zipBundle) throws MojoExecutionException { for (RemoteRepository repo : repos) { - String[] credentials = resolveCredentials( - project.getDistributionManagement().getRepository().getId()); + String[] credentials = resolveCredentials(repo.getId()); String username = credentials[0]; String password = credentials[1]; String deployUrl = repo.getUrl(); - CentralPortalClient centralPortalClient = new CentralPortalClient(username, password, deployUrl, getLog()); - getLog().info("Deploying " + zipBundle + " to " + centralPortalClient.getPublishUrl()); + centralPortalClient.setVariables(username, password, deployUrl, getLog()); + getLog().info("Deploying " + zipBundle + " to " + repo.getId() + " at " + + centralPortalClient.getPublishUrl()); centralPortalClient.uploadAndCheck(zipBundle, autoDeploy); } } @@ -544,4 +547,9 @@ private String[] resolveCredentials(String serverId) throws MojoExecutionExcepti return new String[] {username, password}; } + + // Allow mockito to mock the centralPortalClient + void setCentralPortalClient(CentralPortalClient centralPortalClient) { + this.centralPortalClient = centralPortalClient; + } } diff --git a/src/test/java/org/apache/maven/plugins/deploy/CentralDeployTest.java b/src/test/java/org/apache/maven/plugins/deploy/CentralDeployTest.java new file mode 100644 index 0000000..05e14ec --- /dev/null +++ b/src/test/java/org/apache/maven/plugins/deploy/CentralDeployTest.java @@ -0,0 +1,295 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.plugins.deploy; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.zip.ZipFile; + +import org.apache.maven.artifact.Artifact; +import org.apache.maven.artifact.DefaultArtifact; +import org.apache.maven.artifact.handler.ArtifactHandler; +import org.apache.maven.artifact.handler.DefaultArtifactHandler; +import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager; +import org.apache.maven.execution.MavenSession; +import org.apache.maven.model.Build; +import org.apache.maven.model.DeploymentRepository; +import org.apache.maven.model.DistributionManagement; +import org.apache.maven.plugin.descriptor.PluginDescriptor; +import org.apache.maven.plugin.testing.AbstractMojoTestCase; +import org.apache.maven.plugins.deploy.stubs.MavenProjectBigStub; +import org.apache.maven.project.DefaultMavenProjectHelper; +import org.apache.maven.project.MavenProject; +import org.apache.maven.project.MavenProjectHelper; +import org.apache.maven.settings.Server; +import org.apache.maven.settings.Settings; +import org.codehaus.plexus.util.FileUtils; +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.internal.impl.DefaultLocalPathComposer; +import org.eclipse.aether.internal.impl.SimpleLocalRepositoryManagerFactory; +import org.eclipse.aether.repository.LocalRepository; +import org.mockito.MockitoAnnotations; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author Per Nyfelt + */ +public class CentralDeployTest extends AbstractMojoTestCase { + private static final String GROUP_ID = "org.apache.maven.test"; + private static final String ARTIFACT_ID = "central-deploy-test"; + private static final String VERSION = "1.0.0"; + private static final String BASE_NAME = ARTIFACT_ID + "-" + VERSION; + private static final String LOCAL_REPO = getBasedir() + "/target/local-repo"; + private static final String SERVER_ID = "central"; + private static final String SERVER_URL = "http://localhost:8081/api/v1"; + + MavenProjectBigStub project; + + private AutoCloseable openMocks; + + private MavenSession session; + + private File localRepo; + + private CentralPortalClient centralPortalClient; + + private DeployMojo mojo; + + public void setUp() throws Exception { + super.setUp(); + project = new MavenProjectBigStub(); + session = mock(MavenSession.class); + Settings settings = mock(Settings.class); + Server server = new Server(); + server.setId(SERVER_ID); + server.setUsername("dummy-user"); + server.setPassword("dummy-password"); + when(session.getPluginContext(any(PluginDescriptor.class), any(MavenProject.class))) + .thenReturn(new ConcurrentHashMap<>()); + DefaultRepositorySystemSession repositorySession = new DefaultRepositorySystemSession(); + repositorySession.setLocalRepositoryManager( + new SimpleLocalRepositoryManagerFactory(new DefaultLocalPathComposer()) + .newInstance(repositorySession, new LocalRepository(LOCAL_REPO))); + when(session.getRepositorySession()).thenReturn(repositorySession); + when(settings.getServer(SERVER_ID)).thenReturn(server); + when(session.getSettings()).thenReturn(settings); + + localRepo = new File(LOCAL_REPO); + + if (localRepo.exists()) { + FileUtils.deleteDirectory(localRepo); + } + } + + public void tearDown() throws Exception { + super.tearDown(); + + if (openMocks != null) { + openMocks.close(); + } + } + + // (1) autoDeploy = true, uploadToCentral = true + public void testCentralPortalAutoDeployTrueUploadToCentralTrue() throws Exception { + File testPom = new File(getBasedir(), "target/test-classes/unit/central-deploy-test/plugin-config.xml"); + mojo = (DeployMojo) lookupMojo("deploy", testPom); + openMocks = MockitoAnnotations.openMocks(this); + assertNotNull(mojo); + + setVariableValueToObject(mojo, "session", session); + + setVariableValueToObject(mojo, "useCentralPortalApi", true); + setVariableValueToObject(mojo, "autoDeploy", true); + setVariableValueToObject(mojo, "uploadToCentral", true); + + centralPortalClient = mock(CentralPortalClient.class); + String fakeDeploymentId = "deployment-123"; + when(centralPortalClient.upload(any(File.class), anyBoolean())).thenReturn(fakeDeploymentId); + when(centralPortalClient.getStatus(fakeDeploymentId)).thenReturn("PUBLISHING"); + when(centralPortalClient.getPublishUrl()).thenReturn(SERVER_URL); + + setVariableValueToObject(mojo, "centralPortalClient", centralPortalClient); + + DefaultRepositorySystemSession repoSession = new DefaultRepositorySystemSession(); + repoSession.setLocalRepositoryManager(new SimpleLocalRepositoryManagerFactory(new DefaultLocalPathComposer()) + .newInstance(repoSession, new LocalRepository(LOCAL_REPO))); + when(session.getRepositorySession()).thenReturn(repoSession); + + setVariableValueToObject(mojo, "project", project); + ArtifactHandler artifactHandler = new DefaultArtifactHandler("jar"); + Artifact projectArtifact = new DefaultArtifact( + GROUP_ID, + ARTIFACT_ID, + VERSION, + null, // scope + "jar", // type + null, // classifier + artifactHandler); + + project.setArtifact(projectArtifact); + project.setGroupId(GROUP_ID); + project.setArtifactId(ARTIFACT_ID); + project.setVersion(VERSION); + setVariableValueToObject(mojo, "pluginContext", new ConcurrentHashMap<>()); + setVariableValueToObject(mojo, "reactorProjects", Collections.singletonList(project)); + + File baseDir = new File(getBasedir(), "target"); + String baseName = "central-deploy-test-1.0.0"; + + File artifactFile = createAndAttachFakeSignedArtifacts(baseDir); + + Artifact artifact = project.getArtifact(); + artifact.setFile(artifactFile); + project.setFile(new File(baseDir, baseName + ".pom")); + + Build build = new Build(); + build.setDirectory(baseDir.getAbsolutePath()); + project.setBuild(build); + + DistributionManagement distributionManagement = new DistributionManagement(); + DeploymentRepository deploymentRepository = new DeploymentRepository(); + deploymentRepository.setId(SERVER_ID); + deploymentRepository.setUrl(SERVER_URL); + distributionManagement.setRepository(deploymentRepository); + project.setDistributionManagement(distributionManagement); + + mojo.execute(); + + File bundleZip = new File(project.getBasedir(), "target/" + BASE_NAME + "-bundle.zip"); + assertTrue("Expected central bundle zip to be created at " + bundleZip.getAbsolutePath(), bundleZip.exists()); + String prefix = GROUP_ID.replace('.', '/') + "/" + ARTIFACT_ID + "/" + VERSION + "/"; + assertBundleContains(prefix, bundleZip); + + // Also assert that nothing was deployed to the mock remote repo + File remoteDir = new File(getBasedir(), "target/remote-repo/" + ARTIFACT_ID); + assertFalse("Jar should NOT be deployed to remote repo", new File(remoteDir, baseName + ".jar").exists()); + } + + // Helper method to create fake signed files for central bundle + private File createAndAttachFakeSignedArtifacts(File baseDir) + throws IOException, NoSuchFieldException, IllegalAccessException { + MavenProjectHelper projectHelper = new DefaultMavenProjectHelper(); + ArtifactHandlerManager artifactHandlerManager = mock(ArtifactHandlerManager.class); + when(artifactHandlerManager.getArtifactHandler(anyString())).thenAnswer(invocation -> { + String type = invocation.getArgument(0); + DefaultArtifactHandler handler = new DefaultArtifactHandler(type); + handler.setExtension(type); + return handler; + }); + Field handlerField = DefaultMavenProjectHelper.class.getDeclaredField("artifactHandlerManager"); + handlerField.setAccessible(true); + handlerField.set(projectHelper, artifactHandlerManager); + + baseDir.mkdirs(); + + // === Main artifact === + File mainJar = new File(baseDir, BASE_NAME + ".jar"); + File mainJarAsc = new File(baseDir, BASE_NAME + ".jar.asc"); + + // === Sources === + File sourcesJar = new File(baseDir, BASE_NAME + "-sources.jar"); + File sourcesAsc = new File(baseDir, BASE_NAME + "-sources.jar.asc"); + + // === Javadoc === + File javadocJar = new File(baseDir, BASE_NAME + "-javadoc.jar"); + File javadocAsc = new File(baseDir, BASE_NAME + "-javadoc.jar.asc"); + + // === POM === + File pomFile = new File(baseDir, BASE_NAME + ".pom"); + File pomAsc = new File(baseDir, BASE_NAME + ".pom.asc"); + + // Write fake content + List content = Collections.singletonList("generated " + new Date()); + Files.write(mainJar.toPath(), content, StandardCharsets.UTF_8); + Files.write(mainJarAsc.toPath(), Collections.singletonList("signature"), StandardCharsets.UTF_8); + + Files.write(sourcesJar.toPath(), content, StandardCharsets.UTF_8); + Files.write(sourcesAsc.toPath(), Collections.singletonList("signature"), StandardCharsets.UTF_8); + + Files.write(javadocJar.toPath(), content, StandardCharsets.UTF_8); + Files.write(javadocAsc.toPath(), Collections.singletonList("signature"), StandardCharsets.UTF_8); + + // Write minimal POM XML + String pomXml = "\n" + + "\n" + + " 4.0.0\n" + + " " + GROUP_ID + "\n" + + " " + ARTIFACT_ID + "\n" + + " " + VERSION + "\n" + + " Test deployment with sources and javadoc\n" + + " \n" + + " \n" + + " The Apache License, Version 2.0\n" + + " https://www.apache.org/licenses/LICENSE-2.0.txt\n" + + " repo\n" + + " \n" + + " \n" + + " \n" + + " https://github.com/apache/maven-deploy-plugin\n" + + " scm:git:https://github.com/apache/maven-deploy-plugin.git\n" + + " scm:git:https://github.com/apache/maven-deploy-plugin.git\n" + + " \n" + + " \n" + + " \n" + + " jdoe\n" + + " John Doe\n" + + " jdoe@example.com\n" + + " \n" + + " \n" + + "\n"; + + Files.write(pomFile.toPath(), pomXml.getBytes(StandardCharsets.UTF_8)); + Files.write(pomAsc.toPath(), Collections.singletonList("signature"), StandardCharsets.UTF_8); + + // === Attach main artifacts === + project.getArtifact().setFile(mainJar); + project.setFile(pomFile); + + // === Attach other artifacts === + projectHelper.attachArtifact(project, "jar", "sources", sourcesJar); + projectHelper.attachArtifact(project, "jar", "javadoc", javadocJar); + + return mainJar; + } + + // Helper method to verify central bundle contents + private void assertBundleContains(String prefix, File bundleZip) throws IOException { + try (ZipFile zip = new ZipFile(bundleZip)) { + assertNotNull(zip.getEntry(prefix + BASE_NAME + ".jar")); + assertNotNull(zip.getEntry(prefix + BASE_NAME + ".jar.asc")); + assertNotNull(zip.getEntry(prefix + BASE_NAME + ".pom")); + assertNotNull(zip.getEntry(prefix + BASE_NAME + ".pom.asc")); + } + } +} diff --git a/src/test/java/org/apache/maven/plugins/deploy/stubs/MavenProjectBigStub.java b/src/test/java/org/apache/maven/plugins/deploy/stubs/MavenProjectBigStub.java new file mode 100644 index 0000000..3461658 --- /dev/null +++ b/src/test/java/org/apache/maven/plugins/deploy/stubs/MavenProjectBigStub.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.plugins.deploy.stubs; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.maven.artifact.Artifact; +import org.apache.maven.artifact.repository.ArtifactRepository; +import org.apache.maven.artifact.repository.DefaultArtifactRepository; +import org.apache.maven.artifact.repository.layout.DefaultRepositoryLayout; +import org.apache.maven.model.Build; +import org.apache.maven.model.DeploymentRepository; +import org.apache.maven.model.DistributionManagement; +import org.apache.maven.model.Plugin; + +public class MavenProjectBigStub extends org.apache.maven.plugin.testing.stubs.MavenProjectStub { + private ArtifactRepository deploymentRepository; + private Build build; + private Artifact artifact; + private File file; + private final List attachedArtifacts = new ArrayList<>(); + private DistributionManagement distributionManagement; + + @Override + public Artifact getArtifact() { + return artifact; + } + + @Override + public void setArtifact(Artifact artifact) { + this.artifact = artifact; + } + + @Override + public File getFile() { + return file; + } + + @Override + public void setFile(File file) { + this.file = file; + } + + public ArtifactRepository getDistributionManagementArtifactRepository() { + if (deploymentRepository != null) { + return deploymentRepository; + } + if (distributionManagement != null && distributionManagement.getRepository() != null) { + DeploymentRepository repo = distributionManagement.getRepository(); + return new DefaultArtifactRepository(repo.getId(), repo.getUrl(), new DefaultRepositoryLayout()); + } + return null; + } + + public void setReleaseArtifactRepository(ArtifactRepositoryStub repo) { + this.deploymentRepository = repo; + } + + public ArtifactRepository getReleaseArtifactRepository() { + return deploymentRepository; + } + + @Override + public Build getBuild() { + if (build != null) { + return build; + } + Plugin plugin = new Plugin(); + Build build = new Build(); + build.setPlugins(Collections.singletonList(plugin)); + return build; + } + + @Override + public void setBuild(Build build) { + this.build = build; + } + + @Override + public void addAttachedArtifact(Artifact artifact) { + this.attachedArtifacts.add(artifact); + } + + @Override + public List getAttachedArtifacts() { + return this.attachedArtifacts; + } + + @Override + public DistributionManagement getDistributionManagement() { + return distributionManagement; + } + + @Override + public void setDistributionManagement(DistributionManagement distributionManagement) { + this.distributionManagement = distributionManagement; + } +} diff --git a/src/test/resources/unit/central-deploy-test/plugin-config.xml b/src/test/resources/unit/central-deploy-test/plugin-config.xml new file mode 100644 index 0000000..1e1caf2 --- /dev/null +++ b/src/test/resources/unit/central-deploy-test/plugin-config.xml @@ -0,0 +1,34 @@ + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + + true + true + true + + + + + From 2fdde74e6598c10d70f039515088e1d72f7aaa90 Mon Sep 17 00:00:00 2001 From: pernyf Date: Thu, 7 Aug 2025 16:23:02 +0200 Subject: [PATCH 19/25] Cleanup unit test --- .../apache/maven/plugins/deploy/Bundler.java | 2 +- .../plugins/deploy/CentralDeployTest.java | 334 +++++++++++++----- .../deploy/stubs/MavenProjectBigStub.java | 11 +- .../central-deploy-test/plugin-config.xml | 4 + 4 files changed, 251 insertions(+), 100 deletions(-) diff --git a/src/main/java/org/apache/maven/plugins/deploy/Bundler.java b/src/main/java/org/apache/maven/plugins/deploy/Bundler.java index bb65fd4..5bd17de 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/Bundler.java +++ b/src/main/java/org/apache/maven/plugins/deploy/Bundler.java @@ -192,7 +192,7 @@ private void validateForPublishing(File pomFile) throws MojoExecutionException { } private void addToZip(File file, String prefix, ZipOutputStream zipOut) throws IOException { - log.info("Create bundle, addToZip - " + file.getAbsolutePath()); + log.debug("Create bundle, addToZip - " + file.getAbsolutePath()); zipOut.putNextEntry(new ZipEntry(prefix + file.getName())); Files.copy(file.toPath(), zipOut); zipOut.closeEntry(); diff --git a/src/test/java/org/apache/maven/plugins/deploy/CentralDeployTest.java b/src/test/java/org/apache/maven/plugins/deploy/CentralDeployTest.java index 05e14ec..061c743 100644 --- a/src/test/java/org/apache/maven/plugins/deploy/CentralDeployTest.java +++ b/src/test/java/org/apache/maven/plugins/deploy/CentralDeployTest.java @@ -35,27 +35,19 @@ import org.apache.maven.artifact.handler.DefaultArtifactHandler; import org.apache.maven.artifact.handler.manager.ArtifactHandlerManager; import org.apache.maven.execution.MavenSession; -import org.apache.maven.model.Build; import org.apache.maven.model.DeploymentRepository; import org.apache.maven.model.DistributionManagement; -import org.apache.maven.plugin.descriptor.PluginDescriptor; import org.apache.maven.plugin.testing.AbstractMojoTestCase; import org.apache.maven.plugins.deploy.stubs.MavenProjectBigStub; import org.apache.maven.project.DefaultMavenProjectHelper; -import org.apache.maven.project.MavenProject; import org.apache.maven.project.MavenProjectHelper; import org.apache.maven.settings.Server; import org.apache.maven.settings.Settings; -import org.codehaus.plexus.util.FileUtils; import org.eclipse.aether.DefaultRepositorySystemSession; -import org.eclipse.aether.internal.impl.DefaultLocalPathComposer; -import org.eclipse.aether.internal.impl.SimpleLocalRepositoryManagerFactory; -import org.eclipse.aether.repository.LocalRepository; import org.mockito.MockitoAnnotations; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -67,7 +59,6 @@ public class CentralDeployTest extends AbstractMojoTestCase { private static final String ARTIFACT_ID = "central-deploy-test"; private static final String VERSION = "1.0.0"; private static final String BASE_NAME = ARTIFACT_ID + "-" + VERSION; - private static final String LOCAL_REPO = getBasedir() + "/target/local-repo"; private static final String SERVER_ID = "central"; private static final String SERVER_URL = "http://localhost:8081/api/v1"; @@ -77,8 +68,6 @@ public class CentralDeployTest extends AbstractMojoTestCase { private MavenSession session; - private File localRepo; - private CentralPortalClient centralPortalClient; private DeployMojo mojo; @@ -92,21 +81,11 @@ public void setUp() throws Exception { server.setId(SERVER_ID); server.setUsername("dummy-user"); server.setPassword("dummy-password"); - when(session.getPluginContext(any(PluginDescriptor.class), any(MavenProject.class))) - .thenReturn(new ConcurrentHashMap<>()); DefaultRepositorySystemSession repositorySession = new DefaultRepositorySystemSession(); - repositorySession.setLocalRepositoryManager( - new SimpleLocalRepositoryManagerFactory(new DefaultLocalPathComposer()) - .newInstance(repositorySession, new LocalRepository(LOCAL_REPO))); when(session.getRepositorySession()).thenReturn(repositorySession); + when(settings.getServer(SERVER_ID)).thenReturn(server); when(session.getSettings()).thenReturn(settings); - - localRepo = new File(LOCAL_REPO); - - if (localRepo.exists()) { - FileUtils.deleteDirectory(localRepo); - } } public void tearDown() throws Exception { @@ -124,6 +103,8 @@ public void testCentralPortalAutoDeployTrueUploadToCentralTrue() throws Exceptio openMocks = MockitoAnnotations.openMocks(this); assertNotNull(mojo); + setVariableValueToObject(mojo, "pluginContext", new ConcurrentHashMap<>()); + setVariableValueToObject(mojo, "reactorProjects", Collections.singletonList(project)); setVariableValueToObject(mojo, "session", session); setVariableValueToObject(mojo, "useCentralPortalApi", true); @@ -135,14 +116,8 @@ public void testCentralPortalAutoDeployTrueUploadToCentralTrue() throws Exceptio when(centralPortalClient.upload(any(File.class), anyBoolean())).thenReturn(fakeDeploymentId); when(centralPortalClient.getStatus(fakeDeploymentId)).thenReturn("PUBLISHING"); when(centralPortalClient.getPublishUrl()).thenReturn(SERVER_URL); - setVariableValueToObject(mojo, "centralPortalClient", centralPortalClient); - DefaultRepositorySystemSession repoSession = new DefaultRepositorySystemSession(); - repoSession.setLocalRepositoryManager(new SimpleLocalRepositoryManagerFactory(new DefaultLocalPathComposer()) - .newInstance(repoSession, new LocalRepository(LOCAL_REPO))); - when(session.getRepositorySession()).thenReturn(repoSession); - setVariableValueToObject(mojo, "project", project); ArtifactHandler artifactHandler = new DefaultArtifactHandler("jar"); Artifact projectArtifact = new DefaultArtifact( @@ -158,71 +133,230 @@ public void testCentralPortalAutoDeployTrueUploadToCentralTrue() throws Exceptio project.setGroupId(GROUP_ID); project.setArtifactId(ARTIFACT_ID); project.setVersion(VERSION); + + project.setDistributionManagement(createDistributionManagement()); + + createAndAttachFakeSignedArtifacts( + new File(getBasedir(), project.getBuild().getDirectory())); + + mojo.execute(); + + File bundleZip = new File(project.getBasedir(), "target/" + BASE_NAME + "-bundle.zip"); + assertTrue("Expected central bundle zip to be created at " + bundleZip.getAbsolutePath(), bundleZip.exists()); + String prefix = GROUP_ID.replace('.', '/') + "/" + ARTIFACT_ID + "/" + VERSION + "/"; + assertBundleContent(prefix, bundleZip); + } + + /* + // (2) autoDeploy = false, uploadToCentral = true + public void testCentralPortalAutoDeployFalseUploadToCentralTrue() throws Exception { + File testPom = new File(getBasedir(), "target/test-classes/unit/central-deploy-test/plugin-config.xml"); + mojo = (DeployMojo) lookupMojo("deploy", testPom); + openMocks = MockitoAnnotations.openMocks(this); + assertNotNull(mojo); + + setVariableValueToObject(mojo, "useCentralPortalApi", true); + setVariableValueToObject(mojo, "autoDeploy", false); + setVariableValueToObject(mojo, "uploadToCentral", true); + + DefaultRepositorySystemSession repoSession = new DefaultRepositorySystemSession(); + repoSession.setLocalRepositoryManager(new SimpleLocalRepositoryManagerFactory(new DefaultLocalPathComposer()) + .newInstance(repoSession, new LocalRepository(LOCAL_REPO))); + when(session.getRepositorySession()).thenReturn(repoSession); + + setVariableValueToObject(mojo, "project", project); + project.setArtifact(new DeployArtifactStub()); + project.setGroupId("org.apache.maven.test"); + project.setArtifactId("central-deploy-test"); + project.setVersion("1.0.1"); + setVariableValueToObject(mojo, "pluginContext", new ConcurrentHashMap<>()); + setVariableValueToObject(mojo, "reactorProjects", Collections.singletonList(project)); + + File baseDir = new File(getBasedir(), "target/fake-central-artifacts-2"); + String baseName = "central-deploy-test-1.0.1"; + File artifactFile = createFakeSignedArtifacts(baseName, baseDir); + + DeployArtifactStub artifact = (DeployArtifactStub) project.getArtifact(); + artifact.setFile(artifactFile); + project.setFile(new File(baseDir, baseName + ".pom")); + + ArtifactRepositoryStub repoStub = getRepoStub(mojo); + repoStub.setAppendToUrl("central-deploy-test"); + + mojo.execute(); + + File bundleZip = new File(project.getBasedir(), "target/central-bundle.zip"); + assertTrue("Expected central bundle zip to be created", bundleZip.exists()); + assertBundleContains(bundleZip, baseName); + + File remoteDir = new File(getBasedir(), "target/remote-repo/central-deploy-test"); + assertFalse("Jar should NOT be deployed to remote repo", new File(remoteDir, baseName + ".jar").exists()); + } + + // (3) autoDeploy = true, uploadToCentral = false + public void testCentralPortalAutoDeployTrueUploadToCentralFalse() throws Exception { + File testPom = new File(getBasedir(), "target/test-classes/unit/central-deploy-test/plugin-config.xml"); + mojo = (DeployMojo) lookupMojo("deploy", testPom); + openMocks = MockitoAnnotations.openMocks(this); + assertNotNull(mojo); + + setVariableValueToObject(mojo, "useCentralPortalApi", true); + setVariableValueToObject(mojo, "autoDeploy", true); + setVariableValueToObject(mojo, "uploadToCentral", false); + + DefaultRepositorySystemSession repoSession = new DefaultRepositorySystemSession(); + repoSession.setLocalRepositoryManager(new SimpleLocalRepositoryManagerFactory(new DefaultLocalPathComposer()) + .newInstance(repoSession, new LocalRepository(LOCAL_REPO))); + when(session.getRepositorySession()).thenReturn(repoSession); + + setVariableValueToObject(mojo, "project", project); + project.setArtifact(new DeployArtifactStub()); + project.setGroupId("org.apache.maven.test"); + project.setArtifactId("central-deploy-test"); + project.setVersion("1.0.2"); setVariableValueToObject(mojo, "pluginContext", new ConcurrentHashMap<>()); setVariableValueToObject(mojo, "reactorProjects", Collections.singletonList(project)); - File baseDir = new File(getBasedir(), "target"); - String baseName = "central-deploy-test-1.0.0"; + File baseDir = new File(getBasedir(), "target/fake-central-artifacts-3"); + String baseName = "central-deploy-test-1.0.2"; + File artifactFile = createFakeSignedArtifacts(baseName, baseDir); - File artifactFile = createAndAttachFakeSignedArtifacts(baseDir); + DeployArtifactStub artifact = (DeployArtifactStub) project.getArtifact(); + artifact.setFile(artifactFile); + project.setFile(new File(baseDir, baseName + ".pom")); + + ArtifactRepositoryStub repoStub = getRepoStub(mojo); + repoStub.setAppendToUrl("central-deploy-test"); + + mojo.execute(); - Artifact artifact = project.getArtifact(); + File remoteDir = new File(getBasedir(), "target/remote-repo/central-deploy-test"); + assertTrue("Jar should be deployed to remote repo", new File(remoteDir, baseName + ".jar").exists()); + assertTrue("POM should be deployed to remote repo", new File(remoteDir, baseName + ".pom").exists()); + } + + // (4) autoDeploy = false, uploadToCentral = false + public void testCentralPortalAutoDeployFalseUploadToCentralFalse() throws Exception { + File testPom = new File(getBasedir(), "target/test-classes/unit/central-deploy-test/plugin-config.xml"); + mojo = (DeployMojo) lookupMojo("deploy", testPom); + openMocks = MockitoAnnotations.openMocks(this); + assertNotNull(mojo); + + setVariableValueToObject(mojo, "useCentralPortalApi", true); + setVariableValueToObject(mojo, "autoDeploy", false); + setVariableValueToObject(mojo, "uploadToCentral", false); + + DefaultRepositorySystemSession repoSession = new DefaultRepositorySystemSession(); + repoSession.setLocalRepositoryManager(new SimpleLocalRepositoryManagerFactory(new DefaultLocalPathComposer()) + .newInstance(repoSession, new LocalRepository(LOCAL_REPO))); + when(session.getRepositorySession()).thenReturn(repoSession); + + setVariableValueToObject(mojo, "project", project); + project.setArtifact(new DeployArtifactStub()); + project.setGroupId("org.apache.maven.test"); + project.setArtifactId("central-deploy-test"); + project.setVersion("1.0.3"); + setVariableValueToObject(mojo, "pluginContext", new ConcurrentHashMap<>()); + setVariableValueToObject(mojo, "reactorProjects", Collections.singletonList(project)); + + File baseDir = new File(getBasedir(), "target/fake-central-artifacts-4"); + String baseName = "central-deploy-test-1.0.3"; + File artifactFile = createFakeSignedArtifacts(baseName, baseDir); + + DeployArtifactStub artifact = (DeployArtifactStub) project.getArtifact(); artifact.setFile(artifactFile); project.setFile(new File(baseDir, baseName + ".pom")); - Build build = new Build(); - build.setDirectory(baseDir.getAbsolutePath()); - project.setBuild(build); + ArtifactRepositoryStub repoStub = getRepoStub(mojo); + repoStub.setAppendToUrl("central-deploy-test"); + + mojo.execute(); + + File remoteDir = new File(getBasedir(), "target/remote-repo/central-deploy-test"); + assertTrue("Jar should be deployed to remote repo", new File(remoteDir, baseName + ".jar").exists()); + assertTrue("POM should be deployed to remote repo", new File(remoteDir, baseName + ".pom").exists()); + } + + // (5) Negative test: missing .asc files should fail + public void testCentralPortalFailsIfSignatureMissing() throws Exception { + File testPom = new File(getBasedir(), "target/test-classes/unit/central-deploy-test/plugin-config.xml"); + mojo = (DeployMojo) lookupMojo("deploy", testPom); + openMocks = MockitoAnnotations.openMocks(this); + assertNotNull(mojo); + setVariableValueToObject(mojo, "useCentralPortalApi", true); + setVariableValueToObject(mojo, "autoDeploy", true); + setVariableValueToObject(mojo, "uploadToCentral", true); + + DefaultRepositorySystemSession repoSession = new DefaultRepositorySystemSession(); + repoSession.setLocalRepositoryManager(new SimpleLocalRepositoryManagerFactory(new DefaultLocalPathComposer()) + .newInstance(repoSession, new LocalRepository(LOCAL_REPO))); + when(session.getRepositorySession()).thenReturn(repoSession); + + setVariableValueToObject(mojo, "project", project); + project.setArtifact(new DeployArtifactStub()); + project.setGroupId("org.apache.maven.test"); + project.setArtifactId("central-deploy-test"); + project.setVersion("1.0.4"); + setVariableValueToObject(mojo, "pluginContext", new ConcurrentHashMap<>()); + setVariableValueToObject(mojo, "reactorProjects", Collections.singletonList(project)); + + File baseDir = new File(getBasedir(), "target/fake-central-artifacts-5"); + baseDir.mkdirs(); + String baseName = "central-deploy-test-1.0.4"; + + File artifactFile = new File(baseDir, baseName + ".jar"); + File pomFile = new File(baseDir, baseName + ".pom"); + Files.write( + artifactFile.toPath(), + Collections.singletonList("jar content at " + new Date().toString()), + StandardCharsets.UTF_8); + Files.write( + pomFile.toPath(), + Collections.singletonList("pom content at " + new Date().toString()), + StandardCharsets.UTF_8); + + DeployArtifactStub artifact = (DeployArtifactStub) project.getArtifact(); + artifact.setFile(artifactFile); + project.setFile(pomFile); + + ArtifactRepositoryStub repoStub = getRepoStub(mojo); + repoStub.setAppendToUrl("central-deploy-test"); + + MojoExecutionException thrown = assertThrows(MojoExecutionException.class, () -> mojo.execute()); + assertTrue(thrown.getMessage().contains("Missing required signature files")); + }*/ + + private DistributionManagement createDistributionManagement() { DistributionManagement distributionManagement = new DistributionManagement(); DeploymentRepository deploymentRepository = new DeploymentRepository(); deploymentRepository.setId(SERVER_ID); deploymentRepository.setUrl(SERVER_URL); distributionManagement.setRepository(deploymentRepository); - project.setDistributionManagement(distributionManagement); - - mojo.execute(); - - File bundleZip = new File(project.getBasedir(), "target/" + BASE_NAME + "-bundle.zip"); - assertTrue("Expected central bundle zip to be created at " + bundleZip.getAbsolutePath(), bundleZip.exists()); - String prefix = GROUP_ID.replace('.', '/') + "/" + ARTIFACT_ID + "/" + VERSION + "/"; - assertBundleContains(prefix, bundleZip); - - // Also assert that nothing was deployed to the mock remote repo - File remoteDir = new File(getBasedir(), "target/remote-repo/" + ARTIFACT_ID); - assertFalse("Jar should NOT be deployed to remote repo", new File(remoteDir, baseName + ".jar").exists()); + return distributionManagement; } // Helper method to create fake signed files for central bundle - private File createAndAttachFakeSignedArtifacts(File baseDir) + private void createAndAttachFakeSignedArtifacts(File baseDir) throws IOException, NoSuchFieldException, IllegalAccessException { MavenProjectHelper projectHelper = new DefaultMavenProjectHelper(); + ArtifactHandlerManager artifactHandlerManager = mock(ArtifactHandlerManager.class); - when(artifactHandlerManager.getArtifactHandler(anyString())).thenAnswer(invocation -> { - String type = invocation.getArgument(0); - DefaultArtifactHandler handler = new DefaultArtifactHandler(type); - handler.setExtension(type); - return handler; - }); Field handlerField = DefaultMavenProjectHelper.class.getDeclaredField("artifactHandlerManager"); handlerField.setAccessible(true); handlerField.set(projectHelper, artifactHandlerManager); baseDir.mkdirs(); - // === Main artifact === File mainJar = new File(baseDir, BASE_NAME + ".jar"); File mainJarAsc = new File(baseDir, BASE_NAME + ".jar.asc"); - // === Sources === File sourcesJar = new File(baseDir, BASE_NAME + "-sources.jar"); File sourcesAsc = new File(baseDir, BASE_NAME + "-sources.jar.asc"); - // === Javadoc === File javadocJar = new File(baseDir, BASE_NAME + "-javadoc.jar"); File javadocAsc = new File(baseDir, BASE_NAME + "-javadoc.jar.asc"); - // === POM === File pomFile = new File(baseDir, BASE_NAME + ".pom"); File pomAsc = new File(baseDir, BASE_NAME + ".pom.asc"); @@ -238,36 +372,36 @@ private File createAndAttachFakeSignedArtifacts(File baseDir) Files.write(javadocAsc.toPath(), Collections.singletonList("signature"), StandardCharsets.UTF_8); // Write minimal POM XML - String pomXml = "\n" - + "" + + "\n" - + " 4.0.0\n" - + " " + GROUP_ID + "\n" - + " " + ARTIFACT_ID + "\n" - + " " + VERSION + "\n" - + " Test deployment with sources and javadoc\n" - + " \n" - + " \n" - + " The Apache License, Version 2.0\n" - + " https://www.apache.org/licenses/LICENSE-2.0.txt\n" - + " repo\n" - + " \n" - + " \n" - + " \n" - + " https://github.com/apache/maven-deploy-plugin\n" - + " scm:git:https://github.com/apache/maven-deploy-plugin.git\n" - + " scm:git:https://github.com/apache/maven-deploy-plugin.git\n" - + " \n" - + " \n" - + " \n" - + " jdoe\n" - + " John Doe\n" - + " jdoe@example.com\n" - + " \n" - + " \n" - + "\n"; + + "http://maven.apache.org/xsd/maven-4.0.0.xsd\">" + + " 4.0.0" + + " " + GROUP_ID + "" + + " " + ARTIFACT_ID + "" + + " " + VERSION + "" + + " Test deployment with sources and javadoc" + + " " + + " " + + " The Apache License, Version 2.0" + + " https://www.apache.org/licenses/LICENSE-2.0.txt" + + " repo" + + " " + + " " + + " " + + " https://github.com/apache/maven-deploy-plugin" + + " scm:git:https://github.com/apache/maven-deploy-plugin.git" + + " scm:git:https://github.com/apache/maven-deploy-plugin.git" + + " " + + " " + + " " + + " jdoe" + + " John Doe" + + " jdoe@example.com" + + " " + + " " + + ""; Files.write(pomFile.toPath(), pomXml.getBytes(StandardCharsets.UTF_8)); Files.write(pomAsc.toPath(), Collections.singletonList("signature"), StandardCharsets.UTF_8); @@ -279,17 +413,29 @@ private File createAndAttachFakeSignedArtifacts(File baseDir) // === Attach other artifacts === projectHelper.attachArtifact(project, "jar", "sources", sourcesJar); projectHelper.attachArtifact(project, "jar", "javadoc", javadocJar); - - return mainJar; } // Helper method to verify central bundle contents - private void assertBundleContains(String prefix, File bundleZip) throws IOException { + private void assertBundleContent(String prefix, File bundleZip) throws IOException { try (ZipFile zip = new ZipFile(bundleZip)) { - assertNotNull(zip.getEntry(prefix + BASE_NAME + ".jar")); - assertNotNull(zip.getEntry(prefix + BASE_NAME + ".jar.asc")); - assertNotNull(zip.getEntry(prefix + BASE_NAME + ".pom")); - assertNotNull(zip.getEntry(prefix + BASE_NAME + ".pom.asc")); + assertZipHasEntries( + zip, + prefix, + ".pom", + ".pom.asc", + ".jar", + ".jar.asc", + "-javadoc.jar", + "-javadoc.jar.asc", + "-sources.jar", + "-sources.jar.asc"); + } + } + + private void assertZipHasEntries(ZipFile zip, String prefix, String... suffixes) { + for (String suffix : suffixes) { + String entryName = prefix + BASE_NAME + suffix; + assertNotNull("Missing zip entry: " + entryName, zip.getEntry(entryName)); } } } diff --git a/src/test/java/org/apache/maven/plugins/deploy/stubs/MavenProjectBigStub.java b/src/test/java/org/apache/maven/plugins/deploy/stubs/MavenProjectBigStub.java index 3461658..a222c71 100644 --- a/src/test/java/org/apache/maven/plugins/deploy/stubs/MavenProjectBigStub.java +++ b/src/test/java/org/apache/maven/plugins/deploy/stubs/MavenProjectBigStub.java @@ -81,12 +81,13 @@ public ArtifactRepository getReleaseArtifactRepository() { @Override public Build getBuild() { - if (build != null) { - return build; + if (build == null) { + Plugin plugin = new Plugin(); + Build bld = new Build(); + bld.setPlugins(Collections.singletonList(plugin)); + bld.setDirectory("target"); + this.build = bld; } - Plugin plugin = new Plugin(); - Build build = new Build(); - build.setPlugins(Collections.singletonList(plugin)); return build; } diff --git a/src/test/resources/unit/central-deploy-test/plugin-config.xml b/src/test/resources/unit/central-deploy-test/plugin-config.xml index 1e1caf2..caa16fd 100644 --- a/src/test/resources/unit/central-deploy-test/plugin-config.xml +++ b/src/test/resources/unit/central-deploy-test/plugin-config.xml @@ -17,6 +17,10 @@ specific language governing permissions and limitations under the License. --> + 4.0.0 + org.apache.maven.test + central-deploy-test + 1.0.0 From 8ca28213b7fdae9501210cea0c81fc8ffb79c070 Mon Sep 17 00:00:00 2001 From: pernyf Date: Thu, 7 Aug 2025 17:52:57 +0200 Subject: [PATCH 20/25] create a subdir under target for each test method --- .../maven/plugins/deploy/DeployMojo.java | 7 + .../plugins/deploy/CentralDeployTest.java | 218 +++--------------- .../deploy/stubs/MavenProjectBigStub.java | 6 + 3 files changed, 44 insertions(+), 187 deletions(-) diff --git a/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java b/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java index 6f29ba0..59ab366 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java +++ b/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java @@ -204,6 +204,8 @@ private enum State { private CentralPortalClient centralPortalClient = new CentralPortalClient(); private void putState(State state) { + getLog().info("putState: pluginContext@" + getPluginContext().hashCode() + " putting " + DEPLOY_PROCESSED_MARKER + + "=" + state.name()); getPluginContext().put(DEPLOY_PROCESSED_MARKER, state.name()); } @@ -263,6 +265,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { } } + getLog().info("Setting state to " + state.name() + " for " + project.getArtifactId()); putState(state); List allProjectsUsingPlugin = getAllProjectsUsingPlugin(); @@ -315,9 +318,11 @@ private void deployAllAtOnce(List allProjectsUsingPlugin) throws M private boolean allProjectsMarked(List allProjectsUsingPlugin) { for (MavenProject reactorProject : allProjectsUsingPlugin) { if (!hasState(reactorProject)) { + getLog().info(reactorProject.getArtifactId() + " not marked for deploy"); return false; } } + getLog().info("All projects marked for deploy"); return true; } @@ -326,6 +331,7 @@ private List getAllProjectsUsingPlugin() { for (MavenProject reactorProject : reactorProjects) { if (hasExecution(reactorProject.getPlugin("org.apache.maven.plugins:maven-deploy-plugin"))) { result.add(reactorProject); + getLog().info(reactorProject.getArtifactId() + " added to All projects using plugin"); } } return result; @@ -507,6 +513,7 @@ private void createAndDeploySingleProjectBundle(RemoteRepository deploymentRepos File bundleFile = new File(targetDir, project.getArtifactId() + "-" + project.getVersion() + "-bundle.zip"); try { bundler.createZipBundle(bundleFile); + getLog().info("Bundle created successfully: " + bundleFile); } catch (IOException | NoSuchAlgorithmException e) { throw new MojoExecutionException("Failed to create zip bundle", e); } diff --git a/src/test/java/org/apache/maven/plugins/deploy/CentralDeployTest.java b/src/test/java/org/apache/maven/plugins/deploy/CentralDeployTest.java index 061c743..7078465 100644 --- a/src/test/java/org/apache/maven/plugins/deploy/CentralDeployTest.java +++ b/src/test/java/org/apache/maven/plugins/deploy/CentralDeployTest.java @@ -72,10 +72,13 @@ public class CentralDeployTest extends AbstractMojoTestCase { private DeployMojo mojo; + private ConcurrentHashMap pluginContext; + public void setUp() throws Exception { super.setUp(); project = new MavenProjectBigStub(); session = mock(MavenSession.class); + pluginContext = new ConcurrentHashMap<>(); Settings settings = mock(Settings.class); Server server = new Server(); server.setId(SERVER_ID); @@ -83,6 +86,7 @@ public void setUp() throws Exception { server.setPassword("dummy-password"); DefaultRepositorySystemSession repositorySession = new DefaultRepositorySystemSession(); when(session.getRepositorySession()).thenReturn(repositorySession); + when(session.getPluginContext(any(), any())).thenReturn(pluginContext); when(settings.getServer(SERVER_ID)).thenReturn(server); when(session.getSettings()).thenReturn(settings); @@ -96,20 +100,37 @@ public void tearDown() throws Exception { } } - // (1) autoDeploy = true, uploadToCentral = true - public void testCentralPortalAutoDeployTrueUploadToCentralTrue() throws Exception { + /** + * (1.0) autoDeploy = true, uploadToCentral = true, deployAtEnd = false + * Individual bundles (even when there is only a simple project) are named after the artifactId + */ + public void testCentralPortalAutoDeployTrueUploadToCentralTrueDeployAtEndFalse() throws Exception { + autoDeployTrueUploadToCentralTrue(BASE_NAME + "-bundle.zip", false, "test-deployAtEnd-false"); + } + + /** + * (1.1) autoDeploy = true, uploadToCentral = true, deployAtEnd = true + * Mega-bundles are named after the groupId + */ + public void testCentralPortalAutoDeployTrueUploadToCentralTrueDeployAtEndTrue() throws Exception { + autoDeployTrueUploadToCentralTrue(GROUP_ID + "-" + VERSION + "-bundle.zip", true, "test-deployAtEnd-true"); + } + + private void autoDeployTrueUploadToCentralTrue(String bundleName, boolean deployAtEnd, String subDirName) + throws Exception { File testPom = new File(getBasedir(), "target/test-classes/unit/central-deploy-test/plugin-config.xml"); mojo = (DeployMojo) lookupMojo("deploy", testPom); openMocks = MockitoAnnotations.openMocks(this); assertNotNull(mojo); - setVariableValueToObject(mojo, "pluginContext", new ConcurrentHashMap<>()); + setVariableValueToObject(mojo, "pluginContext", pluginContext); setVariableValueToObject(mojo, "reactorProjects", Collections.singletonList(project)); setVariableValueToObject(mojo, "session", session); setVariableValueToObject(mojo, "useCentralPortalApi", true); setVariableValueToObject(mojo, "autoDeploy", true); setVariableValueToObject(mojo, "uploadToCentral", true); + setVariableValueToObject(mojo, "deployAtEnd", deployAtEnd); centralPortalClient = mock(CentralPortalClient.class); String fakeDeploymentId = "deployment-123"; @@ -136,197 +157,19 @@ public void testCentralPortalAutoDeployTrueUploadToCentralTrue() throws Exceptio project.setDistributionManagement(createDistributionManagement()); - createAndAttachFakeSignedArtifacts( - new File(getBasedir(), project.getBuild().getDirectory())); + // create a subdir under target to isolate tests from each other + File targetSubDir = new File(getBasedir(), project.getBuild().getDirectory() + "/" + subDirName); + project.getBuild().setDirectory(targetSubDir.getAbsolutePath()); + createAndAttachFakeSignedArtifacts(targetSubDir, artifactHandler); mojo.execute(); - File bundleZip = new File(project.getBasedir(), "target/" + BASE_NAME + "-bundle.zip"); + File bundleZip = new File(targetSubDir, bundleName); assertTrue("Expected central bundle zip to be created at " + bundleZip.getAbsolutePath(), bundleZip.exists()); String prefix = GROUP_ID.replace('.', '/') + "/" + ARTIFACT_ID + "/" + VERSION + "/"; assertBundleContent(prefix, bundleZip); } - /* - // (2) autoDeploy = false, uploadToCentral = true - public void testCentralPortalAutoDeployFalseUploadToCentralTrue() throws Exception { - File testPom = new File(getBasedir(), "target/test-classes/unit/central-deploy-test/plugin-config.xml"); - mojo = (DeployMojo) lookupMojo("deploy", testPom); - openMocks = MockitoAnnotations.openMocks(this); - assertNotNull(mojo); - - setVariableValueToObject(mojo, "useCentralPortalApi", true); - setVariableValueToObject(mojo, "autoDeploy", false); - setVariableValueToObject(mojo, "uploadToCentral", true); - - DefaultRepositorySystemSession repoSession = new DefaultRepositorySystemSession(); - repoSession.setLocalRepositoryManager(new SimpleLocalRepositoryManagerFactory(new DefaultLocalPathComposer()) - .newInstance(repoSession, new LocalRepository(LOCAL_REPO))); - when(session.getRepositorySession()).thenReturn(repoSession); - - setVariableValueToObject(mojo, "project", project); - project.setArtifact(new DeployArtifactStub()); - project.setGroupId("org.apache.maven.test"); - project.setArtifactId("central-deploy-test"); - project.setVersion("1.0.1"); - setVariableValueToObject(mojo, "pluginContext", new ConcurrentHashMap<>()); - setVariableValueToObject(mojo, "reactorProjects", Collections.singletonList(project)); - - File baseDir = new File(getBasedir(), "target/fake-central-artifacts-2"); - String baseName = "central-deploy-test-1.0.1"; - File artifactFile = createFakeSignedArtifacts(baseName, baseDir); - - DeployArtifactStub artifact = (DeployArtifactStub) project.getArtifact(); - artifact.setFile(artifactFile); - project.setFile(new File(baseDir, baseName + ".pom")); - - ArtifactRepositoryStub repoStub = getRepoStub(mojo); - repoStub.setAppendToUrl("central-deploy-test"); - - mojo.execute(); - - File bundleZip = new File(project.getBasedir(), "target/central-bundle.zip"); - assertTrue("Expected central bundle zip to be created", bundleZip.exists()); - assertBundleContains(bundleZip, baseName); - - File remoteDir = new File(getBasedir(), "target/remote-repo/central-deploy-test"); - assertFalse("Jar should NOT be deployed to remote repo", new File(remoteDir, baseName + ".jar").exists()); - } - - // (3) autoDeploy = true, uploadToCentral = false - public void testCentralPortalAutoDeployTrueUploadToCentralFalse() throws Exception { - File testPom = new File(getBasedir(), "target/test-classes/unit/central-deploy-test/plugin-config.xml"); - mojo = (DeployMojo) lookupMojo("deploy", testPom); - openMocks = MockitoAnnotations.openMocks(this); - assertNotNull(mojo); - - setVariableValueToObject(mojo, "useCentralPortalApi", true); - setVariableValueToObject(mojo, "autoDeploy", true); - setVariableValueToObject(mojo, "uploadToCentral", false); - - DefaultRepositorySystemSession repoSession = new DefaultRepositorySystemSession(); - repoSession.setLocalRepositoryManager(new SimpleLocalRepositoryManagerFactory(new DefaultLocalPathComposer()) - .newInstance(repoSession, new LocalRepository(LOCAL_REPO))); - when(session.getRepositorySession()).thenReturn(repoSession); - - setVariableValueToObject(mojo, "project", project); - project.setArtifact(new DeployArtifactStub()); - project.setGroupId("org.apache.maven.test"); - project.setArtifactId("central-deploy-test"); - project.setVersion("1.0.2"); - setVariableValueToObject(mojo, "pluginContext", new ConcurrentHashMap<>()); - setVariableValueToObject(mojo, "reactorProjects", Collections.singletonList(project)); - - File baseDir = new File(getBasedir(), "target/fake-central-artifacts-3"); - String baseName = "central-deploy-test-1.0.2"; - File artifactFile = createFakeSignedArtifacts(baseName, baseDir); - - DeployArtifactStub artifact = (DeployArtifactStub) project.getArtifact(); - artifact.setFile(artifactFile); - project.setFile(new File(baseDir, baseName + ".pom")); - - ArtifactRepositoryStub repoStub = getRepoStub(mojo); - repoStub.setAppendToUrl("central-deploy-test"); - - mojo.execute(); - - File remoteDir = new File(getBasedir(), "target/remote-repo/central-deploy-test"); - assertTrue("Jar should be deployed to remote repo", new File(remoteDir, baseName + ".jar").exists()); - assertTrue("POM should be deployed to remote repo", new File(remoteDir, baseName + ".pom").exists()); - } - - // (4) autoDeploy = false, uploadToCentral = false - public void testCentralPortalAutoDeployFalseUploadToCentralFalse() throws Exception { - File testPom = new File(getBasedir(), "target/test-classes/unit/central-deploy-test/plugin-config.xml"); - mojo = (DeployMojo) lookupMojo("deploy", testPom); - openMocks = MockitoAnnotations.openMocks(this); - assertNotNull(mojo); - - setVariableValueToObject(mojo, "useCentralPortalApi", true); - setVariableValueToObject(mojo, "autoDeploy", false); - setVariableValueToObject(mojo, "uploadToCentral", false); - - DefaultRepositorySystemSession repoSession = new DefaultRepositorySystemSession(); - repoSession.setLocalRepositoryManager(new SimpleLocalRepositoryManagerFactory(new DefaultLocalPathComposer()) - .newInstance(repoSession, new LocalRepository(LOCAL_REPO))); - when(session.getRepositorySession()).thenReturn(repoSession); - - setVariableValueToObject(mojo, "project", project); - project.setArtifact(new DeployArtifactStub()); - project.setGroupId("org.apache.maven.test"); - project.setArtifactId("central-deploy-test"); - project.setVersion("1.0.3"); - setVariableValueToObject(mojo, "pluginContext", new ConcurrentHashMap<>()); - setVariableValueToObject(mojo, "reactorProjects", Collections.singletonList(project)); - - File baseDir = new File(getBasedir(), "target/fake-central-artifacts-4"); - String baseName = "central-deploy-test-1.0.3"; - File artifactFile = createFakeSignedArtifacts(baseName, baseDir); - - DeployArtifactStub artifact = (DeployArtifactStub) project.getArtifact(); - artifact.setFile(artifactFile); - project.setFile(new File(baseDir, baseName + ".pom")); - - ArtifactRepositoryStub repoStub = getRepoStub(mojo); - repoStub.setAppendToUrl("central-deploy-test"); - - mojo.execute(); - - File remoteDir = new File(getBasedir(), "target/remote-repo/central-deploy-test"); - assertTrue("Jar should be deployed to remote repo", new File(remoteDir, baseName + ".jar").exists()); - assertTrue("POM should be deployed to remote repo", new File(remoteDir, baseName + ".pom").exists()); - } - - // (5) Negative test: missing .asc files should fail - public void testCentralPortalFailsIfSignatureMissing() throws Exception { - File testPom = new File(getBasedir(), "target/test-classes/unit/central-deploy-test/plugin-config.xml"); - mojo = (DeployMojo) lookupMojo("deploy", testPom); - openMocks = MockitoAnnotations.openMocks(this); - assertNotNull(mojo); - - setVariableValueToObject(mojo, "useCentralPortalApi", true); - setVariableValueToObject(mojo, "autoDeploy", true); - setVariableValueToObject(mojo, "uploadToCentral", true); - - DefaultRepositorySystemSession repoSession = new DefaultRepositorySystemSession(); - repoSession.setLocalRepositoryManager(new SimpleLocalRepositoryManagerFactory(new DefaultLocalPathComposer()) - .newInstance(repoSession, new LocalRepository(LOCAL_REPO))); - when(session.getRepositorySession()).thenReturn(repoSession); - - setVariableValueToObject(mojo, "project", project); - project.setArtifact(new DeployArtifactStub()); - project.setGroupId("org.apache.maven.test"); - project.setArtifactId("central-deploy-test"); - project.setVersion("1.0.4"); - setVariableValueToObject(mojo, "pluginContext", new ConcurrentHashMap<>()); - setVariableValueToObject(mojo, "reactorProjects", Collections.singletonList(project)); - - File baseDir = new File(getBasedir(), "target/fake-central-artifacts-5"); - baseDir.mkdirs(); - String baseName = "central-deploy-test-1.0.4"; - - File artifactFile = new File(baseDir, baseName + ".jar"); - File pomFile = new File(baseDir, baseName + ".pom"); - Files.write( - artifactFile.toPath(), - Collections.singletonList("jar content at " + new Date().toString()), - StandardCharsets.UTF_8); - Files.write( - pomFile.toPath(), - Collections.singletonList("pom content at " + new Date().toString()), - StandardCharsets.UTF_8); - - DeployArtifactStub artifact = (DeployArtifactStub) project.getArtifact(); - artifact.setFile(artifactFile); - project.setFile(pomFile); - - ArtifactRepositoryStub repoStub = getRepoStub(mojo); - repoStub.setAppendToUrl("central-deploy-test"); - - MojoExecutionException thrown = assertThrows(MojoExecutionException.class, () -> mojo.execute()); - assertTrue(thrown.getMessage().contains("Missing required signature files")); - }*/ - private DistributionManagement createDistributionManagement() { DistributionManagement distributionManagement = new DistributionManagement(); DeploymentRepository deploymentRepository = new DeploymentRepository(); @@ -337,11 +180,12 @@ private DistributionManagement createDistributionManagement() { } // Helper method to create fake signed files for central bundle - private void createAndAttachFakeSignedArtifacts(File baseDir) + private void createAndAttachFakeSignedArtifacts(File baseDir, ArtifactHandler artifactHandler) throws IOException, NoSuchFieldException, IllegalAccessException { MavenProjectHelper projectHelper = new DefaultMavenProjectHelper(); ArtifactHandlerManager artifactHandlerManager = mock(ArtifactHandlerManager.class); + when(artifactHandlerManager.getArtifactHandler(any())).thenReturn(artifactHandler); Field handlerField = DefaultMavenProjectHelper.class.getDeclaredField("artifactHandlerManager"); handlerField.setAccessible(true); handlerField.set(projectHelper, artifactHandlerManager); diff --git a/src/test/java/org/apache/maven/plugins/deploy/stubs/MavenProjectBigStub.java b/src/test/java/org/apache/maven/plugins/deploy/stubs/MavenProjectBigStub.java index a222c71..09421a0 100644 --- a/src/test/java/org/apache/maven/plugins/deploy/stubs/MavenProjectBigStub.java +++ b/src/test/java/org/apache/maven/plugins/deploy/stubs/MavenProjectBigStub.java @@ -31,6 +31,7 @@ import org.apache.maven.model.DeploymentRepository; import org.apache.maven.model.DistributionManagement; import org.apache.maven.model.Plugin; +import org.apache.maven.model.PluginExecution; public class MavenProjectBigStub extends org.apache.maven.plugin.testing.stubs.MavenProjectStub { private ArtifactRepository deploymentRepository; @@ -83,6 +84,11 @@ public ArtifactRepository getReleaseArtifactRepository() { public Build getBuild() { if (build == null) { Plugin plugin = new Plugin(); + plugin.setGroupId("org.apache.maven.plugins"); + plugin.setArtifactId("maven-deploy-plugin"); + PluginExecution pluginExecution = new PluginExecution(); + pluginExecution.setGoals(Collections.singletonList("deploy")); + plugin.setExecutions(Collections.singletonList(pluginExecution)); Build bld = new Build(); bld.setPlugins(Collections.singletonList(plugin)); bld.setDirectory("target"); From c1eb0b6e96d060d0d70527ad04f0c4b743900641 Mon Sep 17 00:00:00 2001 From: pernyf Date: Fri, 8 Aug 2025 10:05:40 +0200 Subject: [PATCH 21/25] move common config code to the setup method --- .../plugins/deploy/CentralDeployTest.java | 192 ++++++++++++++++-- 1 file changed, 178 insertions(+), 14 deletions(-) diff --git a/src/test/java/org/apache/maven/plugins/deploy/CentralDeployTest.java b/src/test/java/org/apache/maven/plugins/deploy/CentralDeployTest.java index 7078465..d3058ce 100644 --- a/src/test/java/org/apache/maven/plugins/deploy/CentralDeployTest.java +++ b/src/test/java/org/apache/maven/plugins/deploy/CentralDeployTest.java @@ -74,6 +74,8 @@ public class CentralDeployTest extends AbstractMojoTestCase { private ConcurrentHashMap pluginContext; + private ArtifactHandler artifactHandler; + public void setUp() throws Exception { super.setUp(); project = new MavenProjectBigStub(); @@ -90,6 +92,17 @@ public void setUp() throws Exception { when(settings.getServer(SERVER_ID)).thenReturn(server); when(session.getSettings()).thenReturn(settings); + File testPom = new File(getBasedir(), "target/test-classes/unit/central-deploy-test/plugin-config.xml"); + mojo = (DeployMojo) lookupMojo("deploy", testPom); + openMocks = MockitoAnnotations.openMocks(this); + assertNotNull(mojo); + + setVariableValueToObject(mojo, "pluginContext", pluginContext); + setVariableValueToObject(mojo, "reactorProjects", Collections.singletonList(project)); + setVariableValueToObject(mojo, "session", session); + setVariableValueToObject(mojo, "project", project); + artifactHandler = new DefaultArtifactHandler("jar"); + project.setDistributionManagement(createDistributionManagement()); } public void tearDown() throws Exception { @@ -118,14 +131,6 @@ public void testCentralPortalAutoDeployTrueUploadToCentralTrueDeployAtEndTrue() private void autoDeployTrueUploadToCentralTrue(String bundleName, boolean deployAtEnd, String subDirName) throws Exception { - File testPom = new File(getBasedir(), "target/test-classes/unit/central-deploy-test/plugin-config.xml"); - mojo = (DeployMojo) lookupMojo("deploy", testPom); - openMocks = MockitoAnnotations.openMocks(this); - assertNotNull(mojo); - - setVariableValueToObject(mojo, "pluginContext", pluginContext); - setVariableValueToObject(mojo, "reactorProjects", Collections.singletonList(project)); - setVariableValueToObject(mojo, "session", session); setVariableValueToObject(mojo, "useCentralPortalApi", true); setVariableValueToObject(mojo, "autoDeploy", true); @@ -139,8 +144,6 @@ private void autoDeployTrueUploadToCentralTrue(String bundleName, boolean deploy when(centralPortalClient.getPublishUrl()).thenReturn(SERVER_URL); setVariableValueToObject(mojo, "centralPortalClient", centralPortalClient); - setVariableValueToObject(mojo, "project", project); - ArtifactHandler artifactHandler = new DefaultArtifactHandler("jar"); Artifact projectArtifact = new DefaultArtifact( GROUP_ID, ARTIFACT_ID, @@ -155,12 +158,10 @@ private void autoDeployTrueUploadToCentralTrue(String bundleName, boolean deploy project.setArtifactId(ARTIFACT_ID); project.setVersion(VERSION); - project.setDistributionManagement(createDistributionManagement()); - // create a subdir under target to isolate tests from each other File targetSubDir = new File(getBasedir(), project.getBuild().getDirectory() + "/" + subDirName); project.getBuild().setDirectory(targetSubDir.getAbsolutePath()); - createAndAttachFakeSignedArtifacts(targetSubDir, artifactHandler); + createAndAttachFakeSignedArtifacts(targetSubDir); mojo.execute(); @@ -170,6 +171,169 @@ private void autoDeployTrueUploadToCentralTrue(String bundleName, boolean deploy assertBundleContent(prefix, bundleZip); } + /* + // (2) autoDeploy = false, uploadToCentral = true + public void testCentralPortalAutoDeployFalseUploadToCentralTrue() throws Exception { + File testPom = new File(getBasedir(), "target/test-classes/unit/central-deploy-test/plugin-config.xml"); + mojo = (DeployMojo) lookupMojo("deploy", testPom); + openMocks = MockitoAnnotations.openMocks(this); + assertNotNull(mojo); + + setVariableValueToObject(mojo, "useCentralPortalApi", true); + setVariableValueToObject(mojo, "autoDeploy", false); + setVariableValueToObject(mojo, "uploadToCentral", true); + + setVariableValueToObject(mojo, "project", project); + project.setArtifact(new DeployArtifactStub()); + project.setGroupId("org.apache.maven.test"); + project.setArtifactId("central-deploy-test"); + project.setVersion("1.0.1"); + setVariableValueToObject(mojo, "pluginContext", new ConcurrentHashMap<>()); + setVariableValueToObject(mojo, "reactorProjects", Collections.singletonList(project)); + + File baseDir = new File(getBasedir(), "target/fake-central-artifacts-2"); + String baseName = "central-deploy-test-1.0.1"; + File artifactFile = createFakeSignedArtifacts(baseName, baseDir); + + DeployArtifactStub artifact = (DeployArtifactStub) project.getArtifact(); + artifact.setFile(artifactFile); + project.setFile(new File(baseDir, baseName + ".pom")); + + ArtifactRepositoryStub repoStub = getRepoStub(mojo); + repoStub.setAppendToUrl("central-deploy-test"); + + mojo.execute(); + + File bundleZip = new File(project.getBasedir(), "target/central-bundle.zip"); + assertTrue("Expected central bundle zip to be created", bundleZip.exists()); + assertBundleContains(bundleZip, baseName); + + File remoteDir = new File(getBasedir(), "target/remote-repo/central-deploy-test"); + assertFalse("Jar should NOT be deployed to remote repo", new File(remoteDir, baseName + ".jar").exists()); + } + + */ + + /* + // (3) autoDeploy = true, uploadToCentral = false + public void testCentralPortalAutoDeployTrueUploadToCentralFalse() throws Exception { + File testPom = new File(getBasedir(), "target/test-classes/unit/central-deploy-test/plugin-config.xml"); + mojo = (DeployMojo) lookupMojo("deploy", testPom); + openMocks = MockitoAnnotations.openMocks(this); + assertNotNull(mojo); + + setVariableValueToObject(mojo, "useCentralPortalApi", true); + setVariableValueToObject(mojo, "autoDeploy", true); + setVariableValueToObject(mojo, "uploadToCentral", false); + + setVariableValueToObject(mojo, "project", project); + project.setArtifact(new DeployArtifactStub()); + project.setGroupId("org.apache.maven.test"); + project.setArtifactId("central-deploy-test"); + project.setVersion("1.0.2"); + setVariableValueToObject(mojo, "pluginContext", new ConcurrentHashMap<>()); + setVariableValueToObject(mojo, "reactorProjects", Collections.singletonList(project)); + + File baseDir = new File(getBasedir(), "target/fake-central-artifacts-3"); + String baseName = "central-deploy-test-1.0.2"; + File artifactFile = createFakeSignedArtifacts(baseName, baseDir); + + DeployArtifactStub artifact = (DeployArtifactStub) project.getArtifact(); + artifact.setFile(artifactFile); + project.setFile(new File(baseDir, baseName + ".pom")); + + ArtifactRepositoryStub repoStub = getRepoStub(mojo); + repoStub.setAppendToUrl("central-deploy-test"); + + mojo.execute(); + + File remoteDir = new File(getBasedir(), "target/remote-repo/central-deploy-test"); + assertTrue("Jar should be deployed to remote repo", new File(remoteDir, baseName + ".jar").exists()); + assertTrue("POM should be deployed to remote repo", new File(remoteDir, baseName + ".pom").exists()); + } + + // (4) autoDeploy = false, uploadToCentral = false + public void testCentralPortalAutoDeployFalseUploadToCentralFalse() throws Exception { + File testPom = new File(getBasedir(), "target/test-classes/unit/central-deploy-test/plugin-config.xml"); + mojo = (DeployMojo) lookupMojo("deploy", testPom); + openMocks = MockitoAnnotations.openMocks(this); + assertNotNull(mojo); + + setVariableValueToObject(mojo, "useCentralPortalApi", true); + setVariableValueToObject(mojo, "autoDeploy", false); + setVariableValueToObject(mojo, "uploadToCentral", false); + + setVariableValueToObject(mojo, "project", project); + project.setArtifact(new DeployArtifactStub()); + project.setGroupId("org.apache.maven.test"); + project.setArtifactId("central-deploy-test"); + project.setVersion("1.0.3"); + setVariableValueToObject(mojo, "pluginContext", new ConcurrentHashMap<>()); + setVariableValueToObject(mojo, "reactorProjects", Collections.singletonList(project)); + + File baseDir = new File(getBasedir(), "target/fake-central-artifacts-4"); + String baseName = "central-deploy-test-1.0.3"; + File artifactFile = createFakeSignedArtifacts(baseName, baseDir); + + DeployArtifactStub artifact = (DeployArtifactStub) project.getArtifact(); + artifact.setFile(artifactFile); + project.setFile(new File(baseDir, baseName + ".pom")); + + ArtifactRepositoryStub repoStub = getRepoStub(mojo); + repoStub.setAppendToUrl("central-deploy-test"); + + mojo.execute(); + + File remoteDir = new File(getBasedir(), "target/remote-repo/central-deploy-test"); + assertTrue("Jar should be deployed to remote repo", new File(remoteDir, baseName + ".jar").exists()); + assertTrue("POM should be deployed to remote repo", new File(remoteDir, baseName + ".pom").exists()); + } + + // (5) Negative test: missing .asc files should fail + public void testCentralPortalFailsIfSignatureMissing() throws Exception { + File testPom = new File(getBasedir(), "target/test-classes/unit/central-deploy-test/plugin-config.xml"); + mojo = (DeployMojo) lookupMojo("deploy", testPom); + openMocks = MockitoAnnotations.openMocks(this); + assertNotNull(mojo); + + setVariableValueToObject(mojo, "useCentralPortalApi", true); + setVariableValueToObject(mojo, "autoDeploy", true); + setVariableValueToObject(mojo, "uploadToCentral", true); + + setVariableValueToObject(mojo, "project", project); + project.setArtifact(new DeployArtifactStub()); + project.setGroupId("org.apache.maven.test"); + project.setArtifactId("central-deploy-test"); + project.setVersion("1.0.4"); + setVariableValueToObject(mojo, "pluginContext", new ConcurrentHashMap<>()); + setVariableValueToObject(mojo, "reactorProjects", Collections.singletonList(project)); + + File baseDir = new File(getBasedir(), "target/fake-central-artifacts-5"); + baseDir.mkdirs(); + String baseName = "central-deploy-test-1.0.4"; + + File artifactFile = new File(baseDir, baseName + ".jar"); + File pomFile = new File(baseDir, baseName + ".pom"); + Files.write( + artifactFile.toPath(), + Collections.singletonList("jar content at " + new Date().toString()), + StandardCharsets.UTF_8); + Files.write( + pomFile.toPath(), + Collections.singletonList("pom content at " + new Date().toString()), + StandardCharsets.UTF_8); + + DeployArtifactStub artifact = (DeployArtifactStub) project.getArtifact(); + artifact.setFile(artifactFile); + project.setFile(pomFile); + + ArtifactRepositoryStub repoStub = getRepoStub(mojo); + repoStub.setAppendToUrl("central-deploy-test"); + + MojoExecutionException thrown = assertThrows(MojoExecutionException.class, () -> mojo.execute()); + assertTrue(thrown.getMessage().contains("Missing required signature files")); + }*/ + private DistributionManagement createDistributionManagement() { DistributionManagement distributionManagement = new DistributionManagement(); DeploymentRepository deploymentRepository = new DeploymentRepository(); @@ -180,7 +344,7 @@ private DistributionManagement createDistributionManagement() { } // Helper method to create fake signed files for central bundle - private void createAndAttachFakeSignedArtifacts(File baseDir, ArtifactHandler artifactHandler) + private void createAndAttachFakeSignedArtifacts(File baseDir) throws IOException, NoSuchFieldException, IllegalAccessException { MavenProjectHelper projectHelper = new DefaultMavenProjectHelper(); From feb5bafc112e316d8cac3060e8330615478dc939 Mon Sep 17 00:00:00 2001 From: pernyf Date: Fri, 8 Aug 2025 11:01:59 +0200 Subject: [PATCH 22/25] add more unit tests --- .../plugins/deploy/CentralDeployTest.java | 224 +++++------------- 1 file changed, 55 insertions(+), 169 deletions(-) diff --git a/src/test/java/org/apache/maven/plugins/deploy/CentralDeployTest.java b/src/test/java/org/apache/maven/plugins/deploy/CentralDeployTest.java index d3058ce..dda363b 100644 --- a/src/test/java/org/apache/maven/plugins/deploy/CentralDeployTest.java +++ b/src/test/java/org/apache/maven/plugins/deploy/CentralDeployTest.java @@ -37,6 +37,7 @@ import org.apache.maven.execution.MavenSession; import org.apache.maven.model.DeploymentRepository; import org.apache.maven.model.DistributionManagement; +import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.testing.AbstractMojoTestCase; import org.apache.maven.plugins.deploy.stubs.MavenProjectBigStub; import org.apache.maven.project.DefaultMavenProjectHelper; @@ -46,6 +47,7 @@ import org.eclipse.aether.DefaultRepositorySystemSession; import org.mockito.MockitoAnnotations; +import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.mock; @@ -117,41 +119,43 @@ public void tearDown() throws Exception { * (1.0) autoDeploy = true, uploadToCentral = true, deployAtEnd = false * Individual bundles (even when there is only a simple project) are named after the artifactId */ - public void testCentralPortalAutoDeployTrueUploadToCentralTrueDeployAtEndFalse() throws Exception { - autoDeployTrueUploadToCentralTrue(BASE_NAME + "-bundle.zip", false, "test-deployAtEnd-false"); + public void testCentralPortalAutoDeployTrueDeployAtEndFalse() throws Exception { + sunnyDayTest(BASE_NAME + "-bundle.zip", true, false, "central-deploy-test-1"); } /** * (1.1) autoDeploy = true, uploadToCentral = true, deployAtEnd = true * Mega-bundles are named after the groupId */ - public void testCentralPortalAutoDeployTrueUploadToCentralTrueDeployAtEndTrue() throws Exception { - autoDeployTrueUploadToCentralTrue(GROUP_ID + "-" + VERSION + "-bundle.zip", true, "test-deployAtEnd-true"); + public void testCentralPortalAutoDeployTrueDeployAtEndTrue() throws Exception { + sunnyDayTest(GROUP_ID + "-" + VERSION + "-bundle.zip", true, true, "central-deploy-test-2"); } - private void autoDeployTrueUploadToCentralTrue(String bundleName, boolean deployAtEnd, String subDirName) + public void testCentralPortalAutoDeployFalseDeployAtEndTrue() throws Exception { + sunnyDayTest(GROUP_ID + "-" + VERSION + "-bundle.zip", false, true, "central-deploy-test-3"); + } + + public void testCentralPortalAutoDeployFalseDeployAtEndFalse() throws Exception { + sunnyDayTest(BASE_NAME + "-bundle.zip", false, false, "central-deploy-test-4"); + } + + private void sunnyDayTest(String bundleName, boolean autoDeploy, boolean deployAtEnd, String subDirName) throws Exception { setVariableValueToObject(mojo, "useCentralPortalApi", true); - setVariableValueToObject(mojo, "autoDeploy", true); + setVariableValueToObject(mojo, "autoDeploy", autoDeploy); setVariableValueToObject(mojo, "uploadToCentral", true); setVariableValueToObject(mojo, "deployAtEnd", deployAtEnd); centralPortalClient = mock(CentralPortalClient.class); String fakeDeploymentId = "deployment-123"; when(centralPortalClient.upload(any(File.class), anyBoolean())).thenReturn(fakeDeploymentId); - when(centralPortalClient.getStatus(fakeDeploymentId)).thenReturn("PUBLISHING"); + String status = autoDeploy ? "PUBLISHING" : "VALIDATED"; + when(centralPortalClient.getStatus(fakeDeploymentId)).thenReturn(status); when(centralPortalClient.getPublishUrl()).thenReturn(SERVER_URL); setVariableValueToObject(mojo, "centralPortalClient", centralPortalClient); - Artifact projectArtifact = new DefaultArtifact( - GROUP_ID, - ARTIFACT_ID, - VERSION, - null, // scope - "jar", // type - null, // classifier - artifactHandler); + Artifact projectArtifact = createProjectArtifact(); project.setArtifact(projectArtifact); project.setGroupId(GROUP_ID); @@ -167,172 +171,53 @@ private void autoDeployTrueUploadToCentralTrue(String bundleName, boolean deploy File bundleZip = new File(targetSubDir, bundleName); assertTrue("Expected central bundle zip to be created at " + bundleZip.getAbsolutePath(), bundleZip.exists()); - String prefix = GROUP_ID.replace('.', '/') + "/" + ARTIFACT_ID + "/" + VERSION + "/"; - assertBundleContent(prefix, bundleZip); - } - - /* - // (2) autoDeploy = false, uploadToCentral = true - public void testCentralPortalAutoDeployFalseUploadToCentralTrue() throws Exception { - File testPom = new File(getBasedir(), "target/test-classes/unit/central-deploy-test/plugin-config.xml"); - mojo = (DeployMojo) lookupMojo("deploy", testPom); - openMocks = MockitoAnnotations.openMocks(this); - assertNotNull(mojo); - - setVariableValueToObject(mojo, "useCentralPortalApi", true); - setVariableValueToObject(mojo, "autoDeploy", false); - setVariableValueToObject(mojo, "uploadToCentral", true); - - setVariableValueToObject(mojo, "project", project); - project.setArtifact(new DeployArtifactStub()); - project.setGroupId("org.apache.maven.test"); - project.setArtifactId("central-deploy-test"); - project.setVersion("1.0.1"); - setVariableValueToObject(mojo, "pluginContext", new ConcurrentHashMap<>()); - setVariableValueToObject(mojo, "reactorProjects", Collections.singletonList(project)); - - File baseDir = new File(getBasedir(), "target/fake-central-artifacts-2"); - String baseName = "central-deploy-test-1.0.1"; - File artifactFile = createFakeSignedArtifacts(baseName, baseDir); - - DeployArtifactStub artifact = (DeployArtifactStub) project.getArtifact(); - artifact.setFile(artifactFile); - project.setFile(new File(baseDir, baseName + ".pom")); - - ArtifactRepositoryStub repoStub = getRepoStub(mojo); - repoStub.setAppendToUrl("central-deploy-test"); - - mojo.execute(); - - File bundleZip = new File(project.getBasedir(), "target/central-bundle.zip"); - assertTrue("Expected central bundle zip to be created", bundleZip.exists()); - assertBundleContains(bundleZip, baseName); - - File remoteDir = new File(getBasedir(), "target/remote-repo/central-deploy-test"); - assertFalse("Jar should NOT be deployed to remote repo", new File(remoteDir, baseName + ".jar").exists()); - } - - */ - - /* - // (3) autoDeploy = true, uploadToCentral = false - public void testCentralPortalAutoDeployTrueUploadToCentralFalse() throws Exception { - File testPom = new File(getBasedir(), "target/test-classes/unit/central-deploy-test/plugin-config.xml"); - mojo = (DeployMojo) lookupMojo("deploy", testPom); - openMocks = MockitoAnnotations.openMocks(this); - assertNotNull(mojo); - - setVariableValueToObject(mojo, "useCentralPortalApi", true); - setVariableValueToObject(mojo, "autoDeploy", true); - setVariableValueToObject(mojo, "uploadToCentral", false); - - setVariableValueToObject(mojo, "project", project); - project.setArtifact(new DeployArtifactStub()); - project.setGroupId("org.apache.maven.test"); - project.setArtifactId("central-deploy-test"); - project.setVersion("1.0.2"); - setVariableValueToObject(mojo, "pluginContext", new ConcurrentHashMap<>()); - setVariableValueToObject(mojo, "reactorProjects", Collections.singletonList(project)); - - File baseDir = new File(getBasedir(), "target/fake-central-artifacts-3"); - String baseName = "central-deploy-test-1.0.2"; - File artifactFile = createFakeSignedArtifacts(baseName, baseDir); - - DeployArtifactStub artifact = (DeployArtifactStub) project.getArtifact(); - artifact.setFile(artifactFile); - project.setFile(new File(baseDir, baseName + ".pom")); - - ArtifactRepositoryStub repoStub = getRepoStub(mojo); - repoStub.setAppendToUrl("central-deploy-test"); - - mojo.execute(); - File remoteDir = new File(getBasedir(), "target/remote-repo/central-deploy-test"); - assertTrue("Jar should be deployed to remote repo", new File(remoteDir, baseName + ".jar").exists()); - assertTrue("POM should be deployed to remote repo", new File(remoteDir, baseName + ".pom").exists()); - } - - // (4) autoDeploy = false, uploadToCentral = false - public void testCentralPortalAutoDeployFalseUploadToCentralFalse() throws Exception { - File testPom = new File(getBasedir(), "target/test-classes/unit/central-deploy-test/plugin-config.xml"); - mojo = (DeployMojo) lookupMojo("deploy", testPom); - openMocks = MockitoAnnotations.openMocks(this); - assertNotNull(mojo); - - setVariableValueToObject(mojo, "useCentralPortalApi", true); - setVariableValueToObject(mojo, "autoDeploy", false); - setVariableValueToObject(mojo, "uploadToCentral", false); - - setVariableValueToObject(mojo, "project", project); - project.setArtifact(new DeployArtifactStub()); - project.setGroupId("org.apache.maven.test"); - project.setArtifactId("central-deploy-test"); - project.setVersion("1.0.3"); - setVariableValueToObject(mojo, "pluginContext", new ConcurrentHashMap<>()); - setVariableValueToObject(mojo, "reactorProjects", Collections.singletonList(project)); - - File baseDir = new File(getBasedir(), "target/fake-central-artifacts-4"); - String baseName = "central-deploy-test-1.0.3"; - File artifactFile = createFakeSignedArtifacts(baseName, baseDir); - - DeployArtifactStub artifact = (DeployArtifactStub) project.getArtifact(); - artifact.setFile(artifactFile); - project.setFile(new File(baseDir, baseName + ".pom")); - - ArtifactRepositoryStub repoStub = getRepoStub(mojo); - repoStub.setAppendToUrl("central-deploy-test"); - - mojo.execute(); - - File remoteDir = new File(getBasedir(), "target/remote-repo/central-deploy-test"); - assertTrue("Jar should be deployed to remote repo", new File(remoteDir, baseName + ".jar").exists()); - assertTrue("POM should be deployed to remote repo", new File(remoteDir, baseName + ".pom").exists()); + assertBundleContent(bundleZip); } // (5) Negative test: missing .asc files should fail public void testCentralPortalFailsIfSignatureMissing() throws Exception { - File testPom = new File(getBasedir(), "target/test-classes/unit/central-deploy-test/plugin-config.xml"); - mojo = (DeployMojo) lookupMojo("deploy", testPom); - openMocks = MockitoAnnotations.openMocks(this); - assertNotNull(mojo); - setVariableValueToObject(mojo, "useCentralPortalApi", true); setVariableValueToObject(mojo, "autoDeploy", true); setVariableValueToObject(mojo, "uploadToCentral", true); + setVariableValueToObject(mojo, "deployAtEnd", true); - setVariableValueToObject(mojo, "project", project); - project.setArtifact(new DeployArtifactStub()); - project.setGroupId("org.apache.maven.test"); - project.setArtifactId("central-deploy-test"); - project.setVersion("1.0.4"); - setVariableValueToObject(mojo, "pluginContext", new ConcurrentHashMap<>()); - setVariableValueToObject(mojo, "reactorProjects", Collections.singletonList(project)); + centralPortalClient = mock(CentralPortalClient.class); + String fakeDeploymentId = "deployment-123"; + when(centralPortalClient.upload(any(File.class), anyBoolean())).thenReturn(fakeDeploymentId); + when(centralPortalClient.getStatus(fakeDeploymentId)).thenReturn("PUBLISHING"); + when(centralPortalClient.getPublishUrl()).thenReturn(SERVER_URL); + setVariableValueToObject(mojo, "centralPortalClient", centralPortalClient); - File baseDir = new File(getBasedir(), "target/fake-central-artifacts-5"); - baseDir.mkdirs(); - String baseName = "central-deploy-test-1.0.4"; - - File artifactFile = new File(baseDir, baseName + ".jar"); - File pomFile = new File(baseDir, baseName + ".pom"); - Files.write( - artifactFile.toPath(), - Collections.singletonList("jar content at " + new Date().toString()), - StandardCharsets.UTF_8); - Files.write( - pomFile.toPath(), - Collections.singletonList("pom content at " + new Date().toString()), - StandardCharsets.UTF_8); - - DeployArtifactStub artifact = (DeployArtifactStub) project.getArtifact(); - artifact.setFile(artifactFile); - project.setFile(pomFile); + Artifact projectArtifact = createProjectArtifact(); - ArtifactRepositoryStub repoStub = getRepoStub(mojo); - repoStub.setAppendToUrl("central-deploy-test"); + project.setArtifact(projectArtifact); + project.setGroupId(GROUP_ID); + project.setArtifactId(ARTIFACT_ID); + project.setVersion(VERSION); + + // create a subdir under target to isolate tests from each other + File targetSubDir = new File(getBasedir(), project.getBuild().getDirectory() + "/central-deploy-test-5"); + project.getBuild().setDirectory(targetSubDir.getAbsolutePath()); + createAndAttachFakeSignedArtifacts(targetSubDir); + new File(targetSubDir, ARTIFACT_ID + "-" + VERSION + ".pom.asc").delete(); MojoExecutionException thrown = assertThrows(MojoExecutionException.class, () -> mojo.execute()); - assertTrue(thrown.getMessage().contains("Missing required signature files")); - }*/ + assertTrue( + "Expected MojoExecutionException to be Failed to create bundle but was " + thrown.toString(), + thrown.getMessage().contains("Failed to create bundle")); + } + + private Artifact createProjectArtifact() { + return new DefaultArtifact( + GROUP_ID, + ARTIFACT_ID, + VERSION, + null, // scope + "jar", // type + null, // classifier + artifactHandler); + } private DistributionManagement createDistributionManagement() { DistributionManagement distributionManagement = new DistributionManagement(); @@ -424,7 +309,8 @@ private void createAndAttachFakeSignedArtifacts(File baseDir) } // Helper method to verify central bundle contents - private void assertBundleContent(String prefix, File bundleZip) throws IOException { + private void assertBundleContent(File bundleZip) throws IOException { + String prefix = GROUP_ID.replace('.', '/') + "/" + ARTIFACT_ID + "/" + VERSION + "/"; try (ZipFile zip = new ZipFile(bundleZip)) { assertZipHasEntries( zip, From f85d7a626a18ac069c15b70ee0cdc893298d1343 Mon Sep 17 00:00:00 2001 From: pernyf Date: Fri, 8 Aug 2025 14:09:19 +0200 Subject: [PATCH 23/25] fix documentation. add rainy day test for DeployMojo. --- src/it/central-deploy-bundles/README.md | 23 ++++- .../addDependencies.groovy | 87 +++++++++++++++++++ src/it/central-deploy-megabundle/README.md | 30 +++++-- .../addDependencies.groovy | 87 +++++++++++++++++++ src/it/central-deploy-simplebundle/README.md | 19 ++-- .../addDependencies.groovy | 87 +++++++++++++++++++ src/it/central-zip-bundles/README.md | 16 ++-- src/it/central-zip-megabundle/README.md | 2 +- .../maven/plugins/deploy/DeployMojo.java | 2 + .../plugins/deploy/CentralDeployTest.java | 54 ++++++++++-- 10 files changed, 372 insertions(+), 35 deletions(-) create mode 100755 src/it/central-deploy-bundles/addDependencies.groovy create mode 100755 src/it/central-deploy-megabundle/addDependencies.groovy create mode 100755 src/it/central-deploy-simplebundle/addDependencies.groovy diff --git a/src/it/central-deploy-bundles/README.md b/src/it/central-deploy-bundles/README.md index 9003f79..b937487 100644 --- a/src/it/central-deploy-bundles/README.md +++ b/src/it/central-deploy-bundles/README.md @@ -16,13 +16,11 @@ specific language governing permissions and limitations under the License. --> -# maven-central-publishing-example +# central-deploy-bundles This is a multimodule example showing deployment to maven central using the new central publishing rest api. -The maven-deploy-plugin used is a [modified fork](https://github.com/perNyfelt/maven-deploy-plugin/tree/add_central_support) of the apache maven deploy plugin. - The project consist of - an aggregator - a common sub module @@ -33,4 +31,21 @@ I.e. when deploying the whole project, 4 zip files will be created and uploaded 1. The aggregator pom (+ asc, md5 and sha1 files) 2. common, contains the pom, jar, javadoc and sources (all signed and with md5 and sha1 files) 3. subA, contains the pom, jar, javadoc and sources (all signed and with md5 and sha1 files) -4. subB, contains the pom, jar, javadoc and sources (all signed and with md5 and sha1 files) \ No newline at end of file +4. subB, contains the pom, jar, javadoc and sources (all signed and with md5 and sha1 files) + +## Running only this test +```shell +mvn -Prun-its verify -Dinvoker.test=central-deploy-bundles +``` + +## Running the test manually +```shell +# copy resources +mvn -Prun-its verify -Dinvoker.test=central-deploy-bundles +cd target/it/central-deploy-bundles +CLASSPATH=$(find "$MAVEN_HOME/lib" -name "*.jar" | tr '\n' ':' | sed 's/:$//') +CLASSPATH=$CLASSPATH:$(./addDependencies.groovy) +groovy -cp $CLASSPATH -Dbasedir=$PWD setup.groovy & +mvn --settings ../../../src/it/settings.xml deploy +groovy -cp $CLASSPATH -Dbasedir=$PWD verify.groovy +``` \ No newline at end of file diff --git a/src/it/central-deploy-bundles/addDependencies.groovy b/src/it/central-deploy-bundles/addDependencies.groovy new file mode 100755 index 0000000..06fa39d --- /dev/null +++ b/src/it/central-deploy-bundles/addDependencies.groovy @@ -0,0 +1,87 @@ +#!/usr/bin/env groovy +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ +// This script creates a classpath string based on the dependencies for the invoker plugin +// It is used when you want to change the groovy scripts and have more control from the command line +// i.e allows you to run each step manually. See readme.md for details. +import groovy.xml.XmlParser +import groovy.xml.XmlNodePrinter +import groovy.xml.StreamingMarkupBuilder + +// Paths +def realPom = new File("../../../pom.xml") +def tmpDir = File.createTempFile("tmp", "") +tmpDir.delete() +tmpDir.mkdirs() +def tmpPom = new File(tmpDir, "pom.xml") +def depsDir = new File(tmpDir, "deps") +depsDir.mkdirs() + +// Parse original POM without namespaces +def parser = new XmlParser(false, false) // disable namespaces +def pom = parser.parse(realPom) + +// Locate dependencies inside maven-invoker-plugin / run-its profile +def runItsProfile = pom.profiles.profile.find { it.id.text() == "run-its" } +def rawDeps = [] +if (runItsProfile) { + rawDeps = runItsProfile.depthFirst().findAll { + it.name() == "plugin" && it.artifactId.text() == "maven-invoker-plugin" + }*.dependencies*.dependency.flatten() +} + +// Build minimal POM +def newPom = { + mkp.xmlDeclaration() + project(xmlns: "http://maven.apache.org/POM/4.0.0", + 'xmlns:xsi': "http://www.w3.org/2001/XMLSchema-instance", + 'xsi:schemaLocation': "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd") { + modelVersion("4.0.0") + groupId("temp") + artifactId("invoker-plugin-deps") + version("1.0-SNAPSHOT") + dependencies { + rawDeps.each { dep -> + dependency { + groupId(dep.groupId.text()) + artifactId(dep.artifactId.text()) + version(dep.version.text()) + if (dep.scope) scope(dep.scope.text()) + } + } + } + } +} + +// Write temp POM +tmpPom.text = new StreamingMarkupBuilder().bind(newPom).toString() + +//println "Downloading dependencies..." +def proc = ["mvn", "-q", "-f", tmpPom.absolutePath, + "dependency:copy-dependencies", + "-DoutputDirectory=${depsDir.absolutePath}", + "-DincludeScope=runtime"].execute() +proc.in.eachLine { println it } +proc.err.eachLine { System.err.println it } +proc.waitFor() + +// Build classpath +def jars = depsDir.listFiles().findAll { it.name.endsWith(".jar") } +def classpath = jars.collect { it.absolutePath }.join(":") +println classpath \ No newline at end of file diff --git a/src/it/central-deploy-megabundle/README.md b/src/it/central-deploy-megabundle/README.md index e8067e6..0a98432 100644 --- a/src/it/central-deploy-megabundle/README.md +++ b/src/it/central-deploy-megabundle/README.md @@ -14,21 +14,37 @@ See the License for the specific language governing permissions and limitations under the License. --> -# maven-central-publishing-example +# central-deploy-megabundle This is a multimodule example showing deployment to maven central using the new central publishing rest api. -The maven-deploy-plugin used is a [modified fork](https://github.com/perNyfelt/maven-deploy-plugin/tree/add_central_support) of the apache maven deploy plugin. - The project consist of - an aggregator - a common sub module - two sub modules that each depends on the common submodule -Each module (including the main aggregator) will be deployed separately. -I.e. when deploying the whole project, 4 zip files will be created and uploaded to central: +All modules (including the main aggregator) will be deployed together. +I.e. when deploying the whole project, 1 zip file will be created and uploaded to central: +This 1 zip will contain: 1. The aggregator pom (+ asc, md5 and sha1 files) 2. common, contains the pom, jar, javadoc and sources (all signed and with md5 and sha1 files) -3. subA, contains the pom, jar, javadoc and sources (all signed and with md5 and sha1 files) -4. subB, contains the pom, jar, javadoc and sources (all signed and with md5 and sha1 files) \ No newline at end of file +3. subA, contains the pom, jar, javadoc and sources (all signed and with md5 and sha1 files) +4. subB, contains the pom, jar, javadoc and sources (all signed and with md5 and sha1 files) + +## Running only this test +```shell +mvn -Prun-its verify -Dinvoker.test=central-deploy-megabundle +``` + +## Running the test manually +```shell +# copy resources +mvn -Prun-its verify -Dinvoker.test=central-deploy-megabundle +cd target/it/central-deploy-bundles +CLASSPATH=$(find "$MAVEN_HOME/lib" -name "*.jar" | tr '\n' ':' | sed 's/:$//') +CLASSPATH=$CLASSPATH:$(./addDependencies.groovy) +groovy -cp $CLASSPATH -Dbasedir=$PWD setup.groovy & +mvn --settings ../../../src/it/settings.xml deploy +groovy -cp $CLASSPATH -Dbasedir=$PWD verify.groovy +``` \ No newline at end of file diff --git a/src/it/central-deploy-megabundle/addDependencies.groovy b/src/it/central-deploy-megabundle/addDependencies.groovy new file mode 100755 index 0000000..06fa39d --- /dev/null +++ b/src/it/central-deploy-megabundle/addDependencies.groovy @@ -0,0 +1,87 @@ +#!/usr/bin/env groovy +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ +// This script creates a classpath string based on the dependencies for the invoker plugin +// It is used when you want to change the groovy scripts and have more control from the command line +// i.e allows you to run each step manually. See readme.md for details. +import groovy.xml.XmlParser +import groovy.xml.XmlNodePrinter +import groovy.xml.StreamingMarkupBuilder + +// Paths +def realPom = new File("../../../pom.xml") +def tmpDir = File.createTempFile("tmp", "") +tmpDir.delete() +tmpDir.mkdirs() +def tmpPom = new File(tmpDir, "pom.xml") +def depsDir = new File(tmpDir, "deps") +depsDir.mkdirs() + +// Parse original POM without namespaces +def parser = new XmlParser(false, false) // disable namespaces +def pom = parser.parse(realPom) + +// Locate dependencies inside maven-invoker-plugin / run-its profile +def runItsProfile = pom.profiles.profile.find { it.id.text() == "run-its" } +def rawDeps = [] +if (runItsProfile) { + rawDeps = runItsProfile.depthFirst().findAll { + it.name() == "plugin" && it.artifactId.text() == "maven-invoker-plugin" + }*.dependencies*.dependency.flatten() +} + +// Build minimal POM +def newPom = { + mkp.xmlDeclaration() + project(xmlns: "http://maven.apache.org/POM/4.0.0", + 'xmlns:xsi': "http://www.w3.org/2001/XMLSchema-instance", + 'xsi:schemaLocation': "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd") { + modelVersion("4.0.0") + groupId("temp") + artifactId("invoker-plugin-deps") + version("1.0-SNAPSHOT") + dependencies { + rawDeps.each { dep -> + dependency { + groupId(dep.groupId.text()) + artifactId(dep.artifactId.text()) + version(dep.version.text()) + if (dep.scope) scope(dep.scope.text()) + } + } + } + } +} + +// Write temp POM +tmpPom.text = new StreamingMarkupBuilder().bind(newPom).toString() + +//println "Downloading dependencies..." +def proc = ["mvn", "-q", "-f", tmpPom.absolutePath, + "dependency:copy-dependencies", + "-DoutputDirectory=${depsDir.absolutePath}", + "-DincludeScope=runtime"].execute() +proc.in.eachLine { println it } +proc.err.eachLine { System.err.println it } +proc.waitFor() + +// Build classpath +def jars = depsDir.listFiles().findAll { it.name.endsWith(".jar") } +def classpath = jars.collect { it.absolutePath }.join(":") +println classpath \ No newline at end of file diff --git a/src/it/central-deploy-simplebundle/README.md b/src/it/central-deploy-simplebundle/README.md index 60ac684..51e16eb 100644 --- a/src/it/central-deploy-simplebundle/README.md +++ b/src/it/central-deploy-simplebundle/README.md @@ -14,20 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. --> -# maven-central-publishing-example +# central-deploy-simplebundle -This is a multimodule example showing deployment to maven central +This is a basic example showing deployment to maven central using the new central publishing rest api. -The maven-deploy-plugin used is a [modified fork](https://github.com/perNyfelt/maven-deploy-plugin/tree/add_central_support) of the apache maven deploy plugin. The project consist of -- an aggregator -- a common sub module -- two sub modules that each depends on the common submodule +- a main pom that produces a jar, javadoc and sources -Each module (including the main aggregator) will be deployed separately. -I.e. when deploying the whole project, 1 zip file will be created and uploaded to central. +When deploying the whole project, 1 zip file will be created and uploaded to central. ## Running only this test ```shell @@ -36,7 +32,12 @@ mvn -Prun-its verify -Dinvoker.test=central-deploy-simplebundle ## Running the test manually ```shell +# copy resources +mvn -Prun-its verify -Dinvoker.test=central-deploy-simplebundle +cd target/it/central-deploy-bundles CLASSPATH=$(find "$MAVEN_HOME/lib" -name "*.jar" | tr '\n' ':' | sed 's/:$//') -mvn verify deploy:bundle +CLASSPATH=$CLASSPATH:$(./addDependencies.groovy) +groovy -cp $CLASSPATH -Dbasedir=$PWD setup.groovy & +mvn --settings ../../../src/it/settings.xml deploy groovy -cp $CLASSPATH -Dbasedir=$PWD verify.groovy ``` \ No newline at end of file diff --git a/src/it/central-deploy-simplebundle/addDependencies.groovy b/src/it/central-deploy-simplebundle/addDependencies.groovy new file mode 100755 index 0000000..06fa39d --- /dev/null +++ b/src/it/central-deploy-simplebundle/addDependencies.groovy @@ -0,0 +1,87 @@ +#!/usr/bin/env groovy +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * http://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. + */ +// This script creates a classpath string based on the dependencies for the invoker plugin +// It is used when you want to change the groovy scripts and have more control from the command line +// i.e allows you to run each step manually. See readme.md for details. +import groovy.xml.XmlParser +import groovy.xml.XmlNodePrinter +import groovy.xml.StreamingMarkupBuilder + +// Paths +def realPom = new File("../../../pom.xml") +def tmpDir = File.createTempFile("tmp", "") +tmpDir.delete() +tmpDir.mkdirs() +def tmpPom = new File(tmpDir, "pom.xml") +def depsDir = new File(tmpDir, "deps") +depsDir.mkdirs() + +// Parse original POM without namespaces +def parser = new XmlParser(false, false) // disable namespaces +def pom = parser.parse(realPom) + +// Locate dependencies inside maven-invoker-plugin / run-its profile +def runItsProfile = pom.profiles.profile.find { it.id.text() == "run-its" } +def rawDeps = [] +if (runItsProfile) { + rawDeps = runItsProfile.depthFirst().findAll { + it.name() == "plugin" && it.artifactId.text() == "maven-invoker-plugin" + }*.dependencies*.dependency.flatten() +} + +// Build minimal POM +def newPom = { + mkp.xmlDeclaration() + project(xmlns: "http://maven.apache.org/POM/4.0.0", + 'xmlns:xsi': "http://www.w3.org/2001/XMLSchema-instance", + 'xsi:schemaLocation': "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd") { + modelVersion("4.0.0") + groupId("temp") + artifactId("invoker-plugin-deps") + version("1.0-SNAPSHOT") + dependencies { + rawDeps.each { dep -> + dependency { + groupId(dep.groupId.text()) + artifactId(dep.artifactId.text()) + version(dep.version.text()) + if (dep.scope) scope(dep.scope.text()) + } + } + } + } +} + +// Write temp POM +tmpPom.text = new StreamingMarkupBuilder().bind(newPom).toString() + +//println "Downloading dependencies..." +def proc = ["mvn", "-q", "-f", tmpPom.absolutePath, + "dependency:copy-dependencies", + "-DoutputDirectory=${depsDir.absolutePath}", + "-DincludeScope=runtime"].execute() +proc.in.eachLine { println it } +proc.err.eachLine { System.err.println it } +proc.waitFor() + +// Build classpath +def jars = depsDir.listFiles().findAll { it.name.endsWith(".jar") } +def classpath = jars.collect { it.absolutePath }.join(":") +println classpath \ No newline at end of file diff --git a/src/it/central-zip-bundles/README.md b/src/it/central-zip-bundles/README.md index d5dda02..3f10e5e 100644 --- a/src/it/central-zip-bundles/README.md +++ b/src/it/central-zip-bundles/README.md @@ -14,13 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. --> -# maven-central-publishing-example +# central-zip-bundles -This is a multimodule example showing deployment to maven central +This is a multimodule example showing the creation of the zip bundle (for deployment to maven central) using the new central publishing rest api. -The maven-deploy-plugin used is a [modified fork](https://github.com/perNyfelt/maven-deploy-plugin/tree/add_central_support) of the apache maven deploy plugin. - The project consist of - an aggregator - a common sub module @@ -50,10 +48,10 @@ Note, normally we would have the gpg plugin configured in the build, e.g: ``` -But this requires some external setup so we just create fake asc files in this test. -We cannot do it in setup.groovy as the invoker plugin uses true so -instead we mimic what the sign plugin would do in a groovy script (fakeSign.groovy) that we -call from the pom (and hence it is part of the build, which is also closer to reality). +But this requires some external setup so we just create fake asc files in this test using a +a groovy script. We cannot do it in setup.groovy as the invoker plugin uses true +config, so instead we mimic what the sign plugin would do in another groovy script (fakeSign.groovy) +that we call from the pom (and hence it is part of the build, which is also closer to reality). ## Running only this test ```shell @@ -63,7 +61,7 @@ mvn -Prun-its verify -Dinvoker.test=central-zip-bundles ## Running the test manually ```shell CLASSPATH=$(find "$MAVEN_HOME/lib" -name "*.jar" | tr '\n' ':' | sed 's/:$//') -mvn verify deploy:bundle +mvn deploy groovy -cp $CLASSPATH -Dbasedir=$PWD verify.groovy ``` diff --git a/src/it/central-zip-megabundle/README.md b/src/it/central-zip-megabundle/README.md index fbd94e3..b46e26e 100644 --- a/src/it/central-zip-megabundle/README.md +++ b/src/it/central-zip-megabundle/README.md @@ -42,6 +42,6 @@ mvn -Prun-its verify -Dinvoker.test=central-zip-megabundle ## Running the test manually ```shell CLASSPATH=$(find "$MAVEN_HOME/lib" -name "*.jar" | tr '\n' ':' | sed 's/:$//') -mvn verify deploy:bundle +mvn deploy groovy -cp $CLASSPATH -Dbasedir=$PWD verify.groovy ``` \ No newline at end of file diff --git a/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java b/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java index 59ab366..79ebfd6 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java +++ b/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java @@ -500,6 +500,8 @@ protected File createBundle(List allProjectsUsingPlugin) throws Mo Bundler bundler = new Bundler(rootProject, getLog()); bundler.createZipBundle(bundleFile, allProjectsUsingPlugin); getLog().info("Bundle created successfully: " + bundleFile); + } catch (MojoExecutionException e) { + throw e; } catch (Exception e) { throw new MojoExecutionException("Failed to create bundle", e); } diff --git a/src/test/java/org/apache/maven/plugins/deploy/CentralDeployTest.java b/src/test/java/org/apache/maven/plugins/deploy/CentralDeployTest.java index dda363b..e9c2f09 100644 --- a/src/test/java/org/apache/maven/plugins/deploy/CentralDeployTest.java +++ b/src/test/java/org/apache/maven/plugins/deploy/CentralDeployTest.java @@ -26,6 +26,7 @@ import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.zip.ZipFile; @@ -148,7 +149,7 @@ private void sunnyDayTest(String bundleName, boolean autoDeploy, boolean deployA setVariableValueToObject(mojo, "deployAtEnd", deployAtEnd); centralPortalClient = mock(CentralPortalClient.class); - String fakeDeploymentId = "deployment-123"; + String fakeDeploymentId = "deployment-" + subDirName; when(centralPortalClient.upload(any(File.class), anyBoolean())).thenReturn(fakeDeploymentId); String status = autoDeploy ? "PUBLISHING" : "VALIDATED"; when(centralPortalClient.getStatus(fakeDeploymentId)).thenReturn(status); @@ -176,14 +177,14 @@ private void sunnyDayTest(String bundleName, boolean autoDeploy, boolean deployA } // (5) Negative test: missing .asc files should fail - public void testCentralPortalFailsIfSignatureMissing() throws Exception { + public void testRainySignatureMissing() throws Exception { setVariableValueToObject(mojo, "useCentralPortalApi", true); setVariableValueToObject(mojo, "autoDeploy", true); setVariableValueToObject(mojo, "uploadToCentral", true); setVariableValueToObject(mojo, "deployAtEnd", true); centralPortalClient = mock(CentralPortalClient.class); - String fakeDeploymentId = "deployment-123"; + String fakeDeploymentId = "deployment-5"; when(centralPortalClient.upload(any(File.class), anyBoolean())).thenReturn(fakeDeploymentId); when(centralPortalClient.getStatus(fakeDeploymentId)).thenReturn("PUBLISHING"); when(centralPortalClient.getPublishUrl()).thenReturn(SERVER_URL); @@ -200,12 +201,55 @@ public void testCentralPortalFailsIfSignatureMissing() throws Exception { File targetSubDir = new File(getBasedir(), project.getBuild().getDirectory() + "/central-deploy-test-5"); project.getBuild().setDirectory(targetSubDir.getAbsolutePath()); createAndAttachFakeSignedArtifacts(targetSubDir); + // Remove the pom sign file new File(targetSubDir, ARTIFACT_ID + "-" + VERSION + ".pom.asc").delete(); MojoExecutionException thrown = assertThrows(MojoExecutionException.class, () -> mojo.execute()); assertTrue( - "Expected MojoExecutionException to be Failed to create bundle but was " + thrown.toString(), - thrown.getMessage().contains("Failed to create bundle")); + "Expected MojoExecutionException to be 'POM file [pomFile] is not signed, [pomFile].asc is missing' but was " + + thrown.toString(), + thrown.getMessage().contains("pom is not signed,") + && thrown.getMessage().contains(".asc is missing")); + } + + // (6) Negative test: missing javadoc files should fail + public void testRainyJavadocMissing() throws Exception { + setVariableValueToObject(mojo, "useCentralPortalApi", true); + setVariableValueToObject(mojo, "autoDeploy", false); + setVariableValueToObject(mojo, "uploadToCentral", false); + setVariableValueToObject(mojo, "deployAtEnd", false); + + centralPortalClient = mock(CentralPortalClient.class); + String fakeDeploymentId = "deployment-6"; + when(centralPortalClient.upload(any(File.class), anyBoolean())).thenReturn(fakeDeploymentId); + when(centralPortalClient.getStatus(fakeDeploymentId)).thenReturn("PUBLISHING"); + when(centralPortalClient.getPublishUrl()).thenReturn(SERVER_URL); + setVariableValueToObject(mojo, "centralPortalClient", centralPortalClient); + + Artifact projectArtifact = createProjectArtifact(); + + project.setArtifact(projectArtifact); + project.setGroupId(GROUP_ID); + project.setArtifactId(ARTIFACT_ID); + project.setVersion(VERSION); + + // create a subdir under target to isolate tests from each other + File targetSubDir = new File(getBasedir(), project.getBuild().getDirectory() + "/central-deploy-test-6"); + project.getBuild().setDirectory(targetSubDir.getAbsolutePath()); + createAndAttachFakeSignedArtifacts(targetSubDir); + // Remove the javadoc files + for (File file : Objects.requireNonNull(targetSubDir.listFiles())) { + if (file.getName().contains("-javadoc.")) { + assertTrue(file.delete()); + } + } + + MojoExecutionException thrown = assertThrows(MojoExecutionException.class, () -> mojo.execute()); + assertTrue( + "Expected MojoExecutionException to be 'File [javadocFile] is not signed, [javadocFile].asc is missing' but was " + + thrown.toString(), + thrown.getMessage().contains(" is not signed,") + && thrown.getMessage().contains(".asc is missing")); } private Artifact createProjectArtifact() { From 4908518c3e236ebe671252ca758004b7e3307bf3 Mon Sep 17 00:00:00 2001 From: pernyf Date: Fri, 8 Aug 2025 14:38:40 +0200 Subject: [PATCH 24/25] Reduce verbosity of output. --- .../java/org/apache/maven/plugins/deploy/DeployMojo.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java b/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java index 79ebfd6..1f0abd1 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java +++ b/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java @@ -204,8 +204,6 @@ private enum State { private CentralPortalClient centralPortalClient = new CentralPortalClient(); private void putState(State state) { - getLog().info("putState: pluginContext@" + getPluginContext().hashCode() + " putting " + DEPLOY_PROCESSED_MARKER - + "=" + state.name()); getPluginContext().put(DEPLOY_PROCESSED_MARKER, state.name()); } @@ -265,7 +263,6 @@ public void execute() throws MojoExecutionException, MojoFailureException { } } - getLog().info("Setting state to " + state.name() + " for " + project.getArtifactId()); putState(state); List allProjectsUsingPlugin = getAllProjectsUsingPlugin(); @@ -318,11 +315,9 @@ private void deployAllAtOnce(List allProjectsUsingPlugin) throws M private boolean allProjectsMarked(List allProjectsUsingPlugin) { for (MavenProject reactorProject : allProjectsUsingPlugin) { if (!hasState(reactorProject)) { - getLog().info(reactorProject.getArtifactId() + " not marked for deploy"); return false; } } - getLog().info("All projects marked for deploy"); return true; } @@ -331,7 +326,6 @@ private List getAllProjectsUsingPlugin() { for (MavenProject reactorProject : reactorProjects) { if (hasExecution(reactorProject.getPlugin("org.apache.maven.plugins:maven-deploy-plugin"))) { result.add(reactorProject); - getLog().info(reactorProject.getArtifactId() + " added to All projects using plugin"); } } return result; @@ -531,7 +525,7 @@ protected void deployBundle(Set repos, File zipBundle) throws String password = credentials[1]; String deployUrl = repo.getUrl(); centralPortalClient.setVariables(username, password, deployUrl, getLog()); - getLog().info("Deploying " + zipBundle + " to " + repo.getId() + " at " + getLog().info("Deploying " + zipBundle.getName() + " to " + repo.getId() + " at " + centralPortalClient.getPublishUrl()); centralPortalClient.uploadAndCheck(zipBundle, autoDeploy); } From 0e5a1544cc2ac7bfa5d49a4b4cb6607bbf917bfb Mon Sep 17 00:00:00 2001 From: pernyf Date: Fri, 8 Aug 2025 16:23:57 +0200 Subject: [PATCH 25/25] Use ChecksumUtils to generate checksum instead of rolling our own. --- .../apache/maven/plugins/deploy/Bundler.java | 44 ++++++------------- 1 file changed, 13 insertions(+), 31 deletions(-) diff --git a/src/main/java/org/apache/maven/plugins/deploy/Bundler.java b/src/main/java/org/apache/maven/plugins/deploy/Bundler.java index 5bd17de..693215a 100644 --- a/src/main/java/org/apache/maven/plugins/deploy/Bundler.java +++ b/src/main/java/org/apache/maven/plugins/deploy/Bundler.java @@ -45,6 +45,7 @@ import org.apache.maven.plugin.logging.Log; import org.apache.maven.project.MavenProject; import org.codehaus.plexus.util.xml.pull.XmlPullParserException; +import org.eclipse.aether.util.ChecksumUtils; public class Bundler { @@ -200,43 +201,24 @@ private void addToZip(File file, String prefix, ZipOutputStream zipOut) throws I private void generateChecksumsAndAddToZip(File sourceFile, String prefix, ZipOutputStream zipOut) throws NoSuchAlgorithmException, IOException { - for (String algo : CHECKSUM_ALGOS) { - File checksumFile = generateChecksum(sourceFile, algo); + for (File checksumFile : generateChecksums(sourceFile, CHECKSUM_ALGOS)) { addToZip(checksumFile, prefix, zipOut); } } - public File generateChecksum(File file, String algo) throws NoSuchAlgorithmException, IOException { - String extension = algo.toLowerCase().replace("-", ""); - File checksumFile = new File(file.getAbsolutePath() + "." + extension); - // It might have been generated externally. In that case, use that. - if (checksumFile.exists()) { - return checksumFile; - } - - // Create the checksum file - MessageDigest digest = MessageDigest.getInstance(algo); - try (InputStream is = Files.newInputStream(file.toPath()); - OutputStream nullOut = new OutputStream() { - @Override - public void write(int b) {} - }; - DigestOutputStream dos = new DigestOutputStream(nullOut, digest)) { - - byte[] buffer = new byte[8192]; - int bytesRead; - while ((bytesRead = is.read(buffer)) != -1) { - dos.write(buffer, 0, bytesRead); + public List generateChecksums(File file, List algos) throws IOException { + Map results = ChecksumUtils.calc(file, algos); + List checkSumFiles = new ArrayList<>(); + for (Map.Entry entry : results.entrySet()) { + String extension = entry.getKey().toLowerCase().replace("-", ""); + File checksumFile = new File(file.getAbsolutePath() + "." + extension); + log.info("generateChecksums: " + extension); + if (!checksumFile.exists()) { + Files.write(checksumFile.toPath(), entry.getValue().toString().getBytes(StandardCharsets.UTF_8)); } + checkSumFiles.add(checksumFile); } - - StringBuilder sb = new StringBuilder(); - for (byte b : digest.digest()) { - sb.append(String.format("%02x", b)); - } - - Files.write(checksumFile.toPath(), sb.toString().getBytes(StandardCharsets.UTF_8)); - return checksumFile; + return checkSumFiles; } Model readPomFile(File pomFile) throws MojoExecutionException {