From d3df0cfe617b192d456a3196717e424f5e3c90ca Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sun, 2 Aug 2026 13:53:50 +0200 Subject: [PATCH 1/2] fix(android): fail the release when a central deployment does not validate `publishToMavenCentral()` uploads the bundle, prints "Skipping deployment validation!" and exits 0 whatever the portal then makes of it. A release that uploaded something central rejects looked exactly like one that worked, and the only way to tell them apart was opening the portal by hand. The plugin can wait; it just does not by default. `DeploymentValidation.VALIDATED` polls until the portal has validated the deployment and fails the build if it does not, which is what the maven jar's path has always done - the sonatype plugin there blocks and logs "has been validated". Still USER_MANAGED: validating is not releasing, so a human still presses the button and a bad artifact is still recallable. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Lqc6gWzBBnoqvdaQ9HPoEa --- android/build.gradle.kts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 7320fceb..3fee3a7d 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -1,4 +1,5 @@ import com.vanniktech.maven.publish.AndroidSingleVariantLibrary +import com.vanniktech.maven.publish.DeploymentValidation plugins { alias(libs.plugins.android.library) @@ -176,7 +177,11 @@ mavenPublishing { // Uploads to the portal and stops. A human releases it from there, so a bad // artifact is still recallable — Central is immutable once released. - publishToMavenCentral() + // + // Waiting for VALIDATED is what makes a failed deployment fail the release: + // the default uploads, prints "Skipping deployment validation!" and exits 0 + // whatever the portal then makes of the bundle. + publishToMavenCentral(false, DeploymentValidation.VALIDATED) // Only Central demands a signature. Making it unconditional would mean no // `publishToMavenLocal` and no GitHub Packages publish without a private From f07cadc733c5d13f538cdc2e5f216ace0eb76630 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sun, 2 Aug 2026 14:02:33 +0200 Subject: [PATCH 2/2] fix(android): fail the release when a central deployment does not validate `publishToMavenCentral` uploads the bundle, prints "Skipping deployment validation!" and exits 0 whatever the portal then makes of it. A release that uploaded something central rejects looked exactly like one that worked, and the only way to tell them apart was opening the portal by hand. The plugin cannot help here: it wires its validation task in only when `automaticRelease` is on, and that also releases the deployment - which is deliberately a human's job, since central never forgets a version. So the workflow polls the portal itself, the way the maven jar gets for free from the sonatype plugin. The publish step gains `shell: bash` for its pipefail, without which tee's exit code would hide a failing gradle - the same class of silent success this is about. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Lqc6gWzBBnoqvdaQ9HPoEa --- .github/scripts/await-central-validation.sh | 58 +++++++++++++++++++++ .github/workflows/android.yml | 13 ++++- 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100755 .github/scripts/await-central-validation.sh diff --git a/.github/scripts/await-central-validation.sh b/.github/scripts/await-central-validation.sh new file mode 100755 index 00000000..079d389f --- /dev/null +++ b/.github/scripts/await-central-validation.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Waits for a central portal deployment to validate, and fails if it does not. +# +# `publishToMavenCentral` uploads and exits 0 whatever the portal makes of the +# bundle - the vanniktech plugin only validates when `automaticRelease` is on, +# which would also release it, and releasing is deliberately a human's job here. +# So the release path polls the portal itself. The maven jar gets this for free: +# the sonatype maven plugin blocks and reports. +# +# await-central-validation.sh +# +# Takes the gradle output to read the deployment id out of, and the portal token +# as MAVEN_CENTRAL_USERNAME / MAVEN_CENTRAL_PASSWORD. + +set -euo pipefail + +log="${1:?usage: await-central-validation.sh }" +: "${MAVEN_CENTRAL_USERNAME:?}" "${MAVEN_CENTRAL_PASSWORD:?}" + +deployment=$(grep -oE 'deployment id: [0-9a-fA-F-]{36}' "$log" | tail -1 | awk '{print $3}') +if [ -z "$deployment" ]; then + echo "no deployment id in $log - did the upload actually run?" >&2 + exit 1 +fi + +token=$(printf '%s:%s' "$MAVEN_CENTRAL_USERNAME" "$MAVEN_CENTRAL_PASSWORD" | base64 | tr -d '\n') +echo "waiting on deployment ${deployment}" + +# Validation is usually seconds; the ceiling is only here so a portal that never +# answers fails the release instead of hanging until github's own timeout. +for _ in $(seq 60); do + response=$(curl -sS -X POST -H "Authorization: Bearer ${token}" \ + "https://central.sonatype.com/api/v1/publisher/status?id=${deployment}") + state=$(printf '%s' "$response" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("deploymentState",""))') + + case "$state" in + VALIDATED|PUBLISHING|PUBLISHED) + echo "deployment ${deployment} is ${state}" + exit 0 + ;; + FAILED) + echo "deployment ${deployment} failed validation:" >&2 + printf '%s\n' "$response" >&2 + exit 1 + ;; + PENDING|VALIDATING|"") + sleep 10 + ;; + *) + echo "unexpected deployment state '${state}':" >&2 + printf '%s\n' "$response" >&2 + exit 1 + ;; + esac +done + +echo "deployment ${deployment} still not validated after 10 minutes" >&2 +exit 1 diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index a53afc8a..2d09b06f 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -255,9 +255,20 @@ jobs: # still be dropped instead of lived with. - name: publish to maven central working-directory: android - run: ./gradlew publishToMavenCentral -Podr.abis= + # `shell: bash` for the pipefail it sets - the default `bash -e` would + # let tee's exit code hide a failing gradle + shell: bash + run: ./gradlew publishToMavenCentral -Podr.abis= | tee "${RUNNER_TEMP}/publish.log" env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }} ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASS }} + + # The step above exits 0 however the portal judges the bundle, so without + # this a release central rejected looks like one that worked. + - name: await central validation + env: + MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + run: .github/scripts/await-central-validation.sh "${RUNNER_TEMP}/publish.log"