diff --git a/.github/actions/create-github-release/action.yml b/.github/actions/create-github-release/action.yml
new file mode 100644
index 00000000..5ddc30d6
--- /dev/null
+++ b/.github/actions/create-github-release/action.yml
@@ -0,0 +1,21 @@
+name: Create GitHub Release
+description: Create the release on GitHub with a changelog
+inputs:
+  milestone:
+    required: true
+  token:
+    required: true
+runs:
+  using: composite
+  steps:
+    - name: Generate Changelog
+      uses: spring-io/github-changelog-generator@v0.0.10
+      with:
+        milestone: ${{ inputs.milestone }}
+        token: ${{ inputs.token }}
+        config-file: ${{ github.action_path }}/changelog-generator.yml
+    - name: Create GitHub Release
+      env:
+        GITHUB_TOKEN: ${{ inputs.token }}
+      shell: bash
+      run: gh release create ${{ format('v{0}', inputs.milestone) }} --notes-file changelog.md
diff --git a/.github/actions/create-github-release/changelog-generator.yml b/.github/actions/create-github-release/changelog-generator.yml
new file mode 100644
index 00000000..2ce74a09
--- /dev/null
+++ b/.github/actions/create-github-release/changelog-generator.yml
@@ -0,0 +1,2 @@
+changelog:
+  repository: spring-io/spring-javaformat
diff --git a/.github/actions/publish-eclipse-update-site/action.yml b/.github/actions/publish-eclipse-update-site/action.yml
new file mode 100644
index 00000000..4235c9e8
--- /dev/null
+++ b/.github/actions/publish-eclipse-update-site/action.yml
@@ -0,0 +1,22 @@
+name: 'Publish Eclipse Update Site '
+inputs:
+  version:
+    required: true
+  build-number:
+    required: true
+  artifactory-username:
+    required: true
+  artifactory-password:
+    required: true
+runs:
+  using: composite
+  steps:
+    - name: Stage
+      id: stage
+      shell: bash
+      run: . ${{ github.action_path }}/publish-eclipse-update-site.sh;
+      env:
+        VERSION: "${{ inputs.version }}"
+        BUILD_NUMBER: "${{ inputs.build-number }}"
+        ARTIFACTORY_USERNAME: "${{ inputs.artifactory-username }}"
+        ARTIFACTORY_PASSWORD: "${{ inputs.artifactory-password }}"
diff --git a/.github/actions/publish-eclipse-update-site/publish-eclipse-update-site-pom-template.xml b/.github/actions/publish-eclipse-update-site/publish-eclipse-update-site-pom-template.xml
new file mode 100755
index 00000000..6ee635e0
--- /dev/null
+++ b/.github/actions/publish-eclipse-update-site/publish-eclipse-update-site-pom-template.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<groupId>org.eclipse.m2e.maveneclipse</groupId>
+	<artifactId>m2eclipse-maveneclipse-publish</artifactId>
+	<version>0.0.0-SNAPSHOT</version>
+	<packaging>pom</packaging>
+	<properties>
+		<tycho-extras-version>1.1.0</tycho-extras-version>
+	</properties>
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.eclipse.tycho.extras</groupId>
+				<artifactId>tycho-p2-extras-plugin</artifactId>
+				<version>${tycho-extras-version}</version>
+				<executions>
+					<execution>
+						<phase>prepare-package</phase>
+						<goals>
+							<goal>mirror</goal>
+						</goals>
+					</execution>
+				</executions>
+				<configuration>
+					<source>
+##repositories##
+					</source>
+					<destination>${project.build.directory}/repository</destination>
+					<compress>true</compress>
+				</configuration>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-antrun-plugin</artifactId>
+				<version>1.8</version>
+				<executions>
+					<execution>
+						<phase>prepare-package</phase>
+						<configuration>
+							<propertyPrefix>mvn</propertyPrefix>
+							<target>
+								<mkdir dir="${mvnproject.build.directory}/repackage"/>
+								<unzip src="${mvnproject.build.directory}/repository/artifacts.jar" dest="${mvnproject.build.directory}/repackage"/>
+								<replace file="${mvnproject.build.directory}/repackage/artifacts.xml" token="${repoUrl}" value="@@@{repoUrl}/@@@{version}"/>
+								<replace file="${mvnproject.build.directory}/repackage/artifacts.xml" token="@@@" value="$"/>
+								<touch file="${mvnproject.build.directory}/repackage/artifacts.xml" millis="0" />
+								<zip destfile="${mvnproject.build.directory}/repository/artifacts.jar" update="true" basedir="${mvnproject.build.directory}/repackage"/>
+							</target>
+						</configuration>
+						<goals>
+							<goal>run</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+		</plugins>
+	</build>
+</project>
diff --git a/.github/actions/publish-eclipse-update-site/publish-eclipse-update-site.sh b/.github/actions/publish-eclipse-update-site/publish-eclipse-update-site.sh
new file mode 100644
index 00000000..9ddcee29
--- /dev/null
+++ b/.github/actions/publish-eclipse-update-site/publish-eclipse-update-site.sh
@@ -0,0 +1,51 @@
+buildInfo=$( jfrog rt curl api/build/spring-javaformat-${VERSION}/${BUILD_NUMBER} )
+groupId=$( echo ${buildInfo} | jq -r '.buildInfo.modules[0].id' | sed 's/\(.*\):.*:.*/\1/' )
+version=$( echo ${buildInfo} | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' )
+
+echo "Publishing ${buildName}/${buildNumber} (${groupId}:${version}) to Eclipse Update Site"
+
+jfrog rt dl --build spring-javaformat-${VERSION}/${BUILD_NUMBER} '**/io.spring.javaformat.eclipse.site*.zip'
+
+curl \
+	-s \
+	--connect-timeout 240 \
+	--max-time 2700 \
+	-u ${ARTIFACTORY_USERNAME}:${ARTIFACTORY_PASSWORD} \
+	-f \
+	-H "X-Explode-Archive: true" \
+	-X PUT \
+	-T "io/spring/javaformat/io.spring.javaformat.eclipse.site/${version}/io.spring.javaformat.eclipse.site-${version}.zip" \
+	"https://repo.spring.io/javaformat-eclipse-update-site/${version}/" > /dev/null || { echo "Failed to publish" >&2; exit 1; }
+
+releasedVersions=$( curl -s -f -X GET https://repo.spring.io/api/storage/javaformat-eclipse-update-site | jq -r '.children[] | .uri' | cut -c 2- | grep '[0-9].*' | sort -V )
+
+repositories=""
+while read -r releasedVersion; do
+	echo "Adding repository for ${releasedVersion}"
+	repositories="${repositories}<repository><url>https://repo.spring.io/javaformat-eclipse-update-site/${releasedVersion}</url><layout>p2</layout></repository>"
+done <<< "${releasedVersions}"
+
+sed "s|##repositories##|${repositories}|" ${GITHUB_ACTION_PATH}/publish-eclipse-update-site-pom-template.xml > publish-eclipse-update-site-pom.xml
+./mvnw -f publish-eclipse-update-site-pom.xml clean package || { echo "Failed to publish" >&2; exit 1; }
+
+curl \
+		-s \
+		--connect-timeout 240 \
+		--max-time 2700 \
+		-u ${ARTIFACTORY_USERNAME}:${ARTIFACTORY_PASSWORD} \
+		-f \
+		-X PUT \
+		-T "target/repository/content.jar" \
+		"https://repo.spring.io/javaformat-eclipse-update-site/" > /dev/null || { echo "Failed to publish" >&2; exit 1; }
+
+curl \
+		-s \
+		--connect-timeout 240 \
+		--max-time 2700 \
+		-u ${ARTIFACTORY_USERNAME}:${ARTIFACTORY_PASSWORD} \
+		-f \
+		-X PUT \
+		-T "target/repository/artifacts.jar" \
+		"https://repo.spring.io/javaformat-eclipse-update-site/" > /dev/null || { echo "Failed to publish" >&2; exit 1; }
+
+echo "Publish complete"
diff --git a/.github/actions/stage-code/action.yml b/.github/actions/stage-code/action.yml
index a71086eb..1fc1f060 100644
--- a/.github/actions/stage-code/action.yml
+++ b/.github/actions/stage-code/action.yml
@@ -1,5 +1,4 @@
 name: 'Stage '
-description: 'Stage Code'
 inputs:
   current-version:
     required: true
diff --git a/.github/artifacts.spec b/.github/artifacts.spec
new file mode 100644
index 00000000..0f6c3aac
--- /dev/null
+++ b/.github/artifacts.spec
@@ -0,0 +1,23 @@
+{
+  "files": [
+    {
+      "aql": {
+        "items.find": {
+          "$and": [
+            {
+              "@build.name": "${buildName}",
+              "@build.number": "${buildNumber}",
+              "name": {
+                "$nmatch": "*.zip"
+              },
+              "name": {
+                "$nmatch": "*.zip.asc"
+              }
+            }
+          ]
+        }
+      },
+      "target": "nexus/"
+    }
+  ]
+}
diff --git a/.github/workflows/promote.yml b/.github/workflows/promote.yml
index 968b265f..1ac65f98 100644
--- a/.github/workflows/promote.yml
+++ b/.github/workflows/promote.yml
@@ -33,5 +33,58 @@ jobs:
     name: Promote
     runs-on: ubuntu-latest
     steps:
-    - name: Promote
-      run: echo "Promote happens here"
+    - name: Check Out
+      uses: actions/checkout@v4
+    - name: Set Up JFrog CLI
+      uses: jfrog/setup-jfrog-cli@7c95feb32008765e1b4e626b078dfd897c4340ad # v4.1.2
+      env:
+        JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }}
+    - name: Check Maven Central Sync Status
+      id: check-sync-status
+      run: |
+        url=${{ format('https://repo.maven.apache.org/maven2/io/spring/javaformat/spring-javaformat/{0}/spring-javaformat-{0}.pom', inputs.version) }}
+        status_code=$( curl --write-out '%{http_code}' --head --silent --output /dev/null ${url} )
+        if [ "${status_code}" != 200 ] && [ "${status_code}" != 404 ]; then
+          echo "Unexpected status code ${status_code}"
+          exit 1
+        fi
+        echo "status-code=${status_code}" >> $GITHUB_OUTPUT
+    - name: Download Release Artifacts
+      if: ${{ steps.check-sync-status.outputs.status-code == '404' }}
+      run: jf rt download --spec ./.github/artifacts.spec --spec-vars 'buildName=${{ format('spring-javaformat-{0}', inputs.version) }};buildNumber=${{ inputs.build-number }}'
+    - name: Sync to Maven Central
+      if: ${{ steps.check-sync-status.outputs.status-code == '404' }}
+      uses: spring-io/nexus-sync-action@v0.0.1
+      with:
+        username: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }}
+        password: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }}
+        staging-profile-name: ${{ secrets.OSSRH_S01_STAGING_PROFILE }}
+        create: true
+        upload: true
+        close: true
+        release: true
+        generate-checksums: true
+    - name: Await Maven Central Sync
+      if: ${{ steps.check-sync-status.outputs.status-code == '404' }}
+      run: |
+        url=${{ format('https://repo.maven.apache.org/maven2/io/spring/javaformat/spring-javaformat/{0}/spring-javaformat-{0}.pom', inputs.version) }}
+        echo "Waiting for $url"
+        until curl --fail --head --silent $url > /dev/null
+        do
+          echo "."
+          sleep 60
+        done
+        echo "$url is available"
+    - name: Publish Eclipse Update Site
+      if: false
+      uses: ./.github/actions/publish-eclipse-update-site
+      with:
+        version: ${{ inputs.version }}
+        build-number: ${{ inputs.build-number }}
+        artifactory-username: ${{ secrets.ARTIFACTORY_USERNAME }}
+        artifactory-password: ${{ secrets.ARTIFACTORY_PASSWORD }}
+    - name: Create GitHub Release
+      uses: ./.github/actions/create-github-release
+      with:
+        milestone: ${{ inputs.version }}
+        token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}