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" 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