diff --git a/Jenkinsfile b/Jenkinsfile
new file mode 100644
index 0000000..ffc5f5e
--- /dev/null
+++ b/Jenkinsfile
@@ -0,0 +1,186 @@
+pipeline {
+ agent none
+
+ triggers {
+ pollSCM 'H/10 * * * *'
+ }
+
+ options {
+ disableConcurrentBuilds()
+ }
+
+ stages {
+ stage("test: baseline (jdk8)") {
+ agent {
+ docker {
+ image 'adoptopenjdk/openjdk8:latest'
+ args '-v $HOME/.m2:/root/.m2'
+ }
+ }
+ steps {
+ sh "PROFILE=none ci/test.sh"
+ }
+ }
+
+ stage("Test other configurations") {
+ parallel {
+ stage("test: baseline (jdk11)") {
+ agent {
+ docker {
+ image 'adoptopenjdk/openjdk11:latest'
+ args '-v $HOME/.m2:/root/.m2'
+ }
+ }
+ steps {
+ sh "PROFILE=none ci/test.sh"
+ }
+ }
+ stage("test: baseline (jdk12)") {
+ agent {
+ docker {
+ image 'adoptopenjdk/openjdk12:latest'
+ args '-v $HOME/.m2:/root/.m2'
+ }
+ }
+ steps {
+ sh "PROFILE=none ci/test.sh"
+ }
+ }
+ }
+ }
+
+ stage('Deploy to Artifactory') {
+ agent {
+ docker {
+ image 'springci/spring-hateoas-openjdk8-with-graphviz-and-jq:latest'
+ args '-v $HOME/.m2:/root/.m2'
+ }
+ }
+
+ environment {
+ ARTIFACTORY = credentials('02bd1690-b54f-4c9f-819d-a77cb7a9822c')
+ }
+
+ steps {
+ script {
+ // Warm up this plugin quietly before using it.
+ sh "./mvnw -q org.apache.maven.plugins:maven-help-plugin:2.1.1:evaluate -Dexpression=project.version"
+
+ PROJECT_VERSION = sh(
+ script: "./mvnw org.apache.maven.plugins:maven-help-plugin:2.1.1:evaluate -Dexpression=project.version -o | grep -v INFO",
+ returnStdout: true
+ ).trim()
+
+ RELEASE_TYPE = 'milestone' // .RC? or .M?
+
+ if (PROJECT_VERSION.endsWith('BUILD-SNAPSHOT')) {
+ RELEASE_TYPE = 'snapshot'
+ } else if (PROJECT_VERSION.endsWith('RELEASE')) {
+ RELEASE_TYPE = 'release'
+ }
+
+ OUTPUT = sh(
+ script: "PROFILE=ci,${RELEASE_TYPE} ci/build.sh",
+ returnStdout: true
+ ).trim()
+
+ echo "$OUTPUT"
+
+ build_info_path = OUTPUT.split('\n')
+ .find { it.contains('Artifactory Build Info Recorder') }
+ .split('Saving Build Info to ')[1]
+ .trim()[1..-2]
+
+ dir(build_info_path + '/..') {
+ stash name: 'build_info', includes: "*.json"
+ }
+ }
+ }
+ }
+ stage('Promote to Bintray') {
+ when {
+ branch 'release'
+ }
+ agent {
+ docker {
+ image 'springci/spring-hateoas-openjdk8-with-graphviz-and-jq:latest'
+ args '-v $HOME/.m2:/root/.m2'
+ }
+ }
+
+ environment {
+ ARTIFACTORY = credentials('02bd1690-b54f-4c9f-819d-a77cb7a9822c')
+ }
+
+ steps {
+ script {
+ // Warm up this plugin quietly before using it.
+ sh "./mvnw -q org.apache.maven.plugins:maven-help-plugin:2.1.1:evaluate -Dexpression=project.version"
+
+ PROJECT_VERSION = sh(
+ script: "./mvnw org.apache.maven.plugins:maven-help-plugin:2.1.1:evaluate -Dexpression=project.version -o | grep -v INFO",
+ returnStdout: true
+ ).trim()
+
+ if (PROJECT_VERSION.endsWith('RELEASE')) {
+ unstash name: 'build_info'
+ sh "ci/promote-to-bintray.sh"
+ } else {
+ echo "${PROJECT_VERSION} is not a candidate for promotion to Bintray."
+ }
+ }
+ }
+ }
+ stage('Sync to Maven Central') {
+ when {
+ branch 'release'
+ }
+ agent {
+ docker {
+ image 'springci/spring-hateoas-openjdk8-with-graphviz-and-jq:latest'
+ args '-v $HOME/.m2:/root/.m2'
+ }
+ }
+
+ environment {
+ BINTRAY = credentials('Bintray-spring-operator')
+ SONATYPE = credentials('oss-token')
+ }
+
+ steps {
+ script {
+ // Warm up this plugin quietly before using it.
+ sh "./mvnw -q org.apache.maven.plugins:maven-help-plugin:2.1.1:evaluate -Dexpression=project.version"
+
+ PROJECT_VERSION = sh(
+ script: "./mvnw org.apache.maven.plugins:maven-help-plugin:2.1.1:evaluate -Dexpression=project.version -o | grep -v INFO",
+ returnStdout: true
+ ).trim()
+
+ if (PROJECT_VERSION.endsWith('RELEASE')) {
+ unstash name: 'build_info'
+ sh "ci/sync-to-maven-central.sh"
+ } else {
+ echo "${PROJECT_VERSION} is not a candidate for syncing to Maven Central."
+ }
+ }
+ }
+ }
+ }
+
+ post {
+ changed {
+ script {
+ slackSend(
+ color: (currentBuild.currentResult == 'SUCCESS') ? 'good' : 'danger',
+ channel: '#spring-hateoas',
+ message: "${currentBuild.fullDisplayName} - `${currentBuild.currentResult}`\n${env.BUILD_URL}")
+ emailext(
+ subject: "[${currentBuild.fullDisplayName}] ${currentBuild.currentResult}",
+ mimeType: 'text/html',
+ recipientProviders: [[$class: 'CulpritsRecipientProvider'], [$class: 'RequesterRecipientProvider']],
+ body: "${currentBuild.fullDisplayName} is reported as ${currentBuild.currentResult}")
+ }
+ }
+ }
+}
diff --git a/ci/build.sh b/ci/build.sh
new file mode 100755
index 0000000..6a202bb
--- /dev/null
+++ b/ci/build.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+set -euo pipefail
+
+./mvnw -P${PROFILE} -Dmaven.test.skip=true clean deploy -B
diff --git a/ci/promote-to-bintray.sh b/ci/promote-to-bintray.sh
new file mode 100755
index 0000000..a00bb8d
--- /dev/null
+++ b/ci/promote-to-bintray.sh
@@ -0,0 +1,42 @@
+#!/bin/bash
+
+set -e -u
+
+buildName=`jq -r '.name' build-info.json`
+buildNumber=`jq -r '.number' build-info.json`
+groupId=`jq -r '.modules[0].id' build-info.json | sed 's/\(.*\):.*:.*/\1/'`
+version=`jq -r '.modules[0].id' build-info.json | sed 's/.*:.*:\(.*\)/\1/'`
+
+echo "Promoting ${buildName}/${buildNumber}/${groupId}/${version} to libs-release-local"
+
+curl \
+ -s \
+ --connect-timeout 240 \
+ --max-time 2700 \
+ -u ${ARTIFACTORY_USR}:${ARTIFACTORY_PSW} \
+ -H 'Content-type:application/json' \
+ -d '{"sourceRepos": ["libs-release-local"], "targetRepo" : "spring-distributions", "async":"true"}' \
+ -f \
+ -X \
+ POST "https://repo.spring.io/api/build/distribute/${buildName}/${buildNumber}" > /dev/null || { echo "Failed to distribute" >&2; exit 1; }
+
+echo "Waiting for artifacts to be published"
+
+ARTIFACTS_PUBLISHED=false
+WAIT_TIME=10
+COUNTER=0
+
+while [ $ARTIFACTS_PUBLISHED == "false" ] && [ $COUNTER -lt 120 ]; do
+
+ result=$( curl -s https://api.bintray.com/packages/spring/jars/"${groupId}" )
+ versions=$( echo "$result" | jq -r '.versions' )
+ exists=$( echo "$versions" | grep "$version" -o || true )
+
+ if [ "$exists" = "$version" ]; then
+ ARTIFACTS_PUBLISHED=true
+ fi
+
+ COUNTER=$(( COUNTER + 1 ))
+ sleep $WAIT_TIME
+
+done
diff --git a/ci/sync-to-maven-central.sh b/ci/sync-to-maven-central.sh
new file mode 100755
index 0000000..7150ca7
--- /dev/null
+++ b/ci/sync-to-maven-central.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+
+set -e -u
+
+buildName=`jq -r '.name' build-info.json`
+buildNumber=`jq -r '.number' build-info.json`
+groupId=`jq -r '.modules[0].id' build-info.json | sed 's/\(.*\):.*:.*/\1/'`
+version=`jq -r '.modules[0].id' build-info.json | sed 's/.*:.*:\(.*\)/\1/'`
+
+echo "Syncing ${buildName}/${buildNumber}/${groupId}/${version} to Maven Central..."
+
+curl \
+ -s \
+ --connect-timeout 240 \
+ --max-time 2700 \
+ -u ${BINTRAY_USR}:${BINTRAY_PSW} \
+ -H 'Content-Type: application/json' \
+ -d "{ \"username\": \"${SONATYPE_USR}\", \"password\": \"${SONATYPE_PSW}\"}" \
+ -f \
+ -X \
+ POST "https://api.bintray.com/maven_central_sync/spring/jars/${groupId}/versions/${version}" > /dev/null || { echo "Failed to sync" >&2; exit 1; }
+
+echo "Sync complete"
diff --git a/ci/test.sh b/ci/test.sh
new file mode 100755
index 0000000..f6bb0bf
--- /dev/null
+++ b/ci/test.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+set -euo pipefail
+
+./mvnw -P${PROFILE} clean dependency:list test -Dsort -B
diff --git a/pom.xml b/pom.xml
index d4b65cd..0d66587 100644
--- a/pom.xml
+++ b/pom.xml
@@ -78,6 +78,135 @@
5.0.2.BUILD-SNAPSHOT
2.9.2
+
+
+ spring-libs-milestone
+ http://repo.spring.io/libs-milestone
+
+
+
+
+
+ snapshot
+
+
+
+
+ org.jfrog.buildinfo
+ artifactory-maven-plugin
+ 2.6.1
+ false
+
+
+ build-info
+
+ publish
+
+
+
+ {{BUILD_URL}}
+
+
+ spring-hateoas
+ spring-hateoas
+ false
+ *:*:*:*@zip
+
+
+ https://repo.spring.io
+ {{ARTIFACTORY_USR}}
+ {{ARTIFACTORY_PSW}}
+ libs-snapshot-local
+ libs-snapshot-local
+
+
+
+
+
+
+
+
+
+
+ milestone
+
+
+
+
+ org.jfrog.buildinfo
+ artifactory-maven-plugin
+ 2.6.1
+ false
+
+
+ build-info
+
+ publish
+
+
+
+ {{BUILD_URL}}
+
+
+ spring-hateoas
+ spring-hateoas
+ false
+ *:*:*:*@zip
+
+
+ https://repo.spring.io
+ {{ARTIFACTORY_USR}}
+ {{ARTIFACTORY_PSW}}
+ libs-milestone-local
+ libs-milestone-local
+
+
+
+
+
+
+
+
+
+
+ release
+
+
+
+
+ org.jfrog.buildinfo
+ artifactory-maven-plugin
+ 2.6.1
+ false
+
+
+ build-info
+
+ publish
+
+
+
+ {{BUILD_URL}}
+
+
+ spring-hateoas
+ spring-hateoas
+ false
+ *:*:*:*@zip
+
+
+ https://repo.spring.io
+ {{ARTIFACTORY_USR}}
+ {{ARTIFACTORY_PSW}}
+ libs-release-local
+ libs-release-local
+
+
+
+
+
+
+