From 6f5494c1ca8bb43c6b147695fe97910990bfdfbd Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 29 Aug 2022 11:01:33 +0200 Subject: [PATCH] Polishing. Refactor rc-open/close to return a StagingRepository. Include open/close steps in the actual release to avoid additional steps and to retain the repository identifier. Simplify command line conditionals, avoid log parsing and return log contents directly. Reformat Jenkins files. --- Jenkinsfile | 90 +++++------ Jenkinsfile-container | 30 ++-- application-local.template | 1 + ci/prepare-and-build.template | 2 - .../data/release/build/BuildExecutor.java | 16 +- .../data/release/build/BuildOperations.java | 48 ++++-- .../data/release/build/BuildSystem.java | 23 ++- .../data/release/build/CommandLine.java | 19 ++- .../data/release/build/MavenBuildSystem.java | 150 +++++++++++------- .../data/release/build/MavenRuntime.java | 62 ++++++-- .../data/release/cli/ReleaseCommands.java | 19 ++- .../DefaultDeploymentInformation.java | 19 ++- .../deployment/DeploymentInformation.java | 22 ++- .../release/deployment/StagingRepository.java | 48 ++++++ .../data/release/git/GitOperations.java | 26 ++- .../data/release/model/Gpg.java | 9 +- .../resources/application-jenkins.properties | 2 +- 17 files changed, 414 insertions(+), 172 deletions(-) create mode 100644 src/main/java/org/springframework/data/release/deployment/StagingRepository.java 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