From 5d21daad51e0aa346934a96d1d639df6ea87ebb8 Mon Sep 17 00:00:00 2001 From: Ramakrishna Prabhu Date: Wed, 21 Oct 2026 10:31:11 -0500 Subject: [PATCH] ci(java): publish releases through the Central Publisher Portal API The release path uploaded files one by one through the legacy ossrh-staging-api compatibility service and then handed the staging repository to the Portal. Sonatype asked projects to move to the Publisher API. Upload the signed bundle zip with POST /api/v1/publisher/upload as a USER_MANAGED deployment, then poll status as before. Publishing is still a manual step in the Portal UI. The staging profile id has no meaning in the Publisher API, so drop the profile-id input and option. Co-Authored-By: Claude Sonnet 5.5 Signed-off-by: Ramakrishna Prabhu --- .github/workflows/build.yaml | 2 - .github/workflows/maven-publish.yaml | 15 +- .../ci/maven-publish/maven_central_publish.sh | 78 +++-------- .../maven_central_publish_steps.sh | 128 ++---------------- 4 files changed, 32 insertions(+), 191 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index a62de35e22..3a51cf6709 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -164,8 +164,6 @@ jobs: # 'inputs' context at all, so fall back to github.sha or the retained bundle name ends # in a trailing "-" with nothing after it. source-git-sha: ${{ inputs.sha || github.sha }} - # com.nvidia.cuopt isn't its own verified Sonatype namespace; com.nvidia is. - profile-id: com.nvidia # false = validate + drop (safe). true = stage PENDING for manual publish. stage-for-maven-central-publish: true diff --git a/.github/workflows/maven-publish.yaml b/.github/workflows/maven-publish.yaml index 52ca3230fd..704caee592 100644 --- a/.github/workflows/maven-publish.yaml +++ b/.github/workflows/maven-publish.yaml @@ -5,7 +5,7 @@ # file is GPG-signed. The destination is chosen inside this workflow with # `rapids-is-release-build`: # - release build (vYY.MM.PP tag) -> Maven Central via the Sonatype Central -# Publisher Portal (OSSRH staging + validation, human-gated publish). +# Publisher Portal API (validation, human-gated publish). # - non-release build (nightly, branch push) -> Sonatype snapshot repository # (direct HTTP PUT, immediately available, no staging or portal step). # @@ -40,15 +40,6 @@ on: required: false type: string default: '' - profile-id: - description: | - Sonatype staging profile to search, if different from the Maven groupId read from - the POM (e.g. a verified parent namespace like com.nvidia for a com.nvidia.cuopt - groupId). Defaults to the groupId when not set. Only applies to the Maven Central - path (release builds); ignored on the snapshot path. - required: false - type: string - default: '' secrets: GPG_PRIVATE_KEY: description: Armored GPG private key used to sign every artifact at upload. @@ -125,13 +116,9 @@ jobs: --output-bundle central-bundle.zip --auto-drop "${AUTO_DROP}" ) - if [[ -n ${PROFILE_ID} ]]; then - args+=(--profile-id "${PROFILE_ID}") - fi java/cuopt/ci/maven-publish/maven_central_publish.sh "${args[@]}" env: GROUP_ID: ${{ steps.prepare.outputs.GROUP_ID }} - PROFILE_ID: ${{ inputs.profile-id }} ARTIFACT_ID: ${{ steps.prepare.outputs.ARTIFACT_ID }} VERSION: ${{ steps.prepare.outputs.VERSION }} # Invert: stage-for-...=true means don't auto-drop. diff --git a/java/cuopt/ci/maven-publish/maven_central_publish.sh b/java/cuopt/ci/maven-publish/maven_central_publish.sh index 39e79dfe66..6756b281cd 100755 --- a/java/cuopt/ci/maven-publish/maven_central_publish.sh +++ b/java/cuopt/ci/maven-publish/maven_central_publish.sh @@ -2,10 +2,10 @@ # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# Creates a deployment bundle from a signed Maven repository tree, uploads its -# files through Sonatype's OSSRH staging compatibility API, and polls the -# Publisher Portal until the deployment passes validation. -# Never calls the /publish endpoint: publication always requires a human click. +# Creates a deployment bundle from a signed Maven repository tree, uploads it to +# the Central Publisher Portal with the Publisher API, and polls until the +# deployment passes validation. +# Never calls the publish endpoint: publication always requires a human click. set -euo pipefail @@ -19,7 +19,6 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" . "${SCRIPT_DIR}/maven_central_publish_steps.sh" CENTRAL_PORTAL_URL="https://central.sonatype.com" -OSSRH_STAGING_API_URL="https://ossrh-staging-api.central.sonatype.com" # Used by wait_for_validated in the sourced step library. # shellcheck disable=SC2034 POLL_INTERVAL_SEC=15 @@ -28,7 +27,6 @@ POLL_TIMEOUT_SEC=900 INPUT_DIR="" GROUP_ID="" -PROFILE_ID="" ARTIFACT_ID="" VERSION="" OUTPUT_BUNDLE="" @@ -42,9 +40,9 @@ Usage: maven_central_publish.sh --input --group-id \\ --output-bundle [OPTIONS] Copies a signed Maven repository tree into scratch space, generates .md5 and -.sha1 sidecars, and creates a retained ZIP. It uploads the repository files -individually through Sonatype's OSSRH staging compatibility API, then hands the -repository to the Central Publisher Portal without publishing it. +.sha1 sidecars, and creates a retained ZIP. It uploads the ZIP to the Central +Publisher Portal with the Publisher API as a USER_MANAGED deployment, so it is +never published without a human click. REQUIRED: -i, --input Signed Maven repository directory. @@ -54,18 +52,13 @@ REQUIRED: -o, --output-bundle Path for the retained deployment ZIP. OPTIONS: - --profile-id

Sonatype staging profile to search, if different from - --group-id (e.g. a verified parent namespace like - com.nvidia for a com.nvidia.cuopt groupId). Defaults to - --group-id. --auto-drop Drop after VALIDATED (default: true). -h, --help Show this help message. ENVIRONMENT VARIABLES: MAVEN_DEPLOY_USERNAME Publisher Portal user token username. MAVEN_DEPLOY_TOKEN Publisher Portal user token password. - GITHUB_OUTPUT If set, receives STAGING_REPOSITORY_KEY and - DEPLOYMENT_ID. + GITHUB_OUTPUT If set, receives DEPLOYMENT_ID. EOF } @@ -87,11 +80,6 @@ parse_args() { GROUP_ID=$2 shift 2 ;; - --profile-id) - require_value "$1" "${2:-}" - PROFILE_ID=$2 - shift 2 - ;; -a|--artifact-id) require_value "$1" "${2:-}" ARTIFACT_ID=$2 @@ -134,11 +122,6 @@ if [[ ${AUTO_DROP} != "true" && ${AUTO_DROP} != "false" ]]; then fi require_maven_coordinates "${GROUP_ID}" "${ARTIFACT_ID}" "${VERSION}" require_release_version "${VERSION}" -# A groupId nested under a verified parent namespace (e.g. com.nvidia.cuopt under the -# verified com.nvidia) still searches/creates its OSSRH staging repo under the parent's -# Sonatype profile, not the full groupId string -- so default to GROUP_ID, but let callers -# override it. -PROFILE_ID="${PROFILE_ID:-${GROUP_ID}}" if [[ ! -d ${INPUT_DIR} ]]; then fatal "--input '${INPUT_DIR}' does not exist or is not a directory" fi @@ -157,44 +140,18 @@ if [[ -e ${OUTPUT_BUNDLE} ]]; then fi echo "Maven Central upload" echo " coordinates: ${GROUP_ID}:${ARTIFACT_ID}:${VERSION}" -echo " profile: ${PROFILE_ID}" echo " auto-drop: ${AUTO_DROP}" echo " input dir: ${INPUT_DIR}" echo " output bundle: ${OUTPUT_BUNDLE}" -echo " staging API: ${OSSRH_STAGING_API_URL}" -echo " target portal: ${CENTRAL_PORTAL_URL}" +echo " portal: ${CENTRAL_PORTAL_URL}" WORK_DIR="$(mktemp -d)" -STAGING_REPOSITORY_KEY="" -STAGING_FILE_UPLOADED="false" -STAGING_HANDED_OFF="false" - -cleanup() { - local status=$? - if (( status != 0 )) \ - && [[ ${STAGING_FILE_UPLOADED} == "true" ]] \ - && [[ ${STAGING_HANDED_OFF} != "true" ]]; then - if [[ -z ${STAGING_REPOSITORY_KEY} ]]; then - STAGING_REPOSITORY_KEY=$(staging_search_repositories open 2>/dev/null \ - | jq -r 'if (.repositories | length) == 1 then .repositories[0].key else empty end' \ - 2>/dev/null) || true - fi - if [[ -n ${STAGING_REPOSITORY_KEY} ]]; then - echo "Cleaning up open OSSRH staging repository ${STAGING_REPOSITORY_KEY}" >&2 - staging_drop_repository "${STAGING_REPOSITORY_KEY}" \ - || echo "Warning: could not drop staging repository ${STAGING_REPOSITORY_KEY}; remove it before retrying" >&2 - fi - fi - rm -rf "${WORK_DIR}" - return "${status}" -} -trap cleanup EXIT +trap 'rm -rf "${WORK_DIR}"' EXIT BUNDLE_DIR="${WORK_DIR}/bundle" BUNDLE_ARTIFACT_DIR="${BUNDLE_DIR}/$(maven_group_path "${GROUP_ID}")/${ARTIFACT_ID}/${VERSION}" mkdir -p "${BUNDLE_DIR}" -require_no_open_staging_repository copy_bundle "${INPUT_DIR}" "${BUNDLE_DIR}" require_bundle_contents "${BUNDLE_ARTIFACT_DIR}" generate_bundle_checksums "${BUNDLE_ARTIFACT_DIR}" @@ -203,15 +160,14 @@ BUNDLE_ZIP="${WORK_DIR}/${ARTIFACT_ID}-${VERSION}.zip" create_bundle_zip "${BUNDLE_ZIP}" "${BUNDLE_DIR}" mv "${BUNDLE_ZIP}" "${OUTPUT_BUNDLE}" -DEPLOYMENT_ID="" -upload_tree_to_staging "${BUNDLE_DIR}" -find_open_staging_repository -echo "Handing staging repository to the Publisher Portal" -staging_handoff_repository "${STAGING_REPOSITORY_KEY}" -wait_for_portal_handoff "${STAGING_REPOSITORY_KEY}" -STAGING_HANDED_OFF="true" +PUBLISHING_TYPE="USER_MANAGED" +echo "Uploading bundle to the Publisher Portal" +DEPLOYMENT_ID=$(portal_upload_bundle "${OUTPUT_BUNDLE}" "${GROUP_ID}:${ARTIFACT_ID}:${VERSION}" "${PUBLISHING_TYPE}") +if [[ ! ${DEPLOYMENT_ID} =~ ^[A-Za-z0-9-]+$ ]]; then + fatal "Publisher Portal returned an invalid deployment id" +fi +echo " Portal deployment id: ${DEPLOYMENT_ID}" if [[ -n ${GITHUB_OUTPUT:-} ]]; then - echo "STAGING_REPOSITORY_KEY=${STAGING_REPOSITORY_KEY}" >> "${GITHUB_OUTPUT}" echo "DEPLOYMENT_ID=${DEPLOYMENT_ID}" >> "${GITHUB_OUTPUT}" fi diff --git a/java/cuopt/ci/maven-publish/maven_central_publish_steps.sh b/java/cuopt/ci/maven-publish/maven_central_publish_steps.sh index f803c62d4b..1d49f3f9c5 100755 --- a/java/cuopt/ci/maven-publish/maven_central_publish_steps.sh +++ b/java/cuopt/ci/maven-publish/maven_central_publish_steps.sh @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# Sonatype HTTP wrappers and local bundle helpers for maven_central_publish.sh. +# Central Publisher Portal HTTP wrappers and local bundle helpers for maven_central_publish.sh. _portal_auth() { printf '%s:%s' "${MAVEN_DEPLOY_USERNAME}" "${MAVEN_DEPLOY_TOKEN}" \ @@ -38,58 +38,28 @@ _sonatype_call() { rm -f "${body_file}" } -# _sonatype_upload FILE URL -# Basic auth, PUT FILE. Logs failure and returns 1 on non-2xx. -_sonatype_upload() { - local upload_file=$1 url=$2 - local body_file status +# portal_upload_bundle ZIP NAME PUBLISHING_TYPE +# Bearer auth, multipart POST of the bundle. Prints the deployment id on 201; +# logs failure and returns 1 otherwise. +portal_upload_bundle() { + local zip_path=$1 name=$2 publishing_type=$3 + local body_file status url + url="${CENTRAL_PORTAL_URL}/api/v1/publisher/upload?publishingType=${publishing_type}&name=$(printf %s "${name}" | jq -sRr @uri)" body_file=$(mktemp) status=$(curl -sS --retry 3 --retry-delay 2 \ - --user "${MAVEN_DEPLOY_USERNAME}:${MAVEN_DEPLOY_TOKEN}" \ - --upload-file "${upload_file}" \ - -o "${body_file}" -w '%{http_code}' "${url}") \ + -H "Authorization: Bearer $(_portal_auth)" \ + --form "bundle=@${zip_path};type=application/octet-stream" \ + -X POST -o "${body_file}" -w '%{http_code}' "${url}") \ || status="transport-error" - if [[ ${status} != 2* ]]; then - _sonatype_error_log PUT "${url}" "${status}" "${body_file}" >&2 + if [[ ${status} != 201 ]]; then + _sonatype_error_log POST "${url}" "${status}" "${body_file}" >&2 rm -f "${body_file}" return 1 fi + tr -d '[:space:]' < "${body_file}" rm -f "${body_file}" } -staging_search_repositories() { - local state=${1:-} - local url - url="${OSSRH_STAGING_API_URL}/manual/search/repositories?ip=client&profile_id=$(printf %s "${PROFILE_ID}" | jq -sRr @uri)" - if [[ -n ${state} ]]; then - url+="&state=$(printf %s "${state}" | jq -sRr @uri)" - fi - _sonatype_call GET "${url}" -} - -staging_upload_file() { - local file_path=$1 repository_path=$2 - local encoded_path - encoded_path=$(jq -rn --arg path "${repository_path}" \ - '$path | split("/") | map(@uri) | join("/")') - _sonatype_upload "${file_path}" \ - "${OSSRH_STAGING_API_URL}/service/local/staging/deploy/maven2/${encoded_path}" -} - -staging_handoff_repository() { - local repository_key=$1 - _sonatype_call POST \ - "${OSSRH_STAGING_API_URL}/manual/upload/repository/${repository_key}?publishing_type=portal_api" \ - >/dev/null -} - -staging_drop_repository() { - local repository_key=$1 - _sonatype_call DELETE \ - "${OSSRH_STAGING_API_URL}/manual/drop/repository/${repository_key}" \ - >/dev/null -} - portal_get_status() { local deployment_id=$1 _sonatype_call POST \ @@ -149,76 +119,6 @@ create_bundle_zip() { (cd "${bundle_dir}" && zip -qr "${zip_path}" .) } -require_no_open_staging_repository() { - local response count - response=$(staging_search_repositories open) - count=$(jq -er '.repositories | length' <<< "${response}") \ - || fatal "OSSRH staging search returned an invalid response" - if (( count != 0 )); then - jq . <<< "${response}" >&2 || true - fatal "found ${count} open OSSRH staging repository/repositories for ${PROFILE_ID} from this IP; drop or hand them off before retrying" - fi -} - -upload_tree_to_staging() { - local root=$1 file repository_path count=0 - echo "Uploading Maven repository files to the OSSRH staging service" - while IFS= read -r -d '' file; do - repository_path=${file#"${root}/"} - staging_upload_file "${file}" "${repository_path}" - # Consumed by the EXIT cleanup trap in maven_central_publish.sh. - # shellcheck disable=SC2034 - STAGING_FILE_UPLOADED="true" - count=$((count + 1)) - done < <(find "${root}" -type f -print0) - echo " uploaded files: ${count}" -} - -find_open_staging_repository() { - local response count - response=$(staging_search_repositories open) - count=$(jq -er '.repositories | length' <<< "${response}") \ - || fatal "OSSRH staging search returned an invalid response" - if (( count != 1 )); then - jq . <<< "${response}" >&2 || true - fatal "expected exactly one open OSSRH staging repository after upload, found ${count}" - fi - STAGING_REPOSITORY_KEY=$(jq -er '.repositories[0].key | select(type == "string" and length > 0)' \ - <<< "${response}") || fatal "OSSRH staging response did not contain a repository key" - # Keys are composite paths of the form //--default-repository, - # so slashes are expected. Just guard against characters that would break URL path use. - if [[ ! ${STAGING_REPOSITORY_KEY} =~ ^[A-Za-z0-9._/-]+$ ]]; then - fatal "OSSRH staging service returned an invalid repository key: ${STAGING_REPOSITORY_KEY}" - fi - echo " staging repository: ${STAGING_REPOSITORY_KEY}" -} - -wait_for_portal_handoff() { - local repository_key=$1 - local response state elapsed=0 - echo "Polling OSSRH staging handoff (interval=${POLL_INTERVAL_SEC}s, timeout=${POLL_TIMEOUT_SEC}s)" - while (( elapsed < POLL_TIMEOUT_SEC )); do - response=$(staging_search_repositories) - state=$(jq -r --arg key "${repository_key}" \ - '.repositories[]? | select(.key == $key) | .state // "UNKNOWN"' \ - <<< "${response}") - DEPLOYMENT_ID=$(jq -r --arg key "${repository_key}" \ - '.repositories[]? | select(.key == $key) | .portal_deployment_id // empty' \ - <<< "${response}") - echo " [${elapsed}s] staging state=${state:-UNKNOWN}" - if [[ -n ${DEPLOYMENT_ID} ]]; then - if [[ ! ${DEPLOYMENT_ID} =~ ^[A-Za-z0-9-]+$ ]]; then - fatal "OSSRH staging service returned an invalid Portal deployment id" - fi - echo " Portal deployment id: ${DEPLOYMENT_ID}" - return 0 - fi - sleep "${POLL_INTERVAL_SEC}" - elapsed=$((elapsed + POLL_INTERVAL_SEC)) - done - fatal "timed out waiting for OSSRH staging repository '${repository_key}' to reach the Portal" -} - wait_for_validated() { local deployment_id=$1 local response state elapsed=0