diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 020b27f3..43acb256 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -204,6 +204,8 @@ jobs: if: github.event_name != 'push' needs: [aar, instrumented] runs-on: ubuntu-24.04 + # holds the portal token and the signing key + environment: maven-central permissions: contents: read packages: write @@ -237,8 +239,21 @@ jobs: echo "ODR_VERSION=${INPUT_VERSION}" >> $GITHUB_ENV fi - - name: publish + - name: publish to github packages working-directory: android - run: ./gradlew publishReleasePublicationToGitHubPackagesRepository -Podr.abis= + run: ./gradlew publishAllPublicationsToGitHubPackagesRepository -Podr.abis= 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 + 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 }} diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 2a7fea0f..bbe384a2 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -14,13 +14,34 @@ concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true -permissions: - contents: read - packages: write - jobs: build: runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - name: checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: setup java + uses: actions/setup-java@f4f1212c880fdec8162ea9a6493f4495191887b4 # v5 + with: + distribution: temurin + java-version: 21 + cache: maven + + - name: build + run: mvn --batch-mode --no-transfer-progress --file jni/pom.xml verify + + # Split out so the secrets sit behind an environment and are never in reach of + # a plain push build. + publish: + if: github.event_name != 'push' + runs-on: ubuntu-24.04 + environment: maven-central + permissions: + contents: read + packages: write steps: - name: checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 @@ -34,7 +55,6 @@ jobs: server-id: github - name: get version - if: github.event_name != 'push' env: INPUT_VERSION: ${{ inputs.version }} run: | @@ -44,12 +64,31 @@ jobs: echo "REVISION=${INPUT_VERSION}" >> $GITHUB_ENV fi - - name: build - if: github.event_name == 'push' - run: mvn --batch-mode --no-transfer-progress --file jni/pom.xml verify - - - name: publish - if: github.event_name != 'push' + - name: publish to github packages run: mvn --batch-mode --no-transfer-progress --file jni/pom.xml deploy -Drevision="${REVISION}" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # A second setup-java: the central profile deploys to a different server + # id and needs the signing key in the runner's gpg keyring, and the step + # above has already had its turn with settings.xml. + - name: setup java for maven central + uses: actions/setup-java@f4f1212c880fdec8162ea9a6493f4495191887b4 # v5 + with: + distribution: temurin + java-version: 21 + cache: maven + server-id: central + server-username: MAVEN_CENTRAL_USERNAME + server-password: MAVEN_CENTRAL_PASSWORD + 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: + MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + MAVEN_GPG_PASSPHRASE: ${{ secrets.SIGNING_PASS }} diff --git a/android/AGENTS.md b/android/AGENTS.md index 62a05ea7..42f68b84 100644 --- a/android/AGENTS.md +++ b/android/AGENTS.md @@ -50,6 +50,12 @@ calls it. - **Shared test inputs stay in `../jni/testfixtures`**, which both suites compile — so it is limited to what android API 26 offers (no `Path.of`, no `Files.writeString`, no `String.formatted`). +- **Publishing goes to Maven Central and GitHub Packages**, via + `com.vanniktech.maven.publish` — sonatype ships no official gradle plugin for + the portal. Central's extra requirements (sources + javadoc jars, `developers` + in the POM, a PGP signature per file) drive what that block declares; do not + drop one thinking GitHub Packages is the only consumer. Signing is conditional + on a key being present so `publishToMavenLocal` works without one. - **`native/` is build output**, never committed; the artifacts CI downloads land in the same place. `-Podr.abis=` (empty) is what tells the build the libraries are already there. diff --git a/android/README.md b/android/README.md index de45edb3..5de68358 100644 --- a/android/README.md +++ b/android/README.md @@ -19,13 +19,7 @@ ABIs: `arm64-v8a`, `armeabi-v7a`, `x86`, `x86_64`. minSdk 26. ```gradle repositories { - maven { - url = uri("https://maven.pkg.github.com/opendocument-app/OpenDocument.core") - credentials { - username = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR") - password = providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN") - } - } + mavenCentral() } dependencies { @@ -33,8 +27,9 @@ dependencies { } ``` -GitHub Packages needs a token with `read:packages` even for public packages — -see [Publishing](#publishing) for what that rules out. Do not depend on +The same artifact is also on GitHub Packages, but prefer Central: GitHub +Packages needs a token with `read:packages` even for public packages — see +[Publishing](#publishing) for what that rules out. Do not depend on `odr-core-java` as well: the AAR carries the same classes. ```kotlin @@ -104,17 +99,31 @@ ships to — and on a current API level. ## Publishing -Releases go to GitHub Packages, like the maven jar, and only once the +Releases go to **Maven Central and GitHub Packages**, and only once the instrumented suite has passed on every API level — an AAR that assembles and -lints is no evidence that it works on a device. That is enough for -consumers who can hold a token, and **not** enough for the one this is -ultimately built for: GitHub Packages demands `read:packages` even to read a -public artifact, and f-droid builds from source with no credentials at all. -Making the AAR the base of OpenDocument.droid therefore means publishing it to -Maven Central first, which is a separate piece of work — the `app.opendocument` -namespace has to be verified, and every artifact signed. - -Until then OpenDocument.droid keeps building odrcore from the conan package +lints is no evidence that it works on a device. + +Maven Central is the one that matters. GitHub Packages demands `read:packages` +even to read a public artifact, which rules out the consumer this is ultimately +built for: f-droid builds from source with no credentials at all. It stays +published there only because the maven jar is, and dropping it would break +whoever already reads it. + +`app.opendocument` is an established namespace on Central — `pdf2htmlex-android` +and `wvware-android` have been there since 2024 — so this is a new artifact under +an old name, not a new namespace. Central asks for more than GitHub Packages +does, and the module supplies it: sources and javadoc jars, a POM carrying +`developers`, and a PGP signature over every file. Signing is conditional on a +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. + +For now OpenDocument.droid keeps building odrcore from the conan package (`with_jni=True`), deploying `libodr_jni.so`, `odr-core-java.jar` and the assets out of it. That path is unaffected by anything here, and it is the reason the two halves cannot drift: they come out of one build. diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 08e084fb..43c76342 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -1,7 +1,9 @@ +import com.vanniktech.maven.publish.AndroidSingleVariantLibrary + plugins { alias(libs.plugins.android.library) alias(libs.plugins.spotless) - `maven-publish` + alias(libs.plugins.maven.publish) } // ktfmt in the kotlinlang flavor, the same formatter and version @@ -143,13 +145,6 @@ android { // library outside java.library.path; android never takes that branch disable += "UnsafeDynamicallyLoadedCode" } - - publishing { - singleVariant("release") { - withSourcesJar() - withJavadocJar() - } - } } dependencies { @@ -159,37 +154,60 @@ dependencies { androidTestImplementation(libs.androidx.test.runner) } -publishing { - publications { - register("release") { - groupId = "app.opendocument" - artifactId = "odr-core-android" - version = odrVersion - - afterEvaluate { from(components["release"]) } - - pom { - name = "odr-core-android" - description = - "Android bindings for OpenDocument.core — decode office documents " + - "(ODF, OOXML, legacy MS binary, PDF, CSV, ...) and render them to HTML" - url = "https://github.com/opendocument-app/OpenDocument.core" - licenses { - license { - name = "MPL-2.0" - url = "https://www.mozilla.org/en-US/MPL/2.0/" - } - } - scm { - connection = "scm:git:https://github.com/opendocument-app/OpenDocument.core.git" - developerConnection = - "scm:git:git@github.com:opendocument-app/OpenDocument.core.git" - url = "https://github.com/opendocument-app/OpenDocument.core" - } +// Two destinations, deliberately. Maven Central is the one that matters: it is +// the only one f-droid and any other credential-less source builder can resolve +// from, and `app.opendocument` is already a live namespace there +// (`pdf2htmlex-android`, `wvware-android`). GitHub Packages stays because the +// maven jar publishes there and dropping it would break whoever already reads +// it. Central mandates what the publication below carries — sources and javadoc +// jars, a pom with developers, and a PGP signature over every file — none of +// which GitHub Packages asks for. +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. + publishToMavenCentral() + + // Only Central demands a signature. Making it unconditional would mean no + // `publishToMavenLocal` and no GitHub Packages publish without a private + // key on hand, which is a steep price for a repository that never checks + // one. A Central upload missing its .asc files is rejected by the portal, + // so the release path is still guarded. + if (providers.gradleProperty("signingInMemoryKey").isPresent) { + signAllPublications() + } + + coordinates("app.opendocument", "odr-core-android", odrVersion) + + pom { + name = "odr-core-android" + description = + "Android bindings for OpenDocument.core — decode office documents " + + "(ODF, OOXML, legacy MS binary, PDF, CSV, ...) and render them to HTML" + url = "https://github.com/opendocument-app/OpenDocument.core" + licenses { + license { + name = "MPL-2.0" + url = "https://www.mozilla.org/en-US/MPL/2.0/" + } + } + developers { + developer { + id = "opendocument-app" + name = "OpenDocument.app" + url = "https://github.com/opendocument-app" } } + scm { + connection = "scm:git:https://github.com/opendocument-app/OpenDocument.core.git" + developerConnection = "scm:git:git@github.com:opendocument-app/OpenDocument.core.git" + url = "https://github.com/opendocument-app/OpenDocument.core" + } } +} +publishing { repositories { maven { name = "GitHubPackages" diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index a9cb9e5b..05da93a1 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -6,6 +6,10 @@ agp = "9.3.1" spotless = "8.8.0" ktfmt = "0.64" +# sonatype ships no official gradle plugin for the central portal, so this is +# the community one android libraries have settled on +mavenPublish = "0.37.0" + junit = "4.13.2" androidxTest = "1.7.0" androidxTestJunit = "1.3.0" @@ -19,3 +23,4 @@ androidx-test-junit = { module = "androidx.test.ext:junit", version.ref = "andro [plugins] android-library = { id = "com.android.library", version.ref = "agp" } spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } +maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" } diff --git a/jni/AGENTS.md b/jni/AGENTS.md index 9d96bd71..bf627810 100644 --- a/jni/AGENTS.md +++ b/jni/AGENTS.md @@ -9,7 +9,7 @@ package `app.opendocument.core`. Mirrors the surface of the python bindings | Path | What | |------|------| | `CMakeLists.txt` | Builds `libodr_jni` + `odr-core-java.jar`; included from the root build via `ODR_JNI`, or standalone against an installed `odrcore`. | -| `pom.xml` | Maven distribution of the Java classes only (`app.opendocument:odr-core-java`); published to GitHub Packages on release via `.github/workflows/maven.yml`. Keep `--release`/`-Xlint` in sync with `CMAKE_JAVA_COMPILE_FLAGS`. | +| `pom.xml` | Maven distribution of the Java classes only (`app.opendocument:odr-core-java`); published to Maven Central (the `central` profile) and GitHub Packages on release via `.github/workflows/maven.yml`. Keep `--release`/`-Xlint` in sync with `CMAKE_JAVA_COMPILE_FLAGS`. | | `src/` | JNI sources, one `jni_*` unit per public-API area; `odr_jni.hpp` (strings, exceptions, handles) and `jni_convert.hpp` (struct/POJO marshalling) are the helpers. | | `java/app/opendocument/core/` | Java API: enums, POJOs (styles, metas, `HtmlConfig`), and handle-backed wrappers extending `NativeResource`. Also compiled as-is into the AAR (`../android`) — which is kotlin, but this stays java: `add_jar` below has no kotlin toolchain. | | `tests/` | JUnit 5 suite, run via ctest (`odr_jni_junit`); inputs are generated inline (tmp files, zip-built minimal ODT) — no fixture files. | diff --git a/jni/README.md b/jni/README.md index 939061f8..d8072bcb 100644 --- a/jni/README.md +++ b/jni/README.md @@ -24,7 +24,8 @@ The native library is loaded from `java.library.path` as `odr_jni` ## Maven distribution -The Java classes are published as `app.opendocument:odr-core-java` to +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 contains **only the Java API** — consumers build the native `odr_jni` library @@ -36,13 +37,7 @@ native library for every ABI and the runtime assets. See [`../android`](../andro ```gradle repositories { - maven { - url = uri("https://maven.pkg.github.com/opendocument-app/OpenDocument.core") - credentials { - username = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR") - password = providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN") - } - } + mavenCentral() } dependencies { @@ -50,11 +45,14 @@ dependencies { } ``` -Note: GitHub Packages requires authentication (a token with `read:packages`) -even for public packages. +Prefer Central: GitHub Packages requires authentication (a token with +`read:packages`) even for public packages. Local build: `mvn --file jni/pom.xml verify` (produces the jar plus sources -and javadoc jars in `jni/target/`). +and javadoc jars in `jni/target/`). Central additionally wants every file PGP +signed and a POM carrying `developers`; both live in the `central` profile +(`mvn deploy -Pcentral`), kept off the default build so neither a signing key +nor a portal token is needed to build or to deploy to GitHub Packages. ## Building diff --git a/jni/pom.xml b/jni/pom.xml index 4bbdad46..9aba99e6 100644 --- a/jni/pom.xml +++ b/jni/pom.xml @@ -24,6 +24,15 @@ + + + + opendocument-app + OpenDocument.app + https://github.com/opendocument-app + + + scm:git:https://github.com/opendocument-app/OpenDocument.core.git scm:git:git@github.com:opendocument-app/OpenDocument.core.git @@ -111,6 +120,9 @@ + github @@ -118,4 +130,53 @@ https://maven.pkg.github.com/opendocument-app/OpenDocument.core + + + + + central + + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.8 + + + sign-artifacts + verify + + sign + + + + + + + --pinentry-mode + loopback + + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.11.0 + true + + central + + false + + + + + +