diff --git a/Jenkinsfile b/Jenkinsfile index 1cf46db..6e9457e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,7 +1,7 @@ def p = [:] node { - checkout scm - p = readProperties interpolate: true, file: 'ci/release.properties' + checkout scm + p = readProperties interpolate: true, file: 'ci/release.properties' } pipeline { @@ -14,9 +14,9 @@ pipeline { stages { stage('Ship It') { - when { - branch 'release' - } + when { + branch 'release' + } agent { docker { image 'springci/spring-data-release-tools:0.1' @@ -24,56 +24,56 @@ pipeline { } options { timeout(time: 4, unit: 'HOURS') } - environment { - GIT_USERNAME = credentials('spring-data-release-git-username') - GIT_AUTHOR = credentials('spring-data-release-git-author') - GIT_EMAIL = credentials('spring-data-release-git-email') - GIT_PASSWORD = credentials('spring-data-release-git-password') - GITHUB_API_URL = credentials('spring-data-release-github-api-url') - DEPLOYMENT_USERNAME = credentials('spring-data-release-deployment-username') - DEPLOYMENT_PASSWORD = credentials('spring-data-release-deployment-password') - DEPLOYMENT_API_KEY = credentials('spring-data-release-deployment-api-key') - STAGING_PROFILE_ID = credentials('spring-data-release-deployment-maven-central-staging-profile-id') - JIRA_USERNAME = credentials('spring-data-release-jira-username') - JIRA_PASSWORD = credentials('spring-data-release-jira-password') - JIRA_URL = credentials('spring-data-release-jira-url') - PASSPHRASE = credentials('spring-gpg-passphrase') - KEYRING = credentials('spring-signing-secring.gpg') - SONATYPE = credentials('oss-login') - } + environment { + GIT_USERNAME = credentials('spring-data-release-git-username') + GIT_AUTHOR = credentials('spring-data-release-git-author') + GIT_EMAIL = credentials('spring-data-release-git-email') + GIT_PASSWORD = credentials('spring-data-release-git-password') + GITHUB_API_URL = credentials('spring-data-release-github-api-url') + DEPLOYMENT_USERNAME = credentials('spring-data-release-deployment-username') + DEPLOYMENT_PASSWORD = credentials('spring-data-release-deployment-password') + DEPLOYMENT_API_KEY = credentials('spring-data-release-deployment-api-key') + STAGING_PROFILE_ID = credentials('spring-data-release-deployment-maven-central-staging-profile-id') + JIRA_USERNAME = credentials('spring-data-release-jira-username') + JIRA_PASSWORD = credentials('spring-data-release-jira-password') + JIRA_URL = credentials('spring-data-release-jira-url') + PASSPHRASE = credentials('spring-gpg-passphrase') + KEYRING = credentials('spring-signing-secring.gpg') + SONATYPE = credentials('oss-login') + } steps { script { - sh "ci/build-spring-data-release-cli.bash" - sh "ci/prepare-and-build.bash ${p['release.version']}" + sh "ci/build-spring-data-release-cli.bash" + sh "ci/prepare-and-build.bash ${p['release.version']}" - slackSend( - color: (currentBuild.currentResult == 'SUCCESS') ? 'good' : 'danger', - channel: '#spring-data-dev', - message: (currentBuild.currentResult == 'SUCCESS') - ? "`${env.BUILD_URL}` - Build and deploy passed! Conduct smoke tests then report back here." - : "`${env.BUILD_URL}` - Push and distribute failed!") + slackSend( + color: (currentBuild.currentResult == 'SUCCESS') ? 'good' : 'danger', + channel: '#spring-data-dev', + message: (currentBuild.currentResult == 'SUCCESS') + ? "`${env.BUILD_URL}` - Build and deploy passed! Conduct smoke tests then report back here." + : "`${env.BUILD_URL}` - Push and distribute failed!") - input("SMOKE TEST: Did the smoke tests for ${p['release.version']} pass?") + input("SMOKE TEST: Did the smoke tests for ${p['release.version']} pass?") - sh "ci/conclude.bash ${p['release.version']}" + sh "ci/conclude.bash ${p['release.version']}" - slackSend( - color: (currentBuild.currentResult == 'SUCCESS') ? 'good' : 'danger', - channel: '#spring-data-dev', - message: "${env.BUILD_URL} - Ready to push and distribute? Check out the logs and click on either `Proceed` or `Abort`") + slackSend( + color: (currentBuild.currentResult == 'SUCCESS') ? 'good' : 'danger', + channel: '#spring-data-dev', + message: "${env.BUILD_URL} - Ready to push and distribute? Check out the logs and click on either `Proceed` or `Abort`") - input("PUSH AND DISTRIBUTE: Ready to push and distribute ${p['release.version']}? (Can't go back after this)") + input("PUSH AND DISTRIBUTE: Ready to push and distribute ${p['release.version']}? (Can't go back after this)") - sh "ci/push-and-distribute.bash ${p['release.version']}" + sh "ci/push-and-distribute.bash ${p['release.version']}" - slackSend( - color: (currentBuild.currentResult == 'SUCCESS') ? 'good' : 'danger', - channel: '#spring-data-dev', - message: (currentBuild.currentResult == 'SUCCESS') - ? "`${env.BUILD_URL}` - Push and distribute ${p['release.version']} passed! Release the build (if needed)." - : "`${env.BUILD_URL}` - Push and distribute ${p['release.version']} failed!") - } + slackSend( + color: (currentBuild.currentResult == 'SUCCESS') ? 'good' : 'danger', + channel: '#spring-data-dev', + message: (currentBuild.currentResult == 'SUCCESS') + ? "`${env.BUILD_URL}` - Push and distribute ${p['release.version']} passed! Release the build (if needed)." + : "`${env.BUILD_URL}` - Push and distribute ${p['release.version']} failed!") + } } } } diff --git a/Jenkinsfile-container b/Jenkinsfile-container index e9c5b85..c0ae9f5 100644 --- a/Jenkinsfile-container +++ b/Jenkinsfile-container @@ -7,22 +7,22 @@ pipeline { } stages { - stage('Bake the Spring Data release tools into a container') { - when { - branch 'container' - } - agent { - label 'data' - } + stage('Build the Spring Data release tools container') { + when { + branch 'container' + } + agent { + label 'data' + } - steps { - script { - def image = docker.build("springci/spring-data-release-tools:0.1", "ci") - docker.withRegistry('', 'hub.docker.com-springbuildmaster') { - image.push() - } - } - } + steps { + script { + def image = docker.build("springci/spring-data-release-tools:0.1", "ci") + docker.withRegistry('', 'hub.docker.com-springbuildmaster') { + image.push() + } + } + } } } diff --git a/application-local.template b/application-local.template index 8dfff14..568d5bb 100644 --- a/application-local.template +++ b/application-local.template @@ -21,6 +21,7 @@ maven.parallelize=true deployment.username= deployment.password= deployment.api-key= +deployment.maven-central.staging-profile-id= # GPG gpg.keyname= diff --git a/ci/prepare-and-build.template b/ci/prepare-and-build.template index 36bb090..8cf63d4 100644 --- a/ci/prepare-and-build.template +++ b/ci/prepare-and-build.template @@ -1,5 +1,3 @@ workspace cleanup release prepare ${VERSION} -repository open ${VERSION} release build ${VERSION} -repository close ${VERSION} diff --git a/src/main/java/org/springframework/data/release/build/BuildExecutor.java b/src/main/java/org/springframework/data/release/build/BuildExecutor.java index 6d0b0f4..3513f44 100644 --- a/src/main/java/org/springframework/data/release/build/BuildExecutor.java +++ b/src/main/java/org/springframework/data/release/build/BuildExecutor.java @@ -21,7 +21,12 @@ import lombok.SneakyThrows; import java.io.File; import java.io.FileInputStream; -import java.util.*; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; @@ -30,10 +35,12 @@ import java.util.function.BiFunction; import java.util.function.Supplier; import java.util.stream.Collector; import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.annotation.PreDestroy; import org.apache.commons.io.IOUtils; + import org.springframework.data.release.infra.InfrastructureOperations; import org.springframework.data.release.io.Workspace; import org.springframework.data.release.model.JavaVersion; @@ -100,13 +107,6 @@ class BuildExecutor { skip.forEach(it -> results.put(it, CompletableFuture.completedFuture(null))); - for (M moduleIteration : iteration) { - - if (skip.contains(moduleIteration.getProject())) { - continue; - } - } - for (M moduleIteration : iteration) { if (skip.contains(moduleIteration.getProject())) { 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 9ec4d05..64c447a 100644 --- a/src/main/java/org/springframework/data/release/build/BuildOperations.java +++ b/src/main/java/org/springframework/data/release/build/BuildOperations.java @@ -25,8 +25,16 @@ import java.util.function.Supplier; import java.util.stream.Collectors; import org.assertj.core.util.VisibleForTesting; + import org.springframework.data.release.deployment.DeploymentInformation; -import org.springframework.data.release.model.*; +import org.springframework.data.release.deployment.StagingRepository; +import org.springframework.data.release.model.Module; +import org.springframework.data.release.model.ModuleIteration; +import org.springframework.data.release.model.Phase; +import org.springframework.data.release.model.Project; +import org.springframework.data.release.model.Projects; +import org.springframework.data.release.model.Train; +import org.springframework.data.release.model.TrainIteration; import org.springframework.data.release.utils.Logger; import org.springframework.plugin.core.PluginRegistry; import org.springframework.stereotype.Component; @@ -112,8 +120,18 @@ public class BuildOperations { */ public List performRelease(TrainIteration iteration) { + ModuleIteration module = iteration.getModule(Projects.BUILD); + BuildSystem orchestrator = buildSystems.getRequiredPluginFor(module.getProject()); + + StagingRepository stagingRepository = iteration.getIteration().isPublic() ? orchestrator.open() + : StagingRepository.EMPTY; + BuildExecutor.Summary summary = executor.doWithBuildSystemOrdered(iteration, - (buildSystem, moduleIteration) -> performRelease(moduleIteration)); + (buildSystem, moduleIteration) -> buildSystem.deploy(moduleIteration, stagingRepository)); + + if (stagingRepository.isPresent()) { + orchestrator.close(stagingRepository); + } logger.log(iteration, "Release: %s", summary); @@ -175,16 +193,28 @@ public class BuildOperations { /** * Opens a repository to stage artifacts for this {@link ModuleIteration}. * - * @param module must not be {@literal null}. + * @param iteration must not be {@literal null}. */ - public void open() { - buildSystems.getRequiredPluginFor(Projects.BUILD) // - .withJavaVersion(executor.detectJavaVersion(Projects.BUILD)).open(); + public void open(ModuleIteration iteration) { + + doWithBuildSystem(iteration, (buildSystem, moduleIteration) -> buildSystem.open()); } - public void close() { - buildSystems.getRequiredPluginFor(Projects.BUILD) // - .withJavaVersion(executor.detectJavaVersion(Projects.BUILD)).close(); + /** + * Closes a repository to stage artifacts for this {@link ModuleIteration}. + * + * @param iteration must not be {@literal null}. + * @param stagingRepository must not be {@literal null}. + */ + public void close(ModuleIteration iteration, StagingRepository stagingRepository) { + + Assert.notNull(stagingRepository, "StagingRepository must not be null"); + Assert.isTrue(stagingRepository.isPresent(), "StagingRepository must be present"); + + doWithBuildSystem(iteration, (buildSystem, moduleIteration) -> { + buildSystem.close(stagingRepository); + return null; + }); } /** 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 9cc0381..7a3f04a 100644 --- a/src/main/java/org/springframework/data/release/build/BuildSystem.java +++ b/src/main/java/org/springframework/data/release/build/BuildSystem.java @@ -16,7 +16,12 @@ package org.springframework.data.release.build; import org.springframework.data.release.deployment.DeploymentInformation; -import org.springframework.data.release.model.*; +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.Project; +import org.springframework.data.release.model.ProjectAware; import org.springframework.plugin.core.Plugin; /** @@ -47,9 +52,21 @@ interface BuildSystem extends Plugin { /** * Open a remote repository for staging artifacts. */ - void open(); + StagingRepository open(); - void close(); + /** + * Close a remote repository for staging artifacts. + */ + void close(StagingRepository stagingRepository); + + /** + * Deploy artifacts for the given {@link ModuleIteration} using {@link DeploymentInformation}. + * + * @param module must not be {@literal null}. + * @param stagingRepository must not be {@literal null}. + * @return + */ + DeploymentInformation deploy(ModuleIteration module, StagingRepository stagingRepository); /** * Deploy artifacts for the given {@link ModuleIteration} and return the {@link DeploymentInformation}. diff --git a/src/main/java/org/springframework/data/release/build/CommandLine.java b/src/main/java/org/springframework/data/release/build/CommandLine.java index 98d9909..d61af23 100644 --- a/src/main/java/org/springframework/data/release/build/CommandLine.java +++ b/src/main/java/org/springframework/data/release/build/CommandLine.java @@ -23,6 +23,7 @@ import lombok.Value; import java.util.*; import java.util.function.BooleanSupplier; import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -66,12 +67,24 @@ class CommandLine { * Returns a new {@link CommandLine} with the given {@link Argument} added in case the given {@link BooleanSupplier} * evaluates to {@literal true}. * - * @param argument must not be {@literal null}. * @param condition must not be {@literal null}. + * @param argument must not be {@literal null}. * @return */ - public CommandLine conditionalAnd(Argument argument, BooleanSupplier condition) { - return condition.getAsBoolean() ? and(argument) : this; + public CommandLine andIf(boolean condition, Argument argument) { + return condition ? and(argument) : this; + } + + /** + * Returns a new {@link CommandLine} with the given {@link Argument} added in case the given {@link BooleanSupplier} + * evaluates to {@literal true}. + * + * @param condition must not be {@literal null}. + * @param argument must not be {@literal null}. + * @return + */ + public CommandLine andIf(boolean condition, Supplier argument) { + return condition ? and(argument.get()) : this; } /** 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 b41e337..9de9775 100644 --- a/src/main/java/org/springframework/data/release/build/MavenBuildSystem.java +++ b/src/main/java/org/springframework/data/release/build/MavenBuildSystem.java @@ -16,18 +16,21 @@ package org.springframework.data.release.build; import static org.springframework.data.release.build.CommandLine.Argument.*; -import static org.springframework.data.release.model.Projects.BOM; -import static org.springframework.data.release.model.Projects.BUILD; +import static org.springframework.data.release.build.CommandLine.Goal.*; +import static org.springframework.data.release.model.Projects.*; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; -import java.io.*; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.List; import java.util.function.Consumer; import java.util.regex.Pattern; @@ -37,24 +40,34 @@ 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.core.env.Profiles; import org.springframework.data.release.build.CommandLine.Argument; import org.springframework.data.release.build.CommandLine.Goal; import org.springframework.data.release.build.Pom.Artifact; import org.springframework.data.release.deployment.DefaultDeploymentInformation; import org.springframework.data.release.deployment.DeploymentInformation; import org.springframework.data.release.deployment.DeploymentProperties; +import org.springframework.data.release.deployment.StagingRepository; import org.springframework.data.release.io.Workspace; -import org.springframework.data.release.model.*; +import org.springframework.data.release.model.ArtifactVersion; +import org.springframework.data.release.model.Gpg; +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.Project; +import org.springframework.data.release.model.ProjectAware; +import org.springframework.data.release.model.TrainIteration; import org.springframework.data.release.utils.Logger; import org.springframework.stereotype.Component; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + import org.xmlbeam.ProjectionFactory; import org.xmlbeam.XBProjector; import org.xmlbeam.dom.DOMAccess; -import org.xmlbeam.io.XBStreamInput; +import org.xmlbeam.io.StreamInput; /** * @author Oliver Gierke @@ -78,8 +91,6 @@ class MavenBuildSystem implements BuildSystem { Environment env; - static String stagingRepositoryId = null; - static final String REPO_OPENING_TAG = ""; static final String REPO_CLOSING_TAG = ""; @@ -243,7 +254,7 @@ class MavenBuildSystem implements BuildSystem { Project project = module.getProject(); UpdateInformation information = UpdateInformation.of(module.getTrainIteration(), phase); - CommandLine goals = CommandLine.of(Goal.goal("versions:set"), Goal.goal("versions:commit")); + CommandLine goals = CommandLine.of(goal("versions:set"), goal("versions:commit")); if (BOM.equals(project)) { @@ -276,54 +287,50 @@ class MavenBuildSystem implements BuildSystem { } /** - * Perform a {@literal nexus-staging:rc-open} and extract the stagingProfileId from the results. + * Perform a {@literal nexus-staging:rc-open} and extract the {@code stagingProfileId} from the results. */ @Override - public void open() { + public StagingRepository open() { - try { - CommandLine arguments = CommandLine.of(Goal.goal("nexus-staging:rc-open"), // - profile("central"), // - of("-s " + properties.getSettingsXml()), // - arg("stagingProfileId").withValue(properties.getMavenCentral().getStagingProfileId()), // - arg("openedRepositoryMessageFormat").withValue("'" + REPO_OPENING_TAG + "%s" + REPO_CLOSING_TAG + "'")); + Assert.notNull(properties.getMavenCentral(), "Maven Central properties must not be nu,,"); + Assert.hasText(properties.getMavenCentral().getStagingProfileId(), "Staging Profile Identifier must not be empty"); - mvn.execute(BUILD, arguments); + CommandLine arguments = CommandLine.of(goal("nexus-staging:rc-open"), // + profile("central"), // + arg("stagingProfileId").withValue(properties.getMavenCentral().getStagingProfileId()), // + arg("openedRepositoryMessageFormat").withValue("'" + REPO_OPENING_TAG + "%s" + REPO_CLOSING_TAG + "'")) + .andIf(!ObjectUtils.isEmpty(properties.getSettingsXml()), () -> settingsXml(properties.getSettingsXml())); - String rcOpenLogfile = "mvn-" + BUILD.getName() + "-nexus-staging.rc-open.log"; + MavenRuntime.MavenInvocationResult invocationResult = mvn.execute(BUILD, arguments); - logger.log(BUILD, "Searching " + this.workspace.getLogsDirectory().getAbsolutePath() + " for " + rcOpenLogfile); + List rcOpenLogContents = invocationResult.getLog(); - Path rcOpenLogfilePath = Paths.get(this.workspace.getLogsDirectory().getAbsolutePath(), rcOpenLogfile); - logger.log(BUILD, "The log file is at " + rcOpenLogfilePath.toAbsolutePath() + " and " - + (rcOpenLogfilePath.toFile().exists() ? " it exists!" : " it does NOT exist!")); + String stagingRepositoryId = rcOpenLogContents.stream() // + .filter(line -> line.contains(REPO_OPENING_TAG) && !line.contains("%s")) // + .reduce((first, second) -> second) // find the last entry, a.k.a. the most recent log line + .map(s -> s.substring( // + s.indexOf(REPO_OPENING_TAG) + REPO_OPENING_TAG.length(), // + s.indexOf(REPO_CLOSING_TAG))) // + .orElse(""); - List rcOpenLogContents = Files.readAllLines(rcOpenLogfilePath); + logger.log(BUILD, "Opened staging repository with Id: " + stagingRepositoryId); - stagingRepositoryId = rcOpenLogContents.stream() // - .filter(line -> line.contains(REPO_OPENING_TAG) && !line.contains("%s")) // - .reduce((first, second) -> second) // find the last entry, a.k.a. the most recent log line - .map(s -> s.substring( // - s.indexOf(REPO_OPENING_TAG) + REPO_OPENING_TAG.length(), // - s.indexOf(REPO_CLOSING_TAG))) // - .orElse(""); - - logger.log(BUILD, "We just grabbed the staging repository ID at " + stagingRepositoryId); - } catch (IOException e) { - throw new RuntimeException(e); - } + return StagingRepository.of(stagingRepositoryId); } /** * Perform a {@literal nexus-staging:rc-close}. */ @Override - public void close() { + public void close(StagingRepository stagingRepository) { - CommandLine arguments = CommandLine.of(Goal.goal("nexus-staging:rc-close"), // + 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"), // - of("-s " + properties.getSettingsXml()), // - arg("stagingRepositoryId").withValue(stagingRepositoryId)); + arg("stagingRepositoryId").withValue(stagingRepository.getId())) + .andIf(!ObjectUtils.isEmpty(properties.getSettingsXml()), () -> settingsXml(properties.getSettingsXml())); mvn.execute(BUILD, arguments); } @@ -339,13 +346,34 @@ class MavenBuildSystem implements BuildSystem { DeploymentInformation information = new DefaultDeploymentInformation(module, properties); - deployToArtifactory(module, information); - - deployToMavenCentral(module); + deploy(module, information); return information; } + @Override + public DeploymentInformation deploy(ModuleIteration module, StagingRepository stagingRepository) { + + Assert.notNull(module, "Module must not be null!"); + Assert.notNull(stagingRepository, "StagingRepository must not be null!"); + + DeploymentInformation information = new DefaultDeploymentInformation(module, properties, stagingRepository); + + deploy(module, information); + + return information; + } + + private void deploy(ModuleIteration module, DeploymentInformation information) { + + Assert.notNull(module, "Module must not be null!"); + Assert.notNull(information, "DeploymentInformation must not be null!"); + + deployToArtifactory(module, information); + + deployToMavenCentral(module, information); + } + /* * (non-Javadoc) * @see org.springframework.data.release.build.BuildSystem#triggerBuild(org.springframework.data.release.model.ModuleIteration) @@ -354,7 +382,7 @@ class MavenBuildSystem implements BuildSystem { public M triggerBuild(M module) { CommandLine arguments = CommandLine.of(Goal.CLEAN, Goal.INSTALL)// - .conditionalAnd(SKIP_TESTS, () -> module.getProject().skipTests()); + .andIf(module.getProject().skipTests(), SKIP_TESTS); mvn.execute(module.getProject(), arguments); @@ -394,13 +422,19 @@ class MavenBuildSystem implements BuildSystem { profile("central"), // SKIP_TESTS, // arg("gpg.executable").withValue(gpg.getExecutable()), // - arg("gpg.passphrase").withValue(gpg.getPassphrase()), // - arg("gpg.secretKeyring").withValue(gpg.getSecretKeyring())); + arg("gpg.keyname").withValue(gpg.getKeyname()), // + arg("gpg.passphrase").withValue(gpg.getPassphrase())) // + .andIf(gpg.hasSecretKeyring(), () -> arg("gpg.secretKeyring").withValue(gpg.getSecretKeyring())); mvn.execute(BUILD, arguments); - mvn.execute(BUILD, CommandLine.of(Goal.goal("nexus-staging:rc-list-profiles"), // + mvn.execute(BUILD, CommandLine.of(goal("nexus-staging:rc-list-profiles"), // profile("central"))); + + Assert.notNull(properties.getMavenCentral(), + "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=…)"); } /** @@ -439,10 +473,12 @@ class MavenBuildSystem implements BuildSystem { * that has to be publicly released. * * @param module must not be {@literal null}. + * @param deploymentInformation must not be {@literal null}. */ - private void deployToMavenCentral(ModuleIteration module) { + private void deployToMavenCentral(ModuleIteration module, DeploymentInformation deploymentInformation) { Assert.notNull(module, "Module iteration must not be null!"); + Assert.notNull(deploymentInformation, "DeploymentInformation iteration must not be null!"); if (!module.getIteration().isPublic()) { @@ -455,13 +491,13 @@ class MavenBuildSystem implements BuildSystem { CommandLine arguments = CommandLine.of(Goal.CLEAN, Goal.DEPLOY, // profile("ci,release,central"), // SKIP_TESTS, // - settingsXml(properties.getSettingsXml()), // arg("gpg.executable").withValue(gpg.getExecutable()), // arg("gpg.keyname").withValue(gpg.getKeyname()), // - arg("gpg.passphrase").withValue(gpg.getPassphrase()), // - arg("stagingRepositoryId").withValue(stagingRepositoryId)) // - .conditionalAnd(arg("gpg.secretKeyring").withValue(gpg.getSecretKeyring()), - () -> env.acceptsProfiles(Profiles.of("jenkins"))); + arg("gpg.passphrase").withValue(gpg.getPassphrase())) // + .andIf(!ObjectUtils.isEmpty(properties.getSettingsXml()), settingsXml(properties.getSettingsXml())) + .andIf(deploymentInformation.getStagingRepositoryId().isPresent(), + () -> arg("stagingRepositoryId").withValue(deploymentInformation.getStagingRepositoryId())) + .andIf(gpg.hasSecretKeyring(), () -> arg("gpg.secretKeyring").withValue(gpg.getSecretKeyring())); mvn.execute(module.getProject(), arguments); } @@ -493,7 +529,7 @@ class MavenBuildSystem implements BuildSystem { static byte[] doWithProjection(XBProjector projector, InputStream stream, Class type, Consumer callback) throws IOException { - XBStreamInput io = projector.io().stream(stream); + StreamInput io = projector.io().stream(stream); T pom = io.read(type); callback.accept(pom); diff --git a/src/main/java/org/springframework/data/release/build/MavenRuntime.java b/src/main/java/org/springframework/data/release/build/MavenRuntime.java index b623293..7f21f9b 100644 --- a/src/main/java/org/springframework/data/release/build/MavenRuntime.java +++ b/src/main/java/org/springframework/data/release/build/MavenRuntime.java @@ -18,11 +18,21 @@ package org.springframework.data.release.build; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import java.io.*; +import java.io.Closeable; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; -import org.apache.maven.shared.invoker.*; +import org.apache.maven.shared.invoker.DefaultInvocationRequest; +import org.apache.maven.shared.invoker.DefaultInvoker; +import org.apache.maven.shared.invoker.InvocationRequest; +import org.apache.maven.shared.invoker.InvocationResult; +import org.apache.maven.shared.invoker.Invoker; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.release.io.JavaRuntimes; import org.springframework.data.release.io.Workspace; @@ -70,11 +80,7 @@ class MavenRuntime { return new MavenRuntime(workspace, logger, properties, javaVersion); } - public MavenProperties getProperties() { - return this.properties; - } - - public void execute(Project project, CommandLine arguments) { + public MavenInvocationResult execute(Project project, CommandLine arguments) { logger.log(project, "📦 Executing mvn %s", arguments.toString()); @@ -108,10 +114,14 @@ class MavenRuntime { if (result.getExitCode() != 0) { logger.warn(project, "🙈 Failed execution mvn %s", arguments.toString()); - throw new IllegalStateException("🙈 Failed execution mvn " + arguments.toString(), - result.getExecutionException()); + throw new IllegalStateException("🙈 Failed execution mvn " + arguments, result.getExecutionException()); } logger.log(project, "🆗 Successful execution mvn %s", arguments.toString()); + + MavenInvocationResult invocationResult = new MavenInvocationResult(); + invocationResult.getLog().addAll(mavenLogger.getLines()); + + return invocationResult; } catch (Exception e) { if (e instanceof RuntimeException) { throw (RuntimeException) e; @@ -133,6 +143,15 @@ class MavenRuntime { return new FileLogger(log, project, this.workspace.getLogsDirectory(), goals); } + public static class MavenInvocationResult { + + private final List log = new ArrayList<>(); + + public List getLog() { + return log; + } + } + /** * Maven Logging Forwarder. */ @@ -141,6 +160,8 @@ class MavenRuntime { void info(String message); void warn(String message); + + List getLines(); } @RequiredArgsConstructor @@ -148,32 +169,44 @@ class MavenRuntime { private final org.slf4j.Logger logger; private final String logPrefix; + private final List contents; SlfLogger(org.slf4j.Logger logger, Project project) { this.logger = logger; this.logPrefix = StringUtils.padRight(project.getName(), 10); + this.contents = new ArrayList<>(); } @Override public void info(String message) { - logger.info(logPrefix + ": " + message); + String msg = logPrefix + ": " + message; + contents.add(msg); + logger.info(msg); } @Override public void warn(String message) { - logger.warn(logPrefix + ": " + message); + String msg = logPrefix + ": " + message; + contents.add(msg); + logger.warn(msg); } @Override public void close() throws IOException { // no-op } + + @Override + public List getLines() { + return contents; + } } static class FileLogger implements MavenLogger { private final PrintWriter printWriter; private final FileOutputStream outputStream; + private final List contents = new ArrayList<>(); FileLogger(org.slf4j.Logger logger, Project project, File logsDirectory, List goals) { @@ -199,11 +232,13 @@ class MavenRuntime { @Override public void info(String message) { printWriter.println(message); + contents.add(message); } @Override public void warn(String message) { printWriter.println(message); + contents.add(message); } @Override @@ -211,6 +246,11 @@ class MavenRuntime { printWriter.close(); outputStream.close(); } + + @Override + public List getLines() { + return contents; + } } } 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 ad1097c..972b7d6 100644 --- a/src/main/java/org/springframework/data/release/cli/ReleaseCommands.java +++ b/src/main/java/org/springframework/data/release/cli/ReleaseCommands.java @@ -15,7 +15,7 @@ */ package org.springframework.data.release.cli; -import static org.springframework.data.release.model.Projects.COMMONS; +import static org.springframework.data.release.model.Projects.*; import lombok.AccessLevel; import lombok.NonNull; @@ -27,11 +27,19 @@ import org.springframework.data.release.TimedCommand; import org.springframework.data.release.build.BuildOperations; import org.springframework.data.release.deployment.DeploymentInformation; import org.springframework.data.release.deployment.DeploymentOperations; +import org.springframework.data.release.deployment.StagingRepository; import org.springframework.data.release.git.GitOperations; import org.springframework.data.release.issues.IssueTrackerCommands; import org.springframework.data.release.issues.github.GitHubCommands; import org.springframework.data.release.misc.ReleaseOperations; -import org.springframework.data.release.model.*; +import org.springframework.data.release.model.ArtifactVersion; +import org.springframework.data.release.model.ModuleIteration; +import org.springframework.data.release.model.Phase; +import org.springframework.data.release.model.Project; +import org.springframework.data.release.model.Projects; +import org.springframework.data.release.model.ReleaseTrains; +import org.springframework.data.release.model.Train; +import org.springframework.data.release.model.TrainIteration; import org.springframework.shell.core.annotation.CliCommand; import org.springframework.shell.core.annotation.CliOption; import org.springframework.util.Assert; @@ -108,15 +116,16 @@ class ReleaseCommands extends TimedCommand { public void repositoryOpen(@CliOption(key = "", mandatory = true) TrainIteration iteration) { if (iteration.getIteration().isPublic()) { - build.open(); + build.open(iteration.getModule(Projects.BUILD)); } } @CliCommand(value = "repository close") - public void repositoryClose(@CliOption(key = "", mandatory = true) TrainIteration iteration) { + public void repositoryClose(@CliOption(key = "", mandatory = true) TrainIteration iteration, + @CliOption(key = "stagingRepositoryId", mandatory = true) String stagingRepositoryId) { if (iteration.getIteration().isPublic()) { - build.close(); + build.close(iteration.getModule(Projects.BUILD), StagingRepository.of(stagingRepositoryId)); } } diff --git a/src/main/java/org/springframework/data/release/deployment/DefaultDeploymentInformation.java b/src/main/java/org/springframework/data/release/deployment/DefaultDeploymentInformation.java index 7551cca..d7d48af 100644 --- a/src/main/java/org/springframework/data/release/deployment/DefaultDeploymentInformation.java +++ b/src/main/java/org/springframework/data/release/deployment/DefaultDeploymentInformation.java @@ -42,9 +42,26 @@ public class DefaultDeploymentInformation implements DeploymentInformation { private final @Getter @NonNull ModuleIteration module; private final @NonNull DeploymentProperties properties; private final @Getter String buildNumber; + private final @Getter StagingRepository stagingRepositoryId; public DefaultDeploymentInformation(ModuleIteration module, DeploymentProperties properties) { - this(module, properties, String.valueOf(LocalDateTime.now().toEpochSecond(ZoneOffset.UTC))); + this(module, properties, StagingRepository.EMPTY); + } + + public DefaultDeploymentInformation(ModuleIteration module, DeploymentProperties properties, + String stagingRepositoryId) { + this(module, properties, String.valueOf(LocalDateTime.now().toEpochSecond(ZoneOffset.UTC)), + StagingRepository.of(stagingRepositoryId)); + } + + public DefaultDeploymentInformation(ModuleIteration module, DeploymentProperties properties, + StagingRepository stagingRepository) { + this(module, properties, String.valueOf(LocalDateTime.now().toEpochSecond(ZoneOffset.UTC)), stagingRepository); + } + + @Override + public DeploymentInformation withModule(ModuleIteration module) { + return new DefaultDeploymentInformation(module, properties, buildNumber, stagingRepositoryId); } /* diff --git a/src/main/java/org/springframework/data/release/deployment/DeploymentInformation.java b/src/main/java/org/springframework/data/release/deployment/DeploymentInformation.java index 148cccf..052f533 100644 --- a/src/main/java/org/springframework/data/release/deployment/DeploymentInformation.java +++ b/src/main/java/org/springframework/data/release/deployment/DeploymentInformation.java @@ -21,48 +21,58 @@ import org.springframework.data.release.model.ModuleIteration; /** * @author Oliver Gierke + * @author Mark Paluch */ public interface DeploymentInformation { /** * Returns the name of the build. - * + * * @return will never be {@literal null} or empty. */ String getBuildName(); /** * Returns a unique build number for this particular deployment. - * + * * @return will never be {@literal null} or empty. */ String getBuildNumber(); /** * Returns the full URL to be used as deployment target. - * + * * @return will never be {@literal null} or empty. */ String getDeploymentTargetUrl(); /** * Returns the name of the repository to deploy to. - * + * * @return will never be {@literal null} or empty. */ String getTargetRepository(); + /** + * Staging repository identifier. + * + * @return + */ + StagingRepository getStagingRepositoryId(); + /** * Returns the {@link ModuleIteration} the deployment information was created for. - * + * * @return */ ModuleIteration getModule(); /** * Returns a {@link Map} to expand a URI template to access the build information. - * + * * @return */ Map getBuildInfoParameters(); + + DeploymentInformation withModule(ModuleIteration module); } diff --git a/src/main/java/org/springframework/data/release/deployment/StagingRepository.java b/src/main/java/org/springframework/data/release/deployment/StagingRepository.java new file mode 100644 index 0000000..d9007bf --- /dev/null +++ b/src/main/java/org/springframework/data/release/deployment/StagingRepository.java @@ -0,0 +1,48 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.springframework.data.release.deployment; + +import lombok.Value; + +import org.springframework.util.ObjectUtils; + +/** + * @author Mark Paluch + */ +@Value(staticConstructor = "of") +public class StagingRepository { + + public static final StagingRepository EMPTY = StagingRepository.of(""); + + String id; + + public boolean isEmpty() { + return ObjectUtils.isEmpty(id); + } + + public boolean isPresent() { + return !isEmpty(); + } + + @Override + public String toString() { + if (isPresent()) { + return id; + } + + return "(empty)"; + } +} diff --git a/src/main/java/org/springframework/data/release/git/GitOperations.java b/src/main/java/org/springframework/data/release/git/GitOperations.java index fd48b59..5be4057 100644 --- a/src/main/java/org/springframework/data/release/git/GitOperations.java +++ b/src/main/java/org/springframework/data/release/git/GitOperations.java @@ -34,8 +34,12 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; import org.apache.commons.io.FileUtils; -import org.eclipse.jgit.api.*; +import org.eclipse.jgit.api.AddCommand; +import org.eclipse.jgit.api.CheckoutCommand; +import org.eclipse.jgit.api.CommitCommand; import org.eclipse.jgit.api.CreateBranchCommand.SetupUpstreamMode; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.LogCommand; import org.eclipse.jgit.api.ResetCommand.ResetType; import org.eclipse.jgit.api.errors.EmptyCommitException; import org.eclipse.jgit.api.errors.RefNotFoundException; @@ -47,15 +51,29 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; -import org.eclipse.jgit.transport.*; +import org.eclipse.jgit.transport.CredentialItem; import org.eclipse.jgit.transport.CredentialItem.CharArrayType; import org.eclipse.jgit.transport.CredentialItem.InformationalMessage; +import org.eclipse.jgit.transport.CredentialsProvider; +import org.eclipse.jgit.transport.RefSpec; +import org.eclipse.jgit.transport.TagOpt; +import org.eclipse.jgit.transport.URIish; + import org.springframework.data.release.io.Workspace; import org.springframework.data.release.issues.IssueTracker; import org.springframework.data.release.issues.Ticket; import org.springframework.data.release.issues.TicketReference; import org.springframework.data.release.issues.TicketStatus; -import org.springframework.data.release.model.*; +import org.springframework.data.release.model.ArtifactVersion; +import org.springframework.data.release.model.Gpg; +import org.springframework.data.release.model.Iteration; +import org.springframework.data.release.model.ModuleIteration; +import org.springframework.data.release.model.Project; +import org.springframework.data.release.model.ProjectAware; +import org.springframework.data.release.model.Projects; +import org.springframework.data.release.model.ReleaseTrains; +import org.springframework.data.release.model.Train; +import org.springframework.data.release.model.TrainIteration; import org.springframework.data.release.utils.ExecutionUtils; import org.springframework.data.release.utils.Logger; import org.springframework.data.util.Pair; @@ -1101,7 +1119,7 @@ public class GitOperations { for (CredentialItem item : items) { if (item instanceof CharArrayType) { - ((CharArrayType) item).setValueNoCopy(gpg.getPassphrase().toCharArray()); + ((CharArrayType) item).setValueNoCopy(gpg.getPassphrase().toString().toCharArray()); return true; } diff --git a/src/main/java/org/springframework/data/release/model/Gpg.java b/src/main/java/org/springframework/data/release/model/Gpg.java index 5044a7a..5776365 100644 --- a/src/main/java/org/springframework/data/release/model/Gpg.java +++ b/src/main/java/org/springframework/data/release/model/Gpg.java @@ -19,6 +19,7 @@ import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** @@ -32,10 +33,14 @@ public class Gpg { private String keyname; private String executable; - private String passphrase; + private Password passphrase; private String secretKeyring; public boolean isGpgAvailable() { - return this.passphrase != null && StringUtils.hasText(secretKeyring); + return this.passphrase != null && StringUtils.hasText(keyname); + } + + public boolean hasSecretKeyring() { + return !ObjectUtils.isEmpty(getSecretKeyring()); } } diff --git a/src/main/resources/application-jenkins.properties b/src/main/resources/application-jenkins.properties index 4d67adb..5dfa89d 100644 --- a/src/main/resources/application-jenkins.properties +++ b/src/main/resources/application-jenkins.properties @@ -15,7 +15,7 @@ jira.username=${JIRA_USERNAME} jira.password=${JIRA_PASSWORD} jira.url=${JIRA_URL} -gpg.keyname=9A2C7A98E457C53D +gpg.keyname=${GPG_KEYNAME} gpg.passphrase=${PASSPHRASE} gpg.secretKeyring=${GNUPGHOME}/secring.gpg gpg.executable=/usr/bin/gpg