Copilot commented on code in PR #7123:
URL: https://github.com/apache/incubator-kie/pull/7123#discussion_r4223227255


##########
.ci/jenkins/Jenkinsfile.103xplus.promote:
##########
@@ -0,0 +1,256 @@
+/*
+ * 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.
+ */
+
+import org.jenkinsci.plugins.workflow.libs.Library
+@Library('jenkins-pipeline-shared-libraries')_
+
+import org.kie.jenkins.MavenCommand
+
+deployProperties = [:]
+pipelineProperties = [:]
+
+pipeline {
+    agent {
+        docker {
+            image env.AGENT_DOCKER_BUILDER_IMAGE
+            args env.AGENT_DOCKER_BUILDER_ARGS
+            label util.avoidFaultyNodes()
+        }
+    }
+
+    options {
+        timestamps()
+        timeout(time: 180, unit: 'MINUTES')
+    }
+
+    environment {
+        DROOLS_CI_EMAIL_TO = credentials("${JENKINS_EMAIL_CREDS_ID}")
+    }
+
+    stages {
+        stage('Initialization') {
+            steps {
+                script {
+                    cleanWs()
+
+                    if (params.DISPLAY_NAME != '') {
+                        currentBuild.displayName = params.DISPLAY_NAME
+                    }
+
+                    readDeployProperties()
+
+                    assert getProjectVersion()
+                    assert getBuildBranch() == 
util.getReleaseBranchFromVersion(getProjectVersion())
+                }
+            }
+        }
+
+        stage('Merge deploy PR and tag') {
+            steps {
+                script {
+                    dir(getRepoName()) {
+                        approveAndMergePR(getDeployPrLink())
+                        checkoutRepo()
+                        tagLatest()
+                    }
+                }
+            }
+        }
+
+        stage('Create release') {
+            steps {
+                script {
+                    dir(getRepoName()) {
+                        checkoutRepo()
+                        if (githubscm.isReleaseExist(getGitTag(), 
getGitAuthorCredsId())) {
+                            githubscm.deleteRelease(getGitTag(), 
getGitAuthorPushCredsId())
+                        }
+                        
githubscm.createReleaseWithGeneratedReleaseNotes(getGitTag(), getBuildBranch(), 
githubscm.getPreviousTagFromVersion(getGitTag()), getGitAuthorPushCredsId())
+                        githubscm.updateReleaseBody(getGitTag(), 
getGitAuthorPushCredsId())
+                    }
+                }
+            }
+        }
+
+        stage('Upload drools binaries and documentation') {
+            when {
+                expression { return isMainStream() }
+            }
+            steps {
+                script {
+                    configFileProvider([configFile(fileId: 
env.MAVEN_SETTINGS_CONFIG_FILE_ID, variable: 'MAVEN_SETTINGS_FILE')]) {
+                        getMavenCommand()
+                            .withOptions(env.DROOLS_BUILD_MVN_OPTS ? [ 
env.DROOLS_BUILD_MVN_OPTS ] : [])
+                            .withOptions(env.BUILD_MVN_OPTS_CURRENT ? [ 
env.BUILD_MVN_OPTS_CURRENT ] : [])
+                            .inDirectory(getRepoName())
+                            .skipTests(true)
+                            .withProperty('full')
+                            .withSettingsXmlFile(MAVEN_SETTINGS_FILE)
+                            .run('clean install')
+                    }
+                    uploadFileMgmt(getRepoName())
+                }
+            }
+        }
+    }
+
+    post {
+        unsuccessful {
+            sendNotification()
+        }
+        cleanup {
+            script {
+                util.cleanNode()
+            }
+        }
+    }
+}
+
+void sendNotification() {
+    if (params.SEND_NOTIFICATION) {
+        mailer.sendMarkdownTestSummaryNotification('Promote', 
"[${getBuildBranch()}] Drools", [env.DROOLS_CI_EMAIL_TO])
+    } else {
+        echo 'No notification sent per configuration'
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Deployment properties
+//////////////////////////////////////////////////////////////////////////////
+
+void readDeployProperties() {
+    String deployUrl = params.DEPLOY_BUILD_URL
+    if (deployUrl != '') {
+        if (!deployUrl.endsWith('/')) {
+            deployUrl += '/'
+        }
+        sh "wget ${deployUrl}artifact/${PROPERTIES_FILE_NAME} -O 
${PROPERTIES_FILE_NAME}"
+        deployProperties = readProperties file: PROPERTIES_FILE_NAME
+        // echo all properties
+        echo deployProperties.collect { entry -> "${entry.key}=${entry.value}" 
}.join('\n')
+    }
+}
+
+boolean hasDeployProperty(String key) {
+    return deployProperties[key] != null
+}
+
+String getDeployProperty(String key) {
+    if (hasDeployProperty(key)) {
+        return deployProperties[key]
+    }
+    return ''
+}
+
+String getParamOrDeployProperty(String paramKey, String deployPropertyKey) {
+    if (params[paramKey] != '') {
+        return params[paramKey]
+    }
+    return getDeployProperty(deployPropertyKey)
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Getter / Setter
+//////////////////////////////////////////////////////////////////////////////
+
+String getRepoName() {
+    return env.REPO_NAME
+}
+
+String getProjectVersion() {
+    return getParamOrDeployProperty('PROJECT_VERSION', 'project.version')
+}
+
+String getGitTag() {
+    return params.GIT_TAG != '' ? params.GIT_TAG : getProjectVersion()
+}
+
+String getBuildBranch() {
+    return params.BUILD_BRANCH_NAME
+}
+
+String getGitAuthor() {
+    return env.GIT_AUTHOR
+}
+
+String getGitAuthorCredsId() {
+    return env.GIT_AUTHOR_CREDS_ID
+}
+
+String getGitAuthorPushCredsId() {
+    return env.GIT_AUTHOR_PUSH_CREDS_ID
+}
+
+String getDeployPrLink() {
+    return getDeployProperty("${getRepoName()}.pr.link")
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Git
+//////////////////////////////////////////////////////////////////////////////
+
+void checkoutRepo() {
+    deleteDir()
+    checkout(githubscm.resolveRepository(getRepoName(), getGitAuthor(), 
getBuildBranch(), false, getGitAuthorCredsId()))
+    // need to manually checkout branch since on a detached branch after 
checkout command
+    sh "git checkout ${getBuildBranch()}"
+}
+
+void approveAndMergePR(String prLink) {
+    if (prLink?.trim()) {
+        githubscm.approvePR(prLink, getGitAuthorPushCredsId())
+        githubscm.mergePR(prLink, getGitAuthorPushCredsId())
+    }
+}
+
+void tagLatest() {
+    if (getGitTag()) {
+        githubscm.tagLocalAndRemoteRepository('origin', getGitTag(), 
getGitAuthorPushCredsId(), env.BUILD_TAG, true)

Review Comment:
   `checkoutRepo()` selects `getBuildBranch()`, so this creates the final tag 
at the branch tip rather than the approved RC commit. The new RC script 
deliberately keeps the release-version commit only under its RC tag, leaving 
the development branch unchanged. Promotion can therefore tag snapshot POMs or 
later, unvoted changes. Accept the winning RC tag and delegate final-tag 
creation to `05-tag-release.sh`; do not keep a second branch-tip tagging path.



##########
.ci/jenkins/Jenkinsfile.103xplus.deploy:
##########
@@ -0,0 +1,327 @@
+/*
+ * 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.
+ */
+
+import org.jenkinsci.plugins.workflow.libs.Library
+@Library('jenkins-pipeline-shared-libraries')_
+
+import org.kie.jenkins.MavenCommand
+
+deployProperties = [:]
+
+pipeline {
+    agent {
+        docker {
+            image env.AGENT_DOCKER_BUILDER_IMAGE
+            args env.AGENT_DOCKER_BUILDER_ARGS
+            label util.avoidFaultyNodes()
+        }
+    }
+
+    options {
+        timestamps()
+        timeout(time: 180, unit: 'MINUTES')
+    }
+
+    environment {
+        DROOLS_CI_EMAIL_TO = credentials("${JENKINS_EMAIL_CREDS_ID}")
+
+        PR_BRANCH_HASH = "${util.generateHash(10)}"
+    }
+
+    stages {
+        stage('Initialize') {
+            steps {
+                script {
+                    cleanWs(disableDeferredWipeout: true)
+
+                    if (params.DISPLAY_NAME) {
+                        currentBuild.displayName = params.DISPLAY_NAME
+                    }
+
+                    if (isRelease() || isCreatePr()) {
+                        // Verify version is set
+                        assert getProjectVersion()
+
+                        if (isRelease()) {
+                            // Verify if on right release branch
+                            assert getBuildBranch() == 
util.getReleaseBranchFromVersion(getProjectVersion())
+                        }
+                    }
+
+                    dir(getRepoName()) {
+                        checkoutRepo()
+                    }
+                }
+            }
+            post {
+                success {
+                    script {
+                        setDeployPropertyIfNeeded('git.branch', 
getBuildBranch())
+                        setDeployPropertyIfNeeded('git.author', getGitAuthor())
+                        setDeployPropertyIfNeeded('project.version', 
getProjectVersion())
+                        setDeployPropertyIfNeeded('release', isRelease())
+                    }
+                }
+            }
+        }
+
+        stage('Prepare for PR') {
+            when {
+                expression { return isCreatePr() }
+            }
+            steps {
+                script {
+                    dir(getRepoName()) {
+                        if (githubscm.isBranchExist('origin', getPRBranch())) {
+                            githubscm.removeRemoteBranch('origin', 
getPRBranch(), getGitAuthorPushCredsId())
+                        }
+                        githubscm.createBranch(getPRBranch())
+                    }
+                }
+            }
+        }
+
+        stage('Update project version') {
+            when {
+                expression { return getProjectVersion() }
+            }
+            steps {
+                script {
+                    configFileProvider([configFile(fileId: 
env.MAVEN_SETTINGS_CONFIG_FILE_ID, variable: 'MAVEN_SETTINGS_FILE')]) {
+                        maven.mvnVersionsSet(
+                            
getMavenCommand().withSettingsXmlFile(MAVEN_SETTINGS_FILE),
+                            getProjectVersion(),
+                            !isRelease()
+                        )
+                    }
+                }
+            }
+        }
+
+        stage('Build & Deploy repo') {
+            steps {
+                script {
+                    withCredentials([usernamePassword(credentialsId: 
env.MAVEN_REPO_CREDS_ID, usernameVariable: 'REPOSITORY_USER', passwordVariable: 
'REPOSITORY_TOKEN')]) {
+                        def installOrDeploy
+                        if (shouldDeployToRepository()) {
+                            installOrDeploy = "deploy -DdeployAtEnd 
-Dapache.repository.username=${REPOSITORY_USER} 
-Dapache.repository.password=${REPOSITORY_TOKEN} -DretryFailedDeploymentCount=5"
+                        } else {
+                            installOrDeploy = 'install'
+                        }
+                        def mavenCommand = getMavenCommand()
+                            .withOptions(env.DROOLS_BUILD_MVN_OPTS ? [ 
env.DROOLS_BUILD_MVN_OPTS ] : [])
+                            .withOptions(env.BUILD_MVN_OPTS_CURRENT ? [ 
env.BUILD_MVN_OPTS_CURRENT ] : [])
+                            .withProperty('maven.test.failure.ignore', true)
+                            .skipTests(params.SKIP_TESTS)
+
+                        if (isRelease()) {
+                            
releaseUtils.gpgImportKeyFromStringWithoutPassword(getReleaseGpgSignKeyCredsId())
+                            mavenCommand
+                                .withProfiles(['apache-release'])
+                                .withProperty('only.reproducible')
+                        }
+
+                        configFileProvider([configFile(fileId: 
env.MAVEN_SETTINGS_CONFIG_FILE_ID, variable: 'MAVEN_SETTINGS_FILE')]) {
+                            
mavenCommand.withSettingsXmlFile(MAVEN_SETTINGS_FILE).run("clean 
$installOrDeploy")
+                        }
+                    }
+                }
+            }
+            post {
+                always {
+                    script {
+                        saveReports()
+                        util.archiveConsoleLog()
+                    }
+                }
+            }
+        }
+
+        stage('Create PR') {
+            when {
+                expression { return isCreatePr() }
+            }
+            steps {
+                script {
+                    dir(getRepoName()) {
+                        if (githubscm.isThereAnyChanges()) {
+                            commitAndCreatePR()
+                        } else {
+                            println '[WARN] no changes to commit'
+                        }
+                    }
+                }
+            }
+            post {
+                success {
+                    script {
+                        
setDeployPropertyIfNeeded("${getRepoName()}.pr.source.uri", 
"https://github.com/${getGitAuthor()}/${getRepoName()}")
+                        
setDeployPropertyIfNeeded("${getRepoName()}.pr.source.ref", getPRBranch())
+                        
setDeployPropertyIfNeeded("${getRepoName()}.pr.target.uri", 
"https://github.com/${getGitAuthor()}/${getRepoName()}")
+                        
setDeployPropertyIfNeeded("${getRepoName()}.pr.target.ref", getBuildBranch())
+                    }
+                }
+            }
+        }
+
+        stage('Commit and Create Tag') {
+            when {
+                expression { return isRelease() }
+            }
+            steps {
+                script {
+                    dir(getRepoName()) {
+                        if (githubscm.isThereAnyChanges()) {
+                            def commitMsg = "[${getBuildBranch()}] Update 
version to ${getProjectVersion()}"
+                            
githubscm.setUserConfigFromCreds(getGitAuthorPushCredsId())
+                            githubscm.commitChanges(commitMsg, {
+                                
githubscm.findAndStageNotIgnoredFiles('pom.xml')
+                                
githubscm.findAndStageNotIgnoredFiles('antora.yml')
+                            })
+                        } else {
+                            println '[WARN] no changes to commit'
+                        }
+                        githubscm.tagRepository(getGitTagName())
+                        githubscm.pushRemoteTag('origin', getGitTagName(), 
getGitAuthorPushCredsId())

Review Comment:
   Issue #7140 requires Jenkins to call the local scripts instead of embedding 
release logic, but this new deploy pipeline still updates versions, builds, 
signs, commits, and pushes tags through Groovy/shared-library calls. It never 
invokes the local release workflow, leaving two separate release 
implementations. Delegate release mode to `release-all.sh` and standalone 
version updates to `01-update-version.sh`, while preserving credential setup 
and the non-release/PR-generation modes.



##########
.ci/jenkins/Jenkinsfile.103xplus.promote:
##########
@@ -0,0 +1,256 @@
+/*
+ * 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.
+ */
+
+import org.jenkinsci.plugins.workflow.libs.Library
+@Library('jenkins-pipeline-shared-libraries')_
+
+import org.kie.jenkins.MavenCommand
+
+deployProperties = [:]
+pipelineProperties = [:]
+
+pipeline {
+    agent {
+        docker {
+            image env.AGENT_DOCKER_BUILDER_IMAGE
+            args env.AGENT_DOCKER_BUILDER_ARGS
+            label util.avoidFaultyNodes()
+        }
+    }
+
+    options {
+        timestamps()
+        timeout(time: 180, unit: 'MINUTES')
+    }
+
+    environment {
+        DROOLS_CI_EMAIL_TO = credentials("${JENKINS_EMAIL_CREDS_ID}")
+    }
+
+    stages {
+        stage('Initialization') {
+            steps {
+                script {
+                    cleanWs()
+
+                    if (params.DISPLAY_NAME != '') {
+                        currentBuild.displayName = params.DISPLAY_NAME
+                    }
+
+                    readDeployProperties()
+
+                    assert getProjectVersion()
+                    assert getBuildBranch() == 
util.getReleaseBranchFromVersion(getProjectVersion())
+                }
+            }
+        }
+
+        stage('Merge deploy PR and tag') {
+            steps {
+                script {
+                    dir(getRepoName()) {
+                        approveAndMergePR(getDeployPrLink())
+                        checkoutRepo()
+                        tagLatest()
+                    }
+                }
+            }
+        }
+
+        stage('Create release') {
+            steps {
+                script {
+                    dir(getRepoName()) {
+                        checkoutRepo()
+                        if (githubscm.isReleaseExist(getGitTag(), 
getGitAuthorCredsId())) {
+                            githubscm.deleteRelease(getGitTag(), 
getGitAuthorPushCredsId())
+                        }
+                        
githubscm.createReleaseWithGeneratedReleaseNotes(getGitTag(), getBuildBranch(), 
githubscm.getPreviousTagFromVersion(getGitTag()), getGitAuthorPushCredsId())
+                        githubscm.updateReleaseBody(getGitTag(), 
getGitAuthorPushCredsId())
+                    }
+                }
+            }
+        }
+
+        stage('Upload drools binaries and documentation') {
+            when {
+                expression { return isMainStream() }
+            }
+            steps {
+                script {
+                    configFileProvider([configFile(fileId: 
env.MAVEN_SETTINGS_CONFIG_FILE_ID, variable: 'MAVEN_SETTINGS_FILE')]) {
+                        getMavenCommand()
+                            .withOptions(env.DROOLS_BUILD_MVN_OPTS ? [ 
env.DROOLS_BUILD_MVN_OPTS ] : [])
+                            .withOptions(env.BUILD_MVN_OPTS_CURRENT ? [ 
env.BUILD_MVN_OPTS_CURRENT ] : [])
+                            .inDirectory(getRepoName())
+                            .skipTests(true)
+                            .withProperty('full')
+                            .withSettingsXmlFile(MAVEN_SETTINGS_FILE)
+                            .run('clean install')
+                    }
+                    uploadFileMgmt(getRepoName())
+                }
+            }
+        }
+    }
+
+    post {
+        unsuccessful {
+            sendNotification()
+        }
+        cleanup {
+            script {
+                util.cleanNode()
+            }
+        }
+    }
+}
+
+void sendNotification() {
+    if (params.SEND_NOTIFICATION) {
+        mailer.sendMarkdownTestSummaryNotification('Promote', 
"[${getBuildBranch()}] Drools", [env.DROOLS_CI_EMAIL_TO])
+    } else {
+        echo 'No notification sent per configuration'
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Deployment properties
+//////////////////////////////////////////////////////////////////////////////
+
+void readDeployProperties() {
+    String deployUrl = params.DEPLOY_BUILD_URL
+    if (deployUrl != '') {
+        if (!deployUrl.endsWith('/')) {
+            deployUrl += '/'
+        }
+        sh "wget ${deployUrl}artifact/${PROPERTIES_FILE_NAME} -O 
${PROPERTIES_FILE_NAME}"
+        deployProperties = readProperties file: PROPERTIES_FILE_NAME
+        // echo all properties
+        echo deployProperties.collect { entry -> "${entry.key}=${entry.value}" 
}.join('\n')
+    }
+}
+
+boolean hasDeployProperty(String key) {
+    return deployProperties[key] != null
+}
+
+String getDeployProperty(String key) {
+    if (hasDeployProperty(key)) {
+        return deployProperties[key]
+    }
+    return ''
+}
+
+String getParamOrDeployProperty(String paramKey, String deployPropertyKey) {
+    if (params[paramKey] != '') {
+        return params[paramKey]
+    }
+    return getDeployProperty(deployPropertyKey)
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Getter / Setter
+//////////////////////////////////////////////////////////////////////////////
+
+String getRepoName() {
+    return env.REPO_NAME
+}
+
+String getProjectVersion() {
+    return getParamOrDeployProperty('PROJECT_VERSION', 'project.version')
+}
+
+String getGitTag() {
+    return params.GIT_TAG != '' ? params.GIT_TAG : getProjectVersion()
+}
+
+String getBuildBranch() {
+    return params.BUILD_BRANCH_NAME
+}
+
+String getGitAuthor() {
+    return env.GIT_AUTHOR
+}
+
+String getGitAuthorCredsId() {
+    return env.GIT_AUTHOR_CREDS_ID
+}
+
+String getGitAuthorPushCredsId() {
+    return env.GIT_AUTHOR_PUSH_CREDS_ID
+}
+
+String getDeployPrLink() {
+    return getDeployProperty("${getRepoName()}.pr.link")
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Git
+//////////////////////////////////////////////////////////////////////////////
+
+void checkoutRepo() {
+    deleteDir()
+    checkout(githubscm.resolveRepository(getRepoName(), getGitAuthor(), 
getBuildBranch(), false, getGitAuthorCredsId()))
+    // need to manually checkout branch since on a detached branch after 
checkout command
+    sh "git checkout ${getBuildBranch()}"
+}
+
+void approveAndMergePR(String prLink) {
+    if (prLink?.trim()) {
+        githubscm.approvePR(prLink, getGitAuthorPushCredsId())
+        githubscm.mergePR(prLink, getGitAuthorPushCredsId())
+    }
+}
+
+void tagLatest() {
+    if (getGitTag()) {
+        githubscm.tagLocalAndRemoteRepository('origin', getGitTag(), 
getGitAuthorPushCredsId(), env.BUILD_TAG, true)
+    }
+}
+
+MavenCommand getMavenCommand() {
+    mvnCmd = new MavenCommand(this, ['-fae', '-ntp'])
+                    .withOptions(env.BUILD_MVN_OPTS ? [ env.BUILD_MVN_OPTS ] : 
[])
+    if (env.MAVEN_DEPENDENCIES_REPOSITORY) {
+        mvnCmd.withDependencyRepositoryInSettings('deps-repo', 
env.MAVEN_DEPENDENCIES_REPOSITORY)
+    }
+    return mvnCmd
+}
+
+void uploadFileMgmt(String directory) {
+    if (isNotTestingBuild()) {
+        echo "upload binaries and docs for ${directory}"
+        dir(directory) {
+            sshagent(['drools-filemgmt']) {
+                sh "script/release/upload_filemgmt.sh ${getProjectVersion()}"

Review Comment:
   `script/release/upload_filemgmt.sh` is absent from this checkout. When 
`DROOLS_STREAM` is `main` and the Git author is `apache`, this command fails 
after the GitHub release has already been created, leaving distribution 
publishing incomplete. Supply the upload implementation or invoke an existing 
publishing mechanism before enabling this stage.



##########
script/release/02-rc-commit.sh:
##########
@@ -0,0 +1,220 @@
+#!/usr/bin/env bash
+#
+# 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.
+#
+
+set -euo pipefail
+
+# Creates the "R commit" (release commit) and the corresponding RC git tag.
+#
+# The R commit contains only the version bump to the exact release version.
+# It is made on a short-lived local release branch that is never pushed — only
+# the tag is pushed.  This mirrors the branch strategy from release.txt:
+#
+#   10.2.x branch  ──── c ──── c ──── [D commit] ──── c ────> ...
+#                                         |
+#                                 (local release branch)
+#                                         └── [R commit] ──> tag: 10.3.0-rc1
+#
+# Usage:
+#   ./script/release/02-rc-commit.sh <version> [--tag <tag-name>] [--push] 
[--dry-run]
+#   ./script/release/02-rc-commit.sh --version <release-version> --tag 
<tag-name>
+#
+# Examples:
+#   ./script/release/02-rc-commit.sh 10.3.0 --tag 10.3.0-rc1
+#   ./script/release/02-rc-commit.sh --version 10.3.0 --tag 10.3.0-rc2 --push
+#
+# Options:
+#   <version> | --version <ver>   Exact release version, e.g. 10.3.0
+#   --tag | --rc-tag <tag>        Git tag name, e.g. 10.3.0-rc1 (defaults to 
<version>-rc1)
+#   --rc                          Accepted for parity
+#   --push | --push-tag           Push the tag to origin (default: local only)
+#   --dry-run                     Print what would happen without making any 
changes
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
+
+RELEASE_VERSION=""
+TAG_NAME=""
+PUSH=false
+DRY_RUN=false
+
+while [[ $# -gt 0 ]]; do
+    case $1 in
+        --version)
+            RELEASE_VERSION="${2:-}"
+            shift 2
+            ;;
+        --tag|--rc-tag)
+            TAG_NAME="${2:-}"
+            shift 2
+            ;;
+        --rc)
+            shift
+            ;;
+        --push|--push-tag)
+            PUSH=true
+            shift
+            ;;
+        --dry-run)
+            DRY_RUN=true
+            shift
+            ;;
+        *)
+            if [[ -z "${RELEASE_VERSION}" && ! "$1" =~ ^- ]]; then
+                RELEASE_VERSION="$1"
+                shift
+            else
+                echo "Unknown option: $1"
+                echo "Usage: $0 [<version> | --version <version>] [--tag 
<tag>] [--push] [--dry-run]"
+                exit 1
+            fi
+            ;;
+    esac
+done
+
+if [[ -z "${RELEASE_VERSION}" ]]; then
+    echo "ERROR: Version is required."
+    echo "Usage: $0 10.3.0 [--tag 10.3.0-rc1] [--push] [--dry-run]"
+    exit 1
+fi
+
+if [[ -z "${TAG_NAME}" ]]; then
+    TAG_NAME="${RELEASE_VERSION}-rc1"
+fi
+
+# Derive the expected development branch name (e.g. 10.3.0 → 10.3.x).
+STREAM_BRANCH="$(echo "${RELEASE_VERSION}" | sed 
's/^\([0-9]*\.[0-9]*\)\..*/\1.x/')"
+RELEASE_BRANCH="release/${TAG_NAME}"
+
+cd "${REPO_ROOT}"
+
+CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
+
+echo "========================================"
+echo "Apache KIE repo — RC commit + tag"
+echo "Release version : ${RELEASE_VERSION}"
+echo "Tag             : ${TAG_NAME}"
+echo "Release branch  : ${RELEASE_BRANCH} (local only)"
+echo "Push tag        : ${PUSH}"
+echo "Dry run         : ${DRY_RUN}"
+echo "Current branch  : ${CURRENT_BRANCH}"
+echo "========================================"
+
+# Warn if we're not on the expected stream branch (not a fatal error — someone
+# may legitimately be on a different commit).
+if [[ "${CURRENT_BRANCH}" != "${STREAM_BRANCH}" ]]; then
+    echo ""
+    echo "WARNING: Current branch '${CURRENT_BRANCH}' is not the expected 
stream"
+    echo "         branch '${STREAM_BRANCH}'. Proceed with caution."
+    echo ""
+fi
+
+# Check there are no uncommitted changes before we do anything.
+if ! git diff --quiet || ! git diff --cached --quiet; then

Review Comment:
   These checks ignore untracked files, but the later `git add -A` includes 
them in the release commit. An unrelated, non-ignored local file can therefore 
be committed and pushed with the RC tag despite passing the clean-tree guard. 
Include untracked files in this check before creating the release branch.



##########
.ci/jenkins/project/Jenkinsfile.103xplus.release:
##########
@@ -0,0 +1,108 @@
+/*
+ * 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.
+ */
+
+import org.jenkinsci.plugins.workflow.libs.Library
+@Library('jenkins-pipeline-shared-libraries')_
+
+pipeline {
+    agent {
+        label util.avoidFaultyNodes('ubuntu')
+    }
+
+    options {
+        timestamps()
+        timeout(time: 360, unit: 'MINUTES')
+    }
+
+    environment {
+        DROOLS_CI_EMAIL_TO = credentials("${JENKINS_EMAIL_CREDS_ID}")
+    }
+
+    parameters {
+        string(name: 'RELEASE_VERSION', defaultValue: '', description: 'Exact 
release version, e.g. 10.3.0', trim: true)
+        string(name: 'TAG_NAME', defaultValue: '', description: 'RC or Release 
tag name, e.g. 10.3.0-rc1', trim: true)
+        booleanParam(name: 'SKIP_TESTS', defaultValue: true, description: 
'Skip running tests during reactor build')
+        booleanParam(name: 'DEPLOY', defaultValue: true, description: 'Deploy 
artifacts to Apache Nexus staging')
+        string(name: 'STAGING_URL', defaultValue: '', description: 'Custom 
Nexus staging URL (optional)')
+        booleanParam(name: 'PUSH_TAG', defaultValue: true, description: 'Push 
the git tag to origin')
+        string(name: 'EXTRA_MAVEN_OPTS', defaultValue: '', description: 'Extra 
Maven CLI options to pass to build.sh')
+        booleanParam(name: 'DRY_RUN', defaultValue: false, description: 'Dry 
run without making remote git or staging changes')
+        booleanParam(name: 'SEND_NOTIFICATION', defaultValue: false, 
description: 'Send email notification on failure')
+    }
+
+    stages {
+        stage('Initialize & Validate') {
+            steps {
+                script {
+                    assert params.RELEASE_VERSION : 'RELEASE_VERSION parameter 
is mandatory'
+                    assert params.TAG_NAME : 'TAG_NAME parameter is mandatory'
+                    currentBuild.displayName = "Release 
${params.RELEASE_VERSION} (${params.TAG_NAME})"
+                }
+            }
+        }
+
+        stage('Execute Release Scripts') {
+            steps {
+                script {
+                    def releaseCmd = "./script/release/release-all.sh 
--version ${params.RELEASE_VERSION} --tag ${params.TAG_NAME}"
+
+                    if (params.SKIP_TESTS) {
+                        releaseCmd += ' --skip-tests'
+                    }
+                    if (params.DEPLOY) {
+                        releaseCmd += ' --deploy'
+                    }
+                    if (params.STAGING_URL?.trim()) {
+                        releaseCmd += " --staging-url 
\"${params.STAGING_URL.trim()}\""
+                    }
+                    if (params.PUSH_TAG) {
+                        releaseCmd += ' --push-tag'
+                    }
+                    if (params.EXTRA_MAVEN_OPTS?.trim()) {
+                        releaseCmd += " --maven-opts 
\"${params.EXTRA_MAVEN_OPTS.trim()}\""
+                    }
+                    if (params.DRY_RUN) {
+                        releaseCmd += ' --dry-run'
+                    }
+
+                    sh """#!/usr/bin/env bash
+                    set -euo pipefail
+                    chmod +x script/release/*.sh
+                    ${releaseCmd}

Review Comment:
   This invokes the scripts without Maven settings, a signing-key setup, Git 
author configuration, or authentication for their raw `git push`. Only the 
notification email is bound. On an agent without preconfigured release 
credentials, the default `DEPLOY=true` / `PUSH_TAG=true` workflow cannot 
complete. Bind the managed Maven settings as `MAVEN_SETTINGS`, provision the 
GPG key and any required passphrase, and configure Git identity and push 
authentication around this invocation, preserving the setup used by the 
existing deploy pipeline.



##########
.ci/jenkins/Jenkinsfile.103xplus.deploy:
##########
@@ -0,0 +1,327 @@
+/*
+ * 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.
+ */
+
+import org.jenkinsci.plugins.workflow.libs.Library
+@Library('jenkins-pipeline-shared-libraries')_
+
+import org.kie.jenkins.MavenCommand
+
+deployProperties = [:]
+
+pipeline {
+    agent {
+        docker {
+            image env.AGENT_DOCKER_BUILDER_IMAGE
+            args env.AGENT_DOCKER_BUILDER_ARGS
+            label util.avoidFaultyNodes()
+        }
+    }
+
+    options {
+        timestamps()
+        timeout(time: 180, unit: 'MINUTES')
+    }
+
+    environment {
+        DROOLS_CI_EMAIL_TO = credentials("${JENKINS_EMAIL_CREDS_ID}")
+
+        PR_BRANCH_HASH = "${util.generateHash(10)}"
+    }
+
+    stages {
+        stage('Initialize') {
+            steps {
+                script {
+                    cleanWs(disableDeferredWipeout: true)
+
+                    if (params.DISPLAY_NAME) {
+                        currentBuild.displayName = params.DISPLAY_NAME
+                    }
+
+                    if (isRelease() || isCreatePr()) {
+                        // Verify version is set
+                        assert getProjectVersion()
+
+                        if (isRelease()) {
+                            // Verify if on right release branch
+                            assert getBuildBranch() == 
util.getReleaseBranchFromVersion(getProjectVersion())
+                        }
+                    }
+
+                    dir(getRepoName()) {
+                        checkoutRepo()
+                    }
+                }
+            }
+            post {
+                success {
+                    script {
+                        setDeployPropertyIfNeeded('git.branch', 
getBuildBranch())
+                        setDeployPropertyIfNeeded('git.author', getGitAuthor())
+                        setDeployPropertyIfNeeded('project.version', 
getProjectVersion())
+                        setDeployPropertyIfNeeded('release', isRelease())
+                    }
+                }
+            }
+        }
+
+        stage('Prepare for PR') {
+            when {
+                expression { return isCreatePr() }
+            }
+            steps {
+                script {
+                    dir(getRepoName()) {
+                        if (githubscm.isBranchExist('origin', getPRBranch())) {
+                            githubscm.removeRemoteBranch('origin', 
getPRBranch(), getGitAuthorPushCredsId())
+                        }
+                        githubscm.createBranch(getPRBranch())
+                    }
+                }
+            }
+        }
+
+        stage('Update project version') {
+            when {
+                expression { return getProjectVersion() }
+            }
+            steps {
+                script {
+                    configFileProvider([configFile(fileId: 
env.MAVEN_SETTINGS_CONFIG_FILE_ID, variable: 'MAVEN_SETTINGS_FILE')]) {
+                        maven.mvnVersionsSet(
+                            
getMavenCommand().withSettingsXmlFile(MAVEN_SETTINGS_FILE),
+                            getProjectVersion(),
+                            !isRelease()
+                        )
+                    }
+                }
+            }
+        }
+
+        stage('Build & Deploy repo') {
+            steps {
+                script {
+                    withCredentials([usernamePassword(credentialsId: 
env.MAVEN_REPO_CREDS_ID, usernameVariable: 'REPOSITORY_USER', passwordVariable: 
'REPOSITORY_TOKEN')]) {
+                        def installOrDeploy
+                        if (shouldDeployToRepository()) {
+                            installOrDeploy = "deploy -DdeployAtEnd 
-Dapache.repository.username=${REPOSITORY_USER} 
-Dapache.repository.password=${REPOSITORY_TOKEN} -DretryFailedDeploymentCount=5"
+                        } else {
+                            installOrDeploy = 'install'
+                        }
+                        def mavenCommand = getMavenCommand()
+                            .withOptions(env.DROOLS_BUILD_MVN_OPTS ? [ 
env.DROOLS_BUILD_MVN_OPTS ] : [])
+                            .withOptions(env.BUILD_MVN_OPTS_CURRENT ? [ 
env.BUILD_MVN_OPTS_CURRENT ] : [])
+                            .withProperty('maven.test.failure.ignore', true)
+                            .skipTests(params.SKIP_TESTS)
+
+                        if (isRelease()) {
+                            
releaseUtils.gpgImportKeyFromStringWithoutPassword(getReleaseGpgSignKeyCredsId())
+                            mavenCommand
+                                .withProfiles(['apache-release'])
+                                .withProperty('only.reproducible')
+                        }
+
+                        configFileProvider([configFile(fileId: 
env.MAVEN_SETTINGS_CONFIG_FILE_ID, variable: 'MAVEN_SETTINGS_FILE')]) {
+                            
mavenCommand.withSettingsXmlFile(MAVEN_SETTINGS_FILE).run("clean 
$installOrDeploy")
+                        }
+                    }
+                }
+            }
+            post {
+                always {
+                    script {
+                        saveReports()
+                        util.archiveConsoleLog()
+                    }
+                }
+            }
+        }
+
+        stage('Create PR') {
+            when {
+                expression { return isCreatePr() }
+            }
+            steps {
+                script {
+                    dir(getRepoName()) {
+                        if (githubscm.isThereAnyChanges()) {
+                            commitAndCreatePR()
+                        } else {
+                            println '[WARN] no changes to commit'
+                        }
+                    }
+                }
+            }
+            post {
+                success {
+                    script {
+                        
setDeployPropertyIfNeeded("${getRepoName()}.pr.source.uri", 
"https://github.com/${getGitAuthor()}/${getRepoName()}")
+                        
setDeployPropertyIfNeeded("${getRepoName()}.pr.source.ref", getPRBranch())
+                        
setDeployPropertyIfNeeded("${getRepoName()}.pr.target.uri", 
"https://github.com/${getGitAuthor()}/${getRepoName()}")
+                        
setDeployPropertyIfNeeded("${getRepoName()}.pr.target.ref", getBuildBranch())
+                    }
+                }
+            }
+        }
+
+        stage('Commit and Create Tag') {
+            when {
+                expression { return isRelease() }
+            }
+            steps {
+                script {
+                    dir(getRepoName()) {
+                        if (githubscm.isThereAnyChanges()) {
+                            def commitMsg = "[${getBuildBranch()}] Update 
version to ${getProjectVersion()}"
+                            
githubscm.setUserConfigFromCreds(getGitAuthorPushCredsId())
+                            githubscm.commitChanges(commitMsg, {
+                                
githubscm.findAndStageNotIgnoredFiles('pom.xml')
+                                
githubscm.findAndStageNotIgnoredFiles('antora.yml')
+                            })
+                        } else {
+                            println '[WARN] no changes to commit'
+                        }
+                        githubscm.tagRepository(getGitTagName())
+                        githubscm.pushRemoteTag('origin', getGitTagName(), 
getGitAuthorPushCredsId())
+                    }
+                }
+            }
+        }
+    }
+
+    post {
+        always {
+            script {
+                setDeployPropertyIfNeeded("${getRepoName()}.deploy.build.run", 
true)
+                saveDeployProperties()
+            }
+        }
+        unsuccessful {
+            sendNotification()
+        }
+        cleanup {
+            cleanWs(notFailBuild: true, deleteDirs: true, patterns: [[pattern: 
'deploy.properties', type: 'EXCLUDE']])
+        }
+    }
+}
+
+void saveReports() {
+    junit testResults: '**/target/surefire-reports/**/*.xml, 
**/target/failsafe-reports/**/*.xml, **/target/invoker-reports/**/TEST-*.xml', 
allowEmptyResults: true
+}
+
+void checkoutRepo() {
+    deleteDir()
+    checkout(githubscm.resolveRepository(getRepoName(), getGitAuthor(), 
getBuildBranch(), false, getGitAuthorCredsId()))
+    // need to manually checkout branch since on a detached branch after 
checkout command
+    sh "git checkout ${getBuildBranch()}"
+}
+
+void commitAndCreatePR() {
+    def commitMsg = "[${getBuildBranch()}] Update version to 
${getProjectVersion()}"
+    def prBody = "Generated by build ${BUILD_TAG}: ${BUILD_URL}.\nPlease do 
not merge, it should be merged automatically."
+    githubscm.setUserConfigFromCreds(getGitAuthorPushCredsId())
+    githubscm.commitChanges(commitMsg, {
+        githubscm.findAndStageNotIgnoredFiles('pom.xml')
+        githubscm.findAndStageNotIgnoredFiles('antora.yml')
+    })
+    githubscm.pushObject('origin', getPRBranch(), getGitAuthorPushCredsId())
+    deployProperties["${getRepoName()}.pr.link"] = 
githubscm.createPRWithLabels(commitMsg, prBody, getBuildBranch(), ['skip-ci'] 
as String[], getGitAuthorCredsId())
+}
+
+void sendNotification() {
+    if (params.SEND_NOTIFICATION) {
+        mailer.sendMarkdownTestSummaryNotification('Deploy', 
"[${getBuildBranch()}] Drools", [env.DROOLS_CI_EMAIL_TO])
+    } else {
+        echo 'No notification sent per configuration'
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// Deployment properties
+//////////////////////////////////////////////////////////////////////////////
+
+void setDeployPropertyIfNeeded(String key, def value) {
+    if (value != null && value != '') {
+        deployProperties[key] = value
+    }
+}
+
+void saveDeployProperties(String propertiesFileName = 'deploy.properties') {

Review Comment:
   With the existing job configuration, the producer and consumer use different 
artifact names. `.ci/jenkins/dsl/jobs.groovy` sets `PROPERTIES_FILE_NAME` to 
`deployment.properties`, and the new promote pipeline fetches that name, but 
this method archives `deploy.properties`. Promotion through `DEPLOY_BUILD_URL` 
therefore requests a missing artifact. Use the configured filename when writing 
and archiving the properties.



##########
script/release/04-deploy-to-staging.sh:
##########
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+#
+# 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.
+#
+
+set -euo pipefail
+
+# Deploys the locally-built JARs (already in ~/.m2) to a Nexus staging
+# repository for Apache release voting.
+#
+# By default this script runs in DRY RUN mode and will NOT push anything
+# remotely.  Pass --deploy to actually upload to Nexus.
+#
+# Usage:
+#   ./script/release/deploy-to-staging.sh --tag <rc-tag> [--deploy] 
[--staging-url <url>]
+#
+# Examples:
+#   # dry run (safe — prints the Maven command that would be run)
+#   ./script/release/deploy-to-staging.sh --tag 10.3.0-rc1
+#
+#   # actually deploy to Apache Nexus staging
+#   ./script/release/deploy-to-staging.sh --tag 10.3.0-rc1 --deploy
+#
+#   # deploy to a custom staging URL
+#   ./script/release/deploy-to-staging.sh --tag 10.3.0-rc1 --deploy \
+#       --staging-url 
https://repository.apache.org/service/local/staging/deploy/maven2
+#
+# Environment variables consumed when --deploy is active:
+#   MAVEN_SETTINGS   Path to a settings.xml with Nexus credentials (required).
+#                    The settings file must define a server with id 
"apache.releases.https"
+#                    (or the id set via --server-id) carrying the deployer 
credentials.
+#   MAVEN_GPG_PASSPHRASE   GPG passphrase for signing (required for Apache 
releases).
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
+
+TAG_NAME=""
+DEPLOY=false
+STAGING_URL="https://repository.apache.org/service/local/staging/deploy/maven2";
+SERVER_ID="apache.releases.https"
+
+while [[ $# -gt 0 ]]; do
+    case $1 in
+        --tag)
+            TAG_NAME="${2:-}"
+            shift 2
+            ;;
+        --deploy)
+            DEPLOY=true
+            shift
+            ;;
+        --staging-url)
+            STAGING_URL="${2:-}"
+            shift 2
+            ;;
+        --server-id)
+            SERVER_ID="${2:-}"
+            shift 2
+            ;;
+        *)
+            echo "Unknown option: $1"
+            echo "Usage: $0 --tag <rc-tag> [--deploy] [--staging-url <url>] 
[--server-id <id>]"
+            exit 1
+            ;;
+    esac
+done
+
+if [[ -z "${TAG_NAME}" ]]; then
+    echo "ERROR: --tag is required."
+    echo "Usage: $0 --tag 10.3.0-rc1 [--deploy]"
+    exit 1
+fi
+
+cd "${REPO_ROOT}"
+
+# ── Resolve the version from the tag name ────────────────────────────────────
+# Tag format: <version>-rc<N>  (e.g. 10.3.0-rc1)
+RELEASE_VERSION="$(echo "${TAG_NAME}" | sed 's/-rc[0-9]*$//')"
+
+echo "========================================"
+echo "Apache KIE repo — deploy to staging"
+echo "Tag             : ${TAG_NAME}"
+echo "Release version : ${RELEASE_VERSION}"
+echo "Staging URL     : ${STAGING_URL}"
+echo "Server ID       : ${SERVER_ID}"
+echo "Deploy (real)   : ${DEPLOY}"
+echo "========================================"
+
+# ── Verify we are on / can resolve the RC tag ────────────────────────────────
+if ! git rev-parse "${TAG_NAME}" &>/dev/null; then
+    echo ""
+    echo "ERROR: Git tag '${TAG_NAME}' not found in this repository."
+    echo "       Run 02-rc-commit.sh first, or check out the tag manually."
+    exit 1
+fi
+
+# ── Assemble Maven flags ─────────────────────────────────────────────────────
+
+DEPLOY_MVN_FLAGS=(
+    "-DskipTests"
+    "-Dfull"
+    "-DaltDeploymentRepository=${SERVER_ID}::default::${STAGING_URL}"
+)
+
+if [[ -n "${MAVEN_SETTINGS:-}" ]]; then
+    DEPLOY_MVN_FLAGS+=("-s" "${MAVEN_SETTINGS}")
+fi
+
+if [[ -n "${MAVEN_GPG_PASSPHRASE:-}" ]]; then
+    DEPLOY_MVN_FLAGS+=("-Dgpg.passphrase=${MAVEN_GPG_PASSPHRASE}")

Review Comment:
   When `MAVEN_GPG_PASSPHRASE` is set, this adds its plaintext value to Maven's 
arguments. The default dry run prints the entire array at line 133, exposing 
the signing secret in captured console output; a real deployment also exposes 
it through process arguments. Use the inherited GPG plugin's supported 
environment or protected-settings mechanism instead of a plaintext command-line 
property, and keep the secret out of displayed commands.



##########
script/release/04-deploy-to-staging.sh:
##########
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+#
+# 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.
+#
+
+set -euo pipefail
+
+# Deploys the locally-built JARs (already in ~/.m2) to a Nexus staging
+# repository for Apache release voting.
+#
+# By default this script runs in DRY RUN mode and will NOT push anything
+# remotely.  Pass --deploy to actually upload to Nexus.
+#
+# Usage:
+#   ./script/release/deploy-to-staging.sh --tag <rc-tag> [--deploy] 
[--staging-url <url>]
+#
+# Examples:
+#   # dry run (safe — prints the Maven command that would be run)
+#   ./script/release/deploy-to-staging.sh --tag 10.3.0-rc1
+#
+#   # actually deploy to Apache Nexus staging
+#   ./script/release/deploy-to-staging.sh --tag 10.3.0-rc1 --deploy
+#
+#   # deploy to a custom staging URL
+#   ./script/release/deploy-to-staging.sh --tag 10.3.0-rc1 --deploy \
+#       --staging-url 
https://repository.apache.org/service/local/staging/deploy/maven2
+#
+# Environment variables consumed when --deploy is active:
+#   MAVEN_SETTINGS   Path to a settings.xml with Nexus credentials (required).
+#                    The settings file must define a server with id 
"apache.releases.https"
+#                    (or the id set via --server-id) carrying the deployer 
credentials.
+#   MAVEN_GPG_PASSPHRASE   GPG passphrase for signing (required for Apache 
releases).
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
+
+TAG_NAME=""
+DEPLOY=false
+STAGING_URL="https://repository.apache.org/service/local/staging/deploy/maven2";
+SERVER_ID="apache.releases.https"
+
+while [[ $# -gt 0 ]]; do
+    case $1 in
+        --tag)
+            TAG_NAME="${2:-}"
+            shift 2
+            ;;
+        --deploy)
+            DEPLOY=true
+            shift
+            ;;
+        --staging-url)
+            STAGING_URL="${2:-}"
+            shift 2
+            ;;
+        --server-id)
+            SERVER_ID="${2:-}"
+            shift 2
+            ;;
+        *)
+            echo "Unknown option: $1"
+            echo "Usage: $0 --tag <rc-tag> [--deploy] [--staging-url <url>] 
[--server-id <id>]"
+            exit 1
+            ;;
+    esac
+done
+
+if [[ -z "${TAG_NAME}" ]]; then
+    echo "ERROR: --tag is required."
+    echo "Usage: $0 --tag 10.3.0-rc1 [--deploy]"
+    exit 1
+fi
+
+cd "${REPO_ROOT}"
+
+# ── Resolve the version from the tag name ────────────────────────────────────
+# Tag format: <version>-rc<N>  (e.g. 10.3.0-rc1)
+RELEASE_VERSION="$(echo "${TAG_NAME}" | sed 's/-rc[0-9]*$//')"
+
+echo "========================================"
+echo "Apache KIE repo — deploy to staging"
+echo "Tag             : ${TAG_NAME}"
+echo "Release version : ${RELEASE_VERSION}"
+echo "Staging URL     : ${STAGING_URL}"
+echo "Server ID       : ${SERVER_ID}"
+echo "Deploy (real)   : ${DEPLOY}"
+echo "========================================"
+
+# ── Verify we are on / can resolve the RC tag ────────────────────────────────
+if ! git rev-parse "${TAG_NAME}" &>/dev/null; then
+    echo ""
+    echo "ERROR: Git tag '${TAG_NAME}' not found in this repository."
+    echo "       Run 02-rc-commit.sh first, or check out the tag manually."
+    exit 1
+fi
+
+# ── Assemble Maven flags ─────────────────────────────────────────────────────
+
+DEPLOY_MVN_FLAGS=(
+    "-DskipTests"
+    "-Dfull"

Review Comment:
   The Maven command never activates `apache-release`, which the existing 
deploy pipeline enables for artifact signing. Setting `gpg.passphrase` alone 
does not run the signing goal, so the default script stages unsigned artifacts 
instead of the signed release artifacts it promises. Activate the release 
profile here; signing-key provisioning must also be supplied by the caller.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to