This is an automated email from the ASF dual-hosted git repository.

ppalaga pushed a commit to branch 3.27.x-product
in repository https://gitbox.apache.org/repos/asf/camel-quarkus-examples.git

commit 2189d7ee8dba82d981eb793b878d852794f0ff89
Author: Peter Palaga <[email protected]>
AuthorDate: Tue Dec 9 17:21:43 2025 +0100

    Daily job to update RHBQ Platform versions in examples
---
 .../.mvn/wrapper/maven-wrapper.properties          |  19 +
 .github/actions/update-versions/.sdkmanrc          |   5 +
 .github/actions/update-versions/action.yml         |  36 ++
 .github/actions/update-versions/mvnw               | 259 ++++++++
 .github/actions/update-versions/mvnw.cmd           | 149 +++++
 .github/actions/update-versions/pom.xml            | 157 +++++
 .../camel/quarkus/test/ComparableVersion.java      | 719 +++++++++++++++++++++
 .../camel/quarkus/test/UpdateVersionsTest.java     | 417 ++++++++++++
 .../java/com/redhat/camel/quarkus/test/Utils.java  |  86 +++
 .../com/redhat/camel/quarkus/test/UtilsTest.java   |  27 +
 .github/workflows/update-versions.yml              |  39 ++
 11 files changed, 1913 insertions(+)

diff --git 
a/.github/actions/update-versions/.mvn/wrapper/maven-wrapper.properties 
b/.github/actions/update-versions/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 00000000..12fbe1e9
--- /dev/null
+++ b/.github/actions/update-versions/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,19 @@
+# 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
+#
+#   http://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.
+wrapperVersion=3.3.2
+distributionType=only-script
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
diff --git a/.github/actions/update-versions/.sdkmanrc 
b/.github/actions/update-versions/.sdkmanrc
new file mode 100644
index 00000000..46c1a2e5
--- /dev/null
+++ b/.github/actions/update-versions/.sdkmanrc
@@ -0,0 +1,5 @@
+# Enable auto-env through the sdkman_auto_env config
+# Add key=value pairs of SDKs to use below
+java=25-tem
+mvnd=1.0.3
+maven=3.9.11
diff --git a/.github/actions/update-versions/action.yml 
b/.github/actions/update-versions/action.yml
new file mode 100644
index 00000000..e9ba6b06
--- /dev/null
+++ b/.github/actions/update-versions/action.yml
@@ -0,0 +1,36 @@
+name: update-versions
+description: |
+   Update the versions of RHBQ Platform in all active branches
+
+inputs:
+  token:
+    description: "The token to use to authenticate against GitHub API and to 
pull/push"
+    required: true
+  issueId:
+    description: "The ID of the GitHub issue where to report issues"
+    required: true
+  workflowRunUrl:
+    description: "The URL if the workflow run to show in the issue comment"
+    required: true
+
+runs:
+  using: 'composite'
+  steps:
+
+    - name: Set up JDK 25
+      uses: actions/setup-java@v5
+      with:
+        distribution: temurin
+        java-version: 25
+
+    - name: Update versions
+      shell: bash
+      env:
+        GITHUB_ISSUE_ID: ${{ inputs.issueId }}
+        GITHUB_TOKEN: ${{ inputs.token }}
+        GITHUB_REPOSITORY: ${{ github.repository }}
+        CEQ_EXAMPLES_GIT_REPOSITORY: https://github.com/${{ github.repository 
}}.git
+        WORKFLOW_RUN_URL: ${{ inputs.workflowRunUrl }}
+      run: |
+        cd .github/actions/update-versions
+        ./mvnw clean test -B -ntp
diff --git a/.github/actions/update-versions/mvnw 
b/.github/actions/update-versions/mvnw
new file mode 100755
index 00000000..19529ddf
--- /dev/null
+++ b/.github/actions/update-versions/mvnw
@@ -0,0 +1,259 @@
+#!/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
+#
+#    http://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.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Apache Maven Wrapper startup batch script, version 3.3.2
+#
+# Optional ENV vars
+# -----------------
+#   JAVA_HOME - location of a JDK home dir, required when download maven via 
java source
+#   MVNW_REPOURL - repo url base for downloading maven distribution
+#   MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+#   MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; 
others: silence the output
+# ----------------------------------------------------------------------------
+
+set -euf
+[ "${MVNW_VERBOSE-}" != debug ] || set -x
+
+# OS specific support.
+native_path() { printf %s\\n "$1"; }
+case "$(uname)" in
+CYGWIN* | MINGW*)
+  [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
+  native_path() { cygpath --path --windows "$1"; }
+  ;;
+esac
+
+# set JAVACMD and JAVACCMD
+set_java_home() {
+  # For Cygwin and MinGW, ensure paths are in Unix format before anything is 
touched
+  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"
+      JAVACCMD="$JAVA_HOME/jre/sh/javac"
+    else
+      JAVACMD="$JAVA_HOME/bin/java"
+      JAVACCMD="$JAVA_HOME/bin/javac"
+
+      if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
+        echo "The JAVA_HOME environment variable is not defined correctly, so 
mvnw cannot run." >&2
+        echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" 
or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
+        return 1
+      fi
+    fi
+  else
+    JAVACMD="$(
+      'set' +e
+      'unset' -f command 2>/dev/null
+      'command' -v java
+    )" || :
+    JAVACCMD="$(
+      'set' +e
+      'unset' -f command 2>/dev/null
+      'command' -v javac
+    )" || :
+
+    if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
+      echo "The java/javac command does not exist in PATH nor is JAVA_HOME 
set, so mvnw cannot run." >&2
+      return 1
+    fi
+  fi
+}
+
+# hash string like Java String::hashCode
+hash_string() {
+  str="${1:-}" h=0
+  while [ -n "$str" ]; do
+    char="${str%"${str#?}"}"
+    h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
+    str="${str#?}"
+  done
+  printf %x\\n $h
+}
+
+verbose() { :; }
+[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
+
+die() {
+  printf %s\\n "$1" >&2
+  exit 1
+}
+
+trim() {
+  # MWRAPPER-139:
+  #   Trims trailing and leading whitespace, carriage returns, tabs, and 
linefeeds.
+  #   Needed for removing poorly interpreted newline sequences when running in 
more
+  #   exotic environments such as mingw bash on Windows.
+  printf "%s" "${1}" | tr -d '[:space:]'
+}
+
+# parse distributionUrl and optional distributionSha256Sum, requires 
.mvn/wrapper/maven-wrapper.properties
+while IFS="=" read -r key value; do
+  case "${key-}" in
+  distributionUrl) distributionUrl=$(trim "${value-}") ;;
+  distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
+  esac
+done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
+[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in 
${0%/*}/.mvn/wrapper/maven-wrapper.properties"
+
+case "${distributionUrl##*/}" in
+maven-mvnd-*bin.*)
+  MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
+  case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
+  *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
+  :Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
+  :Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
+  :Linux*x86_64*) distributionPlatform=linux-amd64 ;;
+  *)
+    echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use 
pure java version" >&2
+    distributionPlatform=linux-amd64
+    ;;
+  esac
+  distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
+  ;;
+maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
+*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
+esac
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: 
~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
+[ -z "${MVNW_REPOURL-}" ] || 
distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
+distributionUrlName="${distributionUrl##*/}"
+distributionUrlNameMain="${distributionUrlName%.*}"
+distributionUrlNameMain="${distributionUrlNameMain%-bin}"
+MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
+MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string
 "$distributionUrl")"
+
+exec_maven() {
+  unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
+  exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec 
$MAVEN_HOME/bin/$MVN_CMD"
+}
+
+if [ -d "$MAVEN_HOME" ]; then
+  verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+  exec_maven "$@"
+fi
+
+case "${distributionUrl-}" in
+*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
+*) die "distributionUrl is not valid, must match *-bin.zip or 
maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
+esac
+
+# prepare tmp dir
+if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
+  clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
+  trap clean HUP INT TERM EXIT
+else
+  die "cannot create temp dir"
+fi
+
+mkdir -p -- "${MAVEN_HOME%/*}"
+
+# Download and Install Apache Maven
+verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+verbose "Downloading from: $distributionUrl"
+verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+# select .zip or .tar.gz
+if ! command -v unzip >/dev/null; then
+  distributionUrl="${distributionUrl%.zip}.tar.gz"
+  distributionUrlName="${distributionUrl##*/}"
+fi
+
+# verbose opt
+__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q 
__MVNW_QUIET_TAR=''
+[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' 
__MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
+
+# normalize http auth
+case "${MVNW_PASSWORD:+has-password}" in
+'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
+has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' 
;;
+esac
+
+if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
+  verbose "Found wget ... using wget"
+  wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O 
"$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch 
$distributionUrl"
+elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
+  verbose "Found curl ... using curl"
+  curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o 
"$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: 
Failed to fetch $distributionUrl"
+elif set_java_home; then
+  verbose "Falling back to use Java to download"
+  javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
+  targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
+  cat >"$javaSource" <<-END
+       public class Downloader extends java.net.Authenticator
+       {
+         protected java.net.PasswordAuthentication getPasswordAuthentication()
+         {
+           return new java.net.PasswordAuthentication( System.getenv( 
"MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
+         }
+         public static void main( String[] args ) throws Exception
+         {
+           setDefault( new Downloader() );
+           java.nio.file.Files.copy( java.net.URI.create( args[0] 
).toURL().openStream(), java.nio.file.Paths.get( args[1] 
).toAbsolutePath().normalize() );
+         }
+       }
+       END
+  # For Cygwin/MinGW, switch paths to Windows format before running javac and 
java
+  verbose " - Compiling Downloader.java ..."
+  "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed 
to compile Downloader.java"
+  verbose " - Running Downloader.java ..."
+  "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" 
Downloader "$distributionUrl" "$(native_path "$targetZip")"
+fi
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+if [ -n "${distributionSha256Sum-}" ]; then
+  distributionSha256Result=false
+  if [ "$MVN_CMD" = mvnd.sh ]; then
+    echo "Checksum validation is not supported for maven-mvnd." >&2
+    echo "Please disable validation by removing 'distributionSha256Sum' from 
your maven-wrapper.properties." >&2
+    exit 1
+  elif command -v sha256sum >/dev/null; then
+    if echo "$distributionSha256Sum  $TMP_DOWNLOAD_DIR/$distributionUrlName" | 
sha256sum -c >/dev/null 2>&1; then
+      distributionSha256Result=true
+    fi
+  elif command -v shasum >/dev/null; then
+    if echo "$distributionSha256Sum  $TMP_DOWNLOAD_DIR/$distributionUrlName" | 
shasum -a 256 -c >/dev/null 2>&1; then
+      distributionSha256Result=true
+    fi
+  else
+    echo "Checksum validation was requested but neither 'sha256sum' or 
'shasum' are available." >&2
+    echo "Please install either command, or disable validation by removing 
'distributionSha256Sum' from your maven-wrapper.properties." >&2
+    exit 1
+  fi
+  if [ $distributionSha256Result = false ]; then
+    echo "Error: Failed to validate Maven distribution SHA-256, your Maven 
distribution might be compromised." >&2
+    echo "If you updated your Maven version, you need to update the specified 
distributionSha256Sum property." >&2
+    exit 1
+  fi
+fi
+
+# unzip and move
+if command -v unzip >/dev/null; then
+  unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} 
"$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed 
to unzip"
+else
+  tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} 
"$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed 
to untar"
+fi
+printf %s\\n "$distributionUrl" 
>"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
+mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d 
"$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
+
+clean || :
+exec_maven "$@"
diff --git a/.github/actions/update-versions/mvnw.cmd 
b/.github/actions/update-versions/mvnw.cmd
new file mode 100644
index 00000000..249bdf38
--- /dev/null
+++ b/.github/actions/update-versions/mvnw.cmd
@@ -0,0 +1,149 @@
+<# : batch portion
+@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    http://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 Apache Maven Wrapper startup batch script, version 3.3.2
+@REM
+@REM Optional ENV vars
+@REM   MVNW_REPOURL - repo url base for downloading maven distribution
+@REM   MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+@REM   MVNW_VERBOSE - true: enable verbose log; others: silence the output
+@REM 
----------------------------------------------------------------------------
+
+@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
+@SET __MVNW_CMD__=
+@SET __MVNW_ERROR__=
+@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
+@SET PSModulePath=
+@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& 
{$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock 
([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
+  IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE 
(echo %%A=%%B)
+)
+@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
+@SET __MVNW_PSMODULEP_SAVE=
+@SET __MVNW_ARG0_NAME__=
+@SET MVNW_USERNAME=
+@SET MVNW_PASSWORD=
+@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
+@echo Cannot start maven from wrapper >&2 && exit /b 1
+@GOTO :EOF
+: end batch / begin powershell #>
+
+$ErrorActionPreference = "Stop"
+if ($env:MVNW_VERBOSE -eq "true") {
+  $VerbosePreference = "Continue"
+}
+
+# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
+$distributionUrl = (Get-Content -Raw 
"$scriptDir/.mvn/wrapper/maven-wrapper.properties" | 
ConvertFrom-StringData).distributionUrl
+if (!$distributionUrl) {
+  Write-Error "cannot read distributionUrl property in 
$scriptDir/.mvn/wrapper/maven-wrapper.properties"
+}
+
+switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
+  "maven-mvnd-*" {
+    $USE_MVND = $true
+    $distributionUrl = $distributionUrl -replace 
'-bin\.[^.]*$',"-windows-amd64.zip"
+    $MVN_CMD = "mvnd.cmd"
+    break
+  }
+  default {
+    $USE_MVND = $false
+    $MVN_CMD = $script -replace '^mvnw','mvn'
+    break
+  }
+}
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: 
~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
+if ($env:MVNW_REPOURL) {
+  $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { 
"/maven/mvnd/" }
+  $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl 
-replace '^.*'+$MVNW_REPO_PATTERN,'')"
+}
+$distributionUrlName = $distributionUrl -replace '^.*/',''
+$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' 
-replace '-bin$',''
+$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
+if ($env:MAVEN_USER_HOME) {
+  $MAVEN_HOME_PARENT = 
"$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
+}
+$MAVEN_HOME_NAME = 
([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl)
 | ForEach-Object {$_.ToString("x2")}) -join ''
+$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
+
+if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
+  Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+  Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
+  exit $?
+}
+
+if (! $distributionUrlNameMain -or ($distributionUrlName -eq 
$distributionUrlNameMain)) {
+  Write-Error "distributionUrl is not valid, must end with *-bin.zip, but 
found $distributionUrl"
+}
+
+# prepare tmp dir
+$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
+$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path 
"$TMP_DOWNLOAD_DIR_HOLDER.dir"
+$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
+trap {
+  if ($TMP_DOWNLOAD_DIR.Exists) {
+    try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+    catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+  }
+}
+
+New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
+
+# Download and Install Apache Maven
+Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+Write-Verbose "Downloading from: $distributionUrl"
+Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+$webclient = New-Object System.Net.WebClient
+if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
+  $webclient.Credentials = New-Object 
System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
+}
+[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+$webclient.DownloadFile($distributionUrl, 
"$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+$distributionSha256Sum = (Get-Content -Raw 
"$scriptDir/.mvn/wrapper/maven-wrapper.properties" | 
ConvertFrom-StringData).distributionSha256Sum
+if ($distributionSha256Sum) {
+  if ($USE_MVND) {
+    Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease 
disable validation by removing 'distributionSha256Sum' from your 
maven-wrapper.properties."
+  }
+  Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function 
Get-FileHash
+  if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm 
SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
+    Write-Error "Error: Failed to validate Maven distribution SHA-256, your 
Maven distribution might be compromised. If you updated your Maven version, you 
need to update the specified distributionSha256Sum property."
+  }
+}
+
+# unzip and move
+Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath 
"$TMP_DOWNLOAD_DIR" | Out-Null
+Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName 
$MAVEN_HOME_NAME | Out-Null
+try {
+  Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination 
$MAVEN_HOME_PARENT | Out-Null
+} catch {
+  if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
+    Write-Error "fail to move MAVEN_HOME"
+  }
+} finally {
+  try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+  catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+}
+
+Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
diff --git a/.github/actions/update-versions/pom.xml 
b/.github/actions/update-versions/pom.xml
new file mode 100644
index 00000000..3ed4f2dc
--- /dev/null
+++ b/.github/actions/update-versions/pom.xml
@@ -0,0 +1,157 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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
+
+         http://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.
+
+-->
+<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 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+    <modelVersion>4.0.0</modelVersion>
+
+    <groupId>com.redhat.camel</groupId>
+    <artifactId>camel-quarkus-examples-update-versions</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+
+    <name>Camel Quarkus Examples - Update versions</name>
+
+    <properties>
+        <junit.version>6.0.0</junit.version>
+        <assertj.version>3.27.6</assertj.version>
+        <cli-assert.version>0.0.1-alpha2</cli-assert.version>
+        <rest-assured.version>5.5.6</rest-assured.version>
+        <slf4j.version>2.0.17</slf4j.version>
+        <jgit.version>7.3.0.202506031305-r</jgit.version>
+
+        <maven.compiler.release>25</maven.compiler.release>
+
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.junit</groupId>
+                <artifactId>junit-bom</artifactId>
+                <version>${junit.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+            <dependency>
+                <groupId>org.l2x6.cli-assured</groupId>
+                <artifactId>cli-assured</artifactId>
+                <version>${cli-assert.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.assertj</groupId>
+                <artifactId>assertj-core</artifactId>
+                <version>${assertj.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>io.rest-assured</groupId>
+                <artifactId>rest-assured</artifactId>
+                <version>${rest-assured.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <groupId>javax.activation</groupId>
+                        <artifactId>activation</artifactId>
+                    </exclusion>
+                    <exclusion>
+                        <groupId>javax.activation</groupId>
+                        <artifactId>javax.activation-api</artifactId>
+                    </exclusion>
+                    <exclusion>
+                        <groupId>jakarta.activation</groupId>
+                        <artifactId>jakarta.activation-api</artifactId>
+                    </exclusion>
+                    <exclusion>
+                        <groupId>com.sun.xml.bind</groupId>
+                        <artifactId>jaxb-osgi</artifactId>
+                    </exclusion>
+                    <exclusion>
+                        <groupId>commons-logging</groupId>
+                        <artifactId>commons-logging</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+            <dependency>
+                <groupId>org.slf4j</groupId>
+                <artifactId>jcl-over-slf4j</artifactId>
+                <version>${slf4j.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.slf4j</groupId>
+                <artifactId>slf4j-simple</artifactId>
+                <version>${slf4j.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.eclipse.jgit</groupId>
+                <artifactId>org.eclipse.jgit</artifactId>
+                <version>${jgit.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.eclipse.jgit</groupId>
+                <artifactId>org.eclipse.jgit.ssh.jsch</artifactId>
+                <version>${jgit.version}</version>
+            </dependency>
+
+        </dependencies>
+    </dependencyManagement>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.l2x6.cli-assured</groupId>
+            <artifactId>cli-assured</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>io.rest-assured</groupId>
+            <artifactId>rest-assured</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>jcl-over-slf4j</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jgit</groupId>
+            <artifactId>org.eclipse.jgit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jgit</groupId>
+            <artifactId>org.eclipse.jgit.ssh.jsch</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git 
a/.github/actions/update-versions/src/test/java/com/redhat/camel/quarkus/test/ComparableVersion.java
 
b/.github/actions/update-versions/src/test/java/com/redhat/camel/quarkus/test/ComparableVersion.java
new file mode 100644
index 00000000..cddfbab9
--- /dev/null
+++ 
b/.github/actions/update-versions/src/test/java/com/redhat/camel/quarkus/test/ComparableVersion.java
@@ -0,0 +1,719 @@
+/*
+ * 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
+ *
+ *   http://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 com.redhat.camel.quarkus.test;
+
+import java.math.BigInteger;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Properties;
+
+/**
+ * Copied from 
https://github.com/apache/maven/blob/6de6877c2fe772c6d0a19cf518a3fd806b8a0afb/maven-artifact/src/main/java/org/apache/maven/artifact/versioning/ComparableVersion.java
+ * <p>
+ * Generic implementation of version comparison.
+ * </p>
+ *
+ * Features:
+ * <ul>
+ * <li>mixing of '<code>-</code>' (hyphen) and '<code>.</code>' (dot) 
separators,</li>
+ * <li>transition between characters and digits also constitutes a separator:
+ *     <code>1.0alpha1 =&gt; [1, 0, alpha, 1]</code></li>
+ * <li>unlimited number of version components,</li>
+ * <li>version components in the text can be digits or strings,</li>
+ * <li>strings are checked for well-known qualifiers and the qualifier 
ordering is used for version ordering.
+ *     Well-known qualifiers (case insensitive) are:<ul>
+ *     <li><code>alpha</code> or <code>a</code></li>
+ *     <li><code>beta</code> or <code>b</code></li>
+ *     <li><code>milestone</code> or <code>m</code></li>
+ *     <li><code>rc</code> or <code>cr</code></li>
+ *     <li><code>snapshot</code></li>
+ *     <li><code>(the empty string)</code> or <code>ga</code> or 
<code>final</code></li>
+ *     <li><code>sp</code></li>
+ *     </ul>
+ *     Unknown qualifiers are considered after known qualifiers, with lexical 
order (always case insensitive),
+ *   </li>
+ * <li>a hyphen usually precedes a qualifier, and is always less important 
than digits/number, for example
+ *   {@code 1.0.RC2 < 1.0-RC3 < 1.0.1}; but prefer {@code 1.0.0-RC1} over 
{@code 1.0.0.RC1}, and more
+ *   generally: {@code 1.0.X2 < 1.0-X3 < 1.0.1} for any string {@code X}; but 
prefer {@code 1.0.0-X1}
+ *   over {@code 1.0.0.X1}.</li>
+ * </ul>
+ *
+ * @see <a 
href="https://cwiki.apache.org/confluence/display/MAVENOLD/Versioning";>"Versioning"
 on Maven Wiki</a>
+ * @author <a href="mailto:[email protected]";>Kenney Westerhof</a>
+ * @author <a href="mailto:[email protected]";>HervĂ© Boutemy</a>
+ */
+public class ComparableVersion implements Comparable<ComparableVersion> {
+    private static final int MAX_INTITEM_LENGTH = 9;
+
+    private static final int MAX_LONGITEM_LENGTH = 18;
+
+    private String value;
+
+    private String canonical;
+
+    private ListItem items;
+
+    private interface Item {
+        int INT_ITEM = 3;
+        int LONG_ITEM = 4;
+        int BIGINTEGER_ITEM = 0;
+        int STRING_ITEM = 1;
+        int LIST_ITEM = 2;
+
+        int compareTo(Item item);
+
+        int getType();
+
+        boolean isNull();
+    }
+
+    /**
+     * Represents a numeric item in the version item list that can be 
represented with an int.
+     */
+    private static class IntItem implements Item {
+        private final int value;
+
+        public static final IntItem ZERO = new IntItem();
+
+        private IntItem() {
+            this.value = 0;
+        }
+
+        IntItem(String str) {
+            this.value = Integer.parseInt(str);
+        }
+
+        @Override
+        public int getType() {
+            return INT_ITEM;
+        }
+
+        @Override
+        public boolean isNull() {
+            return value == 0;
+        }
+
+        @Override
+        public int compareTo(Item item) {
+            if (item == null) {
+                return (value == 0) ? 0 : 1; // 1.0 == 1, 1.1 > 1
+            }
+
+            switch (item.getType()) {
+                case INT_ITEM:
+                    int itemValue = ((IntItem) item).value;
+                    return (value < itemValue) ? -1 : ((value == itemValue) ? 
0 : 1);
+                case LONG_ITEM:
+                case BIGINTEGER_ITEM:
+                    return -1;
+
+                case STRING_ITEM:
+                    return 1; // 1.1 > 1-sp
+
+                case LIST_ITEM:
+                    return 1; // 1.1 > 1-1
+
+                default:
+                    throw new IllegalStateException("invalid item: " + 
item.getClass());
+            }
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+
+            IntItem intItem = (IntItem) o;
+
+            return value == intItem.value;
+        }
+
+        @Override
+        public int hashCode() {
+            return value;
+        }
+
+        @Override
+        public String toString() {
+            return Integer.toString(value);
+        }
+    }
+
+    /**
+     * Represents a numeric item in the version item list that can be 
represented with a long.
+     */
+    private static class LongItem implements Item {
+        private final long value;
+
+        LongItem(String str) {
+            this.value = Long.parseLong(str);
+        }
+
+        @Override
+        public int getType() {
+            return LONG_ITEM;
+        }
+
+        @Override
+        public boolean isNull() {
+            return value == 0;
+        }
+
+        @Override
+        public int compareTo(Item item) {
+            if (item == null) {
+                return (value == 0) ? 0 : 1; // 1.0 == 1, 1.1 > 1
+            }
+
+            switch (item.getType()) {
+                case INT_ITEM:
+                    return 1;
+                case LONG_ITEM:
+                    long itemValue = ((LongItem) item).value;
+                    return (value < itemValue) ? -1 : ((value == itemValue) ? 
0 : 1);
+                case BIGINTEGER_ITEM:
+                    return -1;
+
+                case STRING_ITEM:
+                    return 1; // 1.1 > 1-sp
+
+                case LIST_ITEM:
+                    return 1; // 1.1 > 1-1
+
+                default:
+                    throw new IllegalStateException("invalid item: " + 
item.getClass());
+            }
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+
+            LongItem longItem = (LongItem) o;
+
+            return value == longItem.value;
+        }
+
+        @Override
+        public int hashCode() {
+            return (int) (value ^ (value >>> 32));
+        }
+
+        @Override
+        public String toString() {
+            return Long.toString(value);
+        }
+    }
+
+    /**
+     * Represents a numeric item in the version item list.
+     */
+    private static class BigIntegerItem implements Item {
+        private final BigInteger value;
+
+        BigIntegerItem(String str) {
+            this.value = new BigInteger(str);
+        }
+
+        @Override
+        public int getType() {
+            return BIGINTEGER_ITEM;
+        }
+
+        @Override
+        public boolean isNull() {
+            return BigInteger.ZERO.equals(value);
+        }
+
+        @Override
+        public int compareTo(Item item) {
+            if (item == null) {
+                return BigInteger.ZERO.equals(value) ? 0 : 1; // 1.0 == 1, 1.1 
> 1
+            }
+
+            switch (item.getType()) {
+                case INT_ITEM:
+                case LONG_ITEM:
+                    return 1;
+
+                case BIGINTEGER_ITEM:
+                    return value.compareTo(((BigIntegerItem) item).value);
+
+                case STRING_ITEM:
+                    return 1; // 1.1 > 1-sp
+
+                case LIST_ITEM:
+                    return 1; // 1.1 > 1-1
+
+                default:
+                    throw new IllegalStateException("invalid item: " + 
item.getClass());
+            }
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+
+            BigIntegerItem that = (BigIntegerItem) o;
+
+            return value.equals(that.value);
+        }
+
+        @Override
+        public int hashCode() {
+            return value.hashCode();
+        }
+
+        public String toString() {
+            return value.toString();
+        }
+    }
+
+    /**
+     * Represents a string in the version item list, usually a qualifier.
+     */
+    private static class StringItem implements Item {
+        private static final List<String> QUALIFIERS =
+                Arrays.asList("alpha", "beta", "milestone", "rc", "snapshot", 
"", "sp");
+
+        private static final Properties ALIASES = new Properties();
+
+        static {
+            ALIASES.put("ga", "");
+            ALIASES.put("final", "");
+            ALIASES.put("release", "");
+            ALIASES.put("cr", "rc");
+        }
+
+        /**
+         * A comparable value for the empty-string qualifier. This one is used 
to determine if a given qualifier makes
+         * the version older than one without a qualifier, or more recent.
+         */
+        private static final String RELEASE_VERSION_INDEX = 
String.valueOf(QUALIFIERS.indexOf(""));
+
+        private final String value;
+
+        StringItem(String value, boolean followedByDigit) {
+            if (followedByDigit && value.length() == 1) {
+                // a1 = alpha-1, b1 = beta-1, m1 = milestone-1
+                switch (value.charAt(0)) {
+                    case 'a':
+                        value = "alpha";
+                        break;
+                    case 'b':
+                        value = "beta";
+                        break;
+                    case 'm':
+                        value = "milestone";
+                        break;
+                    default:
+                }
+            }
+            this.value = ALIASES.getProperty(value, value);
+        }
+
+        @Override
+        public int getType() {
+            return STRING_ITEM;
+        }
+
+        @Override
+        public boolean isNull() {
+            return 
(comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX) == 0);
+        }
+
+        /**
+         * Returns a comparable value for a qualifier.
+         *
+         * This method takes into account the ordering of known qualifiers 
then unknown qualifiers with lexical
+         * ordering.
+         *
+         * just returning an Integer with the index here is faster, but 
requires a lot of if/then/else to check for -1
+         * or QUALIFIERS.size and then resort to lexical ordering. Most 
comparisons are decided by the first character,
+         * so this is still fast. If more characters are needed then it 
requires a lexical sort anyway.
+         *
+         * @param qualifier
+         * @return an equivalent value that can be used with lexical comparison
+         */
+        public static String comparableQualifier(String qualifier) {
+            int i = QUALIFIERS.indexOf(qualifier);
+
+            return i == -1 ? (QUALIFIERS.size() + "-" + qualifier) : 
String.valueOf(i);
+        }
+
+        @Override
+        public int compareTo(Item item) {
+            if (item == null) {
+                // 1-rc < 1, 1-ga > 1
+                return 
comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX);
+            }
+            switch (item.getType()) {
+                case INT_ITEM:
+                case LONG_ITEM:
+                case BIGINTEGER_ITEM:
+                    return -1; // 1.any < 1.1 ?
+
+                case STRING_ITEM:
+                    return 
comparableQualifier(value).compareTo(comparableQualifier(((StringItem) 
item).value));
+
+                case LIST_ITEM:
+                    return -1; // 1.any < 1-1
+
+                default:
+                    throw new IllegalStateException("invalid item: " + 
item.getClass());
+            }
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+
+            StringItem that = (StringItem) o;
+
+            return value.equals(that.value);
+        }
+
+        @Override
+        public int hashCode() {
+            return value.hashCode();
+        }
+
+        public String toString() {
+            return value;
+        }
+    }
+
+    /**
+     * Represents a version list item. This class is used both for the global 
item list and for sub-lists (which start
+     * with '-(number)' in the version specification).
+     */
+    private static class ListItem extends ArrayList<Item> implements Item {
+        @Override
+        public int getType() {
+            return LIST_ITEM;
+        }
+
+        @Override
+        public boolean isNull() {
+            return (size() == 0);
+        }
+
+        void normalize() {
+            for (int i = size() - 1; i >= 0; i--) {
+                Item lastItem = get(i);
+
+                if (lastItem.isNull()) {
+                    // remove null trailing items: 0, "", empty list
+                    remove(i);
+                } else if (!(lastItem instanceof ListItem)) {
+                    break;
+                }
+            }
+        }
+
+        @Override
+        public int compareTo(Item item) {
+            if (item == null) {
+                if (size() == 0) {
+                    return 0; // 1-0 = 1- (normalize) = 1
+                }
+                // Compare the entire list of items with null - not just the 
first one, MNG-6964
+                for (Item i : this) {
+                    int result = i.compareTo(null);
+                    if (result != 0) {
+                        return result;
+                    }
+                }
+                return 0;
+            }
+            switch (item.getType()) {
+                case INT_ITEM:
+                case LONG_ITEM:
+                case BIGINTEGER_ITEM:
+                    return -1; // 1-1 < 1.0.x
+
+                case STRING_ITEM:
+                    return 1; // 1-1 > 1-sp
+
+                case LIST_ITEM:
+                    Iterator<Item> left = iterator();
+                    Iterator<Item> right = ((ListItem) item).iterator();
+
+                    while (left.hasNext() || right.hasNext()) {
+                        Item l = left.hasNext() ? left.next() : null;
+                        Item r = right.hasNext() ? right.next() : null;
+
+                        // if this is shorter, then invert the compare and mul 
with -1
+                        int result = l == null ? (r == null ? 0 : -1 * 
r.compareTo(l)) : l.compareTo(r);
+
+                        if (result != 0) {
+                            return result;
+                        }
+                    }
+
+                    return 0;
+
+                default:
+                    throw new IllegalStateException("invalid item: " + 
item.getClass());
+            }
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder buffer = new StringBuilder();
+            for (Item item : this) {
+                if (buffer.length() > 0) {
+                    buffer.append((item instanceof ListItem) ? '-' : '.');
+                }
+                buffer.append(item);
+            }
+            return buffer.toString();
+        }
+
+        /**
+         * Return the contents in the same format that is used when you call 
toString() on a List.
+         */
+        private String toListString() {
+            StringBuilder buffer = new StringBuilder();
+            buffer.append("[");
+            for (Item item : this) {
+                if (buffer.length() > 1) {
+                    buffer.append(", ");
+                }
+                if (item instanceof ListItem) {
+                    buffer.append(((ListItem) item).toListString());
+                } else {
+                    buffer.append(item);
+                }
+            }
+            buffer.append("]");
+            return buffer.toString();
+        }
+    }
+
+    public ComparableVersion(String version) {
+        parseVersion(version);
+    }
+
+    @SuppressWarnings("checkstyle:innerassignment")
+    public final void parseVersion(String version) {
+        this.value = version;
+
+        items = new ListItem();
+
+        version = version.toLowerCase(Locale.ENGLISH);
+
+        ListItem list = items;
+
+        Deque<Item> stack = new ArrayDeque<>();
+        stack.push(list);
+
+        boolean isDigit = false;
+
+        int startIndex = 0;
+
+        for (int i = 0; i < version.length(); i++) {
+            char c = version.charAt(i);
+
+            if (c == '.') {
+                if (i == startIndex) {
+                    list.add(IntItem.ZERO);
+                } else {
+                    list.add(parseItem(isDigit, version.substring(startIndex, 
i)));
+                }
+                startIndex = i + 1;
+            } else if (c == '-') {
+                if (i == startIndex) {
+                    list.add(IntItem.ZERO);
+                } else {
+                    list.add(parseItem(isDigit, version.substring(startIndex, 
i)));
+                }
+                startIndex = i + 1;
+
+                list.add(list = new ListItem());
+                stack.push(list);
+            } else if (Character.isDigit(c)) {
+                if (!isDigit && i > startIndex) {
+                    // 1.0.0.X1 < 1.0.0-X2
+                    // treat .X as -X for any string qualifier X
+                    if (!list.isEmpty()) {
+                        list.add(list = new ListItem());
+                        stack.push(list);
+                    }
+
+                    list.add(new StringItem(version.substring(startIndex, i), 
true));
+                    startIndex = i;
+
+                    list.add(list = new ListItem());
+                    stack.push(list);
+                }
+
+                isDigit = true;
+            } else {
+                if (isDigit && i > startIndex) {
+                    list.add(parseItem(true, version.substring(startIndex, 
i)));
+                    startIndex = i;
+
+                    list.add(list = new ListItem());
+                    stack.push(list);
+                }
+
+                isDigit = false;
+            }
+        }
+
+        if (version.length() > startIndex) {
+            // 1.0.0.X1 < 1.0.0-X2
+            // treat .X as -X for any string qualifier X
+            if (!isDigit && !list.isEmpty()) {
+                list.add(list = new ListItem());
+                stack.push(list);
+            }
+
+            list.add(parseItem(isDigit, version.substring(startIndex)));
+        }
+
+        while (!stack.isEmpty()) {
+            list = (ListItem) stack.pop();
+            list.normalize();
+        }
+    }
+
+    private static Item parseItem(boolean isDigit, String buf) {
+        if (isDigit) {
+            buf = stripLeadingZeroes(buf);
+            if (buf.length() <= MAX_INTITEM_LENGTH) {
+                // lower than 2^31
+                return new IntItem(buf);
+            } else if (buf.length() <= MAX_LONGITEM_LENGTH) {
+                // lower than 2^63
+                return new LongItem(buf);
+            }
+            return new BigIntegerItem(buf);
+        }
+        return new StringItem(buf, false);
+    }
+
+    private static String stripLeadingZeroes(String buf) {
+        if (buf == null || buf.isEmpty()) {
+            return "0";
+        }
+        for (int i = 0; i < buf.length(); ++i) {
+            char c = buf.charAt(i);
+            if (c != '0') {
+                return buf.substring(i);
+            }
+        }
+        return buf;
+    }
+
+    @Override
+    public int compareTo(ComparableVersion o) {
+        return items.compareTo(o.items);
+    }
+
+    @Override
+    public String toString() {
+        return value;
+    }
+
+    public String getCanonical() {
+        if (canonical == null) {
+            canonical = items.toString();
+        }
+        return canonical;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        return (o instanceof ComparableVersion) && 
items.equals(((ComparableVersion) o).items);
+    }
+
+    @Override
+    public int hashCode() {
+        return items.hashCode();
+    }
+
+    // CHECKSTYLE_OFF: LineLength
+    /**
+     * Main to test version parsing and comparison.
+     * <p>
+     * To check how "1.2.7" compares to "1.2-SNAPSHOT", for example, you can 
issue
+     * <pre>java -jar 
${maven.repo.local}/org/apache/maven/maven-artifact/${maven.version}/maven-artifact-${maven.version}.jar
 "1.2.7" "1.2-SNAPSHOT"</pre>
+     * command to command line. Result of given command will be something like 
this:
+     * <pre>
+     * Display parameters as parsed by Maven (in canonical form) and 
comparison result:
+     * 1. 1.2.7 == 1.2.7
+     *    1.2.7 &gt; 1.2-SNAPSHOT
+     * 2. 1.2-SNAPSHOT == 1.2-snapshot
+     * </pre>
+     *
+     * @param args the version strings to parse and compare. You can pass 
arbitrary number of version strings and always
+     * two adjacent will be compared
+     */
+    public static void main(String... args) {
+        System.out.println("Display parameters as parsed by Maven (in 
canonical form and as a list of tokens) and"
+                + " comparison result:");
+        if (args.length == 0) {
+            return;
+        }
+
+        ComparableVersion prev = null;
+        int i = 1;
+        for (String version : args) {
+            ComparableVersion c = new ComparableVersion(version);
+
+            if (prev != null) {
+                int compare = prev.compareTo(c);
+                System.out.println("   " + prev.toString() + ' ' + ((compare 
== 0) ? "==" : ((compare < 0) ? "<" : ">"))
+                        + ' ' + version);
+            }
+
+            System.out.println(
+                    (i++) + ". " + version + " -> " + c.getCanonical() + "; 
tokens: " + c.items.toListString());
+
+            prev = c;
+        }
+    }
+    // CHECKSTYLE_ON: LineLength
+}
diff --git 
a/.github/actions/update-versions/src/test/java/com/redhat/camel/quarkus/test/UpdateVersionsTest.java
 
b/.github/actions/update-versions/src/test/java/com/redhat/camel/quarkus/test/UpdateVersionsTest.java
new file mode 100644
index 00000000..95cacb27
--- /dev/null
+++ 
b/.github/actions/update-versions/src/test/java/com/redhat/camel/quarkus/test/UpdateVersionsTest.java
@@ -0,0 +1,417 @@
+package com.redhat.camel.quarkus.test;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.UUID;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.assertj.core.api.Assertions;
+import org.assertj.core.api.ListAssert;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.ResetCommand.ResetType;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.api.errors.InvalidRemoteException;
+import org.eclipse.jgit.api.errors.TransportException;
+import org.eclipse.jgit.errors.UnsupportedCredentialItem;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.transport.CredentialItem;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.FetchResult;
+import org.eclipse.jgit.transport.URIish;
+import org.junit.jupiter.api.Test;
+import org.l2x6.cli.assured.CliAssured;
+import org.l2x6.cli.assured.CommandOutput.Line;
+import org.l2x6.cli.assured.CommandOutput.Stream;
+import org.l2x6.cli.assured.CommandResult;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.restassured.RestAssured;
+
+public class UpdateVersionsTest {
+    static final Logger log = 
LoggerFactory.getLogger(GitHubTokenCredentials.class);
+
+    private static final String LATEST_LEGACY_BRANCH = "3.27.0-product";
+    private static final Pattern BRANCH_PATTERN = 
Pattern.compile("[0-9]+\\.[0-9]+\\.x-product");
+    static final String quarkusRegistryBaseUrl = 
"https://registry.quarkus.redhat.com";;
+    static final ComparableVersion minimalCEQVersion = new 
ComparableVersion("3.27.0");
+    private static final Pattern ERROR_PATTERN = Pattern.compile("error", 
Pattern.CASE_INSENSITIVE);
+    @Test
+    void update() throws IOException, InvalidRemoteException, 
TransportException, GitAPIException, URISyntaxException {
+
+        /* Input parameters */
+        final boolean localTest = 
Boolean.parseBoolean(System.getenv("LOCAL_TEST"));
+        String remoteUrl = System.getenv("CEQ_EXAMPLES_GIT_REPOSITORY");
+        if (remoteUrl == null || remoteUrl.isEmpty()) {
+            remoteUrl = 
"[email protected]:jboss-fuse/camel-quarkus-examples.git";
+        }
+        log.info("Using remote " + remoteUrl);
+        final String ghRepository = System.getenv("GITHUB_REPOSITORY");
+        final String ghToken = System.getenv("GITHUB_TOKEN");
+        final String issueId = System.getenv("GITHUB_ISSUE_ID");
+        final String workflowRunUrl = System.getenv("WORKFLOW_RUN_URL");
+        if (!localTest) {
+            Objects.requireNonNull(ghRepository, "GITHUB_REPOSITORY");
+            Objects.requireNonNull(ghToken, "GITHUB_TOKEN");
+            Objects.requireNonNull(issueId, "GITHUB_ISSUE_ID");
+            Objects.requireNonNull(workflowRunUrl, "WORKFLOW_RUN_URL");
+        } else {
+            log.warn("No changes will be pushed because LOCAL_TEST=true");
+        }
+
+        try {
+
+            final String cqPluginVersion = Utils.getLatestCqPluginVersion();
+            final Path mvnwPath = Path.of("mvnw").toAbsolutePath().normalize();
+            Assertions.assertThat(mvnwPath).isRegularFile();
+
+            final String remoteAlias = "midstream";
+            final CredentialsProvider creds = new 
GitHubTokenCredentials(ghToken);
+
+            /* From branch name such as 3.27.x-product to RHBQ Platform 
version, such as 3.27.0.redhat-00001 */
+            final Map<String, String> branchToRhbqPlatformVersion = 
Utils.collectVersions(
+                    quarkusRegistryBaseUrl,
+                    minimalCEQVersion);
+
+            final String uuid = UUID.randomUUID().toString();
+            final Path checkoutDir = Path.of("target/checkout-" + 
uuid).toAbsolutePath().normalize();
+            Files.createDirectories(checkoutDir);
+            try (Git git = Git.init()
+                    .setDirectory(checkoutDir.toFile())
+                    .call()) {
+                git.remoteAdd().setName(remoteAlias).setUri(new 
URIish(remoteUrl)).call();
+
+                /* remoteBranchMap is from branch name such as 3.27.x-product 
to commit hash so that we can properly reset it */
+                final Map<String, String> remoteBranchMap = fetchBranches(git, 
remoteUrl, remoteAlias, creds);
+
+                for (Entry<String, String> en : 
branchToRhbqPlatformVersion.entrySet()) {
+                    final String branch = en.getKey();
+                    final String platformVersion = en.getValue();
+
+                    final String remoteHead;
+                    if (remoteBranchMap.containsKey(branch)) {
+                        /* The branch exists in the remote already */
+                        remoteHead = remoteBranchMap.get(branch);
+                        log.info("Updating branch " + branch + " to RHBQ 
Platform " + platformVersion);
+                    } else {
+                        /*
+                         * The branch does not exist yet in the remote so we 
create it based on the latest existing
+                         * major.minor.x branch
+                         */
+                        final String latestExistingBranch = 
remoteBranchMap.keySet().iterator().next();
+                        log.info("Creating branch " + branch + " from  " + 
latestExistingBranch
+                                + " and updating it to RHBQ Platform " + 
platformVersion);
+                        remoteHead = remoteBranchMap.get(latestExistingBranch);
+                    }
+
+                    /* Checkout or create the local branch */
+                    
git.branchCreate().setName(branch).setForce(true).setStartPoint(remoteHead).call();
+                    git.checkout().setName(branch).call();
+                    
git.reset().setMode(ResetType.HARD).setRef(remoteHead).call();
+                    final Ref ref = git.getRepository().exactRef("HEAD");
+                    log.info("Reset the working copy to {}@{}", branch, 
ref.getObjectId().getName());
+
+                    /*
+                     * # Select or adjust the Platform version
+                     * ./mvnw 
org.l2x6.cq:cq-prod-maven-plugin:${CQ_PLUGIN_VERSION}:sync-examples-from-upstream
 \
+                     *   -Pprod \
+                     *   -Dcq.quarkus.platform.version=${PLATFORM_VERSION}
+                     */
+                    final Path examplesMvnwPath = checkoutDir.resolve("mvnw");
+                    {
+                        List<Line> lines = CliAssured.command(
+                                examplesMvnwPath.toString(),
+                                "org.l2x6.cq:cq-prod-maven-plugin:" + 
cqPluginVersion + ":sync-examples-from-upstream",
+                                "-Dcq.quarkus.platform.version=" + 
platformVersion,
+                                "-ntp",
+                                "-B"
+                                )
+                                .cd(checkoutDir)
+                                .start()
+                                .awaitTermination(Duration.ofMinutes(10))
+                                .assertSuccess()
+                                .output()
+                                .hasLineContaining("BUILD SUCCESS")
+                                .lines()
+                                ;
+                        noErrors(lines);
+                    }
+
+                    /* Run tests if there are changes */
+                    if (localTest || Utils.hasChanges(git)) {
+
+                        /* Commit */
+                        git.add().addFilepattern(".").call();
+                        final String msg = "Upgrade to RHBQ Platform " + 
platformVersion;
+                        log.info("git: {}", msg);
+                        git.commit()
+                            .setAuthor("Camel Quarkus Examples Autoupdater", 
"autoupdater@localhost")
+                            .setMessage(msg)
+                            .call();
+
+                        /* Test */
+                        final List<Path> exampleDirs;
+                        try (java.util.stream.Stream<Path> dirs = 
Files.list(checkoutDir)) {
+                            exampleDirs = dirs.filter(Files::isDirectory)
+                            .filter(dir -> 
Files.exists(dir.resolve("pom.xml")))
+                            .map(checkoutDir::resolve)
+                            .collect(Collectors.toList());
+                        }
+
+                        for (Path exampleDir : exampleDirs) {
+                            Path logFile = Path.of("target/" + 
exampleDir.getFileName() + ".log").toAbsolutePath().normalize();
+                            CommandResult result = CliAssured.command(
+                                    checkoutDir.resolve("mvnw").toString(),
+                                    "clean",
+                                    "verify",
+                                    "-ntp",
+                                    "-B"
+                                    )
+                                    .cd(exampleDir)
+                                    .start()
+                                    .awaitTermination(Duration.ofMinutes(10));
+                            Files.write(
+                                    logFile,
+                                    result.output().lines().stream()
+                                        .map(Line::toString)
+                                        .collect(Collectors.joining("\n"))
+                                        .getBytes(StandardCharsets.UTF_8));
+
+                            result.assertSuccess()
+                                    .output()
+                                    .hasLineContaining("BUILD SUCCESS");
+                        }
+
+                        if (!localTest) {
+                            /* Push */
+                            git.push()
+                                .setRemote(remoteAlias)
+                                .add(branch)
+                                .setCredentialsProvider(creds)
+                                .call();
+                        }
+                    }
+                }
+            }
+            /* Close if needed */
+            if (!localTest) {
+                RestAssured.given()
+                .accept("application/vnd.github+json")
+                .header("Authorization", "Bearer " + ghToken)
+                .header("X-GitHub-Api-Version", "2022-11-28")
+                .body("""
+                        {
+                            "state":"closed"
+                        }
+                        """)
+                .patch("https://api.github.com/repos/"; + ghRepository + 
"/issues/" + issueId)
+                .then()
+                .statusCode(200);
+            }
+        } catch (Exception e) {
+            reportFailure(e, ghRepository, issueId, workflowRunUrl, ghToken, 
localTest);
+        }
+    }
+
+    private ListAssert<Line> noErrors(List<Line> lines) {
+        return Assertions.assertThat(lines).allMatch(l -> l.stream() == 
Stream.stderr ? !ERROR_PATTERN.matcher(l.line()).find() : true);
+    }
+
+    static void reportFailure(Exception e, String ghRepository, String 
issueId, String workflowRunUrl, String ghToken, boolean localTest) {
+
+        if (localTest) {
+            throw new RuntimeException(e);
+        }
+
+        final Writer stackTrace = new StringWriter();
+        try (PrintWriter pw = new PrintWriter(stackTrace)) {
+            log.error("Failed", e);
+        }
+
+
+        /* Add comment */
+        String st = stackTrace.toString()
+        .replace("\"", "\\\"")
+        .replace("\\", "\\\\")
+        .replace("\n", "\\n")
+        .replace("\t", "\\t");
+        if (st.length() > 65000) {
+            st = st.substring(0, 65000);
+        }
+        final String body = """
+                {
+                    "body" : "`update-versions` failed in %s 
:\\n\\n```\\n%s\\n```"
+                }
+                """.formatted(workflowRunUrl, st);
+        //log.info("Creating new comment " + body);
+        RestAssured.given()
+                .accept("application/vnd.github+json")
+                .header("Authorization", "Bearer " + ghToken)
+                .header("X-GitHub-Api-Version", "2022-11-28")
+                .body(body)
+                .post("https://api.github.com/repos/"; + ghRepository + 
"/issues/" + issueId + "/comments")
+                .then()
+                .statusCode(201);
+
+        /* Open the issue if needed */
+        RestAssured.given()
+                .accept("application/vnd.github+json")
+                .header("Authorization", "Bearer " + ghToken)
+                .header("X-GitHub-Api-Version", "2022-11-28")
+                .body("""
+                        {
+                            "state":"open"
+                        }
+                        """)
+                .patch("https://api.github.com/repos/"; + ghRepository + 
"/issues/" + issueId)
+                .then()
+                .statusCode(200);
+    }
+
+    static Map<String, String> fetchBranches(Git git, String remoteUrl, String 
remoteAlias, CredentialsProvider creds)
+            throws InvalidRemoteException, TransportException, GitAPIException 
{
+        final Set<String> remoteBranches = Git.lsRemoteRepository()
+                .setCredentialsProvider(creds)
+                .setHeads(true)
+                .setTags(false)
+                .setRemote(remoteUrl)
+                .call().stream()
+                .map(ref -> ref.getName().substring("refs/heads/".length()))
+                .filter(b -> BRANCH_PATTERN.matcher(b).matches() || 
LATEST_LEGACY_BRANCH.equals(b))
+                .collect(Collectors.toCollection(() -> new TreeSet<>(new 
BranchComparator().reversed())));
+        log.info("Available branches in {}: {}", remoteAlias, remoteBranches);
+
+        Map<String, String> result = new LinkedHashMap<>();
+        for (String branch : remoteBranches) {
+            log.info("Fetching {} from {}", branch, remoteAlias);
+            final String remoteRef = "refs/heads/" + branch;
+            final FetchResult fetchResult = git.fetch()
+                    .setRemote(remoteAlias)
+                    .setRefSpecs(remoteRef)
+                    .setCredentialsProvider(creds)
+                    .call();
+            final String sha1 = 
fetchResult.getAdvertisedRef(remoteRef).getObjectId().getName();
+            result.put(branch, sha1);
+        }
+        return Collections.unmodifiableMap(result);
+    }
+
+    static String toUrl(String url, String groupId, String artifactId, String 
version, String type) {
+        final StringBuilder sb = new StringBuilder();
+        sb.append(url);
+        if (!url.endsWith("/")) {
+            sb.append('/');
+        }
+        sb.append(groupId.replace('.', '/'))
+                .append('/').append(artifactId)
+                .append('/').append(version)
+                
.append('/').append(artifactId).append('-').append(version).append(".").append(type);
+        return sb.toString();
+    }
+
+    static class GitHubTokenCredentials extends CredentialsProvider {
+
+        private String ghToken;
+
+        public GitHubTokenCredentials(String ghToken) {
+            this.ghToken = ghToken;
+        }
+
+        @Override
+        public boolean isInteractive() {
+            return false;
+        }
+
+        @Override
+        public boolean supports(CredentialItem... items) {
+            for (CredentialItem i : items) {
+                if (i instanceof CredentialItem.InformationalMessage) {
+                    continue;
+                }
+                if (i instanceof CredentialItem.Username) {
+                    continue;
+                }
+                if (i instanceof CredentialItem.Password) {
+                    continue;
+                }
+                if (i instanceof CredentialItem.StringType) {
+                    if (i.getPromptText().equals("Password: ")) {
+                        continue;
+                    }
+                }
+                if (i instanceof CredentialItem.YesNoType) {
+                    if (i.getPromptText().startsWith("The authenticity of host 
'github.com' can't be established.")) {
+                        continue;
+                    }
+                }
+                return false;
+            }
+            return true;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public boolean get(URIish uri, CredentialItem... items) throws 
UnsupportedCredentialItem {
+            String username = "x-access-token";
+            for (CredentialItem i : items) {
+                if (i instanceof CredentialItem.InformationalMessage) {
+                    continue;
+                }
+                if (i instanceof CredentialItem.Username) {
+                    ((CredentialItem.Username) i).setValue(username);
+                    continue;
+                }
+                if (i instanceof CredentialItem.Password && ghToken != null) {
+                    ((CredentialItem.Password) 
i).setValue(ghToken.toCharArray());
+                    continue;
+                }
+                if (i instanceof CredentialItem.StringType && ghToken != null) 
{
+                    if (i.getPromptText().equals("Password: ")) {
+                        ((CredentialItem.StringType) i).setValue(ghToken);
+                        continue;
+                    }
+                }
+                if (i instanceof CredentialItem.YesNoType && ghToken != null) {
+                    if (i.getPromptText().startsWith("The authenticity of host 
'github.com' can't be established.")) {
+                        ((CredentialItem.YesNoType) i).setValue(true);
+                        continue;
+                    }
+                }
+                throw new UnsupportedCredentialItem(uri, i.getClass().getName()
+                        + ":" + i.getPromptText());
+            }
+            return true;
+        }
+
+    }
+
+    static class BranchComparator implements Comparator<String> {
+
+        @Override
+        public int compare(String branch1, String branch2) {
+            String v1 = branch1.replace(".x", ".9999");
+            String v2 = branch2.replace(".x", ".9999");
+            return new ComparableVersion(v1).compareTo(new 
ComparableVersion(v2));
+        }
+
+    }
+}
diff --git 
a/.github/actions/update-versions/src/test/java/com/redhat/camel/quarkus/test/Utils.java
 
b/.github/actions/update-versions/src/test/java/com/redhat/camel/quarkus/test/Utils.java
new file mode 100644
index 00000000..a27b4530
--- /dev/null
+++ 
b/.github/actions/update-versions/src/test/java/com/redhat/camel/quarkus/test/Utils.java
@@ -0,0 +1,86 @@
+package com.redhat.camel.quarkus.test;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.Status;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.errors.NoWorkTreeException;
+
+import io.restassured.RestAssured;
+import io.restassured.path.json.JsonPath;
+import io.restassured.path.xml.XmlPath;
+
+public class Utils {
+    public static String getLatestCqPluginVersion() {
+        XmlPath body = 
RestAssured.get("https://repo1.maven.org/maven2/org/l2x6/cq/cq-prod-maven-plugin/maven-metadata.xml";)
+            .then()
+            .statusCode(200)
+            .extract().body().xmlPath();
+
+        String latest = body.get("metadata.versioning.latest");
+        return latest;
+    }
+
+    public static Map<String, String> collectVersions(String 
quarkusRegistryBaseUrl, ComparableVersion minimalCQVersion) {
+        Map<String, String> result = new LinkedHashMap<>();
+
+        final JsonPath jsonPath = RestAssured.get(quarkusRegistryBaseUrl + 
"/client/platforms")
+                .then()
+                .statusCode(200)
+                .extract().jsonPath();
+
+        final List<Map<String, Object>> plfs = jsonPath.get("platforms");
+
+        for (Map<String, Object> plf : plfs) {
+            if ("com.redhat.quarkus.platform".equals(plf.get("platform-key"))) 
{
+                final List<Map<String, Object>> streams = (List<Map<String, 
Object>>) plf.get("streams");
+                for (Map<String, Object> stream : streams) {
+                    final String streamId = (String) stream.get("id");
+                    final String branch = streamId + ".x-product";
+                    final List<Map<String, Object>> releases = 
(List<Map<String, Object>>) stream.get("releases");
+                    for (Map<String, Object> release : releases) {
+
+                        final List<String> boms = (List<String>) 
release.get("member-boms");
+                        boms.stream()
+                                .filter(gav -> 
gav.startsWith("com.redhat.quarkus.platform:quarkus-camel-bom:"))
+                                .findFirst()
+                                .ifPresent(ceqBomGav -> {
+                                    final String[] ceqBomGavSegments = 
ceqBomGav.split(":");
+                                    final String bomVersion = 
ceqBomGavSegments[4];
+                                    final ComparableVersion 
comparableBomVersion = new ComparableVersion(bomVersion);
+                                    if 
(minimalCQVersion.compareTo(comparableBomVersion) > 0) {
+                                        UpdateVersionsTest.log.info("Skipping 
Platform version " + bomVersion + " because it is older than " + 
minimalCQVersion);
+                                        return;
+                                    }
+                                    if (bomVersion.contains(".redhat-")) {
+                                        UpdateVersionsTest.log.info("Found 
Platform BOM " + bomVersion + " in " + ceqBomGav);
+                                        result.put(branch, bomVersion);
+                                    } else {
+                                        UpdateVersionsTest.log.info("Ignoring 
non-redhat Platform BOM " + ceqBomGav);
+                                    }
+                                });
+                    }
+                }
+            }
+        }
+
+        return Collections.unmodifiableMap(result);
+    }
+
+    public static boolean hasChanges(Git git) throws NoWorkTreeException, 
GitAPIException {
+        Status status = git.status().call();
+        boolean hasChanges =
+                !status.getModified().isEmpty() ||
+                !status.getAdded().isEmpty() ||
+                !status.getRemoved().isEmpty() ||
+                !status.getMissing().isEmpty() ||
+                !status.getChanged().isEmpty() ||
+                !status.getConflicting().isEmpty() ||
+                !status.getUntracked().isEmpty();
+        return hasChanges;
+    }
+}
diff --git 
a/.github/actions/update-versions/src/test/java/com/redhat/camel/quarkus/test/UtilsTest.java
 
b/.github/actions/update-versions/src/test/java/com/redhat/camel/quarkus/test/UtilsTest.java
new file mode 100644
index 00000000..1ee777a8
--- /dev/null
+++ 
b/.github/actions/update-versions/src/test/java/com/redhat/camel/quarkus/test/UtilsTest.java
@@ -0,0 +1,27 @@
+package com.redhat.camel.quarkus.test;
+
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class UtilsTest {
+
+    @Test
+    void getLatestCqPluginVersion() {
+        
Assertions.assertThat(Utils.getLatestCqPluginVersion()).matches("[\\d]+\\.[\\d]+\\.[\\d]+");
+    }
+
+    @Test
+    void collectVersions() {
+        Map<String, String> versions = 
Utils.collectVersions(UpdateVersionsTest.quarkusRegistryBaseUrl, 
UpdateVersionsTest.minimalCEQVersion);
+        Assertions.assertThat(versions).hasSizeGreaterThan(0);
+
+        for (Entry<String, String> en : versions.entrySet()) {
+            
Assertions.assertThat(en.getKey()).matches("[\\d]+\\.[\\d]+\\.x-product");
+            
Assertions.assertThat(en.getValue()).matches("[\\d]+\\.[\\d]+\\.[\\d]+\\.redhat-[0-9]{5}");
+        }
+    }
+
+}
diff --git a/.github/workflows/update-versions.yml 
b/.github/workflows/update-versions.yml
new file mode 100644
index 00000000..fad00aa9
--- /dev/null
+++ b/.github/workflows/update-versions.yml
@@ -0,0 +1,39 @@
+name: update-versions
+
+on:
+  workflow_dispatch:
+  schedule:
+    # Run every day at 2AM
+    - cron:  '0 2 * * *'
+
+env:
+  LANG: en_US.UTF-8
+
+concurrency:
+  group: ${{ github.ref }}-${{ github.workflow }}
+  cancel-in-progress: true
+
+permissions:
+  contents: write
+  issues: write
+  
+jobs:
+  update-versions:
+    if: github.repository == 'jboss-fuse/camel-quarkus-examples'
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+
+    steps:
+
+    - uses: actions/checkout@v5
+      with:
+        fetch-depth: 0
+
+    - name: update-versions
+      uses: ./.github/actions/update-versions
+      id: update-versions
+      with:
+        token: "${{ secrets.GITHUB_TOKEN }}"
+        issueId: '40'
+        workflowRunUrl: ${{ github.server_url }}/${{ github.repository 
}}/actions/runs/${{ github.run_id }}

Reply via email to