From 416bb816a533dc22c81f1033da08c9605097ee4a Mon Sep 17 00:00:00 2001 From: Jeremy Parr-Pearson Date: Fri, 4 Sep 2026 18:14:16 +0000 Subject: [PATCH 1/3] Split publishing step into deploy and release Signed-off-by: Jeremy Parr-Pearson --- .github/workflows/release.yml | 71 +++++++++++++++++++++++++++++------ 1 file changed, 60 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 64f21e91..400c81ed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -180,36 +180,85 @@ jobs: exit 1 fi - - name: Deploy and release to Maven Central + - name: Deploy to Maven Central staging id: deploy run: | + # Uploads + closes the staging repo, registering a VALIDATED deployment on the + # Portal. autoReleaseAfterClose is intentionally omitted: it is a no-op on the + # Portal compatibility API; the release is performed explicitly below. ./mvnw -s settings.xml \ -Ddevelocity.storage.directory=$HOME/.develocity-root \ -Dmaven.repo.local=$HOME/.m2/spring-data-valkey \ deploy -DskipTests \ -pl spring-data-valkey,spring-boot-starter-data-valkey \ -Dmaven.deploy.skip=false \ - -DautoReleaseAfterClose=true \ -U -B + - name: Release deployment to Maven Central + id: release + run: | + set -euo pipefail + PORTAL="https://central.sonatype.com/api/v1/publisher" + STAGING="https://ossrh-staging-api.central.sonatype.com/manual/search/repositories" + + state_of() { + curl -s -X POST -u "$OSSRH_USERNAME:$OSSRH_PASSWORD" "${PORTAL}/status?id=$1" | jq -r '.deploymentState' + } + + # Find the Portal deployment id created by the maven deploy above. + REPOS="$(curl -s -u "$OSSRH_USERNAME:$OSSRH_PASSWORD" "${STAGING}?profile_id=io.valkey&state=closed")" + echo "$REPOS" | jq '.repositories | map({key, description, state, portal_deployment_id})' + if [ "$(echo "$REPOS" | jq '.repositories | length')" != "1" ]; then + echo "ERROR: expected exactly 1 closed staging repo." + exit 1 + fi + DEPLOYMENT_ID="$(echo "$REPOS" | jq -r '.repositories[0].portal_deployment_id')" + echo "Deployment: $DEPLOYMENT_ID" + + # Wait for validation to finish before publishing. + for i in $(seq 1 30); do + STATE="$(state_of "$DEPLOYMENT_ID")"; echo " validate [$i] $STATE" + case "$STATE" in + VALIDATED) break ;; + FAILED) echo "ERROR: validation FAILED."; exit 1 ;; + esac + [ "$i" = 30 ] && { echo "ERROR: timed out waiting for VALIDATED."; exit 1; } + sleep 10 + done + + # Publish (irreversible). + CODE="$(curl -s -o /dev/null -w '%{http_code}' -X POST -u "$OSSRH_USERNAME:$OSSRH_PASSWORD" "${PORTAL}/deployment/${DEPLOYMENT_ID}")" + echo "Publish HTTP $CODE" + [ "$CODE" = "204" ] || [ "$CODE" = "200" ] || { echo "ERROR: publish returned $CODE"; exit 1; } + + # Confirm it reaches PUBLISHED. + for i in $(seq 1 30); do + STATE="$(state_of "$DEPLOYMENT_ID")"; echo " publish [$i] $STATE" + case "$STATE" in + PUBLISHED) echo "Released to Maven Central."; exit 0 ;; + FAILED) echo "ERROR: publish FAILED."; exit 1 ;; + esac + sleep 20 + done + echo "ERROR: timed out waiting for PUBLISHED (last: $STATE)." + exit 1 + - name: Drop staging repository on failure - if: failure() && steps.deploy.outcome == 'failure' + if: failure() && (steps.deploy.outcome == 'failure' || steps.release.outcome == 'failure') run: | - echo "Deploy failed - attempting to drop staging repository..." - AUTH_HEADER="Bearer $(echo -n "${OSSRH_USERNAME}:${OSSRH_PASSWORD}" | base64)" + # Clean up the staging repo so a retry starts fresh. + AUTH_HEADER="Bearer $(echo -n "${OSSRH_USERNAME}:${OSSRH_PASSWORD}" | base64 | tr -d '\n')" REPOS=$(curl -s --header "Authorization: ${AUTH_HEADER}" \ - "https://ossrh-staging-api.central.sonatype.com/manual/search/repositories?profile_id=io.valkey.springframework&state=open") + "https://ossrh-staging-api.central.sonatype.com/manual/search/repositories?profile_id=io.valkey") REPO_COUNT=$(echo "$REPOS" | jq '.repositories | length') if [ "$REPO_COUNT" = "1" ]; then REPO_KEY=$(echo "$REPOS" | jq -r '.repositories[0].key') echo "Dropping repository: $REPO_KEY" - curl --fail --request DELETE \ - --header "Authorization: ${AUTH_HEADER}" \ + curl --fail --request DELETE --header "Authorization: ${AUTH_HEADER}" \ "https://ossrh-staging-api.central.sonatype.com/manual/drop/repository/${REPO_KEY}" - echo "Staging repository dropped - safe to retry" elif [ "$REPO_COUNT" = "0" ]; then - echo "No open staging repository found - nothing to clean up" + echo "No staging repository found - nothing to clean up" else - echo "Multiple open staging repositories found ($REPO_COUNT) - skipping automatic cleanup to avoid dropping wrong repo" + echo "Multiple staging repositories found ($REPO_COUNT) - manual cleanup required" echo "$REPOS" | jq '.repositories' fi From 0f300fe4a4812d996ca038a6dcb93624427b9c57 Mon Sep 17 00:00:00 2001 From: Jeremy Parr-Pearson Date: Fri, 4 Sep 2026 18:32:19 +0000 Subject: [PATCH 2/3] Switch release publishing to the Portal Publisher API Signed-off-by: Jeremy Parr-Pearson --- .github/workflows/release.yml | 133 ++++++++++++++++++---------------- 1 file changed, 71 insertions(+), 62 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 400c81ed..3ba9d849 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -180,85 +180,94 @@ jobs: exit 1 fi - - name: Deploy to Maven Central staging - id: deploy + - name: Build and sign artifacts + id: build run: | - # Uploads + closes the staging repo, registering a VALIDATED deployment on the - # Portal. autoReleaseAfterClose is intentionally omitted: it is a no-op on the - # Portal compatibility API; the release is performed explicitly below. + # Build + GPG-sign the publishable modules locally (no deploy to staging). ./mvnw -s settings.xml \ -Ddevelocity.storage.directory=$HOME/.develocity-root \ -Dmaven.repo.local=$HOME/.m2/spring-data-valkey \ - deploy -DskipTests \ - -pl spring-data-valkey,spring-boot-starter-data-valkey \ - -Dmaven.deploy.skip=false \ + clean install javadoc:jar -DskipTests \ + -pl spring-data-valkey,spring-boot-starter-data-valkey -am \ -U -B - - name: Release deployment to Maven Central - id: release + - name: Assemble Central bundle + id: bundle run: | set -euo pipefail - PORTAL="https://central.sonatype.com/api/v1/publisher" - STAGING="https://ossrh-staging-api.central.sonatype.com/manual/search/repositories" + VERSION="$(./mvnw -s settings.xml help:evaluate -Dexpression=project.version -q -DforceStdout -N)" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" - state_of() { - curl -s -X POST -u "$OSSRH_USERNAME:$OSSRH_PASSWORD" "${PORTAL}/status?id=$1" | jq -r '.deploymentState' - } + # module_dir|group_path|artifact_id + MODULES=( + "spring-data-valkey|io/valkey/springframework/data|spring-data-valkey" + "spring-boot-starter-data-valkey|io/valkey/springframework/boot|spring-boot-starter-data-valkey" + ) - # Find the Portal deployment id created by the maven deploy above. - REPOS="$(curl -s -u "$OSSRH_USERNAME:$OSSRH_PASSWORD" "${STAGING}?profile_id=io.valkey&state=closed")" - echo "$REPOS" | jq '.repositories | map({key, description, state, portal_deployment_id})' - if [ "$(echo "$REPOS" | jq '.repositories | length')" != "1" ]; then - echo "ERROR: expected exactly 1 closed staging repo." - exit 1 - fi - DEPLOYMENT_ID="$(echo "$REPOS" | jq -r '.repositories[0].portal_deployment_id')" - echo "Deployment: $DEPLOYMENT_ID" + rm -rf bundle bundle.zip + for entry in "${MODULES[@]}"; do + IFS='|' read -r DIR GROUP ARTIFACT <<< "$entry" + OUT="bundle/${GROUP}/${ARTIFACT}/${VERSION}" + mkdir -p "$OUT" - # Wait for validation to finish before publishing. - for i in $(seq 1 30); do - STATE="$(state_of "$DEPLOYMENT_ID")"; echo " validate [$i] $STATE" - case "$STATE" in - VALIDATED) break ;; - FAILED) echo "ERROR: validation FAILED."; exit 1 ;; - esac - [ "$i" = 30 ] && { echo "ERROR: timed out waiting for VALIDATED."; exit 1; } - sleep 10 + # Published POM = the flattened pom. + cp "${DIR}/.flattened-pom.xml" "${OUT}/${ARTIFACT}-${VERSION}.pom" + + for f in "${ARTIFACT}-${VERSION}.jar" "${ARTIFACT}-${VERSION}-sources.jar" "${ARTIFACT}-${VERSION}-javadoc.jar"; do + cp "${DIR}/target/${f}" "${OUT}/" + cp "${DIR}/target/${f}.asc" "${OUT}/" # signatures from maven-gpg-plugin + done + + # Sign the (flattened) POM, which target/*.asc does not cover. + gpg --batch --yes --pinentry-mode loopback --passphrase "${GPG_PASSPHRASE}" \ + -u "${GPG_KEYNAME}" --armor --detach-sign "${OUT}/${ARTIFACT}-${VERSION}.pom" + + ( cd "$OUT" + for file in *.jar *.pom; do + md5sum "$file" | cut -d' ' -f1 > "${file}.md5" + sha1sum "$file" | cut -d' ' -f1 > "${file}.sha1" + done ) done - # Publish (irreversible). - CODE="$(curl -s -o /dev/null -w '%{http_code}' -X POST -u "$OSSRH_USERNAME:$OSSRH_PASSWORD" "${PORTAL}/deployment/${DEPLOYMENT_ID}")" - echo "Publish HTTP $CODE" - [ "$CODE" = "204" ] || [ "$CODE" = "200" ] || { echo "ERROR: publish returned $CODE"; exit 1; } + ( cd bundle && zip -qr ../bundle.zip . ) + echo "Bundle contents:"; unzip -l bundle.zip + + - name: Upload and release to Maven Central + id: release + run: | + set -euo pipefail + PORTAL="https://central.sonatype.com/api/v1/publisher" + AUTH="Bearer $(printf '%s:%s' "$OSSRH_USERNAME" "$OSSRH_PASSWORD" | base64 | tr -d '\n')" - # Confirm it reaches PUBLISHED. - for i in $(seq 1 30); do - STATE="$(state_of "$DEPLOYMENT_ID")"; echo " publish [$i] $STATE" + # Upload the bundle; AUTOMATIC releases to Central after validation passes. + # Upload returns 201 with the deployment id as the plain-text body. + RESP="$(curl -s --fail-with-body -H "Authorization: $AUTH" \ + --form bundle=@bundle.zip "${PORTAL}/upload?publishingType=AUTOMATIC")" + DEPLOYMENT_ID="$(echo "$RESP" | tail -n1)" + echo "Deployment: $DEPLOYMENT_ID" + # Guard against a malformed upload response (empty, whitespace, or an error + # body) rather than assuming a specific id format. The status poll below is + # the real validator of the id. + if [ -z "${DEPLOYMENT_ID// }" ] || echo "$DEPLOYMENT_ID" | grep -q '[[:space:]<]'; then + echo "ERROR: upload did not return a usable deployment id. Response was:" + echo "$RESP" + exit 1 + fi + + # Confirm it reaches PUBLISHED. Fail fast on FAILED; tolerate transient + # unknown states but give up if they persist (likely a bad id or API outage). + unknown=0 + for i in $(seq 1 40); do + STATE="$(curl -s -X POST -H "Authorization: $AUTH" "${PORTAL}/status?id=${DEPLOYMENT_ID}" | jq -r '.deploymentState')" + echo " [$i] $STATE" case "$STATE" in PUBLISHED) echo "Released to Maven Central."; exit 0 ;; - FAILED) echo "ERROR: publish FAILED."; exit 1 ;; + FAILED) echo "ERROR: deployment FAILED."; exit 1 ;; + PENDING|VALIDATING|VALIDATED|PUBLISHING) unknown=0 ;; + *) unknown=$((unknown+1)); + [ "$unknown" -ge 3 ] && { echo "ERROR: repeated unexpected state '$STATE' (bad id or API error)."; exit 1; } ;; esac sleep 20 done - echo "ERROR: timed out waiting for PUBLISHED (last: $STATE)." + echo "ERROR: timed out (last: $STATE)." exit 1 - - - name: Drop staging repository on failure - if: failure() && (steps.deploy.outcome == 'failure' || steps.release.outcome == 'failure') - run: | - # Clean up the staging repo so a retry starts fresh. - AUTH_HEADER="Bearer $(echo -n "${OSSRH_USERNAME}:${OSSRH_PASSWORD}" | base64 | tr -d '\n')" - REPOS=$(curl -s --header "Authorization: ${AUTH_HEADER}" \ - "https://ossrh-staging-api.central.sonatype.com/manual/search/repositories?profile_id=io.valkey") - REPO_COUNT=$(echo "$REPOS" | jq '.repositories | length') - if [ "$REPO_COUNT" = "1" ]; then - REPO_KEY=$(echo "$REPOS" | jq -r '.repositories[0].key') - echo "Dropping repository: $REPO_KEY" - curl --fail --request DELETE --header "Authorization: ${AUTH_HEADER}" \ - "https://ossrh-staging-api.central.sonatype.com/manual/drop/repository/${REPO_KEY}" - elif [ "$REPO_COUNT" = "0" ]; then - echo "No staging repository found - nothing to clean up" - else - echo "Multiple staging repositories found ($REPO_COUNT) - manual cleanup required" - echo "$REPOS" | jq '.repositories' - fi From b858f0a6c6698c1f518990a80e063ac0c95ed84f Mon Sep 17 00:00:00 2001 From: Jeremy Parr-Pearson Date: Fri, 4 Sep 2026 19:06:05 +0000 Subject: [PATCH 3/3] Raise publish job timeout to cover the polling window Signed-off-by: Jeremy Parr-Pearson --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3ba9d849..d75ba731 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -95,7 +95,7 @@ jobs: publish: needs: release runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 30 environment: maven-publish permissions: id-token: write