From 9dbcd19d6f3bbdcba4dddd91a4c69d41ac145bb5 Mon Sep 17 00:00:00 2001 From: Russell Spitzer Date: Tue, 29 Sep 2026 15:18:30 -0500 Subject: [PATCH 1/3] Prepare RC: stage through nexus-staging-maven-plugin A plain `mvn deploy` to repository.apache.org relies on Nexus implicitly creating a staging repository, and never tells the client which one it used. prepare-rc.sh then had to guess: nexus_find_open_staging_repo listed every open repository in the org.apache.parquet profile and refused to continue unless there was exactly one. That guess fails in two ways. On 2026-09-10 a single 1.19.0 run ended with two open repositories (orgapacheparquet-1085 and -1086) and aborted before closing either. And parquet-format publishes under the same groupId, so it shares the staging profile: any open repository from a parquet-format release would block a parquet-java release, or be mistaken for one. GitHub concurrency groups are per repository, so the workflows cannot serialize across the two. Add nexus-staging-maven-plugin 1.7.0 to the apache-release profile with extensions enabled, so it replaces maven-deploy-plugin for `mvn deploy` under that profile only. It stages every module locally, opens one staging repository, uploads into it, closes it, and records the repository ID in target/nexus-staging/staging/.properties. prepare-rc.sh now reads the ID from that file rather than searching for it, which removes nexus_find_open_staging_repo and its multiple-repo guard. The plugin also takes over the close from our bulk-close call. Its close waits for completion and prints rule failures (such as the missing public key on 2026-09-14), where the bulk call returned while the close was still in progress. keepStagingRepositoryOnCloseRuleFailure keeps a repository that fails a rule so it can be inspected, and the properties file is written before the close, so a failed close still reports which repository was left behind. stagingProgressTimeoutMinutes is raised from 5 to 15 because a 16-module close may run past the default. -DstagingDescription sets "Apache Parquet RC" on the repository at creation, which is the description cancel-rc.sh and publish-release.sh verify. After the deploy, prepare-rc.sh runs the same nexus_verify_staging_repo check against the new repository. stagingProfileId is left unset, so the plugin resolves the profile from the groupId through the profile_evaluate endpoint. If the deployer account cannot call that endpoint, the next run fails before uploading anything and the profile ID has to be configured explicitly. nexus_close_staging_repo had no remaining callers and is removed. cancel-rc.sh and publish-release.sh already take the repository ID as an argument and are unchanged. Generated-by: Claude Opus 5.5 (1M context) --- pom.xml | 20 ++++++ release/bin/prepare-rc.sh | 28 ++++---- release/libs/_constants.sh | 1 + release/libs/_maven.sh | 35 ++++++++++ release/libs/_nexus.sh | 74 -------------------- release/tests/maven.bats | 53 +++++++++++++- release/tests/nexus.bats | 137 ------------------------------------- 7 files changed, 118 insertions(+), 230 deletions(-) diff --git a/pom.xml b/pom.xml index ebb7fbd849..92aad9f7f1 100644 --- a/pom.xml +++ b/pom.xml @@ -725,6 +725,26 @@ + + apache-release + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.7.0 + true + + https://repository.apache.org/ + apache.releases.https + true + 15 + + + + + + vector-plugins diff --git a/release/bin/prepare-rc.sh b/release/bin/prepare-rc.sh index 0c16a78012..c3146d82d1 100755 --- a/release/bin/prepare-rc.sh +++ b/release/bin/prepare-rc.sh @@ -263,27 +263,23 @@ step_summary "### Nexus Deployment" settings_file=$(mktemp "${RUNNER_TEMP:-${TMPDIR:-/tmp}}/parquet-release-settings.XXXXXX") generate_maven_settings "${settings_file}" -maven_deploy "${settings_file}" +staging_description="Apache Parquet ${version} RC${rc_number}" -step_summary "Deployed artifacts to Apache Nexus staging" - -# Find and close the staging repo. Verify it actually contains the -# artifacts we just deployed before closing -- another concurrent run -# could have created the only open repo we see, and closing the wrong -# one would corrupt that release. -nexus_find_open_staging_repo "${NEXUS_PROFILE_NAME}" - -if [[ ${DRY_RUN:-1} -ne 1 ]]; then - if ! nexus_check_staging_artifact "${staging_repo_id}" "${version}"; then - print_error "Open staging repo ${staging_repo_id} does not contain ${version} artifacts." - print_error "Refusing to close it -- another release may be in flight. Investigate Nexus before retrying." - exit 1 +if ! maven_deploy "${settings_file}" "${staging_description}"; then + if maven_read_staging_repo_id; then + print_error "Deploy failed after uploading to ${staging_repo_id}; inspect it in Nexus, then drop it before retrying." + step_summary "Deploy **FAILED**; staging repository \`${staging_repo_id}\` was left in Nexus" fi + exit 1 fi -nexus_close_staging_repo "${staging_repo_id}" "Apache Parquet ${version} RC${rc_number}" +maven_read_staging_repo_id + +step_summary "Deployed and closed staging repository: \`${staging_repo_id}\`" -step_summary "Closed staging repository: \`${staging_repo_id}\`" +if ! nexus_verify_staging_repo "${staging_repo_id}" "${version}" "${rc_number}"; then + exit 1 +fi maven_cleanup_settings "${settings_file}" diff --git a/release/libs/_constants.sh b/release/libs/_constants.sh index 78c3978f26..c8e899d1b5 100644 --- a/release/libs/_constants.sh +++ b/release/libs/_constants.sh @@ -33,6 +33,7 @@ NEXUS_CONTENT_BASE_URL=${NEXUS_CONTENT_BASE_URL:-"${NEXUS_BASE_URL}/repositories NEXUS_STAGING_GROUP_URL="https://repository.apache.org/content/groups/staging/org/apache/parquet/" NEXUS_PROFILE_NAME="org.apache.parquet" +NEXUS_LOCAL_STAGING_DIR="target/nexus-staging" NEXUS_VERIFY_GROUP_PATH="org/apache/parquet" NEXUS_VERIFY_ARTIFACT_ID="parquet-common" diff --git a/release/libs/_maven.sh b/release/libs/_maven.sh index ddb916adc4..359109336e 100644 --- a/release/libs/_maven.sh +++ b/release/libs/_maven.sh @@ -62,22 +62,57 @@ SETTINGS_EOF print_info "Generated Maven settings at ${settings_file} (credentials resolved from env vars at build time)" } +# maven_deploy +# Deploys through nexus-staging-maven-plugin, which opens one staging +# repository, uploads every module into it, and closes it. function maven_deploy { local settings_file="${1:-.release-settings.xml}" + local staging_description="$2" if [[ ! -f "${settings_file}" ]]; then print_info "Generating Maven settings..." generate_maven_settings "${settings_file}" fi + exec_process rm -rf "${NEXUS_LOCAL_STAGING_DIR}" + exec_process ./mvnw deploy \ -Papache-release \ -DskipTests \ -Darguments=-DskipTests \ + "-DstagingDescription=${staging_description}" \ --settings "${settings_file}" \ --batch-mode } +# maven_read_staging_repo_id [local-staging-dir] +# Sets staging_repo_id from the properties file nexus-staging-maven-plugin +# writes after uploading, before it attempts the close. +function maven_read_staging_repo_id { + local staging_dir="${1:-${NEXUS_LOCAL_STAGING_DIR}}/staging" + + if [[ ${DRY_RUN:-1} -eq 1 ]]; then + print_command "Dry-run, WOULD read staging repository ID from ${staging_dir}/*.properties" + staging_repo_id="DRY-RUN-REPO-ID" + return 0 + fi + + local properties_files=("${staging_dir}"/*.properties) + if [[ ${#properties_files[@]} -ne 1 || ! -f "${properties_files[0]}" ]]; then + print_error "Expected one staging properties file in ${staging_dir}, found: ${properties_files[*]}" + return 1 + fi + + staging_repo_id=$(sed -n 's/^stagingRepository\.id=//p' "${properties_files[0]}") + if [[ -z "${staging_repo_id}" ]]; then + print_error "No stagingRepository.id in ${properties_files[0]}" + return 1 + fi + + print_info "Staging repository: ${staging_repo_id}" + return 0 +} + function maven_cleanup_settings { local settings_file="${1:-.release-settings.xml}" if [[ -f "${settings_file}" ]]; then diff --git a/release/libs/_nexus.sh b/release/libs/_nexus.sh index d56adff7d4..60ed3e5044 100644 --- a/release/libs/_nexus.sh +++ b/release/libs/_nexus.sh @@ -50,12 +50,6 @@ function _nexus_bulk_action { fi } -function nexus_close_staging_repo { - local repo_id="$1" - local description="${2:-Closing staging repository}" - _nexus_bulk_action "close" "${repo_id}" "${description}" -} - function nexus_release_staging_repo { local repo_id="$1" local description="${2:-Releasing staging repository}" @@ -170,71 +164,3 @@ function nexus_verify_staging_repo { print_info "Staging repository ${repo_id} verified." return 0 } - -function nexus_find_open_staging_repo { - local profile_name="${1:-${NEXUS_PROFILE_NAME}}" - - print_info "Searching for open staging repository for ${profile_name}..." - - if [[ ${DRY_RUN:-1} -eq 1 ]]; then - print_command "Dry-run, WOULD search Nexus for open staging repo" - staging_repo_id="DRY-RUN-REPO-ID" - return 0 - fi - - local response - if ! response=$(curl --fail --silent --show-error \ - -K <(printf 'user = "%s:%s"\n' "${NEXUS_USERNAME}" "${NEXUS_PASSWORD}") \ - "${NEXUS_BASE_URL}/staging/profile_repositories"); then - print_error "Failed to query Nexus staging repositories" - return 1 - fi - - # Filter on exactly (not on a substring of ), - # and reject the result if multiple open repos exist for the profile — - # Nexus permits this but it indicates a leftover from a previous failed - # run that the operator must clean up before we can pick the right one. - local matches - matches=$(echo "${response}" | \ - NEXUS_PROFILE_NAME_VAR="${profile_name}" python3 -c " -import sys, os, xml.etree.ElementTree as ET -profile = os.environ['NEXUS_PROFILE_NAME_VAR'] -try: - tree = ET.parse(sys.stdin) -except ET.ParseError as e: - print('PARSE_ERROR:' + str(e), file=sys.stderr) - sys.exit(2) -for repo in tree.findall('.//stagingProfileRepository'): - repo_type = repo.find('type') - repo_profile = repo.find('profileName') - repo_id = repo.find('repositoryId') - if repo_type is None or repo_id is None or repo_profile is None: - continue - if repo_type.text == 'open' and repo_profile.text == profile: - print(repo_id.text) -" 2>&1) - local py_status=$? - - if [[ ${py_status} -ne 0 ]]; then - print_error "Failed to parse Nexus staging response: ${matches}" - return 1 - fi - - local match_count - match_count=$(echo -n "${matches}" | grep -c . || true) - - if [[ ${match_count} -eq 0 ]]; then - print_error "No open staging repository found for profile '${profile_name}'" - return 1 - fi - if [[ ${match_count} -gt 1 ]]; then - print_error "Multiple open staging repositories found for profile '${profile_name}':" - echo "${matches}" | while read -r line; do print_error " - ${line}"; done - print_error "Please drop or close the stale repos in Nexus and re-run." - return 1 - fi - - staging_repo_id=$(echo "${matches}" | head -n 1) - print_info "Found staging repository: ${staging_repo_id}" - return 0 -} diff --git a/release/tests/maven.bats b/release/tests/maven.bats index 9dff0e6a9a..c2e901e058 100644 --- a/release/tests/maven.bats +++ b/release/tests/maven.bats @@ -82,7 +82,7 @@ teardown() { DRY_RUN=1 local settings_file="${TEST_TMPDIR}/settings.xml" generate_maven_settings "${settings_file}" - run maven_deploy "${settings_file}" + run maven_deploy "${settings_file}" "Apache Parquet 1.2.3 RC4" [ "$status" -eq 0 ] [[ "$output" == *"Dry-run"* ]] [[ "$output" == *"deploy"* ]] @@ -92,7 +92,7 @@ teardown() { DRY_RUN=1 local settings_file="${TEST_TMPDIR}/settings.xml" generate_maven_settings "${settings_file}" - run maven_deploy "${settings_file}" + run maven_deploy "${settings_file}" "Apache Parquet 1.2.3 RC4" [[ "$output" == *"apache-release"* ]] } @@ -100,10 +100,57 @@ teardown() { DRY_RUN=1 local settings_file="${TEST_TMPDIR}/settings.xml" generate_maven_settings "${settings_file}" - run maven_deploy "${settings_file}" + run maven_deploy "${settings_file}" "Apache Parquet 1.2.3 RC4" [[ "$output" == *"skipTests"* ]] } +@test "maven_deploy: passes staging description in dry-run output" { + DRY_RUN=1 + local settings_file="${TEST_TMPDIR}/settings.xml" + generate_maven_settings "${settings_file}" + run maven_deploy "${settings_file}" "Apache Parquet 1.2.3 RC4" + [[ "$output" == *"-DstagingDescription=Apache Parquet 1.2.3 RC4"* ]] +} + +# ---- maven_read_staging_repo_id ---- + +@test "maven_read_staging_repo_id: dry-run returns placeholder" { + DRY_RUN=1 + maven_read_staging_repo_id "${TEST_TMPDIR}" + [ "${staging_repo_id}" = "DRY-RUN-REPO-ID" ] +} + +@test "maven_read_staging_repo_id: reads ID from plugin properties file" { + DRY_RUN=0 + mkdir -p "${TEST_TMPDIR}/staging" + cat > "${TEST_TMPDIR}/staging/1ab2c3d4e5f6.properties" <<'EOF' +#Generated by org.sonatype.plugins:nexus-staging-maven-plugin:1.7.0 +stagingRepository.managed=true +stagingRepository.id=orgapacheparquet-1090 +stagingRepository.profileId=1ab2c3d4e5f6 +stagingRepository.url=https\://repository.apache.org/content/repositories/orgapacheparquet-1090 +EOF + maven_read_staging_repo_id "${TEST_TMPDIR}" + [ "${staging_repo_id}" = "orgapacheparquet-1090" ] +} + +@test "maven_read_staging_repo_id: fails when no properties file exists" { + DRY_RUN=0 + mkdir -p "${TEST_TMPDIR}/staging" + run maven_read_staging_repo_id "${TEST_TMPDIR}" + [ "$status" -eq 1 ] + [[ "$output" == *"Expected one staging properties file"* ]] +} + +@test "maven_read_staging_repo_id: fails when properties file has no ID" { + DRY_RUN=0 + mkdir -p "${TEST_TMPDIR}/staging" + echo "stagingRepository.profileId=1ab2c3d4e5f6" > "${TEST_TMPDIR}/staging/1ab2c3d4e5f6.properties" + run maven_read_staging_repo_id "${TEST_TMPDIR}" + [ "$status" -eq 1 ] + [[ "$output" == *"No stagingRepository.id"* ]] +} + # ---- maven_cleanup_settings ---- @test "maven_cleanup_settings: removes settings file" { diff --git a/release/tests/nexus.bats b/release/tests/nexus.bats index 6ac108b513..be9c4e6806 100644 --- a/release/tests/nexus.bats +++ b/release/tests/nexus.bats @@ -25,22 +25,6 @@ setup() { export NEXUS_PASSWORD="testpass" } -# ---- nexus_close_staging_repo ---- - -@test "nexus_close_staging_repo: dry-run does not call curl" { - DRY_RUN=1 - run nexus_close_staging_repo "orgapacheparquet-1234" "test close" - [ "$status" -eq 0 ] - [[ "$output" == *"Dry-run"* ]] - [[ "$output" == *"close"* ]] -} - -@test "nexus_close_staging_repo: constructs correct URL in dry-run" { - DRY_RUN=1 - run nexus_close_staging_repo "orgapacheparquet-1234" - [[ "$output" == *"staging/bulk/close"* ]] -} - # ---- nexus_release_staging_repo ---- @test "nexus_release_staging_repo: dry-run includes promote URL" { @@ -72,129 +56,8 @@ setup() { [[ "$output" == *"example.com/nexus/staging/bulk/drop"* ]] } -# ---- nexus_find_open_staging_repo ---- - -@test "nexus_find_open_staging_repo: dry-run returns placeholder" { - DRY_RUN=1 - nexus_find_open_staging_repo "org.apache.parquet" - [ "$staging_repo_id" = "DRY-RUN-REPO-ID" ] -} - -@test "nexus_find_open_staging_repo: real mode picks the open repo for the profile" { - DRY_RUN=0 - curl() { - cat <<'XML' - - - - org.apache.iceberg - open - orgapacheiceberg-7777 - - - org.apache.parquet - closed - orgapacheparquet-1111 - - - org.apache.parquet - open - orgapacheparquet-2222 - - - -XML - return 0 - } - export -f curl - nexus_find_open_staging_repo "org.apache.parquet" - [ "${staging_repo_id}" = "orgapacheparquet-2222" ] -} - -@test "nexus_find_open_staging_repo: rejects when no open repo for profile" { - DRY_RUN=0 - curl() { - cat <<'XML' - - - - org.apache.parquet - closed - orgapacheparquet-1111 - - - -XML - return 0 - } - export -f curl - run nexus_find_open_staging_repo "org.apache.parquet" - [ "$status" -eq 1 ] - [[ "$output" == *"No open staging repository"* ]] -} - -@test "nexus_find_open_staging_repo: rejects when profile has multiple open repos" { - DRY_RUN=0 - curl() { - cat <<'XML' - - - - org.apache.parquet - open - orgapacheparquet-1111 - - - org.apache.parquet - open - orgapacheparquet-2222 - - - -XML - return 0 - } - export -f curl - run nexus_find_open_staging_repo "org.apache.parquet" - [ "$status" -eq 1 ] - [[ "$output" == *"Multiple open staging repositories"* ]] - [[ "$output" == *"orgapacheparquet-1111"* ]] - [[ "$output" == *"orgapacheparquet-2222"* ]] -} - -@test "nexus_find_open_staging_repo: rejects when curl fails" { - DRY_RUN=0 - curl() { return 22; } - export -f curl - run nexus_find_open_staging_repo "org.apache.parquet" - [ "$status" -eq 1 ] - [[ "$output" == *"Failed to query"* ]] -} - -@test "nexus_find_open_staging_repo: rejects malformed XML" { - DRY_RUN=0 - curl() { echo "&2 - return 0 - } - export -f curl - run nexus_close_staging_repo "orgapacheparquet-9999" "test desc" - [ "$status" -eq 0 ] - [[ "$output" == *"staging/bulk/close"* ]] - [[ "$output" == *"orgapacheparquet-9999"* ]] -} - @test "nexus_drop_staging_repo: real mode calls curl with auth" { DRY_RUN=0 curl() { From 8e2524712fe1ca4279e467e17d69375ac5ca4666 Mon Sep 17 00:00:00 2001 From: Russell Spitzer Date: Tue, 6 Oct 2026 15:05:41 -0500 Subject: [PATCH 2/3] Prepare RC: find the staging repo from the deploy log on failure On a close rule failure, nexus-staging-maven-plugin 1.7.0 deletes its staging properties file in afterUploadFailure before deciding whether to keep the repository. With keepStagingRepositoryOnCloseRuleFailure set, that is exactly the case where a repository is left behind, so reading the properties file on the failure path could never report it, and printed a misleading "Expected one staging properties file" error instead. maven_deploy now tees Maven output to target/nexus-deploy.log, and the failure path reads every ` * Created staging repository with ID "..."` line from it. The plugin logs that line for each repository it opens, before any upload, so it survives every failure mode. The message says to check whether the repository still exists, because some failures (an upload IOException) drop it automatically. PIPESTATUS keeps mvnw's exit status through the tee regardless of pipefail. Set autoReleaseAfterClose=false explicitly. It is the default, but the plugin also binds it to -DautoReleaseAfterClose, and a release after close would publish an RC before the vote; configuration in the pom takes precedence over the user property. The plugin version moves to a nexus-staging-maven-plugin.version property like the other build plugins. The step summary now reports success only after nexus_verify_staging_repo passes, and records a line for each failure exit. Tests cover the stale staging directory being cleared, the description reaching mvnw as one argument, mvnw's exit status propagating through the tee, more than one properties file, the default staging directory, and parsing repository IDs from the deploy log. Generated-by: Claude Opus 5.5 (1M context) --- pom.xml | 4 +- release/bin/prepare-rc.sh | 18 ++++++--- release/libs/_constants.sh | 1 + release/libs/_maven.sh | 25 ++++++++++++- release/tests/maven.bats | 76 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 115 insertions(+), 9 deletions(-) diff --git a/pom.xml b/pom.xml index 92aad9f7f1..ab966051fa 100644 --- a/pom.xml +++ b/pom.xml @@ -103,6 +103,7 @@ 0.27ea1 3.6.2 3.6.4 + 1.7.0 3.11.4 1.20.0 @@ -732,11 +733,12 @@ org.sonatype.plugins nexus-staging-maven-plugin - 1.7.0 + ${nexus-staging-maven-plugin.version} true https://repository.apache.org/ apache.releases.https + false true 15 diff --git a/release/bin/prepare-rc.sh b/release/bin/prepare-rc.sh index c3146d82d1..18e2f2177f 100755 --- a/release/bin/prepare-rc.sh +++ b/release/bin/prepare-rc.sh @@ -266,21 +266,27 @@ generate_maven_settings "${settings_file}" staging_description="Apache Parquet ${version} RC${rc_number}" if ! maven_deploy "${settings_file}" "${staging_description}"; then - if maven_read_staging_repo_id; then - print_error "Deploy failed after uploading to ${staging_repo_id}; inspect it in Nexus, then drop it before retrying." - step_summary "Deploy **FAILED**; staging repository \`${staging_repo_id}\` was left in Nexus" + if maven_find_created_staging_repos; then + print_error "Deploy failed after creating staging repository ${created_staging_repo_ids[*]}; check whether it is still in Nexus and drop it before retrying." + step_summary "Deploy **FAILED** after creating staging repository \`${created_staging_repo_ids[*]}\`; check Nexus for leftovers" + else + step_summary "Deploy **FAILED** before a staging repository was created" fi exit 1 fi -maven_read_staging_repo_id - -step_summary "Deployed and closed staging repository: \`${staging_repo_id}\`" +if ! maven_read_staging_repo_id; then + step_summary "Deploy succeeded but the staging repository ID could not be read; see the Maven log for the closed repository" + exit 1 +fi if ! nexus_verify_staging_repo "${staging_repo_id}" "${version}" "${rc_number}"; then + step_summary "Verification **FAILED** for staging repository \`${staging_repo_id}\`" exit 1 fi +step_summary "Deployed, closed and verified staging repository: \`${staging_repo_id}\`" + maven_cleanup_settings "${settings_file}" # --------------------------------------------------------------------------- diff --git a/release/libs/_constants.sh b/release/libs/_constants.sh index c8e899d1b5..108c21825d 100644 --- a/release/libs/_constants.sh +++ b/release/libs/_constants.sh @@ -34,6 +34,7 @@ NEXUS_STAGING_GROUP_URL="https://repository.apache.org/content/groups/staging/or NEXUS_PROFILE_NAME="org.apache.parquet" NEXUS_LOCAL_STAGING_DIR="target/nexus-staging" +MAVEN_DEPLOY_LOG="target/nexus-deploy.log" NEXUS_VERIFY_GROUP_PATH="org/apache/parquet" NEXUS_VERIFY_ARTIFACT_ID="parquet-common" diff --git a/release/libs/_maven.sh b/release/libs/_maven.sh index 359109336e..dc89bdcb0b 100644 --- a/release/libs/_maven.sh +++ b/release/libs/_maven.sh @@ -64,7 +64,8 @@ SETTINGS_EOF # maven_deploy # Deploys through nexus-staging-maven-plugin, which opens one staging -# repository, uploads every module into it, and closes it. +# repository, uploads every module into it, and closes it. Output is +# also written to MAVEN_DEPLOY_LOG. function maven_deploy { local settings_file="${1:-.release-settings.xml}" local staging_description="$2" @@ -75,6 +76,7 @@ function maven_deploy { fi exec_process rm -rf "${NEXUS_LOCAL_STAGING_DIR}" + mkdir -p "$(dirname "${MAVEN_DEPLOY_LOG}")" exec_process ./mvnw deploy \ -Papache-release \ @@ -82,7 +84,8 @@ function maven_deploy { -Darguments=-DskipTests \ "-DstagingDescription=${staging_description}" \ --settings "${settings_file}" \ - --batch-mode + --batch-mode | tee "${MAVEN_DEPLOY_LOG}" + return "${PIPESTATUS[0]}" } # maven_read_staging_repo_id [local-staging-dir] @@ -113,6 +116,24 @@ function maven_read_staging_repo_id { return 0 } +# maven_find_created_staging_repos [deploy-log] +# Sets created_staging_repo_ids to every staging repository the plugin +# reported creating. Used after a failed deploy, when the plugin may +# already have deleted its properties file. +function maven_find_created_staging_repos { + local deploy_log="${1:-${MAVEN_DEPLOY_LOG}}" + + created_staging_repo_ids=() + if [[ -f "${deploy_log}" ]]; then + local repo_id + while IFS= read -r repo_id; do + created_staging_repo_ids+=("${repo_id}") + done < <(sed -n 's/.*Created staging repository with ID "\([^"]*\)".*/\1/p' "${deploy_log}") + fi + + [[ ${#created_staging_repo_ids[@]} -gt 0 ]] +} + function maven_cleanup_settings { local settings_file="${1:-.release-settings.xml}" if [[ -f "${settings_file}" ]]; then diff --git a/release/tests/maven.bats b/release/tests/maven.bats index c2e901e058..b0a65096c3 100644 --- a/release/tests/maven.bats +++ b/release/tests/maven.bats @@ -24,6 +24,7 @@ setup() { export NEXUS_USERNAME="testuser" export NEXUS_PASSWORD="testpass" TEST_TMPDIR=$(mktemp -d) + MAVEN_DEPLOY_LOG="${TEST_TMPDIR}/target/nexus-deploy.log" } teardown() { @@ -112,6 +113,35 @@ teardown() { [[ "$output" == *"-DstagingDescription=Apache Parquet 1.2.3 RC4"* ]] } +@test "maven_deploy: clears local staging directory in dry-run output" { + DRY_RUN=1 + local settings_file="${TEST_TMPDIR}/settings.xml" + generate_maven_settings "${settings_file}" + run maven_deploy "${settings_file}" "Apache Parquet 1.2.3 RC4" + [[ "$output" == *"rm -rf target/nexus-staging"* ]] +} + +@test "maven_deploy: real mode passes description as one argument, logs output, and propagates failure" { + DRY_RUN=0 + cd "${TEST_TMPDIR}" + mkdir -p target/nexus-staging/staging + touch target/nexus-staging/staging/stale.properties + cat > mvnw <<'EOF' +#!/bin/bash +printf '%s\n' "$@" > mvnw-args.txt +echo ' * Created staging repository with ID "orgapacheparquet-1091".' +exit 3 +EOF + chmod +x mvnw + local settings_file="${TEST_TMPDIR}/settings.xml" + generate_maven_settings "${settings_file}" + run maven_deploy "${settings_file}" "Apache Parquet 1.2.3 RC4" + [ "$status" -eq 3 ] + [ ! -e target/nexus-staging ] + grep -qx -- "-DstagingDescription=Apache Parquet 1.2.3 RC4" mvnw-args.txt + grep -q 'orgapacheparquet-1091' "${MAVEN_DEPLOY_LOG}" +} + # ---- maven_read_staging_repo_id ---- @test "maven_read_staging_repo_id: dry-run returns placeholder" { @@ -164,3 +194,49 @@ EOF run maven_cleanup_settings "${TEST_TMPDIR}/nonexistent.xml" [ "$status" -eq 0 ] } + +@test "maven_read_staging_repo_id: fails when more than one properties file exists" { + DRY_RUN=0 + mkdir -p "${TEST_TMPDIR}/staging" + echo "stagingRepository.id=orgapacheparquet-1" > "${TEST_TMPDIR}/staging/a.properties" + echo "stagingRepository.id=orgapacheparquet-2" > "${TEST_TMPDIR}/staging/b.properties" + run maven_read_staging_repo_id "${TEST_TMPDIR}" + [ "$status" -eq 1 ] + [[ "$output" == *"a.properties"* ]] + [[ "$output" == *"b.properties"* ]] +} + +@test "maven_read_staging_repo_id: defaults to the plugin's staging directory" { + DRY_RUN=0 + cd "${TEST_TMPDIR}" + mkdir -p target/nexus-staging/staging + echo "stagingRepository.id=orgapacheparquet-1092" > target/nexus-staging/staging/x.properties + maven_read_staging_repo_id + [ "${staging_repo_id}" = "orgapacheparquet-1092" ] +} + +# ---- maven_find_created_staging_repos ---- + +@test "maven_find_created_staging_repos: finds repository IDs in the deploy log" { + mkdir -p "$(dirname "${MAVEN_DEPLOY_LOG}")" + cat > "${MAVEN_DEPLOY_LOG}" <<'EOF' +[INFO] * Created staging repository with ID "orgapacheparquet-1093". +[ERROR] Rule failure while trying to close staging repository with ID "orgapacheparquet-1093". +[ERROR] * Not dropping failed staging repository with ID "orgapacheparquet-1093" (Rule failure). +EOF + maven_find_created_staging_repos + [ "${#created_staging_repo_ids[@]}" -eq 1 ] + [ "${created_staging_repo_ids[0]}" = "orgapacheparquet-1093" ] +} + +@test "maven_find_created_staging_repos: fails when no repository was created" { + mkdir -p "$(dirname "${MAVEN_DEPLOY_LOG}")" + echo "[ERROR] COMPILATION ERROR" > "${MAVEN_DEPLOY_LOG}" + run maven_find_created_staging_repos + [ "$status" -eq 1 ] +} + +@test "maven_find_created_staging_repos: fails when the deploy log is missing" { + run maven_find_created_staging_repos "${TEST_TMPDIR}/missing.log" + [ "$status" -eq 1 ] +} From 8f6ca6386d2b9067b650c6f67297615e14e27a55 Mon Sep 17 00:00:00 2001 From: Russell Spitzer Date: Wed, 28 Oct 2026 10:42:11 -0500 Subject: [PATCH 3/3] Prepare RC: name the staging repo when its ID cannot be read When the deploy succeeds but maven_read_staging_repo_id fails, the step summary only said to look in the Maven log. The deploy log already records every repository the plugin created, so report those IDs the same way the deploy failure path does. If the log shows no created repository either, nexus-staging-maven-plugin most likely did not run and maven-deploy-plugin deployed into a repository Nexus created implicitly, so say that and point at Nexus. Generated-by: Claude Opus 5.5 (1M context) --- release/bin/prepare-rc.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/release/bin/prepare-rc.sh b/release/bin/prepare-rc.sh index 18e2f2177f..5222ef8500 100755 --- a/release/bin/prepare-rc.sh +++ b/release/bin/prepare-rc.sh @@ -276,7 +276,13 @@ if ! maven_deploy "${settings_file}" "${staging_description}"; then fi if ! maven_read_staging_repo_id; then - step_summary "Deploy succeeded but the staging repository ID could not be read; see the Maven log for the closed repository" + if maven_find_created_staging_repos; then + print_error "Deploy succeeded but the staging repository ID could not be read; the deploy log reports creating ${created_staging_repo_ids[*]}." + step_summary "Deploy succeeded but the staging repository ID could not be read; deploy log reports \`${created_staging_repo_ids[*]}\`" + else + print_error "Deploy succeeded but no staging repository was found; nexus-staging-maven-plugin may not have run, so check Nexus for an implicitly created repository." + step_summary "Deploy succeeded but no staging repository was found; check Nexus for an implicitly created repository" + fi exit 1 fi