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