From d0742e12259a415595dd34755b856f431e7ed6e5 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 19 Jun 2024 11:33:20 +0200 Subject: [PATCH] Prepare Maven Publisher API workflow. We need to stage the artifacts in a local repository (done), zip it and prepare it (done) so for uploading to the publishing endpoint (not yet done). See #86 --- .../data/release/build/BuildOperations.java | 2 +- .../data/release/build/BuildSystem.java | 4 +- .../data/release/build/MavenBuildSystem.java | 150 +++++++++++++++--- .../data/release/build/MavenProperties.java | 14 ++ .../data/release/cli/ReleaseCommands.java | 3 + .../deployment/DeploymentProperties.java | 6 + .../release/deployment/StagingRepository.java | 20 ++- src/main/resources/application.properties | 1 + 8 files changed, 173 insertions(+), 27 deletions(-) diff --git a/src/main/java/org/springframework/data/release/build/BuildOperations.java b/src/main/java/org/springframework/data/release/build/BuildOperations.java index aac318a..79cca52 100644 --- a/src/main/java/org/springframework/data/release/build/BuildOperations.java +++ b/src/main/java/org/springframework/data/release/build/BuildOperations.java @@ -253,7 +253,7 @@ public class BuildOperations { ? openStagingRepository(iteration) // : StagingRepository.EMPTY; - BuildExecutor.Summary summary = executor.doWithBuildSystemOrdered(iteration, + BuildExecutor.Summary summary = executor.doWithBuildSystemOrdered(iteration.filter(it -> it.getProject() == BUILD || it.getProject() == COMMONS), (buildSystem, moduleIteration) -> buildSystem.deploy(moduleIteration, stagingRepository)); Train train = iteration.getTrain(); diff --git a/src/main/java/org/springframework/data/release/build/BuildSystem.java b/src/main/java/org/springframework/data/release/build/BuildSystem.java index e1039a5..877807b 100644 --- a/src/main/java/org/springframework/data/release/build/BuildSystem.java +++ b/src/main/java/org/springframework/data/release/build/BuildSystem.java @@ -20,8 +20,8 @@ import org.springframework.data.release.deployment.StagingRepository; import org.springframework.data.release.model.JavaVersion; import org.springframework.data.release.model.ModuleIteration; import org.springframework.data.release.model.Phase; -import org.springframework.data.release.model.SupportedProject; import org.springframework.data.release.model.ProjectAware; +import org.springframework.data.release.model.SupportedProject; import org.springframework.data.release.model.Train; import org.springframework.data.release.model.TrainIteration; import org.springframework.plugin.core.Plugin; @@ -33,7 +33,7 @@ import org.springframework.plugin.core.Plugin; * @author Mark Paluch * @author Greg Turnquist */ -interface BuildSystem extends Plugin { +public interface BuildSystem extends Plugin { /** * Updates the project descriptors for the given {@link ModuleIteration} using the given {@link UpdateInformation}. diff --git a/src/main/java/org/springframework/data/release/build/MavenBuildSystem.java b/src/main/java/org/springframework/data/release/build/MavenBuildSystem.java index 5fb27ff..19a1e43 100644 --- a/src/main/java/org/springframework/data/release/build/MavenBuildSystem.java +++ b/src/main/java/org/springframework/data/release/build/MavenBuildSystem.java @@ -32,14 +32,18 @@ import java.io.InputStream; import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.UUID; import java.util.function.Consumer; import java.util.regex.Pattern; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; import javax.xml.transform.TransformerException; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.apache.commons.io.IOUtils; + import org.springframework.core.annotation.Order; import org.springframework.core.env.Environment; import org.springframework.data.release.build.CommandLine.Argument; @@ -58,6 +62,7 @@ import org.springframework.stereotype.Component; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; + import org.xmlbeam.ProjectionFactory; import org.xmlbeam.XBProjector; import org.xmlbeam.dom.DOMAccess; @@ -80,18 +85,19 @@ class MavenBuildSystem implements BuildSystem { ProjectionFactory projectionFactory; Logger logger; MavenRuntime mvn; + MavenProperties mavenProperties; DeploymentProperties properties; Gpg gpg; - Environment env; + DeploymentProperties deploymentProperties; static final String REPO_OPENING_TAG = ""; static final String REPO_CLOSING_TAG = ""; @Override public BuildSystem withJavaVersion(JavaVersion javaVersion) { - return new MavenBuildSystem(workspace, projectionFactory, logger, mvn.withJavaVersion(javaVersion), properties, gpg, - env); + return new MavenBuildSystem(workspace, projectionFactory, logger, mvn.withJavaVersion(javaVersion), mavenProperties, + properties, gpg, env, deploymentProperties); } /* @@ -186,14 +192,30 @@ class MavenBuildSystem implements BuildSystem { @Override public StagingRepository open(Train train) { - Assert.notNull(properties.getMavenCentral(), "Maven Central properties must not be null"); - Assert.hasText(properties.getMavenCentral().getStagingProfileId(), "Staging Profile Identifier must not be empty"); + MavenCentral properties = this.properties.getMavenCentral(); + Assert.notNull(properties, "Maven Central properties must not be null"); + Assert.hasText(properties.getStagingProfileId(), "Staging Profile Identifier must not be empty"); + + if (properties.getProcess() == MavenCentral.Publishing.OSSRH) { + return openRemoteStagingRepository(train, properties); + } + + String instance = UUID.randomUUID().toString(); + + File stagingRoot = new File(mavenProperties.getLocalStaging(), instance); + stagingRoot.mkdirs(); + + return StagingRepository.ofFile(stagingRoot.getPath()); + } + + private StagingRepository openRemoteStagingRepository(Train train, MavenCentral properties) { CommandLine arguments = CommandLine.of(goal("nexus-staging:rc-open"), // profile("central"), // - arg("stagingProfileId").withValue(properties.getMavenCentral().getStagingProfileId()), // + arg("stagingProfileId").withValue(properties.getStagingProfileId()), // arg("openedRepositoryMessageFormat").withValue("'" + REPO_OPENING_TAG + "%s" + REPO_CLOSING_TAG + "'")) - .andIf(!ObjectUtils.isEmpty(properties.getSettingsXml()), () -> settingsXml(properties.getSettingsXml())); + .andIf(!ObjectUtils.isEmpty(this.properties.getSettingsXml()), + () -> settingsXml(this.properties.getSettingsXml())); MavenRuntime.MavenInvocationResult invocationResult = mvn.execute(train.getSupportedProject(BUILD), arguments); @@ -221,12 +243,16 @@ class MavenBuildSystem implements BuildSystem { Assert.notNull(stagingRepository, "StagingRepository must not be null"); Assert.isTrue(stagingRepository.isPresent(), "StagingRepository must be present"); - CommandLine arguments = CommandLine.of(goal("nexus-staging:rc-close"), // - profile("central"), // - arg("stagingRepositoryId").withValue(stagingRepository.getId())) - .andIf(!ObjectUtils.isEmpty(properties.getSettingsXml()), () -> settingsXml(properties.getSettingsXml())); + if (deploymentProperties.getMavenCentral().getProcess() == MavenCentral.Publishing.OSSRH + && stagingRepository.isRemote()) { - mvn.execute(train.getSupportedProject(BUILD), arguments); + CommandLine arguments = CommandLine.of(goal("nexus-staging:rc-close"), // + profile("central"), // + arg("stagingRepositoryId").withValue(stagingRepository.getId())) + .andIf(!ObjectUtils.isEmpty(properties.getSettingsXml()), () -> settingsXml(properties.getSettingsXml())); + + mvn.execute(train.getSupportedProject(BUILD), arguments); + } } /* @@ -327,7 +353,8 @@ class MavenBuildSystem implements BuildSystem { arg("gpg.keyname").withValue(gpg.getKeyname()), // arg("gpg.passphrase").withValue(gpg.getPassphrase())) // .andIf(!ObjectUtils.isEmpty(properties.getSettingsXml()), settingsXml(properties.getSettingsXml())) - .andIf(StringUtils.hasText(information.getProject()), () -> arg("artifactory.project").withValue(information.getProject())); + .andIf(StringUtils.hasText(information.getProject()), + () -> arg("artifactory.project").withValue(information.getProject())); mvn.execute(module.getSupportedProject(), arguments); } @@ -353,6 +380,7 @@ class MavenBuildSystem implements BuildSystem { Gpg gpg = getGpg(); + StagingRepository stagingRepository = deploymentInformation.getStagingRepositoryId(); CommandLine arguments = CommandLine.of(Goal.CLEAN, Goal.DEPLOY, // profile("ci,release,central"), // SKIP_TESTS, // @@ -360,8 +388,10 @@ class MavenBuildSystem implements BuildSystem { arg("gpg.keyname").withValue(gpg.getKeyname()), // arg("gpg.passphrase").withValue(gpg.getPassphrase())) // .andIf(!ObjectUtils.isEmpty(properties.getSettingsXml()), settingsXml(properties.getSettingsXml())) - .andIf(deploymentInformation.getStagingRepositoryId().isPresent(), - () -> arg("stagingRepositoryId").withValue(deploymentInformation.getStagingRepositoryId())) + .andIf(stagingRepository.isPresent() && stagingRepository.isRemote(), + () -> arg("stagingRepositoryId").withValue(stagingRepository.getId())) + .andIf(stagingRepository.isPresent() && stagingRepository.isFile(), + () -> arg("altDeploymentRepository").withValue("staging::default::file:" + stagingRepository.getId())) .andIf(gpg.hasSecretKeyring(), () -> arg("gpg.secretKeyring").withValue(gpg.getSecretKeyring())); mvn.execute(module.getSupportedProject(), arguments); @@ -385,16 +415,17 @@ class MavenBuildSystem implements BuildSystem { doWithProjection(workspace.getFile(POM_XML, smokeTests), pom -> { Version version = module.getVersion(); - String targetBootVersion = version.getMajor() == 2 ? "2.7.8" : "3.0.2"; + String targetBootVersion = version.getMajor() == 2 ? "2.7.8" : "3.2.2"; pom.setParentVersion(ArtifactVersion.of(Version.parse(targetBootVersion), true)); }); CommandLine arguments = CommandLine.of(Goal.CLEAN, VERIFY, // profile(profile), // - arg("s").withValue("settings.xml"), // + settingsXml("settings.xml"), // arg("spring-data-bom.version").withValue(iteration.getReleaseTrainNameAndVersion())) // - .andIf(mavenCentral, arg("stagingRepository").withValue(stagingRepository.getId())); + .andIf(mavenCentral && stagingRepository.isPresent() && stagingRepository.isRemote(), + arg("stagingRepository").withValue(stagingRepository.getId())); mvn.execute(smokeTests, arguments); @@ -410,6 +441,84 @@ class MavenBuildSystem implements BuildSystem { Assert.notNull(stagingRepository, "StagingRepository must not be null"); Assert.isTrue(stagingRepository.isPresent(), "StagingRepository must be present"); + MavenCentral.Publishing process = deploymentProperties.getMavenCentral().getProcess(); + + if (process == MavenCentral.Publishing.OSSRH && stagingRepository.isRemote()) { + releaseRemoteRepository(train, stagingRepository); + return; + } + + if (process == MavenCentral.Publishing.PUBLISHER && stagingRepository.isFile()) { + publishRelease(stagingRepository); + return; + } + + throw new UnsupportedOperationException( + String.format("Cannot release train using %s and staging repository %s", process, stagingRepository)); + } + + private void publishRelease(StagingRepository stagingRepository) { + + File root = new File(mavenProperties.getLocalStaging(), stagingRepository.getId()); + + Assert.isTrue(root.exists(), "StagingRepository " + root + " does not exist"); + + try { + File releaseBundle = createReleaseBundle(root); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private File createReleaseBundle(File root) throws IOException { + + String deploymentIdentifier = root.getName(); + String deploymentBundle = deploymentIdentifier + ".zip"; + + File zip = new File(root, deploymentBundle); + File[] files = root.listFiles(); + + Assert.notEmpty(files, "StagingRepository " + root + " is empty"); + + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zip))) { + + // consider only directories on the root level. + for (File file : files) { + if (file.isDirectory()) { + zipRecursively("", file, zos); + } + } + } + + return zip; + } + + private void zipRecursively(String prefix, File file, ZipOutputStream zos) throws IOException { + + String prefixToUse = (StringUtils.hasText(prefix) ? prefix + "/" : ""); + if (file.isDirectory()) { + + File[] files = file + .listFiles((dir, name) -> !name.startsWith(".") && !name.startsWith("_") && !name.endsWith(".lastUpdated")); + if (!ObjectUtils.isEmpty(files)) { + + for (File nested : files) { + zipRecursively(prefixToUse + file.getName(), nested, zos); + } + } + + return; + } + + ZipEntry entry = new ZipEntry(prefixToUse + file.getName()); + zos.putNextEntry(entry); + + FileInputStream in = new FileInputStream(file); + IOUtils.copy(in, zos); + IOUtils.closeQuietly(in); + } + + private void releaseRemoteRepository(Train train, StagingRepository stagingRepository) { CommandLine arguments = CommandLine.of(goal("nexus-staging:rc-release"), // profile("central"), // arg("stagingRepositoryId").withValue(stagingRepository.getId())) @@ -584,9 +693,9 @@ class MavenBuildSystem implements BuildSystem { @Override public void verifyStagingAuthentication(Train train) { - if (train.isOpenSource()) { + if (train.isOpenSource() && properties.getMavenCentral().getProcess() == MavenCentral.Publishing.OSSRH) { - logger.log(BUILD, "Verifying Maven Staging Authentication…"); + logger.log(BUILD, "Verifying Maven OSSRH Staging Authentication…"); mvn.execute(train.getSupportedProject(BUILD), // CommandLine.of(goal("nexus-staging:rc-list-profiles"), // @@ -596,7 +705,6 @@ class MavenBuildSystem implements BuildSystem { "Maven Central properties are not set (deployment.maven-central.staging-profile-id=…)"); Assert.hasText(properties.getMavenCentral().getStagingProfileId(), "Staging Profile Id is not set (deployment.maven-central.staging-profile-id=…)"); - return; } } diff --git a/src/main/java/org/springframework/data/release/build/MavenProperties.java b/src/main/java/org/springframework/data/release/build/MavenProperties.java index f1f06c2..befc0e5 100644 --- a/src/main/java/org/springframework/data/release/build/MavenProperties.java +++ b/src/main/java/org/springframework/data/release/build/MavenProperties.java @@ -41,6 +41,7 @@ public class MavenProperties { private File mavenHome; private File localRepository; + private File localStaging; private Map plugins; private boolean consoleLogger = true; private boolean parallelize = false; @@ -73,6 +74,19 @@ public class MavenProperties { } } + public void setLocalStaging(String localStaging) { + + Assert.hasText(localStaging, "Local staging must not be null!"); + + log.info("Using {} as local Maven local staging!", localStaging); + + this.localStaging = new File(localStaging.replace("~", FileUtils.getUserDirectoryPath())); + + if (!this.localStaging.exists()) { + this.localStaging.mkdirs(); + } + } + /** * Returns the fully-qualified plugin goal for the given local one. * diff --git a/src/main/java/org/springframework/data/release/cli/ReleaseCommands.java b/src/main/java/org/springframework/data/release/cli/ReleaseCommands.java index bf31345..5e1c0cb 100644 --- a/src/main/java/org/springframework/data/release/cli/ReleaseCommands.java +++ b/src/main/java/org/springframework/data/release/cli/ReleaseCommands.java @@ -27,6 +27,7 @@ import java.util.Set; import org.springframework.data.release.CliComponent; import org.springframework.data.release.TimedCommand; import org.springframework.data.release.build.BuildOperations; +import org.springframework.data.release.build.BuildSystem; import org.springframework.data.release.deployment.DeploymentInformation; import org.springframework.data.release.deployment.DeploymentOperations; import org.springframework.data.release.deployment.StagingRepository; @@ -58,6 +59,8 @@ class ReleaseCommands extends TimedCommand { @NonNull BuildOperations build; @NonNull IssueTrackerCommands tracker; @NonNull GitHubCommands gitHub; + @NonNull + BuildSystem bs; /** * Composite command to prepare a release. diff --git a/src/main/java/org/springframework/data/release/deployment/DeploymentProperties.java b/src/main/java/org/springframework/data/release/deployment/DeploymentProperties.java index eb29927..32c6e82 100644 --- a/src/main/java/org/springframework/data/release/deployment/DeploymentProperties.java +++ b/src/main/java/org/springframework/data/release/deployment/DeploymentProperties.java @@ -101,6 +101,8 @@ public class DeploymentProperties implements InitializingBean { private String stagingProfileId; + private Publishing process; + private Gpg gpg; public boolean hasGpgConfiguration() { @@ -113,6 +115,10 @@ public class DeploymentProperties implements InitializingBean { throw new IllegalArgumentException("No staging profile Id for Maven Central"); } } + + public enum Publishing { + OSSRH, PUBLISHER + } } @Data diff --git a/src/main/java/org/springframework/data/release/deployment/StagingRepository.java b/src/main/java/org/springframework/data/release/deployment/StagingRepository.java index 58136e9..de5f6d3 100644 --- a/src/main/java/org/springframework/data/release/deployment/StagingRepository.java +++ b/src/main/java/org/springframework/data/release/deployment/StagingRepository.java @@ -24,12 +24,21 @@ import org.springframework.util.ObjectUtils; * * @author Mark Paluch */ -@Value(staticConstructor = "of") +@Value public class StagingRepository { - public static final StagingRepository EMPTY = StagingRepository.of(""); + public static final StagingRepository EMPTY = new StagingRepository("", false); String id; + boolean file; + + public static StagingRepository of(String id) { + return new StagingRepository(id, false); + } + + public static StagingRepository ofFile(String id) { + return new StagingRepository(id, true); + } public boolean isEmpty() { return ObjectUtils.isEmpty(id); @@ -39,10 +48,15 @@ public class StagingRepository { return !isEmpty(); } + public boolean isRemote() { + return !isEmpty() && !isFile(); + } + @Override public String toString() { + if (isPresent()) { - return id; + return (isFile() ? "file:" : "remote:") + id; } return "(empty)"; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 7d58448..55a054a 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -4,6 +4,7 @@ io.logs=logs # Maven setup maven.local-repository=~/temp/spring-data-shell/repository +maven.local-staging=~/temp/spring-data-shell/staging maven.plugins.versions=org.codehaus.mojo:versions-maven-plugin:2.16.2 maven.console-logger=true