diff --git a/ci/images/build-release-scripts.sh b/ci/images/build-release-scripts.sh new file mode 100755 index 0000000000..7ba7b47457 --- /dev/null +++ b/ci/images/build-release-scripts.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -ex + +pushd /release-scripts + ./mvnw clean install +popd +cp /release-scripts/target/spring-boot-release-scripts.jar . \ No newline at end of file diff --git a/ci/images/releasescripts/.gitignore b/ci/images/releasescripts/.gitignore new file mode 100644 index 0000000000..a2a3040aa8 --- /dev/null +++ b/ci/images/releasescripts/.gitignore @@ -0,0 +1,31 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/** +!**/src/test/** + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ + +### VS Code ### +.vscode/ diff --git a/ci/images/releasescripts/.mvn/wrapper/MavenWrapperDownloader.java b/ci/images/releasescripts/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 0000000000..da7c339695 --- /dev/null +++ b/ci/images/releasescripts/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,118 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.URL; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.util.Properties; + +public class MavenWrapperDownloader { + + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = + "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if (mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } + catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } + finally { + try { + if (mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } + catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: : " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if (!outputFile.getParentFile().exists()) { + if (!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } + catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/ci/images/releasescripts/.mvn/wrapper/maven-wrapper.jar b/ci/images/releasescripts/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000..01e6799737 Binary files /dev/null and b/ci/images/releasescripts/.mvn/wrapper/maven-wrapper.jar differ diff --git a/ci/images/releasescripts/.mvn/wrapper/maven-wrapper.properties b/ci/images/releasescripts/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000000..f5374227f9 --- /dev/null +++ b/ci/images/releasescripts/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,17 @@ +# +# Copyright 2012-2019 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 +# +# https://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. +# + +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.0/apache-maven-3.6.0-bin.zip diff --git a/ci/images/releasescripts/mvnw b/ci/images/releasescripts/mvnw new file mode 100755 index 0000000000..8b9da3b8b6 --- /dev/null +++ b/ci/images/releasescripts/mvnw @@ -0,0 +1,286 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + wget "$jarUrl" -O "$wrapperJarPath" + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + curl -o "$wrapperJarPath" "$jarUrl" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/ci/images/releasescripts/mvnw.cmd b/ci/images/releasescripts/mvnw.cmd new file mode 100644 index 0000000000..fef5a8f7f9 --- /dev/null +++ b/ci/images/releasescripts/mvnw.cmd @@ -0,0 +1,161 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/ci/images/releasescripts/pom.xml b/ci/images/releasescripts/pom.xml new file mode 100644 index 0000000000..6dc3a70680 --- /dev/null +++ b/ci/images/releasescripts/pom.xml @@ -0,0 +1,86 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.2.0.RELEASE + + + io.spring.concourse.releasescripts + release-scripts + 0.0.1-SNAPSHOT + releasescripts + Utility that can be used when releasing Java projects + + + 1.8 + 0.0.15 + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework + spring-web + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.fasterxml.jackson.module + jackson-module-parameter-names + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + + spring-boot-release-scripts + + + org.springframework.boot + spring-boot-maven-plugin + + + io.spring.javaformat + spring-javaformat-maven-plugin + ${spring-javaformat.version} + + + validate + + ${disable.checks} + + + validate + + + + + + + + diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/Application.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/Application.java new file mode 100644 index 0000000000..7e6495b96c --- /dev/null +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/Application.java @@ -0,0 +1,29 @@ +/* +* Copyright 2012-2019 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 +* +* https://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 io.spring.concourse.releasescripts; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/ReleaseInfo.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/ReleaseInfo.java new file mode 100644 index 0000000000..34fec8172c --- /dev/null +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/ReleaseInfo.java @@ -0,0 +1,82 @@ +/* + * Copyright 2012-2019 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 + * + * https://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 io.spring.concourse.releasescripts; + +import io.spring.concourse.releasescripts.artifactory.payload.BuildInfoResponse; + +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.util.StringUtils; + +/** + * Properties corresponding to the release. + * + * @author Madhura Bhave + */ +public class ReleaseInfo { + + private String buildName; + + private String buildNumber; + + private String groupId; + + private String version; + + public static ReleaseInfo from(BuildInfoResponse.BuildInfo buildInfo) { + ReleaseInfo info = new ReleaseInfo(); + PropertyMapper propertyMapper = PropertyMapper.get(); + propertyMapper.from(buildInfo.getName()).to(info::setBuildName); + propertyMapper.from(buildInfo.getNumber()).to(info::setBuildNumber); + String[] moduleInfo = StringUtils.delimitedListToStringArray(buildInfo.getModules()[0].getId(), ":"); + propertyMapper.from(moduleInfo[0]).to(info::setGroupId); + propertyMapper.from(moduleInfo[2]).to(info::setVersion); + return info; + } + + public String getBuildName() { + return this.buildName; + } + + public void setBuildName(String buildName) { + this.buildName = buildName; + } + + public String getBuildNumber() { + return this.buildNumber; + } + + public void setBuildNumber(String buildNumber) { + this.buildNumber = buildNumber; + } + + public String getGroupId() { + return this.groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + public String getVersion() { + return this.version; + } + + public void setVersion(String version) { + this.version = version; + } + +} diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/ReleaseProperties.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/ReleaseProperties.java new file mode 100644 index 0000000000..4973fedc27 --- /dev/null +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/ReleaseProperties.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2019 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 + * + * https://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 io.spring.concourse.releasescripts; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * {@link ConfigurationProperties @ConfigurationProperties} corresponding to the release. + * + * @author Madhura Bhave + */ +@ConfigurationProperties(prefix = "release") +public class ReleaseProperties { + + private String buildName; + + private String buildNumber; + + private String groupId; + + private String version; + + public String getBuildName() { + return this.buildName; + } + + public void setBuildName(String buildName) { + this.buildName = buildName; + } + + public String getBuildNumber() { + return this.buildNumber; + } + + public void setBuildNumber(String buildNumber) { + this.buildNumber = buildNumber; + } + + public String getGroupId() { + return this.groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + public String getVersion() { + return this.version; + } + + public void setVersion(String version) { + this.version = version; + } + +} diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/ReleaseType.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/ReleaseType.java new file mode 100644 index 0000000000..f602958977 --- /dev/null +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/ReleaseType.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2019 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 + * + * https://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 io.spring.concourse.releasescripts; + +/** + * Release type. + * + * @author Madhura Bhave + */ +public enum ReleaseType { + + MILESTONE("M", "libs-milestone-local"), + + RELEASE_CANDIDATE("RC", "libs-milestone-local"), + + RELEASE("RELEASE", "libs-release-local"); + + private final String identifier; + + private final String repo; + + ReleaseType(String identifier, String repo) { + this.identifier = identifier; + this.repo = repo; + } + + public static ReleaseType from(String releaseType) { + for (ReleaseType type : ReleaseType.values()) { + if (type.identifier.equals(releaseType)) { + return type; + } + } + throw new IllegalArgumentException("Invalid release type"); + } + + public String getRepo() { + return this.repo; + } + +} diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/artifactory/ArtifactoryProperties.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/artifactory/ArtifactoryProperties.java new file mode 100644 index 0000000000..9522711673 --- /dev/null +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/artifactory/ArtifactoryProperties.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2019 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 + * + * https://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 io.spring.concourse.releasescripts.artifactory; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * {@link ConfigurationProperties @ConfigurationProperties} for an Artifactory server. + * + * @author Madhura Bhave + */ +@ConfigurationProperties(prefix = "artifactory") +public class ArtifactoryProperties { + + private String username; + + private String password; + + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return this.password; + } + + public void setPassword(String password) { + this.password = password; + } + +} diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/artifactory/ArtifactoryService.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/artifactory/ArtifactoryService.java new file mode 100644 index 0000000000..d8d0071723 --- /dev/null +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/artifactory/ArtifactoryService.java @@ -0,0 +1,138 @@ +/* + * Copyright 2012-2019 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 + * + * https://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 io.spring.concourse.releasescripts.artifactory; + +import java.net.URI; + +import io.spring.concourse.releasescripts.ReleaseInfo; +import io.spring.concourse.releasescripts.artifactory.payload.BuildInfoResponse; +import io.spring.concourse.releasescripts.artifactory.payload.DistributionRequest; +import io.spring.concourse.releasescripts.artifactory.payload.PromotionRequest; +import io.spring.concourse.releasescripts.bintray.BintrayService; +import io.spring.concourse.releasescripts.system.ConsoleLogger; + +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; + +/** + * Central class for interacting with Artifactory's REST API. + * + * @author Madhura Bhave + */ +@Component +public class ArtifactoryService { + + private static final String ARTIFACTORY_URL = "https://repo.spring.io"; + + private static final String PROMOTION_URL = ARTIFACTORY_URL + "/api/build/promote/"; + + private static final String BUILD_INFO_URL = ARTIFACTORY_URL + "/api/build/"; + + private static final String DISTRIBUTION_URL = ARTIFACTORY_URL + "/api/build/distribute/"; + + private static final String STAGING_REPO = "libs-staging-local"; + + private final RestTemplate restTemplate; + + private final ArtifactoryProperties artifactoryProperties; + + private final BintrayService bintrayService; + + private static final ConsoleLogger console = new ConsoleLogger(); + + public ArtifactoryService(RestTemplateBuilder builder, ArtifactoryProperties artifactoryProperties, + BintrayService bintrayService) { + this.artifactoryProperties = artifactoryProperties; + this.bintrayService = bintrayService; + String username = artifactoryProperties.getUsername(); + String password = artifactoryProperties.getPassword(); + builder = builder.basicAuthentication(username, password); + this.restTemplate = builder.build(); + } + + /** + * Move artifacts to a target repository in Artifactory. + * @param targetRepo the targetRepo + * @param releaseInfo the release information + */ + public void promote(String targetRepo, ReleaseInfo releaseInfo) { + PromotionRequest request = getPromotionRequest(targetRepo); + String buildName = releaseInfo.getBuildName(); + String buildNumber = releaseInfo.getBuildNumber(); + console.log("Promoting " + buildName + "/" + buildNumber + " to " + request.getTargetRepo()); + RequestEntity requestEntity = RequestEntity + .post(URI.create(PROMOTION_URL + buildName + "/" + buildNumber)).contentType(MediaType.APPLICATION_JSON) + .body(request); + try { + this.restTemplate.exchange(requestEntity, String.class); + } + catch (HttpClientErrorException ex) { + boolean isAlreadyPromoted = isAlreadyPromoted(buildName, buildNumber, request.getTargetRepo()); + if (isAlreadyPromoted) { + console.log("Already promoted."); + } + else { + console.log("Promotion failed."); + throw ex; + } + } + } + + private boolean isAlreadyPromoted(String buildName, String buildNumber, String targetRepo) { + try { + ResponseEntity entity = this.restTemplate + .getForEntity(BUILD_INFO_URL + buildName + "/" + buildNumber, BuildInfoResponse.class); + BuildInfoResponse.Status status = entity.getBody().getBuildInfo().getStatuses()[0]; + return status.getRepository().equals(targetRepo); + } + catch (HttpClientErrorException ex) { + return false; + } + } + + /** + * Deploy builds from Artifactory to Bintray. + * @param sourceRepo the source repo in Artifactory. + */ + public void distribute(String sourceRepo, ReleaseInfo releaseInfo) { + DistributionRequest request = new DistributionRequest(new String[] { sourceRepo }); + RequestEntity requestEntity = RequestEntity + .post(URI.create(DISTRIBUTION_URL + releaseInfo.getBuildName() + "/" + releaseInfo.getBuildNumber())) + .contentType(MediaType.APPLICATION_JSON).body(request); + try { + this.restTemplate.exchange(requestEntity, Object.class); + } + catch (HttpClientErrorException ex) { + console.log("Failed to distribute."); + throw ex; + } + if (!this.bintrayService.isDistributionComplete(releaseInfo)) { + throw new DistributionTimeoutException("Distribution timed out."); + } + + } + + private PromotionRequest getPromotionRequest(String targetRepo) { + return new PromotionRequest("staged", STAGING_REPO, targetRepo); + } + +} diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/artifactory/DistributionTimeoutException.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/artifactory/DistributionTimeoutException.java new file mode 100644 index 0000000000..c5ba1812b4 --- /dev/null +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/artifactory/DistributionTimeoutException.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2019 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 + * + * https://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 io.spring.concourse.releasescripts.artifactory; + +/** + * Runtime exception if artifact distribution to Bintray fails. + * + * @author Madhura Bhave + */ +public class DistributionTimeoutException extends RuntimeException { + + private String message; + + DistributionTimeoutException(String message) { + super(message); + this.message = message; + } + + @Override + public String getMessage() { + return this.message; + } + +} diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/artifactory/payload/BuildInfoResponse.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/artifactory/payload/BuildInfoResponse.java new file mode 100644 index 0000000000..1ec9ee57e9 --- /dev/null +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/artifactory/payload/BuildInfoResponse.java @@ -0,0 +1,118 @@ +/* + * Copyright 2012-2019 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 + * + * https://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 io.spring.concourse.releasescripts.artifactory.payload; + +/** + * Represents the response from Artifactory's buildInfo endpoint. + * + * @author Madhura Bhave + */ +public class BuildInfoResponse { + + private BuildInfo buildInfo; + + public BuildInfo getBuildInfo() { + return this.buildInfo; + } + + public void setBuildInfo(BuildInfo buildInfo) { + this.buildInfo = buildInfo; + } + + public static class BuildInfo { + + private String name; + + private String number; + + private String version; + + private Status[] statuses; + + private Module[] modules; + + public Status[] getStatuses() { + return this.statuses; + } + + public void setStatuses(Status[] statuses) { + this.statuses = statuses; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getNumber() { + return this.number; + } + + public void setNumber(String number) { + this.number = number; + } + + public Module[] getModules() { + return this.modules; + } + + public void setModules(Module[] modules) { + this.modules = modules; + } + + public String getVersion() { + return this.version; + } + + public void setVersion(String version) { + this.version = version; + } + + } + + public static class Status { + + private String repository; + + public String getRepository() { + return this.repository; + } + + public void setRepository(String repository) { + this.repository = repository; + } + + } + + public static class Module { + + private String id; + + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } + + } + +} diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/artifactory/payload/DistributionRequest.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/artifactory/payload/DistributionRequest.java new file mode 100644 index 0000000000..241f6a6600 --- /dev/null +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/artifactory/payload/DistributionRequest.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-2019 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 + * + * https://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 io.spring.concourse.releasescripts.artifactory.payload; + +/** + * Represents a request to distribute artifacts from Artifactory to Bintray. + * + * @author Madhura Bhave + */ +public class DistributionRequest { + + private final String[] sourceRepos; + + private final String targetRepo = "spring-distributions"; + + private final String async = "true"; + + public DistributionRequest(String[] sourceRepos) { + this.sourceRepos = sourceRepos; + } + + public String[] getSourceRepos() { + return sourceRepos; + } + + public String getTargetRepo() { + return targetRepo; + } + + public String getAsync() { + return async; + } + +} diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/artifactory/payload/PromotionRequest.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/artifactory/payload/PromotionRequest.java new file mode 100644 index 0000000000..cf9974c531 --- /dev/null +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/artifactory/payload/PromotionRequest.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2019 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 + * + * https://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 io.spring.concourse.releasescripts.artifactory.payload; + +/** + * Represents a request to promote artifacts from a sourceRepo to a targetRepo. + * + * @author Madhura Bhave + */ +public class PromotionRequest { + + private final String status; + + private final String sourceRepo; + + private final String targetRepo; + + public PromotionRequest(String status, String sourceRepo, String targetRepo) { + this.status = status; + this.sourceRepo = sourceRepo; + this.targetRepo = targetRepo; + } + + public String getTargetRepo() { + return this.targetRepo; + } + + public String getSourceRepo() { + return this.sourceRepo; + } + + public String getStatus() { + return this.status; + } + +} diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/bintray/BintrayProperties.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/bintray/BintrayProperties.java new file mode 100644 index 0000000000..7612ff7776 --- /dev/null +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/bintray/BintrayProperties.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2019 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 + * + * https://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 io.spring.concourse.releasescripts.bintray; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * {@link ConfigurationProperties @ConfigurationProperties} for the Bintray API. + * + * @author Madhura Bhave + */ +@ConfigurationProperties(prefix = "bintray") +public class BintrayProperties { + + private String username; + + private String apiKey; + + private String repo; + + private String subject; + + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getApiKey() { + return this.apiKey; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + + public String getRepo() { + return this.repo; + } + + public void setRepo(String repo) { + this.repo = repo; + } + + public String getSubject() { + return this.subject; + } + + public void setSubject(String subject) { + this.subject = subject; + } + +} diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/bintray/BintrayService.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/bintray/BintrayService.java new file mode 100644 index 0000000000..f3ee0d9cd4 --- /dev/null +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/bintray/BintrayService.java @@ -0,0 +1,136 @@ +/* + * Copyright 2012-2019 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 + * + * https://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 io.spring.concourse.releasescripts.bintray; + +import java.net.URI; + +import io.spring.concourse.releasescripts.ReleaseInfo; +import io.spring.concourse.releasescripts.sonatype.SonatypeProperties; +import io.spring.concourse.releasescripts.sonatype.SonatypeService; +import io.spring.concourse.releasescripts.system.ConsoleLogger; + +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; + +/** + * Central class for interacting with Bintray's REST API. + * + * @author Madhura Bhave + */ +@Component +public class BintrayService { + + private static final String BINTRAY_URL = "https://api.bintray.com/"; + + private static final String GRADLE_PLUGIN_REQUEST = "[ { \"name\": \"gradle-plugin\", \"values\": [\"org.springframework.boot:org.springframework.boot:spring-boot-gradle-plugin\"] } ]"; + + private final RestTemplate restTemplate; + + private final BintrayProperties bintrayProperties; + + private final SonatypeProperties sonatypeProperties; + + private final SonatypeService sonatypeService; + + private static final ConsoleLogger console = new ConsoleLogger(); + + public BintrayService(RestTemplateBuilder builder, BintrayProperties bintrayProperties, + SonatypeProperties sonatypeProperties, SonatypeService sonatypeService) { + this.bintrayProperties = bintrayProperties; + this.sonatypeProperties = sonatypeProperties; + this.sonatypeService = sonatypeService; + String username = bintrayProperties.getUsername(); + String apiKey = bintrayProperties.getApiKey(); + builder = builder.basicAuthentication(username, apiKey); + this.restTemplate = builder.build(); + } + + public boolean isDistributionComplete(ReleaseInfo releaseInfo) { + RequestEntity publishedFilesRequest = getRequest(releaseInfo, 0); + RequestEntity allFilesRequest = getRequest(releaseInfo, 1); + Object[] allFiles = this.restTemplate.exchange(allFilesRequest, Object[].class).getBody(); + int count = 0; + while (count < 120) { + Object[] publishedFiles = this.restTemplate.exchange(publishedFilesRequest, Object[].class).getBody(); + int unpublished = allFiles.length - publishedFiles.length; + if (unpublished == 0) { + return true; + } + count++; + try { + Thread.sleep(20000); + } + catch (InterruptedException e) { + + } + } + return false; + } + + private RequestEntity getRequest(ReleaseInfo releaseInfo, int includeUnpublished) { + return RequestEntity.get(URI.create(BINTRAY_URL + "packages/" + this.bintrayProperties.getSubject() + "/" + + this.bintrayProperties.getRepo() + "/" + releaseInfo.getGroupId() + "/versions/" + + releaseInfo.getVersion() + "/files?include_unpublished=" + includeUnpublished)).build(); + } + + /** + * Add attributes to Spring Boot's Gradle plugin. + * @param releaseInfo the release information + */ + public void publishGradlePlugin(ReleaseInfo releaseInfo) { + RequestEntity requestEntity = RequestEntity + .post(URI.create(BINTRAY_URL + "packages/" + this.bintrayProperties.getSubject() + "/" + + this.bintrayProperties.getRepo() + "/" + releaseInfo.getGroupId() + "/versions/" + + releaseInfo.getVersion() + "/attributes")) + .contentType(MediaType.APPLICATION_JSON).body(GRADLE_PLUGIN_REQUEST); + try { + this.restTemplate.exchange(requestEntity, Object.class); + } + catch (HttpClientErrorException ex) { + console.log("Failed to add attribute to gradle plugin."); + throw ex; + } + } + + /** + * Sync artifacts from Bintray to Maven Central. + * @param releaseInfo the release information + */ + public void syncToMavenCentral(ReleaseInfo releaseInfo) { + console.log("Calling Bintray to sync to Sonatype"); + if (this.sonatypeService.artifactsPublished(releaseInfo)) { + return; + } + RequestEntity requestEntity = RequestEntity + .post(URI.create(String.format(BINTRAY_URL + "maven_central_sync/%s/%s/%s/versions/%s", + this.bintrayProperties.getSubject(), this.bintrayProperties.getRepo(), releaseInfo.getGroupId(), + releaseInfo.getVersion()))) + .contentType(MediaType.APPLICATION_JSON).body(this.sonatypeProperties); + try { + this.restTemplate.exchange(requestEntity, Object.class); + } + catch (HttpClientErrorException ex) { + console.log("Failed to sync."); + throw ex; + } + } + +} diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/Command.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/Command.java new file mode 100644 index 0000000000..e8541ae128 --- /dev/null +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/Command.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2019 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 + * + * https://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 io.spring.concourse.releasescripts.command; + +import org.springframework.boot.ApplicationArguments; +import org.springframework.util.ClassUtils; + +/** + * @author Madhura Bhave + */ +public interface Command { + + default String getName() { + String name = ClassUtils.getShortName(getClass()); + int lastDot = name.lastIndexOf("."); + if (lastDot != -1) { + name = name.substring(lastDot + 1, name.length()); + } + if (name.endsWith("Command")) { + name = name.substring(0, name.length() - "Command".length()); + } + return name.toLowerCase(); + } + + void run(ApplicationArguments args) throws Exception; + +} \ No newline at end of file diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/CommandProcessor.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/CommandProcessor.java new file mode 100644 index 0000000000..6b7de70dff --- /dev/null +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/CommandProcessor.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2019 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 + * + * https://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 io.spring.concourse.releasescripts.command; + +import java.util.Collections; +import java.util.List; + +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +/** + * {@link ApplicationRunner} to delegate incoming requests to commands. + * + * @author Madhura Bhave + */ +@Component +public class CommandProcessor implements ApplicationRunner { + + private final List commands; + + public CommandProcessor(List commands) { + this.commands = Collections.unmodifiableList(commands); + } + + @Override + public void run(ApplicationArguments args) throws Exception { + List nonOptionArgs = args.getNonOptionArgs(); + Assert.state(!nonOptionArgs.isEmpty(), "No command argument specified"); + String request = nonOptionArgs.get(0); + this.commands.stream().filter((c) -> c.getName().equals(request)).findFirst() + .orElseThrow(() -> new IllegalStateException("Unknown command '" + request + "'")).run(args); + } + +} diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/DistributeCommand.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/DistributeCommand.java new file mode 100644 index 0000000000..3d9e97ee94 --- /dev/null +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/DistributeCommand.java @@ -0,0 +1,67 @@ +/* + * Copyright 2012-2019 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 + * + * https://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 io.spring.concourse.releasescripts.command; + +import java.io.File; +import java.nio.file.Files; +import java.util.List; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.spring.concourse.releasescripts.ReleaseInfo; +import io.spring.concourse.releasescripts.ReleaseType; +import io.spring.concourse.releasescripts.artifactory.ArtifactoryService; +import io.spring.concourse.releasescripts.artifactory.payload.BuildInfoResponse; + +import org.springframework.boot.ApplicationArguments; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +/** + * Command used to deploy builds from Artifactory to Bintray. + * + * @author Madhura Bhave + */ +@Component +public class DistributeCommand implements Command { + + private final ArtifactoryService service; + + private final ObjectMapper objectMapper; + + public DistributeCommand(ArtifactoryService service, ObjectMapper objectMapper) { + this.service = service; + this.objectMapper = objectMapper; + } + + @Override + public void run(ApplicationArguments args) throws Exception { + List nonOptionArgs = args.getNonOptionArgs(); + Assert.state(!nonOptionArgs.isEmpty(), "No command argument specified"); + Assert.state(nonOptionArgs.size() == 3, "Release type or build info not specified"); + String releaseType = nonOptionArgs.get(1); + ReleaseType type = ReleaseType.from(releaseType); + if (!ReleaseType.RELEASE.equals(type)) { + return; + } + String buildInfoLocation = nonOptionArgs.get(2); + byte[] content = Files.readAllBytes(new File(buildInfoLocation).toPath()); + BuildInfoResponse buildInfoResponse = this.objectMapper.readValue(content, BuildInfoResponse.class); + ReleaseInfo releaseInfo = ReleaseInfo.from(buildInfoResponse.getBuildInfo()); + this.service.distribute(type.getRepo(), releaseInfo); + } + +} diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/PromoteCommand.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/PromoteCommand.java new file mode 100644 index 0000000000..230059d88f --- /dev/null +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/PromoteCommand.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2019 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 + * + * https://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 io.spring.concourse.releasescripts.command; + +import java.io.File; +import java.nio.file.Files; +import java.util.List; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.spring.concourse.releasescripts.ReleaseInfo; +import io.spring.concourse.releasescripts.ReleaseType; +import io.spring.concourse.releasescripts.artifactory.ArtifactoryService; +import io.spring.concourse.releasescripts.artifactory.payload.BuildInfoResponse; + +import org.springframework.boot.ApplicationArguments; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +/** + * Command used to move the build artifacts to a target repository in Artifactory. + * + * @author Madhura Bhave + */ +@Component +public class PromoteCommand implements Command { + + private final ArtifactoryService service; + + private final ObjectMapper objectMapper; + + public PromoteCommand(ArtifactoryService service, ObjectMapper objectMapper) { + this.service = service; + this.objectMapper = objectMapper; + } + + @Override + public void run(ApplicationArguments args) throws Exception { + List nonOptionArgs = args.getNonOptionArgs(); + Assert.state(!nonOptionArgs.isEmpty(), "No command argument specified"); + Assert.state(nonOptionArgs.size() == 3, "Release type or build info location not specified"); + String releaseType = nonOptionArgs.get(1); + ReleaseType type = ReleaseType.from(releaseType); + String buildInfoLocation = nonOptionArgs.get(2); + byte[] content = Files.readAllBytes(new File(buildInfoLocation).toPath()); + BuildInfoResponse buildInfoResponse = this.objectMapper.readValue(new String(content), BuildInfoResponse.class); + ReleaseInfo releaseInfo = ReleaseInfo.from(buildInfoResponse.getBuildInfo()); + this.service.promote(type.getRepo(), releaseInfo); + } + +} diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/PublishGradlePlugin.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/PublishGradlePlugin.java new file mode 100644 index 0000000000..68af1dce26 --- /dev/null +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/PublishGradlePlugin.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2019 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 + * + * https://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 io.spring.concourse.releasescripts.command; + +import java.io.File; +import java.nio.file.Files; +import java.util.List; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.spring.concourse.releasescripts.ReleaseInfo; +import io.spring.concourse.releasescripts.ReleaseType; +import io.spring.concourse.releasescripts.artifactory.payload.BuildInfoResponse; +import io.spring.concourse.releasescripts.bintray.BintrayService; + +import org.springframework.boot.ApplicationArguments; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +/** + * Command used to add attributes to the gradle plugin. + * + * @author Madhura Bhave + */ +@Component +public class PublishGradlePlugin implements Command { + + private static final String PUBLISH_GRADLE_PLUGIN_COMMAND = "publishGradlePlugin"; + + private final BintrayService service; + + private final ObjectMapper objectMapper; + + public PublishGradlePlugin(BintrayService service, ObjectMapper objectMapper) { + this.service = service; + this.objectMapper = objectMapper; + } + + @Override + public String getName() { + return PUBLISH_GRADLE_PLUGIN_COMMAND; + } + + @Override + public void run(ApplicationArguments args) throws Exception { + List nonOptionArgs = args.getNonOptionArgs(); + Assert.state(!nonOptionArgs.isEmpty(), "No command argument specified"); + Assert.state(nonOptionArgs.size() == 3, "Release type or build info not specified"); + String releaseType = nonOptionArgs.get(1); + ReleaseType type = ReleaseType.from(releaseType); + if (!ReleaseType.RELEASE.equals(type)) { + return; + } + String buildInfoLocation = nonOptionArgs.get(2); + byte[] content = Files.readAllBytes(new File(buildInfoLocation).toPath()); + BuildInfoResponse buildInfoResponse = this.objectMapper.readValue(content, BuildInfoResponse.class); + ReleaseInfo releaseInfo = ReleaseInfo.from(buildInfoResponse.getBuildInfo()); + this.service.publishGradlePlugin(releaseInfo); + } + +} diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/SyncToCentralCommand.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/SyncToCentralCommand.java new file mode 100644 index 0000000000..23c20482eb --- /dev/null +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/command/SyncToCentralCommand.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2019 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 + * + * https://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 io.spring.concourse.releasescripts.command; + +import java.io.File; +import java.nio.file.Files; +import java.util.List; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.spring.concourse.releasescripts.ReleaseInfo; +import io.spring.concourse.releasescripts.ReleaseType; +import io.spring.concourse.releasescripts.artifactory.payload.BuildInfoResponse; +import io.spring.concourse.releasescripts.bintray.BintrayService; + +import org.springframework.boot.ApplicationArguments; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +/** + * Command used to sync artifacts to Maven Central. + * + * @author Madhura Bhave + */ +@Component +public class SyncToCentralCommand implements Command { + + private static final String SYNC_TO_CENTRAL_COMMAND = "syncToCentral"; + + private final BintrayService service; + + private final ObjectMapper objectMapper; + + public SyncToCentralCommand(BintrayService service, ObjectMapper objectMapper) { + this.service = service; + this.objectMapper = objectMapper; + } + + @Override + public String getName() { + return SYNC_TO_CENTRAL_COMMAND; + } + + @Override + public void run(ApplicationArguments args) throws Exception { + List nonOptionArgs = args.getNonOptionArgs(); + Assert.state(!nonOptionArgs.isEmpty(), "No command argument specified"); + Assert.state(nonOptionArgs.size() == 3, "Release type or build info not specified"); + String releaseType = nonOptionArgs.get(1); + ReleaseType type = ReleaseType.from(releaseType); + if (!ReleaseType.RELEASE.equals(type)) { + return; + } + String buildInfoLocation = nonOptionArgs.get(2); + byte[] content = Files.readAllBytes(new File(buildInfoLocation).toPath()); + BuildInfoResponse buildInfoResponse = this.objectMapper.readValue(content, BuildInfoResponse.class); + ReleaseInfo releaseInfo = ReleaseInfo.from(buildInfoResponse.getBuildInfo()); + this.service.syncToMavenCentral(releaseInfo); + } + +} diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sonatype/SonatypeProperties.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sonatype/SonatypeProperties.java new file mode 100644 index 0000000000..165bcfea38 --- /dev/null +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sonatype/SonatypeProperties.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2019 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 + * + * https://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 io.spring.concourse.releasescripts.sonatype; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * {@link ConfigurationProperties @ConfigurationProperties} for Sonatype. + * + * @author Madhura Bhave + */ +@ConfigurationProperties(prefix = "sonatype") +public class SonatypeProperties { + + @JsonProperty("username") + private String userToken; + + @JsonProperty("password") + private String passwordToken; + + public String getUserToken() { + return this.userToken; + } + + public void setUserToken(String userToken) { + this.userToken = userToken; + } + + public String getPasswordToken() { + return this.passwordToken; + } + + public void setPasswordToken(String passwordToken) { + this.passwordToken = passwordToken; + } + +} diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sonatype/SonatypeService.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sonatype/SonatypeService.java new file mode 100644 index 0000000000..61a4f3e26d --- /dev/null +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/sonatype/SonatypeService.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2019 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 + * + * https://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 io.spring.concourse.releasescripts.sonatype; + +import io.spring.concourse.releasescripts.ReleaseInfo; +import io.spring.concourse.releasescripts.system.ConsoleLogger; + +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; + +/** + * Central class for interacting with Sonatype. + * + * @author Madhura Bhave + */ +@Component +public class SonatypeService { + + private static final String SONATYPE_REPOSITORY_URI = "https://oss.sonatype.org/service/local/repositories/releases/content/org/springframework/boot/spring-boot/"; + + private final RestTemplate restTemplate; + + private final SonatypeProperties sonatypeProperties; + + private static final ConsoleLogger console = new ConsoleLogger(); + + public SonatypeService(RestTemplateBuilder builder, SonatypeProperties sonatypeProperties) { + this.sonatypeProperties = sonatypeProperties; + String username = sonatypeProperties.getUserToken(); + String apiKey = sonatypeProperties.getPasswordToken(); + builder = builder.basicAuthentication(username, apiKey); + this.restTemplate = builder.build(); + } + + /** + * Checks if artifacts are already published to Maven Central. + * @return true if artifacts are published + * @param releaseInfo the release information + */ + public boolean artifactsPublished(ReleaseInfo releaseInfo) { + try { + ResponseEntity entity = this.restTemplate + .getForEntity(String.format(SONATYPE_REPOSITORY_URI + "%s/spring-boot-%s.jar.sha1", + releaseInfo.getVersion(), releaseInfo.getVersion()), Object.class); + if (HttpStatus.OK.equals(entity.getStatusCode())) { + console.log("Already published to Sonatype."); + return true; + } + } + catch (HttpClientErrorException ex) { + + } + return false; + } + +} diff --git a/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/system/ConsoleLogger.java b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/system/ConsoleLogger.java new file mode 100644 index 0000000000..1b8e6d29fa --- /dev/null +++ b/ci/images/releasescripts/src/main/java/io/spring/concourse/releasescripts/system/ConsoleLogger.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2019 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 + * + * https://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 io.spring.concourse.releasescripts.system; + +import org.slf4j.helpers.MessageFormatter; + +/** + * Simple console logger used to output progress messages. + * + * @author Madhura Bhave + */ +public class ConsoleLogger { + + public void log(String message, Object... args) { + System.err.println(MessageFormatter.arrayFormat(message, args).getMessage()); + } + +} diff --git a/ci/images/releasescripts/src/main/resources/application.properties b/ci/images/releasescripts/src/main/resources/application.properties new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/ci/images/releasescripts/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/artifactory/ArtifactoryServiceTests.java b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/artifactory/ArtifactoryServiceTests.java new file mode 100644 index 0000000000..d0b8a2b34d --- /dev/null +++ b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/artifactory/ArtifactoryServiceTests.java @@ -0,0 +1,199 @@ +/* + * Copyright 2012-2019 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 + * + * https://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 io.spring.concourse.releasescripts.artifactory; + +import io.spring.concourse.releasescripts.ReleaseInfo; +import io.spring.concourse.releasescripts.bintray.BintrayService; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.autoconfigure.web.client.RestClientTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.test.web.client.response.DefaultResponseCreator; +import org.springframework.util.Base64Utils; +import org.springframework.web.client.HttpClientErrorException; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.content; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.header; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +/** + * Tests for {@link ArtifactoryService}. + * + * @author Madhura Bhave + */ +@RestClientTest(ArtifactoryService.class) +@EnableConfigurationProperties(ArtifactoryProperties.class) +class ArtifactoryServiceTests { + + @Autowired + private ArtifactoryService service; + + @MockBean + private BintrayService bintrayService; + + @Autowired + private ArtifactoryProperties properties; + + @Autowired + private MockRestServiceServer server; + + @AfterEach + void tearDown() { + this.server.reset(); + } + + @Test + void promoteWhenSuccessful() { + this.server + .expect(requestTo( + "https://repo.spring.io/api/build/promote/" + "example-build" + "/" + "example-build-1")) + .andExpect(method(HttpMethod.POST)) + .andExpect(content().json( + "{\"status\": \"staged\", \"sourceRepo\": \"libs-staging-local\", \"targetRepo\": \"libs-milestone-local\"}")) + .andExpect(header("Authorization", "Basic " + Base64Utils.encodeToString(String + .format("%s:%s", this.properties.getUsername(), this.properties.getPassword()).getBytes()))) + .andExpect(header("Content-Type", MediaType.APPLICATION_JSON.toString())).andRespond(withSuccess()); + this.service.promote("libs-milestone-local", getReleaseInfo()); + this.server.verify(); + } + + @Test + void promoteWhenArtifactsAlreadyPromoted() { + this.server + .expect(requestTo( + "https://repo.spring.io/api/build/promote/" + "example-build" + "/" + "example-build-1")) + .andRespond(withStatus(HttpStatus.CONFLICT)); + this.server.expect(requestTo("https://repo.spring.io/api/build/" + "example-build" + "/" + "example-build-1")) + .andRespond(withJsonFrom("build-info-response.json")); + this.service.promote("libs-release-local", getReleaseInfo()); + this.server.verify(); + } + + @Test + void promoteWhenCheckForArtifactsAlreadyPromotedFails() { + this.server + .expect(requestTo( + "https://repo.spring.io/api/build/promote/" + "example-build" + "/" + "example-build-1")) + .andRespond(withStatus(HttpStatus.CONFLICT)); + this.server.expect(requestTo("https://repo.spring.io/api/build/" + "example-build" + "/" + "example-build-1")) + .andRespond(withStatus(HttpStatus.FORBIDDEN)); + assertThatExceptionOfType(HttpClientErrorException.class) + .isThrownBy(() -> this.service.promote("libs-release-local", getReleaseInfo())); + this.server.verify(); + } + + @Test + void promoteWhenPromotionFails() { + this.server + .expect(requestTo( + "https://repo.spring.io/api/build/promote/" + "example-build" + "/" + "example-build-1")) + .andRespond(withStatus(HttpStatus.CONFLICT)); + this.server.expect(requestTo("https://repo.spring.io/api/build/" + "example-build" + "/" + "example-build-1")) + .andRespond(withJsonFrom("staged-build-info-response.json")); + assertThatExceptionOfType(HttpClientErrorException.class) + .isThrownBy(() -> this.service.promote("libs-release-local", getReleaseInfo())); + this.server.verify(); + } + + @Test + void distributeWhenSuccessful() throws Exception { + ReleaseInfo releaseInfo = getReleaseInfo(); + given(this.bintrayService.isDistributionComplete(releaseInfo)).willReturn(true); + this.server + .expect(requestTo( + "https://repo.spring.io/api/build/distribute/" + "example-build" + "/" + "example-build-1")) + .andExpect(method(HttpMethod.POST)) + .andExpect(content().json( + "{\"sourceRepos\": [\"libs-release-local\"], \"targetRepo\" : \"spring-distributions\", \"async\":\"true\"}")) + .andExpect(header("Authorization", "Basic " + Base64Utils.encodeToString(String + .format("%s:%s", this.properties.getUsername(), this.properties.getPassword()).getBytes()))) + .andExpect(header("Content-Type", MediaType.APPLICATION_JSON.toString())).andRespond(withSuccess()); + this.service.distribute("libs-release-local", releaseInfo); + this.server.verify(); + verify(this.bintrayService, times(1)).isDistributionComplete(releaseInfo); + } + + @Test + void distributeWhenFailure() throws Exception { + ReleaseInfo releaseInfo = getReleaseInfo(); + this.server + .expect(requestTo( + "https://repo.spring.io/api/build/distribute/" + "example-build" + "/" + "example-build-1")) + .andExpect(method(HttpMethod.POST)) + .andExpect(content().json( + "{\"sourceRepos\": [\"libs-release-local\"], \"targetRepo\" : \"spring-distributions\", \"async\":\"true\"}")) + .andExpect(header("Authorization", "Basic " + Base64Utils.encodeToString(String + .format("%s:%s", this.properties.getUsername(), this.properties.getPassword()).getBytes()))) + .andExpect(header("Content-Type", MediaType.APPLICATION_JSON.toString())) + .andRespond(withStatus(HttpStatus.FORBIDDEN)); + assertThatExceptionOfType(HttpClientErrorException.class) + .isThrownBy(() -> this.service.distribute("libs-release-local", releaseInfo)); + this.server.verify(); + verifyNoInteractions(this.bintrayService); + } + + @Test + void distributeWhenGettingPackagesTimesOut() throws Exception { + ReleaseInfo releaseInfo = getReleaseInfo(); + given(this.bintrayService.isDistributionComplete(releaseInfo)).willReturn(false); + this.server + .expect(requestTo( + "https://repo.spring.io/api/build/distribute/" + "example-build" + "/" + "example-build-1")) + .andExpect(method(HttpMethod.POST)) + .andExpect(content().json( + "{\"sourceRepos\": [\"libs-release-local\"], \"targetRepo\" : \"spring-distributions\", \"async\":\"true\"}")) + .andExpect(header("Authorization", "Basic " + Base64Utils.encodeToString(String + .format("%s:%s", this.properties.getUsername(), this.properties.getPassword()).getBytes()))) + .andExpect(header("Content-Type", MediaType.APPLICATION_JSON.toString())).andRespond(withSuccess()); + assertThatExceptionOfType(DistributionTimeoutException.class) + .isThrownBy(() -> this.service.distribute("libs-release-local", releaseInfo)); + this.server.verify(); + verify(this.bintrayService, times(1)).isDistributionComplete(releaseInfo); + } + + private ReleaseInfo getReleaseInfo() { + ReleaseInfo releaseInfo = new ReleaseInfo(); + releaseInfo.setBuildName("example-build"); + releaseInfo.setBuildNumber("example-build-1"); + return releaseInfo; + } + + private DefaultResponseCreator withJsonFrom(String path) { + return withSuccess(getClassPathResource(path), MediaType.APPLICATION_JSON); + } + + private ClassPathResource getClassPathResource(String path) { + return new ClassPathResource(path, getClass()); + } + +} \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/bintray/BintrayServiceTests.java b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/bintray/BintrayServiceTests.java new file mode 100644 index 0000000000..0cbccbe149 --- /dev/null +++ b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/bintray/BintrayServiceTests.java @@ -0,0 +1,155 @@ +/* + * Copyright 2012-2019 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 + * + * https://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 io.spring.concourse.releasescripts.bintray; + +import io.spring.concourse.releasescripts.ReleaseInfo; +import io.spring.concourse.releasescripts.sonatype.SonatypeProperties; +import io.spring.concourse.releasescripts.sonatype.SonatypeService; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.autoconfigure.web.client.RestClientTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.test.web.client.ExpectedCount; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.test.web.client.response.DefaultResponseCreator; +import org.springframework.util.Base64Utils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.content; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.header; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +/** + * Tests for {@link BintrayService}. + * + * @author Madhura Bhave + */ +@RestClientTest(BintrayService.class) +@EnableConfigurationProperties({ BintrayProperties.class, SonatypeProperties.class }) +class BintrayServiceTests { + + @Autowired + private BintrayService service; + + @Autowired + private BintrayProperties properties; + + @Autowired + private SonatypeProperties sonatypeProperties; + + @MockBean + private SonatypeService sonatypeService; + + @Autowired + private MockRestServiceServer server; + + @AfterEach + void tearDown() { + this.server.reset(); + } + + @Test + void isDistributionComplete() throws Exception { + setupGetPackageFiles(1, "all-package-files.json"); + setupGetPackageFiles(0, "published-files.json"); + setupGetPackageFiles(0, "all-package-files.json"); + assertThat(this.service.isDistributionComplete(getReleaseInfo())).isTrue(); + this.server.verify(); + } + + private void setupGetPackageFiles(int includeUnpublished, String path) { + this.server + .expect(requestTo(String.format( + "https://api.bintray.com/packages/%s/%s/%s/versions/%s/files?include_unpublished=%s", + this.properties.getSubject(), this.properties.getRepo(), "example", "1.1.0.RELEASE", + includeUnpublished))) + .andExpect(method(HttpMethod.GET)) + .andExpect(header("Authorization", "Basic " + Base64Utils.encodeToString( + String.format("%s:%s", this.properties.getUsername(), this.properties.getApiKey()).getBytes()))) + .andRespond(withJsonFrom(path)); + } + + @Test + void publishGradlePluginWhenSuccessful() { + this.server + .expect(requestTo(String.format("https://api.bintray.com/packages/%s/%s/%s/versions/%s/attributes", + this.properties.getSubject(), this.properties.getRepo(), "example", "1.1.0.RELEASE"))) + .andExpect(method(HttpMethod.POST)) + .andExpect(content().json( + "[ { \"name\": \"gradle-plugin\", \"values\": [\"org.springframework.boot:org.springframework.boot:spring-boot-gradle-plugin\"] } ]")) + .andExpect(header("Authorization", "Basic " + Base64Utils.encodeToString( + String.format("%s:%s", this.properties.getUsername(), this.properties.getApiKey()).getBytes()))) + .andExpect(header("Content-Type", MediaType.APPLICATION_JSON.toString())).andRespond(withSuccess()); + this.service.publishGradlePlugin(getReleaseInfo()); + this.server.verify(); + } + + @Test + void syncToMavenCentralWhenSuccessful() { + ReleaseInfo releaseInfo = getReleaseInfo(); + given(this.sonatypeService.artifactsPublished(releaseInfo)).willReturn(false); + this.server + .expect(requestTo(String.format("https://api.bintray.com/maven_central_sync/%s/%s/%s/versions/%s", + this.properties.getSubject(), this.properties.getRepo(), "example", "1.1.0.RELEASE"))) + .andExpect(method(HttpMethod.POST)) + .andExpect(content().json(String.format("{\"username\": \"%s\", \"password\": \"%s\"}", + this.sonatypeProperties.getUserToken(), this.sonatypeProperties.getPasswordToken()))) + .andExpect(header("Authorization", "Basic " + Base64Utils.encodeToString( + String.format("%s:%s", this.properties.getUsername(), this.properties.getApiKey()).getBytes()))) + .andExpect(header("Content-Type", MediaType.APPLICATION_JSON.toString())).andRespond(withSuccess()); + this.service.syncToMavenCentral(releaseInfo); + this.server.verify(); + } + + @Test + void syncToMavenCentralWhenArtifactsAlreadyPublished() { + ReleaseInfo releaseInfo = getReleaseInfo(); + given(this.sonatypeService.artifactsPublished(releaseInfo)).willReturn(true); + this.server.expect(ExpectedCount.never(), + requestTo(String.format("https://api.bintray.com/maven_central_sync/%s/%s/%s/versions/%s", + this.properties.getSubject(), this.properties.getRepo(), "example", "1.1.0.RELEASE"))); + this.service.syncToMavenCentral(releaseInfo); + this.server.verify(); + } + + private ReleaseInfo getReleaseInfo() { + ReleaseInfo releaseInfo = new ReleaseInfo(); + releaseInfo.setBuildName("example-build"); + releaseInfo.setBuildNumber("example-build-1"); + releaseInfo.setGroupId("example"); + releaseInfo.setVersion("1.1.0.RELEASE"); + return releaseInfo; + } + + private DefaultResponseCreator withJsonFrom(String path) { + return withSuccess(getClassPathResource(path), MediaType.APPLICATION_JSON); + } + + private ClassPathResource getClassPathResource(String path) { + return new ClassPathResource(path, getClass()); + } + +} \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/CommandProcessorTests.java b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/CommandProcessorTests.java new file mode 100644 index 0000000000..c21953c559 --- /dev/null +++ b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/CommandProcessorTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-2019 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 + * + * https://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 io.spring.concourse.releasescripts.command; + +import java.util.Arrays; +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.DefaultApplicationArguments; + +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link CommandProcessor}. + * + * @author Madhura Bhave + */ +class CommandProcessorTests { + + private static final String[] NO_ARGS = {}; + + @Test + void runWhenNoArgumentThrowsException() { + CommandProcessor processor = new CommandProcessor(Collections.singletonList(mock(Command.class))); + assertThatIllegalStateException().isThrownBy(() -> processor.run(new DefaultApplicationArguments(NO_ARGS))) + .withMessage("No command argument specified"); + } + + @Test + void runWhenUnknownCommandThrowsException() { + Command fooCommand = mock(Command.class); + given(fooCommand.getName()).willReturn("foo"); + CommandProcessor processor = new CommandProcessor(Collections.singletonList(fooCommand)); + DefaultApplicationArguments args = new DefaultApplicationArguments(new String[] { "bar", "go" }); + assertThatIllegalStateException().isThrownBy(() -> processor.run(args)).withMessage("Unknown command 'bar'"); + } + + @Test + void runDelegatesToCommand() throws Exception { + Command fooCommand = mock(Command.class); + given(fooCommand.getName()).willReturn("foo"); + Command barCommand = mock(Command.class); + given(barCommand.getName()).willReturn("bar"); + CommandProcessor processor = new CommandProcessor(Arrays.asList(fooCommand, barCommand)); + DefaultApplicationArguments args = new DefaultApplicationArguments(new String[] { "bar", "go" }); + processor.run(args); + verify(fooCommand, never()).run(any()); + verify(barCommand).run(args); + } + +} \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/DistributeCommandTests.java b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/DistributeCommandTests.java new file mode 100644 index 0000000000..c8949f2240 --- /dev/null +++ b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/DistributeCommandTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2012-2019 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 + * + * https://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 io.spring.concourse.releasescripts.command; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.spring.concourse.releasescripts.ReleaseInfo; +import io.spring.concourse.releasescripts.ReleaseType; +import io.spring.concourse.releasescripts.artifactory.ArtifactoryService; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import org.springframework.boot.DefaultApplicationArguments; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +/** + * Tests for {@link DistributeCommand}. + * + * @author Madhura Bhave + */ +class DistributeCommandTests { + + @Mock + private ArtifactoryService service; + + private DistributeCommand command; + + private ObjectMapper objectMapper; + + @BeforeEach + void setup() { + MockitoAnnotations.initMocks(this); + this.objectMapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + this.command = new DistributeCommand(this.service, objectMapper); + } + + @Test + void distributeWhenReleaseTypeNotSpecifiedShouldThrowException() { + Assertions.assertThatIllegalStateException() + .isThrownBy(() -> this.command.run(new DefaultApplicationArguments("distribute"))); + } + + @Test + void distributeWhenReleaseTypeMilestoneShouldDoNothing() throws Exception { + this.command.run(new DefaultApplicationArguments("distribute", "M", getBuildInfoLocation())); + verifyNoInteractions(this.service); + } + + @Test + void distributeWhenReleaseTypeRCShouldDoNothing() throws Exception { + this.command.run(new DefaultApplicationArguments("distribute", "RC", getBuildInfoLocation())); + verifyNoInteractions(this.service); + } + + @Test + void distributeWhenReleaseTypeReleaseShouldCallService() throws Exception { + ArgumentCaptor captor = ArgumentCaptor.forClass(ReleaseInfo.class); + this.command.run(new DefaultApplicationArguments("distribute", "RELEASE", getBuildInfoLocation())); + verify(this.service).distribute(eq(ReleaseType.RELEASE.getRepo()), captor.capture()); + ReleaseInfo releaseInfo = captor.getValue(); + assertThat(releaseInfo.getBuildName()).isEqualTo("example"); + assertThat(releaseInfo.getBuildNumber()).isEqualTo("example-build-1"); + assertThat(releaseInfo.getGroupId()).isEqualTo("org.example.demo"); + assertThat(releaseInfo.getVersion()).isEqualTo("2.2.0"); + } + + private String getBuildInfoLocation() throws Exception { + return new ClassPathResource("build-info-response.json", ArtifactoryService.class).getFile().getAbsolutePath(); + } + +} \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/PromoteCommandTests.java b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/PromoteCommandTests.java new file mode 100644 index 0000000000..f1c776094a --- /dev/null +++ b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/PromoteCommandTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2012-2019 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 + * + * https://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 io.spring.concourse.releasescripts.command; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.spring.concourse.releasescripts.ReleaseInfo; +import io.spring.concourse.releasescripts.ReleaseType; +import io.spring.concourse.releasescripts.artifactory.ArtifactoryService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import org.springframework.boot.DefaultApplicationArguments; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +/** + * @author Madhura Bhave + */ +class PromoteCommandTests { + + @Mock + private ArtifactoryService service; + + private PromoteCommand command; + + private ObjectMapper objectMapper; + + @BeforeEach + void setup() { + MockitoAnnotations.initMocks(this); + this.objectMapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + this.command = new PromoteCommand(this.service, this.objectMapper); + } + + @Test + void runWhenReleaseTypeNotSpecifiedShouldThrowException() { + assertThatIllegalStateException() + .isThrownBy(() -> this.command.run(new DefaultApplicationArguments("promote"))); + } + + @Test + void runWhenReleaseTypeMilestoneShouldCallService() throws Exception { + this.command.run(new DefaultApplicationArguments("promote", "M", getBuildInfoLocation())); + verify(this.service).promote(eq(ReleaseType.MILESTONE.getRepo()), any(ReleaseInfo.class)); + } + + @Test + void runWhenReleaseTypeRCShouldCallService() throws Exception { + this.command.run(new DefaultApplicationArguments("promote", "RC", getBuildInfoLocation())); + verify(this.service).promote(eq(ReleaseType.RELEASE_CANDIDATE.getRepo()), any(ReleaseInfo.class)); + } + + @Test + void runWhenReleaseTypeReleaseShouldCallService() throws Exception { + this.command.run(new DefaultApplicationArguments("promote", "RELEASE", getBuildInfoLocation())); + verify(this.service).promote(eq(ReleaseType.RELEASE.getRepo()), any(ReleaseInfo.class)); + } + + @Test + void runWhenBuildInfoNotSpecifiedShouldThrowException() { + assertThatIllegalStateException() + .isThrownBy(() -> this.command.run(new DefaultApplicationArguments("promote", "M"))); + } + + @Test + void runShouldParseBuildInfoProperly() throws Exception { + ArgumentCaptor captor = ArgumentCaptor.forClass(ReleaseInfo.class); + this.command.run(new DefaultApplicationArguments("promote", "RELEASE", getBuildInfoLocation())); + verify(this.service).promote(eq(ReleaseType.RELEASE.getRepo()), captor.capture()); + ReleaseInfo releaseInfo = captor.getValue(); + assertThat(releaseInfo.getBuildName()).isEqualTo("example"); + assertThat(releaseInfo.getBuildNumber()).isEqualTo("example-build-1"); + assertThat(releaseInfo.getGroupId()).isEqualTo("org.example.demo"); + assertThat(releaseInfo.getVersion()).isEqualTo("2.2.0"); + } + + private String getBuildInfoLocation() throws Exception { + return new ClassPathResource("build-info-response.json", ArtifactoryService.class).getFile().getAbsolutePath(); + } + +} \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/PublishGradlePluginTests.java b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/PublishGradlePluginTests.java new file mode 100644 index 0000000000..e7ceb1c8b4 --- /dev/null +++ b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/PublishGradlePluginTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2012-2019 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 + * + * https://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 io.spring.concourse.releasescripts.command; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.spring.concourse.releasescripts.ReleaseInfo; +import io.spring.concourse.releasescripts.artifactory.ArtifactoryService; +import io.spring.concourse.releasescripts.bintray.BintrayService; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import org.springframework.boot.DefaultApplicationArguments; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +/** + * Tests for {@link PublishGradlePlugin}. + * + * @author Madhura Bhave + */ +class PublishGradlePluginTests { + + @Mock + private BintrayService service; + + private PublishGradlePlugin command; + + private ObjectMapper objectMapper; + + @BeforeEach + void setup() { + MockitoAnnotations.initMocks(this); + this.objectMapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + this.command = new PublishGradlePlugin(this.service, objectMapper); + } + + @Test + void runWhenReleaseTypeNotSpecifiedShouldThrowException() throws Exception { + Assertions.assertThatIllegalStateException() + .isThrownBy(() -> this.command.run(new DefaultApplicationArguments("publishGradlePlugin"))); + } + + @Test + void runWhenReleaseTypeMilestoneShouldDoNothing() throws Exception { + this.command.run(new DefaultApplicationArguments("publishGradlePlugin", "M", getBuildInfoLocation())); + verifyNoInteractions(this.service); + } + + @Test + void runWhenReleaseTypeRCShouldDoNothing() throws Exception { + this.command.run(new DefaultApplicationArguments("publishGradlePlugin", "RC", getBuildInfoLocation())); + verifyNoInteractions(this.service); + } + + @Test + void runWhenReleaseTypeReleaseShouldCallService() throws Exception { + ArgumentCaptor captor = ArgumentCaptor.forClass(ReleaseInfo.class); + this.command.run(new DefaultApplicationArguments("promote", "RELEASE", getBuildInfoLocation())); + verify(this.service).publishGradlePlugin(captor.capture()); + ReleaseInfo releaseInfo = captor.getValue(); + assertThat(releaseInfo.getBuildName()).isEqualTo("example"); + assertThat(releaseInfo.getBuildNumber()).isEqualTo("example-build-1"); + assertThat(releaseInfo.getGroupId()).isEqualTo("org.example.demo"); + assertThat(releaseInfo.getVersion()).isEqualTo("2.2.0"); + } + + private String getBuildInfoLocation() throws Exception { + return new ClassPathResource("build-info-response.json", ArtifactoryService.class).getFile().getAbsolutePath(); + } + +} \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/SyncToCentralCommandTests.java b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/SyncToCentralCommandTests.java new file mode 100644 index 0000000000..2f79452c3a --- /dev/null +++ b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/command/SyncToCentralCommandTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2012-2019 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 + * + * https://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 io.spring.concourse.releasescripts.command; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.spring.concourse.releasescripts.ReleaseInfo; +import io.spring.concourse.releasescripts.artifactory.ArtifactoryService; +import io.spring.concourse.releasescripts.bintray.BintrayService; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import org.springframework.boot.DefaultApplicationArguments; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +/** + * Tests for {@link SyncToCentralCommand}. + * + * @author Madhura Bhave + */ +class SyncToCentralCommandTests { + + @Mock + private BintrayService service; + + private SyncToCentralCommand command; + + private ObjectMapper objectMapper; + + @BeforeEach + void setup() { + MockitoAnnotations.initMocks(this); + this.objectMapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + this.command = new SyncToCentralCommand(this.service, objectMapper); + } + + @Test + void runWhenReleaseTypeNotSpecifiedShouldThrowException() throws Exception { + Assertions.assertThatIllegalStateException() + .isThrownBy(() -> this.command.run(new DefaultApplicationArguments("syncToCentral"))); + } + + @Test + void runWhenReleaseTypeMilestoneShouldDoNothing() throws Exception { + this.command.run(new DefaultApplicationArguments("syncToCentral", "M", getBuildInfoLocation())); + verifyNoInteractions(this.service); + } + + @Test + void runWhenReleaseTypeRCShouldDoNothing() throws Exception { + this.command.run(new DefaultApplicationArguments("syncToCentral", "RC", getBuildInfoLocation())); + verifyNoInteractions(this.service); + } + + @Test + void runWhenReleaseTypeReleaseShouldCallService() throws Exception { + ArgumentCaptor captor = ArgumentCaptor.forClass(ReleaseInfo.class); + this.command.run(new DefaultApplicationArguments("syncToCentral", "RELEASE", getBuildInfoLocation())); + verify(this.service).syncToMavenCentral(captor.capture()); + ReleaseInfo releaseInfo = captor.getValue(); + assertThat(releaseInfo.getBuildName()).isEqualTo("example"); + assertThat(releaseInfo.getBuildNumber()).isEqualTo("example-build-1"); + assertThat(releaseInfo.getGroupId()).isEqualTo("org.example.demo"); + assertThat(releaseInfo.getVersion()).isEqualTo("2.2.0"); + } + + private String getBuildInfoLocation() throws Exception { + return new ClassPathResource("build-info-response.json", ArtifactoryService.class).getFile().getAbsolutePath(); + } + +} \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/sonatype/SonatypeServiceTests.java b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/sonatype/SonatypeServiceTests.java new file mode 100644 index 0000000000..ee477feeaf --- /dev/null +++ b/ci/images/releasescripts/src/test/java/io/spring/concourse/releasescripts/sonatype/SonatypeServiceTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2012-2019 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 + * + * https://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 io.spring.concourse.releasescripts.sonatype; + +import io.spring.concourse.releasescripts.ReleaseInfo; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.autoconfigure.web.client.RestClientTest; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.test.web.client.MockRestServiceServer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +/** + * Tests for {@link SonatypeService}. + * + * @author Madhura Bhave + */ +@RestClientTest(SonatypeService.class) +@EnableConfigurationProperties(SonatypeProperties.class) +class SonatypeServiceTests { + + @Autowired + private SonatypeService service; + + @Autowired + private SonatypeProperties properties; + + @Autowired + private MockRestServiceServer server; + + @AfterEach + void tearDown() { + this.server.reset(); + } + + @Test + void artifactsPublishedWhenPublishedShouldReturnTrue() { + this.server.expect(requestTo(String.format( + "https://oss.sonatype.org/service/local/repositories/releases/content/org/springframework/boot/spring-boot/%s/spring-boot-%s.jar.sha1", + "1.1.0.RELEASE", "1.1.0.RELEASE"))).andExpect(method(HttpMethod.GET)).andRespond(withSuccess()); + boolean published = this.service.artifactsPublished(getReleaseInfo()); + assertThat(published).isTrue(); + this.server.verify(); + } + + @Test + void artifactsPublishedWhenNotPublishedShouldReturnFalse() { + this.server.expect(requestTo(String.format( + "https://oss.sonatype.org/service/local/repositories/releases/content/org/springframework/boot/spring-boot/%s/spring-boot-%s.jar.sha1", + "1.1.0.RELEASE", "1.1.0.RELEASE"))).andExpect(method(HttpMethod.GET)) + .andRespond(withStatus(HttpStatus.NOT_FOUND)); + boolean published = this.service.artifactsPublished(getReleaseInfo()); + assertThat(published).isFalse(); + this.server.verify(); + } + + private ReleaseInfo getReleaseInfo() { + ReleaseInfo releaseInfo = new ReleaseInfo(); + releaseInfo.setBuildName("example-build"); + releaseInfo.setBuildNumber("example-build-1"); + releaseInfo.setVersion("1.1.0.RELEASE"); + releaseInfo.setGroupId("example"); + return releaseInfo; + } + +} \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/application.yml b/ci/images/releasescripts/src/test/resources/application.yml new file mode 100644 index 0000000000..88fe8a8f34 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/application.yml @@ -0,0 +1,11 @@ +artifactory: + username: user + password: password +bintray: + username: bintray-user + api-key: bintray-api-key + repo: test + subject: jars +sonatype: + user-token: sonatype-user + password-token: sonatype-password diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/artifactory/build-info-response.json b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/artifactory/build-info-response.json new file mode 100644 index 0000000000..bfe0d3be18 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/artifactory/build-info-response.json @@ -0,0 +1,35 @@ +{ + "buildInfo": { + "version": "1.0.1", + "name": "example", + "number": "example-build-1", + "started": "2019-09-10T12:18:05.430+0000", + "durationMillis": 0, + "artifactoryPrincipal": "user", + "url": "https://my-ci.com", + "modules": [ + { + "id": "org.example.demo:demo:2.2.0", + "artifacts": [ + { + "type": "jar", + "sha1": "ayyyya9151a22cb3145538e523dbbaaaaaaaa", + "sha256": "aaaaaaaaa85f5c5093721f3ed0edda8ff8290yyyyyyyyyy", + "md5": "aaaaaacddea1724b0b69d8yyyyyyy", + "name": "demo-2.2.0.jar" + } + ] + } + ], + "statuses": [ + { + "status": "staged", + "repository": "libs-release-local", + "timestamp": "2019-09-10T12:42:24.716+0000", + "user": "user", + "timestampDate": 1568119344716 + } + ] + }, + "uri": "https://my-artifactory-repo.com/api/build/example/example-build-1" +} \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/artifactory/staged-build-info-response.json b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/artifactory/staged-build-info-response.json new file mode 100644 index 0000000000..33be7a0ffe --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/artifactory/staged-build-info-response.json @@ -0,0 +1,35 @@ +{ + "buildInfo": { + "version": "1.0.1", + "name": "example", + "number": "example-build-1", + "started": "2019-09-10T12:18:05.430+0000", + "durationMillis": 0, + "artifactoryPrincipal": "user", + "url": "https://my-ci.com", + "modules": [ + { + "id": "org.example.demo:demo:2.2.0", + "artifacts": [ + { + "type": "jar", + "sha1": "ayyyya9151a22cb3145538e523dbbaaaaaaaa", + "sha256": "aaaaaaaaa85f5c5093721f3ed0edda8ff8290yyyyyyyyyy", + "md5": "aaaaaacddea1724b0b69d8yyyyyyy", + "name": "demo-2.2.0.jar" + } + ] + } + ], + "statuses": [ + { + "status": "staged", + "repository": "libs-staging-local", + "timestamp": "2019-09-10T12:42:24.716+0000", + "user": "user", + "timestampDate": 1568119344716 + } + ] + }, + "uri": "https://my-artifactory-repo.com/api/build/example/example-build-1" +} \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/bintray/all-package-files.json b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/bintray/all-package-files.json new file mode 100644 index 0000000000..cb4839cd13 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/bintray/all-package-files.json @@ -0,0 +1,35 @@ +[ + { + "name": "nutcracker-1.1-sources.jar", + "path": "org/jfrog/powerutils/nutcracker/1.1/nutcracker-1.1-sources.jar", + "package": "jfrog-power-utils", + "version": "1.1", + "repo": "jfrog-jars", + "owner": "jfrog", + "created": "ISO8601 (yyyy-MM-dd'T'HH:mm:ss.SSSZ)", + "size": 1234, + "sha1": "602e20176706d3cc7535f01ffdbe91b270ae5012" + }, + { + "name": "nutcracker-1.1.pom", + "path": "org/jfrog/powerutils/nutcracker/1.1/nutcracker-1.1.pom", + "package": "jfrog-power-utils", + "version": "1.1", + "repo": "jfrog-jars", + "owner": "jfrog", + "created": "ISO8601 (yyyy-MM-dd'T'HH:mm:ss.SSSZ)", + "size": 1234, + "sha1": "602e20176706d3cc7535f01ffdbe91b270ae5012" + }, + { + "name": "nutcracker-1.1.jar", + "path": "org/jfrog/powerutils/nutcracker/1.1/nutcracker-1.1.jar", + "package": "jfrog-power-utils", + "version": "1.1", + "repo": "jfrog-jars", + "owner": "jfrog", + "created": "ISO8601 (yyyy-MM-dd'T'HH:mm:ss.SSSZ)", + "size": 1234, + "sha1": "602e20176706d3cc7535f01ffdbe91b270ae5012" + } +] \ No newline at end of file diff --git a/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/bintray/published-files.json b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/bintray/published-files.json new file mode 100644 index 0000000000..05d2bfc0e8 --- /dev/null +++ b/ci/images/releasescripts/src/test/resources/io/spring/concourse/releasescripts/bintray/published-files.json @@ -0,0 +1,13 @@ +[ + { + "name": "nutcracker-1.1-sources.jar", + "path": "org/jfrog/powerutils/nutcracker/1.1/nutcracker-1.1-sources.jar", + "package": "jfrog-power-utils", + "version": "1.1", + "repo": "jfrog-jars", + "owner": "jfrog", + "created": "ISO8601 (yyyy-MM-dd'T'HH:mm:ss.SSSZ)", + "size": 1234, + "sha1": "602e20176706d3cc7535f01ffdbe91b270ae5012" + } +] \ No newline at end of file diff --git a/ci/images/spring-boot-ci-image/Dockerfile b/ci/images/spring-boot-ci-image/Dockerfile index 5a204b3646..d128866ad9 100644 --- a/ci/images/spring-boot-ci-image/Dockerfile +++ b/ci/images/spring-boot-ci-image/Dockerfile @@ -8,4 +8,7 @@ ENV JAVA_HOME /opt/openjdk ENV PATH $JAVA_HOME/bin:$PATH ADD docker-lib.sh /docker-lib.sh +ADD build-release-scripts.sh /build-release-scripts.sh +ADD releasescripts /release-scripts +RUN ./build-release-scripts.sh ENTRYPOINT [ "switch", "shell=/bin/bash", "--", "codep", "/bin/docker daemon" ] diff --git a/ci/scripts/promote.sh b/ci/scripts/promote.sh index d9bef5edc5..ce4f08b8bc 100755 --- a/ci/scripts/promote.sh +++ b/ci/scripts/promote.sh @@ -2,89 +2,13 @@ source $(dirname $0)/common.sh -buildName=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.name' ) -buildNumber=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.number' ) -groupId=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/\(.*\):.*:.*/\1/' ) -version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' ) +export BUILD_INFO_LOCATION=$(pwd)/artifactory-repo/build-info.json +java -jar /spring-boot-release-scripts.jar promote $RELEASE_TYPE $BUILD_INFO_LOCATION > /dev/null || { exit 1; } -if [[ $RELEASE_TYPE = "M" ]]; then - targetRepo="libs-milestone-local" -elif [[ $RELEASE_TYPE = "RC" ]]; then - targetRepo="libs-milestone-local" -elif [[ $RELEASE_TYPE = "RELEASE" ]]; then - targetRepo="libs-release-local" -else - echo "Unknown release type $RELEASE_TYPE" >&2; exit 1; -fi - -echo "Promoting ${buildName}/${buildNumber} to ${targetRepo}" - -curl \ - -s \ - --connect-timeout 240 \ - --max-time 900 \ - -u ${ARTIFACTORY_USERNAME}:${ARTIFACTORY_PASSWORD} \ - -H "Content-type:application/json" \ - -d "{\"status\": \"staged\", \"sourceRepo\": \"libs-staging-local\", \"targetRepo\": \"${targetRepo}\"}" \ - -f \ - -X \ - POST "${ARTIFACTORY_SERVER}/api/build/promote/${buildName}/${buildNumber}" > /dev/null || { - result=$( curl -s -f -u ${ARTIFACTORY_USERNAME}:${ARTIFACTORY_PASSWORD} "${ARTIFACTORY_SERVER}/api/build/${buildName}/${buildNumber}" ) - resultRepo=$( echo $result | jq -r '.buildInfo.statuses[0].repository' ) - if [[ $resultRepo = "libs-release-local" ]]; then - echo "Already promoted" - else - echo "Failed to promote" >&2 - exit 1 - fi - } - -if [[ $RELEASE_TYPE = "RELEASE" ]]; then - curl \ - -s \ - --connect-timeout 240 \ - --max-time 2700 \ - -u ${ARTIFACTORY_USERNAME}:${ARTIFACTORY_PASSWORD} \ - -H "Content-type:application/json" \ - -d "{\"sourceRepos\": [\"libs-release-local\"], \"targetRepo\" : \"spring-distributions\", \"async\":\"true\"}" \ - -f \ - -X \ - POST "${ARTIFACTORY_SERVER}/api/build/distribute/${buildName}/${buildNumber}" > /dev/null || { echo "Failed to distribute" >&2; exit 1; } - - echo "Waiting for artifacts to be published" - - WAIT_TIME=20 - WAIT_ATTEMPTS=120 - - artifacts_published=false - retry_counter=0 - while [ $artifacts_published == "false" ] && [ $retry_counter -lt $WAIT_ATTEMPTS ]; do - result=$( curl -s -f -u ${BINTRAY_USERNAME}:${BINTRAY_API_KEY} https://api.bintray.com/packages/"${BINTRAY_SUBJECT}"/"${BINTRAY_REPO}"/"${groupId}" ) - if [ $? -eq 0 ]; then - versions=$( echo "$result" | jq -r '.versions' ) - exists=$( echo "$versions" | grep "$version" -o || true ) - if [ "$exists" = "$version" ]; then - artifacts_published=true - fi - fi - retry_counter=$(( retry_counter + 1 )) - sleep $WAIT_TIME - done - if [[ $artifacts_published = "false" ]]; then - echo "Failed to publish" - exit 1 - else - curl \ - -s \ - -u ${BINTRAY_USERNAME}:${BINTRAY_API_KEY} \ - -H "Content-Type: application/json" \ - -d '[ { "name": "gradle-plugin", "values": ["org.springframework.boot:org.springframework.boot:spring-boot-gradle-plugin"] } ]' \ - -X POST \ - https://api.bintray.com/packages/${BINTRAY_SUBJECT}/${BINTRAY_REPO}/${groupId}/versions/${version}/attributes > /dev/null || { echo "Failed to add attributes" >&2; exit 1; } - fi -fi +java -jar /spring-boot-release-scripts.jar distribute $RELEASE_TYPE $BUILD_INFO_LOCATION > /dev/null || { exit 1; } +java -jar /spring-boot-release-scripts.jar publishGradlePlugin $RELEASE_TYPE $BUILD_INFO_LOCATION > /dev/null || { exit 1; } echo "Promotion complete" echo $version > version/version diff --git a/ci/scripts/sync-to-maven-central.sh b/ci/scripts/sync-to-maven-central.sh index 56b305c886..1c324ac508 100755 --- a/ci/scripts/sync-to-maven-central.sh +++ b/ci/scripts/sync-to-maven-central.sh @@ -1,33 +1,8 @@ #!/bin/bash -buildName=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.name' ) -buildNumber=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.number' ) -groupId=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/\(.*\):.*:.*/\1/' ) -version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' ) +export BUILD_INFO_LOCATION=$(pwd)/artifactory-repo/build-info.json -echo "Syncing ${buildName}/${buildNumber} to Maven Central" - - publishStatus=$(curl \ - -s \ - -o /dev/null \ - -I \ - -w "%{http_code}" \ - "https://oss.sonatype.org/service/local/repositories/releases/content/org/springframework/boot/spring-boot/${version}/spring-boot-${version}.jar.sha1") - - if [[ ${publishStatus} == "200" ]]; then - echo "Already published to Sonatype" - else - echo "Calling Bintray to sync to Sonatype" - curl \ - -s \ - --connect-timeout 240 \ - --max-time 2700 \ - -u ${BINTRAY_USERNAME}:${BINTRAY_API_KEY} \ - -H "Content-Type: application/json" -d "{\"username\": \"${SONATYPE_USER_TOKEN}\", \"password\": \"${SONATYPE_PASSWORD_TOKEN}\"}" \ - -f \ - -X \ - POST "https://api.bintray.com/maven_central_sync/${BINTRAY_SUBJECT}/${BINTRAY_REPO}/${groupId}/versions/${version}" > /dev/null || { echo "Failed to sync" >&2; exit 1; } - fi +java -jar /spring-boot-release-scripts.jar syncToCentral "RELEASE" $BUILD_INFO_LOCATION > /dev/null || { exit 1; } echo "Sync complete" echo $version > version/version