diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2e9707f --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +target/ +.settings/ +.project +.classpath +.factorypath +.springBeans +application-local.properties +spring-shell.log +.idea/ +logs/ +*.iml +dependency-upgrade-*.properties diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..1cf46db --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,96 @@ +def p = [:] +node { + checkout scm + p = readProperties interpolate: true, file: 'ci/release.properties' +} + +pipeline { + agent none + + options { + disableConcurrentBuilds() + buildDiscarder(logRotator(numToKeepStr: '14')) + } + + stages { + stage('Ship It') { + when { + branch 'release' + } + agent { + docker { + image 'springci/spring-data-release-tools:0.1' + } + } + 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') + } + + steps { + script { + 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!") + + input("SMOKE TEST: Did the smoke tests for ${p['release.version']} pass?") + + 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`") + + 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']}" + + 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!") + } + } + } + } + + post { + changed { + script { + slackSend( + color: (currentBuild.currentResult == 'SUCCESS') ? 'good' : 'danger', + channel: '#spring-data-dev', + message: "${currentBuild.fullDisplayName} - `${currentBuild.currentResult}`\n${env.BUILD_URL}") + emailext( + subject: "[${currentBuild.fullDisplayName}] ${currentBuild.currentResult}", + mimeType: 'text/html', + recipientProviders: [[$class: 'CulpritsRecipientProvider'], [$class: 'RequesterRecipientProvider']], + body: "${currentBuild.fullDisplayName} is reported as ${currentBuild.currentResult}") + } + } + } +} diff --git a/Jenkinsfile-container b/Jenkinsfile-container new file mode 100644 index 0000000..e9c5b85 --- /dev/null +++ b/Jenkinsfile-container @@ -0,0 +1,44 @@ +pipeline { + agent none + + options { + disableConcurrentBuilds() + buildDiscarder(logRotator(numToKeepStr: '14')) + } + + stages { + stage('Bake the Spring Data release tools into a 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() + } + } + } + } + } + + post { + changed { + script { + slackSend( + color: (currentBuild.currentResult == 'SUCCESS') ? 'good' : 'danger', + channel: '#spring-data-dev', + message: "${currentBuild.fullDisplayName} - `${currentBuild.currentResult}`\n${env.BUILD_URL}") + emailext( + subject: "[${currentBuild.fullDisplayName}] ${currentBuild.currentResult}", + mimeType: 'text/html', + recipientProviders: [[$class: 'CulpritsRecipientProvider'], [$class: 'RequesterRecipientProvider']], + body: "${currentBuild.fullDisplayName} is reported as ${currentBuild.currentResult}") + } + } + } +} diff --git a/application-local.template b/application-local.template index 7f47f84..8dfff14 100644 --- a/application-local.template +++ b/application-local.template @@ -24,7 +24,7 @@ deployment.api-key= # GPG gpg.keyname= -gpg.password= +gpg.passphrase= gpg.executable=/usr/local/bin/gpg2 # A GitHub token with user:email, read:user and read:org scopes. diff --git a/ci/CI.adoc b/ci/CI.adoc new file mode 100644 index 0000000..90c88b5 --- /dev/null +++ b/ci/CI.adoc @@ -0,0 +1,21 @@ +== Running CI tasks locally + +IMPORTANT: To do this you must have Docker installed on your machine. + +1. `cd ..` ++ +Because this tool is actually a subfolder, you must run the Docker container from up above. ++ +2. `docker run -it --mount type=bind,source="$(pwd)",target=/release-tools springci/spring-data-release-tools:0.1 /bin/bash` ++ +This will launch the Docker image and mount your source code at `release-tools-github`. ++ +3. `cd release-tools` ++ +Next, run the `ci/prepare-and-build.bash` script from inside the container: ++ +4. `SONATYPE_USR=foo SONATYPE_PSW=bar ci/prepare-and-build.bash ` + +Since the container is binding to your source, you can make edits from your IDE and continue to run build jobs. + +NOTE: Docker containers can eat up disk space fast! From time to time, run `docker system prune` to clean out old images. diff --git a/ci/Dockerfile b/ci/Dockerfile new file mode 100755 index 0000000..f181cd8 --- /dev/null +++ b/ci/Dockerfile @@ -0,0 +1,30 @@ +FROM ubuntu:22.10 + +ARG USER_UID="1001" +ARG USER_GID="1001" +ARG USER_NAME="jenkins" + +RUN groupadd -g $USER_GID $USER_NAME && \ + useradd -m -g $USER_GID -u $USER_UID $USER_NAME + +RUN apt-get update && \ + apt-get -y install curl zip gnupg +RUN rm -rf /var/lib/apt/lists/* && \ + rm -rf /tmp/* + +USER $USER_UID:$USER_GID + +RUN curl -s "https://get.sdkman.io" | bash + +RUN bash -c "source $HOME/.sdkman/bin/sdkman-init.sh && \ + yes | sdk install java 17.0.2-tem && \ + yes | sdk install java 17.0.3-tem && \ + yes | sdk install java 8.0.322-tem && \ + yes | sdk install java 8.0.332-tem && \ + yes | sdk install maven && \ + rm -rf $HOME/.sdkman/archives/* && \ + rm -rf $HOME/.sdkman/tmp/*" + +ENV MAVEN_HOME="$HOME/.sdkman/candidates/maven/current" +ENV JAVA_HOME="$HOME/.sdkman/candidates/java/current" +ENV PATH="$MAVEN_HOME/bin:$JAVA_HOME/bin:$PATH" diff --git a/ci/build-spring-data-release-cli.bash b/ci/build-spring-data-release-cli.bash new file mode 100755 index 0000000..c70aff5 --- /dev/null +++ b/ci/build-spring-data-release-cli.bash @@ -0,0 +1,11 @@ +#!/bin/bash -x + +set -euo pipefail + +export MAVEN_HOME="$HOME/.sdkman/candidates/maven/current" +export JAVA_HOME="$HOME/.sdkman/candidates/java/current" +export PATH="$MAVEN_HOME/bin:$JAVA_HOME/bin:$PATH" + +export JENKINS_HOME=/tmp/jenkins-home + +mvn clean package -DskipTests diff --git a/ci/conclude.bash b/ci/conclude.bash new file mode 100755 index 0000000..8202a5a --- /dev/null +++ b/ci/conclude.bash @@ -0,0 +1,51 @@ +#!/bin/bash -x + +set -euo pipefail + +VERSION=$1 +echo "You want me to conclude ${VERSION} ??" + +export MAVEN_HOME="$HOME/.sdkman/candidates/maven/current" +export JAVA_HOME="$HOME/.sdkman/candidates/java/current" +export PATH="$MAVEN_HOME/bin:$JAVA_HOME/bin:$PATH" + +export JENKINS_HOME=/tmp/jenkins-home +export RELEASE_TOOLS_CACHE=${JENKINS_HOME}/.m2/spring-data-release-tools +export LOGS_DIR=${JENKINS_HOME}/spring-data-shell/logs +export SETTINGS_XML=${JENKINS_HOME}/settings.xml +export GNUPGHOME=~/.gnupg/ + +if test -f application-local.properties; then + echo "You are running from dev environment! Using application-local.properties." + + GIT_BRANCH="" + + function spring-data-release-shell { + java \ + "-Ddeployment.settings-xml=${SETTINGS_XML}" \ + "-Ddeployment.local=true" \ + "-Dmaven.mavenHome=${MAVEN_HOME}" \ + "-Dgpg.executable=/usr/bin/gpg" \ + "-Dio.workDir=dist" \ + -jar target/spring-data-release-cli.jar + } +else + echo "You are running inside Jenkins! Using parameters fed from the agent." + + # Reinstall Jenkins' GPG keys + \rm -rf ${GNUPGHOME} + mkdir -p ${GNUPGHOME} + chmod 700 ${GNUPGHOME} + cp $KEYRING $GNUPGHOME + + function spring-data-release-shell { + java \ + -Dspring.profiles.active=jenkins \ + -Dmaven.home=${MAVEN_HOME} \ + -jar target/spring-data-release-cli.jar + } +fi + +echo "About to conclude ${VERSION}." + +echo "release conclude ${VERSION}" | spring-data-release-shell diff --git a/ci/prepare-and-build.bash b/ci/prepare-and-build.bash new file mode 100755 index 0000000..7da0f58 --- /dev/null +++ b/ci/prepare-and-build.bash @@ -0,0 +1,63 @@ +#!/bin/bash -x + +set -euo pipefail + +VERSION=$1 +echo "You want me to build and deploy ${VERSION} ??" + +export MAVEN_HOME="$HOME/.sdkman/candidates/maven/current" +export JAVA_HOME="$HOME/.sdkman/candidates/java/current" +export PATH="$MAVEN_HOME/bin:$JAVA_HOME/bin:$PATH" + +export JENKINS_HOME=/tmp/jenkins-home +export RELEASE_TOOLS_CACHE=${JENKINS_HOME}/.m2/spring-data-release-tools +export LOGS_DIR=${JENKINS_HOME}/spring-data-shell/logs +export SETTINGS_XML=${JENKINS_HOME}/settings.xml + +mkdir -p ${RELEASE_TOOLS_CACHE} +mkdir -p ${LOGS_DIR} + +export GNUPGHOME=~/.gnupg/ +mkdir -p ${GNUPGHOME} +chmod 700 ${GNUPGHOME} + +cp ci/settings.xml ${JENKINS_HOME} + +if test -f application-local.properties; then + echo "You are running from dev environment! Using application-local.properties." + + GIT_BRANCH="" + + function spring-data-release-shell { + java \ + "-Ddeployment.settings-xml=${SETTINGS_XML}" \ + "-Ddeployment.local=true" \ + "-Dmaven.mavenHome=${MAVEN_HOME}" \ + "-Dgpg.executable=/usr/bin/gpg" \ + "-Dio.workDir=dist" \ + -jar target/spring-data-release-cli.jar \ + --cmdfile target/prepare-and-build.shell + } +else + echo "You are running inside Jenkins! Using parameters fed from the agent." + + cp $KEYRING $GNUPGHOME + + ls -ld ~/.gnupg + ls -lR ~/.gnupg + id + + function spring-data-release-shell { + java \ + -Dspring.profiles.active=jenkins \ + -Dmaven.home=${MAVEN_HOME} \ + -jar target/spring-data-release-cli.jar \ + --cmdfile target/prepare-and-build.shell + } +fi + +echo "About to prepare and build ${VERSION}." + +sed "s|\${VERSION}|${VERSION}|g" < ci/prepare-and-build.template > target/prepare-and-build.shell + +spring-data-release-shell diff --git a/ci/prepare-and-build.template b/ci/prepare-and-build.template new file mode 100644 index 0000000..36bb090 --- /dev/null +++ b/ci/prepare-and-build.template @@ -0,0 +1,5 @@ +workspace cleanup +release prepare ${VERSION} +repository open ${VERSION} +release build ${VERSION} +repository close ${VERSION} diff --git a/ci/push-and-distribute.bash b/ci/push-and-distribute.bash new file mode 100755 index 0000000..cb7dd18 --- /dev/null +++ b/ci/push-and-distribute.bash @@ -0,0 +1,50 @@ +#!/bin/bash -x + +set -euo pipefail + +VERSION=$1 +echo "You want me to push and distribute ${VERSION} ??" + +export MAVEN_HOME="$HOME/.sdkman/candidates/maven/current" +export JAVA_HOME="$HOME/.sdkman/candidates/java/current" +export PATH="$MAVEN_HOME/bin:$JAVA_HOME/bin:$PATH" + +export JENKINS_HOME=/tmp/jenkins-home +export RELEASE_TOOLS_CACHE=${JENKINS_HOME}/.m2/spring-data-release-tools +export LOGS_DIR=${JENKINS_HOME}/spring-data-shell/logs +export SETTINGS_XML=${JENKINS_HOME}/settings.xml +export GNUPGHOME=~/.gnupg/ + +if test -f application-local.properties; then + echo "You are running from dev environment! Using application-local.properties." + + GIT_BRANCH="" + + function spring-data-release-shell { + java \ + "-Ddeployment.settings-xml=${SETTINGS_XML}" \ + "-Ddeployment.local=true" \ + "-Dmaven.mavenHome=${MAVEN_HOME}" \ + "-Dgpg.executable=/usr/bin/gpg" \ + "-Dio.workDir=dist" \ + -jar target/spring-data-release-cli.jar \ + --cmdfile target/push-and-distribute.shell + } +else + echo "You are running inside Jenkins! Using parameters fed from the agent." + + function spring-data-release-shell { + java \ + -Dspring.profiles.active=jenkins \ + -Dmaven.home=${MAVEN_HOME} \ + -jar target/spring-data-release-cli.jar \ + --cmdfile target/push-and-distribute.shell + } + +fi + +echo "About to push and distribute ${VERSION}." + +sed "s|\${VERSION}|${VERSION}|g" < ci/push-and-distribute.template > target/push-and-distribute.shell + +spring-data-release-shell diff --git a/ci/push-and-distribute.template b/ci/push-and-distribute.template new file mode 100755 index 0000000..31715ff --- /dev/null +++ b/ci/push-and-distribute.template @@ -0,0 +1,2 @@ +github push ${VERSION} +release distribute ${VERSION} diff --git a/ci/release.properties b/ci/release.properties new file mode 100644 index 0000000..a7f91f9 --- /dev/null +++ b/ci/release.properties @@ -0,0 +1,3 @@ +# Release train version +release.version=2021.1.7 +#release.version=2022.0.0-M6 diff --git a/ci/settings.xml b/ci/settings.xml new file mode 100644 index 0000000..e25d87c --- /dev/null +++ b/ci/settings.xml @@ -0,0 +1,12 @@ + + + + + sonatype + ${env.SONATYPE_USR} + ${env.SONATYPE_PSW} + + + diff --git a/readme.md b/readme.md index 1696e55..1ee32b5 100644 --- a/readme.md +++ b/readme.md @@ -25,7 +25,7 @@ Add an `application-local.properties` to the project root and add the following - `deployment.api-key` - The Artifactory API key to use for artifact promotion. - `deployment.password` - The encrypted Artifactory password.. - `gpg.keyname` - The GPG key name. -- `gpg.password` - The password of your GPG key. +- `gpg.passphrase` - The password of your GPG key. - `gpg.executable` - Path to your GPG executable, typically `/usr/local/MacGPG2/bin/gpg2` or `/usr/local/bin/gpg`. - `sagan.key` - Sagan authentication token. Must be a valid GitHub token. Can be the same 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 e91b211..6d0b0f4 100644 --- a/src/main/java/org/springframework/data/release/build/BuildExecutor.java +++ b/src/main/java/org/springframework/data/release/build/BuildExecutor.java @@ -21,12 +21,7 @@ import lombok.SneakyThrows; import java.io.File; import java.io.FileInputStream; -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.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; @@ -39,7 +34,6 @@ import java.util.stream.Collectors; 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; @@ -106,6 +100,13 @@ 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())) { @@ -134,6 +135,7 @@ class BuildExecutor { } CompletableFuture result = run(moduleIteration, function); + results.put(moduleIteration.getProject(), result); } 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 d36dc37..9ec4d05 100644 --- a/src/main/java/org/springframework/data/release/build/BuildOperations.java +++ b/src/main/java/org/springframework/data/release/build/BuildOperations.java @@ -25,15 +25,8 @@ 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.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.model.*; import org.springframework.data.release.utils.Logger; import org.springframework.plugin.core.PluginRegistry; import org.springframework.stereotype.Component; @@ -179,6 +172,21 @@ public class BuildOperations { return properties.getLocalRepository().toPath(); } + /** + * Opens a repository to stage artifacts for this {@link ModuleIteration}. + * + * @param module must not be {@literal null}. + */ + public void open() { + buildSystems.getRequiredPluginFor(Projects.BUILD) // + .withJavaVersion(executor.detectJavaVersion(Projects.BUILD)).open(); + } + + public void close() { + buildSystems.getRequiredPluginFor(Projects.BUILD) // + .withJavaVersion(executor.detectJavaVersion(Projects.BUILD)).close(); + } + /** * Builds the release for the given {@link ModuleIteration} and deploys it to the staging repository. * @@ -241,5 +249,4 @@ public class BuildOperations { return function.apply(buildSystem.withJavaVersion(executor.detectJavaVersion(module.getProject())), module); } - } 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 1f77a7c..9cc0381 100644 --- a/src/main/java/org/springframework/data/release/build/BuildSystem.java +++ b/src/main/java/org/springframework/data/release/build/BuildSystem.java @@ -16,11 +16,7 @@ package org.springframework.data.release.build; import org.springframework.data.release.deployment.DeploymentInformation; -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.*; import org.springframework.plugin.core.Plugin; /** @@ -48,6 +44,13 @@ interface BuildSystem extends Plugin { */ ModuleIteration prepareVersion(ModuleIteration module, Phase phase); + /** + * Open a remote repository for staging artifacts. + */ + void open(); + + void close(); + /** * 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 0c7c58b..98d9909 100644 --- a/src/main/java/org/springframework/data/release/build/CommandLine.java +++ b/src/main/java/org/springframework/data/release/build/CommandLine.java @@ -20,11 +20,7 @@ import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.Value; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.function.BooleanSupplier; import java.util.function.Function; import java.util.stream.Collectors; @@ -181,6 +177,10 @@ class CommandLine { return Argument.of("-X"); } + public static Argument settingsXml(String path) { + return Argument.of("-s " + path); + } + public Argument withValue(Object value) { return new Argument(name, ArgumentValue.of(value)); } 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 c108886..b41e337 100644 --- a/src/main/java/org/springframework/data/release/build/MavenBuildSystem.java +++ b/src/main/java/org/springframework/data/release/build/MavenBuildSystem.java @@ -16,21 +16,18 @@ package org.springframework.data.release.build; import static org.springframework.data.release.build.CommandLine.Argument.*; -import static org.springframework.data.release.build.CommandLine.Goal.*; -import static org.springframework.data.release.model.Projects.*; +import static org.springframework.data.release.model.Projects.BOM; +import static org.springframework.data.release.model.Projects.BUILD; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; import lombok.experimental.FieldDefaults; -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.io.*; 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; @@ -40,8 +37,9 @@ 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; @@ -49,18 +47,10 @@ 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.io.Workspace; -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.model.*; import org.springframework.data.release.utils.Logger; import org.springframework.stereotype.Component; import org.springframework.util.Assert; - import org.xmlbeam.ProjectionFactory; import org.xmlbeam.XBProjector; import org.xmlbeam.dom.DOMAccess; @@ -69,6 +59,7 @@ import org.xmlbeam.io.XBStreamInput; /** * @author Oliver Gierke * @author Mark Paluch + * @author Greg Turnquist */ @Component @Order(100) @@ -85,10 +76,17 @@ class MavenBuildSystem implements BuildSystem { DeploymentProperties properties; Gpg gpg; + Environment env; + + static String stagingRepositoryId = null; + + 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); + return new MavenBuildSystem(workspace, projectionFactory, logger, mvn.withJavaVersion(javaVersion), properties, gpg, + env); } /* @@ -245,7 +243,7 @@ class MavenBuildSystem implements BuildSystem { Project project = module.getProject(); UpdateInformation information = UpdateInformation.of(module.getTrainIteration(), phase); - CommandLine goals = CommandLine.of(goal("versions:set"), goal("versions:commit")); + CommandLine goals = CommandLine.of(Goal.goal("versions:set"), Goal.goal("versions:commit")); if (BOM.equals(project)) { @@ -277,11 +275,64 @@ class MavenBuildSystem implements BuildSystem { return module; } - /* - * (non-Javadoc) - * @see org.springframework.data.release.build.BuildSystem#deploy(org.springframework.data.release.model.ModuleIteration) + /** + * Perform a {@literal nexus-staging:rc-open} and extract the stagingProfileId from the results. */ @Override + public void 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 + "'")); + + mvn.execute(BUILD, arguments); + + String rcOpenLogfile = "mvn-" + BUILD.getName() + "-nexus-staging.rc-open.log"; + + logger.log(BUILD, "Searching " + this.workspace.getLogsDirectory().getAbsolutePath() + " for " + rcOpenLogfile); + + 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!")); + + List rcOpenLogContents = Files.readAllLines(rcOpenLogfilePath); + + 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); + } + } + + /** + * Perform a {@literal nexus-staging:rc-close}. + */ + @Override + public void close() { + + CommandLine arguments = CommandLine.of(Goal.goal("nexus-staging:rc-close"), // + profile("central"), // + of("-s " + properties.getSettingsXml()), // + arg("stagingRepositoryId").withValue(stagingRepositoryId)); + + mvn.execute(BUILD, arguments); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.release.build.BuildSystem#deploy(org.springframework.data.release.model.ModuleIteration) + */ + @Override public DeploymentInformation deploy(ModuleIteration module) { Assert.notNull(module, "Module must not be null!"); @@ -289,6 +340,7 @@ class MavenBuildSystem implements BuildSystem { DeploymentInformation information = new DefaultDeploymentInformation(module, properties); deployToArtifactory(module, information); + deployToMavenCentral(module); return information; @@ -342,8 +394,8 @@ class MavenBuildSystem implements BuildSystem { profile("central"), // SKIP_TESTS, // arg("gpg.executable").withValue(gpg.getExecutable()), // - arg("gpg.keyname").withValue(gpg.getKeyname()), // - arg("gpg.password").withValue(gpg.getPassword())); + arg("gpg.passphrase").withValue(gpg.getPassphrase()), // + arg("gpg.secretKeyring").withValue(gpg.getSecretKeyring())); mvn.execute(BUILD, arguments); @@ -403,9 +455,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.password").withValue(gpg.getPassword())); + arg("gpg.passphrase").withValue(gpg.getPassphrase()), // + arg("stagingRepositoryId").withValue(stagingRepositoryId)) // + .conditionalAnd(arg("gpg.secretKeyring").withValue(gpg.getSecretKeyring()), + () -> env.acceptsProfiles(Profiles.of("jenkins"))); mvn.execute(module.getProject(), arguments); } 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 a49d68e..b623293 100644 --- a/src/main/java/org/springframework/data/release/build/MavenRuntime.java +++ b/src/main/java/org/springframework/data/release/build/MavenRuntime.java @@ -18,20 +18,11 @@ package org.springframework.data.release.build; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import java.io.Closeable; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.PrintWriter; +import java.io.*; import java.util.List; import java.util.stream.Collectors; -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.apache.maven.shared.invoker.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.release.io.JavaRuntimes; import org.springframework.data.release.io.Workspace; @@ -79,6 +70,10 @@ class MavenRuntime { return new MavenRuntime(workspace, logger, properties, javaVersion); } + public MavenProperties getProperties() { + return this.properties; + } + public void execute(Project project, CommandLine arguments) { logger.log(project, "📦 Executing mvn %s", arguments.toString()); 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 50b168e..ad1097c 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.*; +import static org.springframework.data.release.model.Projects.COMMONS; import lombok.AccessLevel; import lombok.NonNull; @@ -31,14 +31,7 @@ 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.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.data.release.model.*; import org.springframework.shell.core.annotation.CliCommand; import org.springframework.shell.core.annotation.CliOption; import org.springframework.util.Assert; @@ -111,6 +104,22 @@ class ReleaseCommands extends TimedCommand { git.commit(iteration, "Release version %s."); } + @CliCommand(value = "repository open") + public void repositoryOpen(@CliOption(key = "", mandatory = true) TrainIteration iteration) { + + if (iteration.getIteration().isPublic()) { + build.open(); + } + } + + @CliCommand(value = "repository close") + public void repositoryClose(@CliOption(key = "", mandatory = true) TrainIteration iteration) { + + if (iteration.getIteration().isPublic()) { + build.close(); + } + } + @CliCommand(value = "release build") public void buildRelease(@CliOption(key = "", mandatory = true) TrainIteration iteration, // @CliOption(key = "project", mandatory = false) String projectName) { 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 f952e83..adedb37 100644 --- a/src/main/java/org/springframework/data/release/deployment/DeploymentProperties.java +++ b/src/main/java/org/springframework/data/release/deployment/DeploymentProperties.java @@ -62,8 +62,12 @@ public class DeploymentProperties { */ private String distributionRepository; + private String settingsXml; + private String repositoryPrefix = ""; + private MavenCentral mavenCentral; + public String getStagingRepository() { return repositoryPrefix.concat(stagingRepository); } @@ -117,4 +121,11 @@ public class DeploymentProperties { } } + @Data + public static class MavenCentral { + + private String stagingProfileId; + + } + } 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 b545f4b..fd48b59 100644 --- a/src/main/java/org/springframework/data/release/git/GitOperations.java +++ b/src/main/java/org/springframework/data/release/git/GitOperations.java @@ -34,12 +34,8 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; import org.apache.commons.io.FileUtils; -import org.eclipse.jgit.api.AddCommand; -import org.eclipse.jgit.api.CheckoutCommand; -import org.eclipse.jgit.api.CommitCommand; +import org.eclipse.jgit.api.*; 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; @@ -51,29 +47,15 @@ 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.CredentialItem; +import org.eclipse.jgit.transport.*; 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.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.model.*; import org.springframework.data.release.utils.ExecutionUtils; import org.springframework.data.release.utils.Logger; import org.springframework.data.util.Pair; @@ -710,7 +692,9 @@ public class GitOperations { .setAll(all); if (gpg.isGpgAvailable()) { - commitCommand.setSign(true).setSigningKey(gpg.getKeyname()) + commitCommand // + .setSign(true) // + .setSigningKey(gpg.getKeyname()) // .setCredentialsProvider(new GpgPassphraseProvider(gpg)); } else { commitCommand.setSign(false); @@ -1101,6 +1085,7 @@ public class GitOperations { } private boolean matchesKey(CredentialItem[] items) { + return Arrays.stream(items).filter(InformationalMessage.class::isInstance) // .map(CredentialItem::getPromptText) // .map(it -> it.toLowerCase(Locale.US)) // @@ -1116,7 +1101,7 @@ public class GitOperations { for (CredentialItem item : items) { if (item instanceof CharArrayType) { - ((CharArrayType) item).setValueNoCopy(gpg.getPassword().toString().toCharArray()); + ((CharArrayType) item).setValueNoCopy(gpg.getPassphrase().toCharArray()); return true; } diff --git a/src/main/java/org/springframework/data/release/io/IoProperties.java b/src/main/java/org/springframework/data/release/io/IoProperties.java index 1fff619..da7d661 100644 --- a/src/main/java/org/springframework/data/release/io/IoProperties.java +++ b/src/main/java/org/springframework/data/release/io/IoProperties.java @@ -21,7 +21,6 @@ import lombok.extern.slf4j.Slf4j; import java.io.File; import org.apache.commons.io.FileUtils; - import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; 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 30ad27e..5044a7a 100644 --- a/src/main/java/org/springframework/data/release/model/Gpg.java +++ b/src/main/java/org/springframework/data/release/model/Gpg.java @@ -30,11 +30,12 @@ import org.springframework.util.StringUtils; @ConfigurationProperties(prefix = "gpg") public class Gpg { - private String keyname, executable; - - private Password password; + private String keyname; + private String executable; + private String passphrase; + private String secretKeyring; public boolean isGpgAvailable() { - return this.password != null && StringUtils.hasText(keyname); + return this.passphrase != null && StringUtils.hasText(secretKeyring); } } diff --git a/src/main/java/org/springframework/data/release/model/ReleaseTrains.java b/src/main/java/org/springframework/data/release/model/ReleaseTrains.java index 57eb821..03920cd 100644 --- a/src/main/java/org/springframework/data/release/model/ReleaseTrains.java +++ b/src/main/java/org/springframework/data/release/model/ReleaseTrains.java @@ -78,7 +78,7 @@ public class ReleaseTrains { .filterModules(module -> !module.getProject().equals(ENVERS)) .filterModules(module -> !module.getProject().equals(R2DBC)) .filterModules(module -> !module.getProject().equals(JDBC)) // filter "old" JDBC without R2DBC submodule - .withIterations(new Train.Iterations(M1, M2, M3, M4, M5, RC1, RC2, GA, SR1, SR2, SR3, SR4, SR5)); + .withIterations(new Train.Iterations(M1, M2, M3, M4, M5, M6, RC1, RC2, GA, SR1, SR2, SR3, SR4, SR5)); // Trains diff --git a/src/main/java/org/springframework/data/release/model/Train.java b/src/main/java/org/springframework/data/release/model/Train.java index 986c2ed..e41831e 100644 --- a/src/main/java/org/springframework/data/release/model/Train.java +++ b/src/main/java/org/springframework/data/release/model/Train.java @@ -267,7 +267,7 @@ public class Train implements Streamable { @ToString public static class Iterations implements Iterable { - public static Iterations DEFAULT = new Iterations(M1, M2, M3, RC1, RC2, GA, SR1, SR2, SR3, SR4, SR5, SR6, SR7, SR8, + public static Iterations DEFAULT = new Iterations(M1, M2, M3, M4, M5, M6, RC1, RC2, GA, SR1, SR2, SR3, SR4, SR5, SR6, SR7, SR8, SR9, SR10, SR11, SR12, SR13, SR14, SR15, SR16, SR17, SR18, SR19, SR20, SR21, SR22, SR23, SR24); private final List iterations; diff --git a/src/main/resources/application-jenkins.properties b/src/main/resources/application-jenkins.properties new file mode 100644 index 0000000..4d67adb --- /dev/null +++ b/src/main/resources/application-jenkins.properties @@ -0,0 +1,30 @@ +git.username=${GIT_USERNAME} +git.author=${GIT_AUTHOR} +git.email=${GIT_EMAIL} +git.password=${GIT_PASSWORD} + +github.api.url=${GITHUB_API_URL} + +deployment.username=${DEPLOYMENT_USERNAME} +deployment.password=${DEPLOYMENT_PASSWORD} +deployment.api-key=${DEPLOYMENT_API_KEY} +deployment.maven-central.stagingProfileId=${STAGING_PROFILE_ID} +deployment.settings-xml=${SETTINGS_XML} + +jira.username=${JIRA_USERNAME} +jira.password=${JIRA_PASSWORD} +jira.url=${JIRA_URL} + +gpg.keyname=9A2C7A98E457C53D +gpg.passphrase=${PASSPHRASE} +gpg.secretKeyring=${GNUPGHOME}/secring.gpg +gpg.executable=/usr/bin/gpg + +sagan.key= + +maven.home=${MAVEN_HOME} +maven.console-logger=false +maven.repo.local=${RELEASE_TOOLS_CACHE} + +io.workDir=dist +io.logs=${LOGS_DIR} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 4f0d97b..993e64d 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -15,7 +15,7 @@ github.team=christophstrobl,gregturn,jxblum,mp911de,odrotbohm,schauder,meisterme # GPG setup gpg.executable=/usr/local/bin/gpg2 # gpg.keyname -# gpg.password +# gpg.passphrase # JIRA jira.api-url=https://jira.spring.io # GitHub diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties index 06c30a0..c1f540e 100644 --- a/src/test/resources/application-test.properties +++ b/src/test/resources/application-test.properties @@ -10,7 +10,7 @@ jira.username=dummy jira.password=dummy jira.api-url=http://localhost:8888 git.username=dummy -git.password=dummy +git.passphrase=dummy git.email=dummy@dummy.com git.author=dummy github.api-url=http://localhost:8888