diff --git a/jenkins/release-deploy/Jenkinsfile b/jenkins/release-deploy/Jenkinsfile
new file mode 100644
index 00000000000..28151512947
--- /dev/null
+++ b/jenkins/release-deploy/Jenkinsfile
@@ -0,0 +1,68 @@
+pipeline {
+ agent any
+
+ options {
+ buildDiscarder(logRotator(numToKeepStr:'15'))
+ disableConcurrentBuilds()
+ timeout(time: 120, unit: 'MINUTES')
+ }
+
+ parameters {
+ booleanParam(name: 'P2_DEPLOY_DRY_RUN', defaultValue: false, description:
+ '''
+ If set, the composite update site is created/updated locally (and archived), but it will
+ not be uploaded to the remote area: rsync will be executed with "--dry-run, -n"
+ ''')
+ }
+
+ environment {
+ DOWNLOAD_AREA = '/home/data/httpd/download.eclipse.org/modeling/tmf/xtext'
+ REPOSITORY_PATH="${DOWNLOAD_AREA}/updates/releases"
+ }
+
+ tools {
+ maven "apache-maven-3.8.6"
+ jdk "temurin-jdk17-latest"
+ }
+
+ stages {
+ stage('Prepare download area') {
+ steps {
+ sshagent(['projects-storage.eclipse.org-bot-ssh']) {
+ sh '''
+ echo ${REPOSITORY_PATH}
+ ssh genie.xtext@projects-storage.eclipse.org "mkdir -p $REPOSITORY_PATH"
+ '''
+ }
+ }
+ }
+ stage('Prepare versions for Release') {
+ steps {
+ sh './scripts/prepare-for-release.sh'
+ }
+ }
+ stage('Maven Tycho Build, Sign, Deploy') {
+ steps {
+ withCredentials([file(credentialsId: 'secret-subkeys.asc', variable: 'KEYRING')]) {
+ sh 'gpg --batch --import "${KEYRING}"'
+ sh 'for fpr in $(gpg --list-keys --with-colons | awk -F: \'/fpr:/ {print $10}\' | sort -u); do echo -e "5\ny\n" | gpg --batch --command-fd 0 --expert --edit-key ${fpr} trust; done'
+ }
+ sshagent(['projects-storage.eclipse.org-bot-ssh']) {
+ sh """
+ ./full-deploy.sh -Peclipse-sign,sonatype-oss-release,release-release ${rsyncAdditionalArgs()}
+ """
+ }
+ }
+ }
+ }
+
+ post {
+ success {
+ archiveArtifacts artifacts: 'build/**, org.eclipse.xtext.p2repository/target/checkout/**'
+ }
+ }
+}
+
+def rsyncAdditionalArgs() {
+ return params.P2_DEPLOY_DRY_RUN ? '-Dadditional-rsync-commit-args="-n"' : ''
+}
diff --git a/org.eclipse.xtext.p2repository/pom.xml b/org.eclipse.xtext.p2repository/pom.xml
index e84219cd02e..72591de9c23 100644
--- a/org.eclipse.xtext.p2repository/pom.xml
+++ b/org.eclipse.xtext.p2repository/pom.xml
@@ -437,5 +437,75 @@
+
+
+ release-release
+
+ updates/releases
+
+ ${qualifiedVersion}
+ tmf-xtext-Update-${qualifiedVersion}
+ TMF Xtext Update Site (Releases)
+
+
+
+
+ org.eclipse.tycho
+ tycho-p2-repository-plugin
+
+
+ ${p2.mirrorsURL}
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+
+
+ md5sum
+ package
+
+
+ rsync-update
+ prepare-package
+
+
+ rsync-commit
+ verify
+
+
+ rsync-commit-zip
+ verify
+
+
+
+
+ maven-resources-plugin
+
+
+ copy-repository
+ package
+
+
+ copy-zipped-repository
+ package
+
+
+
+
+ org.eclipse.tycho.extras
+ tycho-eclipserun-plugin
+
+
+
+ add-p2-composite-repository
+ package
+
+
+
+
+
+
diff --git a/pom.xml b/pom.xml
index a6fdd7ac368..2b1b580ac92 100644
--- a/pom.xml
+++ b/pom.xml
@@ -251,6 +251,22 @@
+
+
+ release-release
+
+
+
+ org.eclipse.tycho
+ tycho-packaging-plugin
+
+ false
+
+
+
+
+
replace-qualifier-with-timestamp
diff --git a/scripts/prepare-for-release.sh b/scripts/prepare-for-release.sh
new file mode 100755
index 00000000000..2f8539d2e32
--- /dev/null
+++ b/scripts/prepare-for-release.sh
@@ -0,0 +1,50 @@
+#!/bin/sh
+
+
+# First, update the version of the BOM, which is disconnected from the parent.
+# For example, 2.31.0-SNAPSHOT becomes 2.31.0
+
+mvn -f org.eclipse.xtext.dev-bom \
+ build-helper:parse-version \
+ versions:set \
+ -DgenerateBackupPoms=false \
+ -DnewVersion=\${parsedVersion.majorVersion}.\${parsedVersion.minorVersion}.\${parsedVersion.incrementalVersion}
+
+# The updated BOM must be installed, or the next runs will complain they can't find it.
+
+mvn -f org.eclipse.xtext.dev-bom install
+
+# Replace the property "xtext-dev-bom-version" in the parent POM, with the new version of the BOM.
+# For example,
+# ${project.version}
+# becomes
+# 2.31.0
+
+mvn \
+ build-helper:parse-version \
+ versions:set-property \
+ -DgenerateBackupPoms=false \
+ -Dproperty=xtext-dev-bom-version \
+ -DnewVersion=\${parsedVersion.majorVersion}.\${parsedVersion.minorVersion}.\${parsedVersion.incrementalVersion}
+
+# With Tycho, replace all versions (in POMs and Eclipse metadata) with the timestamp as the qualifier.
+# For example, in POMs, 2.31.0-SNAPSHOT becomes 2.31.0.v2023...
+# In MANIFEST and features, 2.31.0.qualifier becomes 2.31.0.v2023...
+
+mvn \
+ -Preplace-qualifier-with-timestamp \
+ build-helper:parse-version \
+ build-helper:timestamp-property@timestamp \
+ org.eclipse.tycho:tycho-versions-plugin:set-version \
+ -DnewVersion=\${parsedVersion.majorVersion}.\${parsedVersion.minorVersion}.\${parsedVersion.incrementalVersion}.\${computedTimestamp} \
+ -DgenerateBackupPoms=false
+
+# Update the versions in the POMs only with the release number as the qualifier
+# For example, in POMs, 2.31.0.v2023... becomes 2.31.0
+# In MANIFEST and features, 2.31.0.v2023... stays the same
+
+mvn \
+ build-helper:parse-version \
+ versions:set \
+ -DgenerateBackupPoms=false \
+ -DnewVersion=\${parsedVersion.majorVersion}.\${parsedVersion.minorVersion}.\${parsedVersion.incrementalVersion}