From d28b5c9f3ab530813bc066438b574f2ee5e3a2c1 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Mon, 10 Aug 2026 19:46:18 +0200 Subject: [PATCH 1/3] ci(maven): publish the java jar to central without the portal click Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AHuzgqeyTCZgudXffVR45c --- .github/workflows/maven.yml | 2 -- jni/README.md | 3 ++- jni/pom.xml | 6 +++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index e357f922e..de39d2234 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -86,8 +86,6 @@ jobs: gpg-private-key: ${{ secrets.SIGNING_KEY }} gpg-passphrase: MAVEN_GPG_PASSPHRASE - # Uploads and stops, the way the AAR does — released by hand from the - # portal, because Central never forgets a version. - name: publish to maven central run: mvn --batch-mode --no-transfer-progress --file jni/pom.xml deploy -Pcentral -Drevision="${REVISION}" env: diff --git a/jni/README.md b/jni/README.md index 9387913d2..431291716 100644 --- a/jni/README.md +++ b/jni/README.md @@ -27,7 +27,8 @@ The native library is loaded from `java.library.path` as `odr_jni` The Java classes are published as `app.opendocument:odr-core-java` to Maven Central and to [GitHub Packages](https://github.com/orgs/opendocument-app/packages?repo_name=OpenDocument.core) -on release (`.github/workflows/maven.yml`, built from `pom.xml`). The artifact +on release (`.github/workflows/maven.yml`, built from `pom.xml`); the Central +deployment publishes itself, no click in the portal. The artifact contains **only the Java API** — consumers build the native `odr_jni` library themselves for their target platform (see below) and provide it at runtime. diff --git a/jni/pom.xml b/jni/pom.xml index ec2ff9a86..bd79ed312 100644 --- a/jni/pom.xml +++ b/jni/pom.xml @@ -170,9 +170,9 @@ true central - - false + + true From 88d7ccbad34090fd0142c607a0d17eaf3a50a34a Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Mon, 10 Aug 2026 19:50:25 +0200 Subject: [PATCH 2/3] ci(android): release the aar to central without the portal click The vanniktech plugin validates the deployment on the automatic path, so the polling script is no longer needed. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AHuzgqeyTCZgudXffVR45c --- .github/scripts/await-central-validation.sh | 58 --------------------- .github/workflows/android.yml | 17 +----- android/README.md | 7 ++- android/build.gradle.kts | 12 ++--- 4 files changed, 9 insertions(+), 85 deletions(-) delete mode 100755 .github/scripts/await-central-validation.sh diff --git a/.github/scripts/await-central-validation.sh b/.github/scripts/await-central-validation.sh deleted file mode 100755 index 079d389fc..000000000 --- a/.github/scripts/await-central-validation.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/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 91889a2e0..2144028bb 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -243,26 +243,11 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # Uploads a deployment to the portal and leaves it there. Releasing it to - # Maven Central is a click in the portal UI, deliberately: Central never - # forgets a version, so this is the last point at which a bad artifact can - # still be dropped instead of lived with. - name: publish to maven central working-directory: android - # `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" + run: ./gradlew publishToMavenCentral -Podr.abis= 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/README.md b/android/README.md index b88ae8bbe..d34f8a06c 100644 --- a/android/README.md +++ b/android/README.md @@ -124,10 +124,9 @@ key being configured, so `publishToMavenLocal` and the GitHub Packages publish still work without one; an unsigned upload is rejected by the portal, so the release path stays guarded either way. -Publishing **uploads a deployment to the portal and stops**. Releasing it is a -deliberate click in the portal UI, because Central never forgets a version — -that click is the last point at which a bad artifact can be dropped rather than -lived with. +Publishing **releases the deployment**, no click in the portal. The build waits +for the portal's verdict, so a bundle Central rejects fails the release instead +of sitting there unnoticed. For now OpenDocument.droid keeps building odrcore from the conan package (`with_jni=True`), deploying `libodr_jni.so` and `odr-core-java.jar` out of it. That path is unaffected by anything here, and it is the reason the diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 3fee3a7d8..6242852c0 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -175,13 +175,11 @@ dependencies { mavenPublishing { configure(AndroidSingleVariantLibrary(variant = "release")) - // Uploads to the portal and stops. A human releases it from there, so a bad - // artifact is still recallable — Central is immutable once released. - // - // 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) + // Releases the deployment rather than parking it in the portal. VALIDATED + // makes a deployment the portal rejects fail the build; without it the + // upload prints "Skipping deployment validation!" and exits 0 whatever the + // portal then makes of the bundle. + publishToMavenCentral(true, DeploymentValidation.VALIDATED) // Only Central demands a signature. Making it unconditional would mean no // `publishToMavenLocal` and no GitHub Packages publish without a private From bd4d45b925913d6df4c44b7f67eb27291920bc95 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Mon, 10 Aug 2026 19:56:33 +0200 Subject: [PATCH 3/3] ci: wait for the central deployment to publish, not just validate The release status reports a version as published from these jobs' exit codes, and validation passing is no promise that publishing did. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AHuzgqeyTCZgudXffVR45c --- android/README.md | 4 ++-- android/build.gradle.kts | 10 +++++----- jni/pom.xml | 6 ++++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/android/README.md b/android/README.md index d34f8a06c..a0912552f 100644 --- a/android/README.md +++ b/android/README.md @@ -125,8 +125,8 @@ still work without one; an unsigned upload is rejected by the portal, so the release path stays guarded either way. Publishing **releases the deployment**, no click in the portal. The build waits -for the portal's verdict, so a bundle Central rejects fails the release instead -of sitting there unnoticed. +until it is published, so a green publish job means the version is on Central +and a bundle the portal rejects fails the release. For now OpenDocument.droid keeps building odrcore from the conan package (`with_jni=True`), deploying `libodr_jni.so` and `odr-core-java.jar` out of it. That path is unaffected by anything here, and it is the reason the diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 6242852c0..b49b4c98d 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -175,11 +175,11 @@ dependencies { mavenPublishing { configure(AndroidSingleVariantLibrary(variant = "release")) - // Releases the deployment rather than parking it in the portal. VALIDATED - // makes a deployment the portal rejects fail the build; without it the - // upload prints "Skipping deployment validation!" and exits 0 whatever the - // portal then makes of the bundle. - publishToMavenCentral(true, DeploymentValidation.VALIDATED) + // Releases the deployment rather than parking it in the portal, and waits + // for it to land: a release is reported as published on the strength of + // this task's exit code, and the default prints "Skipping deployment + // validation!" and exits 0 whatever the portal makes of the bundle. + publishToMavenCentral(true, DeploymentValidation.PUBLISHED) // Only Central demands a signature. Making it unconditional would mean no // `publishToMavenLocal` and no GitHub Packages publish without a private diff --git a/jni/pom.xml b/jni/pom.xml index bd79ed312..5d0c1a5f9 100644 --- a/jni/pom.xml +++ b/jni/pom.xml @@ -170,9 +170,11 @@ true central - true + + published