Publishing the addon-facing API artifacts¶
RTP supports two addon-development dependencies: rtp-api (the RTPAddon SPI and the
configurable selection model) and rtp-core (the RTP facade, the Shape hierarchy,
ConfigParser, etc.). Out-of-repo addon authors cannot use the in-build
compileOnly project(':rtp-api') references, so those two modules (plus their
inter-module project-dependency closure) are published to a public Maven coordinate.
This document covers the channels:
- JitPack - active now, zero-credentials, builds straight from a git tag.
- GitHub Packages - repo-scoped Maven registry, credentialed; wiring is in place.
- Maven Central - the durable, discoverable channel; not yet enabled, full how-to below.
The publication wiring itself lives in the root build.gradle (maven-publish applied to
the publishedModulePaths closure) and is shared by all channels.
What gets published¶
maven-publish is applied to exactly this closure (root build.gradle):
| Module | Why it is in the closure |
|---|---|
rtp-api |
addon SPI (direct addon dependency) |
rtp-core |
RTP facade + Shape (direct addon dependency) |
commands-api |
api dep of rtp-api / rtp-core |
yaml-api |
api dep of rtp-api / rtp-core |
metrics-api |
api dep of rtp-api |
effects-api |
api dep of rtp-core |
maps-api |
api dep of rtp-core |
rtp-proxy:rtp-proxy-common |
api dep of rtp-core |
Every coordinate that can appear in a generated POM must itself be published, otherwise a
consumer's transitive resolution fails. Platform adapters (rtp-bukkit, rtp-paper,
rtp-folia, rtp-fabric), the shaded rtp-plugin jar, and the JDK-25 Fabric/Loom
submodules are deliberately not published - they are runtime artifacts, not compile
dependencies for addons.
The artifacts are thin (un-shaded) jars: RTP provides the classes at runtime, so addons
depend on them with compileOnly.
JitPack (active)¶
Configuration in this repo¶
- Root
build.gradleappliesmaven-publishto the closure above (publication namemavenJava,from components.java). jitpack.yml(repo root) pins JDK 21 and overrides the install command to publish only the closure withpublishToMavenLocal -PexcludeJdk25. The-PexcludeJdk25flag (handled insettings.gradle) drops the JDK-25 / unobfuscated-Loom modules (effects-api:effects-api-fabric-unobf,rtp-fabric:rtp-fabric-common-unobf,rtp-fabric:rtp-fabric-v26_1_R1, and the shadedrtp-pluginaggregator that bundles them) from the build graph entirely. Without it, Gradle still configures those modules and fails resolving their Java 25 toolchain, which is absent on JitPack's JDK-21-only host - the published closure never depends on them, so excluding them is safe.
How JitPack serves it¶
JitPack builds the tag, runs the install: command, and harvests the artifacts from the
build's local Maven repo. For multi-module builds JitPack publishes every module it finds
and exposes each as:
com.github.DailyStruggle.RTP:<module>:<tag>
JitPack handles the inter-module references (the generated POMs use the
io.github.dailystruggle group; JitPack maps the same-group cross-references to the
com.github.DailyStruggle.RTP group it serves), so a consumer only needs the JitPack repo.
Consumer build.gradle (out-of-repo addon)¶
repositories {
mavenCentral()
maven { url 'https://jitpack.io' }
}
dependencies {
compileOnly 'com.github.DailyStruggle.RTP:rtp-api:3.0.1'
compileOnly 'com.github.DailyStruggle.RTP:rtp-core:3.0.1'
}
Replace 3.0.1 with any git tag, a branch (master-SNAPSHOT), or a commit SHA. The first
build for a new tag triggers JitPack to compile; subsequent resolves are cached.
Triggering / verifying a build¶
- Push a git tag (e.g.
3.0.1) togithub.com/DailyStruggle/RTP. - Visit
https://jitpack.io/#DailyStruggle/RTP/3.0.1and click Get it (or just let a consumer's first dependency resolve trigger the build). - Inspect the build log on that page if a module fails to resolve.
Local smoke test (no JitPack needed)¶
The exact command JitPack runs can be reproduced locally to confirm the publication graph:
.\gradlew --no-daemon -PexcludeJdk25 `
:commands-api:publishToMavenLocal :yaml-api:publishToMavenLocal `
:metrics-api:publishToMavenLocal :maps-api:publishToMavenLocal `
:effects-api:publishToMavenLocal :rtp-proxy:rtp-proxy-common:publishToMavenLocal `
:rtp-api:publishToMavenLocal :rtp-core:publishToMavenLocal
-PexcludeJdk25 reproduces JitPack's JDK-21-only environment by dropping the JDK-25 Loom
modules from the build graph (see settings.gradle). On a workstation that has JDK 25
installed the flag is optional, but include it to mirror exactly what JitPack runs.
The artifacts land under ~/.m2/repository/io/github/dailystruggle/.
CI publishing on push / merge to V3¶
The Jenkins pipeline (Jenkinsfile) runs a Publish API Artifacts stage on every push or
merge to the V3 (and V3-beta) branch. It runs the same publishToMavenLocal closure as the
local smoke test (PUBLISH_TASKS env var, kept in sync with publishedModulePaths in the root
build.gradle and the install: block in jitpack.yml). This validates that the addon-API
publication graph still builds and that every transitive coordinate is publishable, catching a
broken POM before a consumer's JitPack resolve hits it.
Note that JitPack itself needs no push from CI: it builds the branch on demand the first time a consumer resolves a snapshot. So out-of-repo addon authors can track the development line directly:
repositories { maven { url 'https://jitpack.io' } }
dependencies {
compileOnly 'com.github.DailyStruggle.RTP:rtp-api:V3-SNAPSHOT'
compileOnly 'com.github.DailyStruggle.RTP:rtp-core:V3-SNAPSHOT'
}
V3-SNAPSHOT resolves to the latest commit on the V3 branch (JitPack refreshes it; add
changing = true / a short cache TTL on the consumer side to pick up new commits). Tagged
releases (com.github.DailyStruggle.RTP:rtp-api:<tag>) remain the stable, immutable channel.
To turn the CI stage into a remote publish (e.g. Maven Central snapshots or GitHub
Packages), replace the publishToMavenLocal tasks in PUBLISH_TASKS with the credentialed
publish target and inject the repository credentials via Jenkins secrets - see the Maven
Central section below for the publication/signing wiring.
GitHub Packages (repo-scoped Maven registry)¶
GitHub Packages hosts a Maven registry per repository at
https://maven.pkg.github.com/DailyStruggle/RTP. It needs credentials on both ends (publish
and consume), so it is not as frictionless as JitPack, but it requires no Sonatype account
or GPG signing and it lives right next to the code. Use it when you want authenticated,
immutable coordinates without the Maven Central onboarding.
Configuration in this repo¶
The root build.gradle publishing closure already declares the GitHub Packages repository
(name = 'GitHubPackages') for the same publishedModulePaths closure. It is only wired
when a token is present, so a credential-free .\gradlew build is unaffected and no
unauthenticated publish target is exposed. Credentials are resolved in this order:
- Gradle properties
gpr.user/gpr.key(put them in~/.gradle/gradle.properties, never commit them). - Env vars
GITHUB_ACTOR/GITHUB_TOKEN(GitHub Actions injects these automatically).
The publish token needs the write:packages scope (a classic PAT with write:packages,
or the workflow GITHUB_TOKEN with packages: write permission). Reading needs
read:packages.
Publishing locally¶
Add credentials to ~/.gradle/gradle.properties:
gpr.user=<your-github-username>
gpr.key=<personal-access-token-with-write:packages>
Then publish the closure to GitHub Packages (mirror the smoke-test task list, swapping
publishToMavenLocal for publishMavenJavaPublicationToGitHubPackagesRepository). Do not
pass -PexcludeJdk25 here: that flag is only for JDK-21-only hosts like JitPack, and this
publish runs on a box that has JDK 25, so the JDK-25 Loom modules should stay in the build
graph:
.\gradlew --no-daemon `
:commands-api:publishMavenJavaPublicationToGitHubPackagesRepository `
:yaml-api:publishMavenJavaPublicationToGitHubPackagesRepository `
:metrics-api:publishMavenJavaPublicationToGitHubPackagesRepository `
:maps-api:publishMavenJavaPublicationToGitHubPackagesRepository `
:effects-api:publishMavenJavaPublicationToGitHubPackagesRepository `
:rtp-proxy:rtp-proxy-common:publishMavenJavaPublicationToGitHubPackagesRepository `
:rtp-api:publishMavenJavaPublicationToGitHubPackagesRepository `
:rtp-core:publishMavenJavaPublicationToGitHubPackagesRepository
The shorter .\gradlew publish also works but fans out to every publishable
module and every declared repository; the explicit task list keeps it to the addon-API
closure and the GitHub Packages target only.
Publishing from CI (GitHub Actions)¶
A minimal workflow needs packages: write permission and the built-in token:
permissions:
contents: read
packages: write
# ...
- name: Publish to GitHub Packages
run: ./gradlew publishMavenJavaPublicationToGitHubPackagesRepository
env:
GITHUB_ACTOR: ${{ github.actor }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
For the Jenkins pipeline, inject a PAT as gpr.user / gpr.key (or GITHUB_ACTOR /
GITHUB_TOKEN) secrets and swap the publishToMavenLocal tasks in PUBLISH_TASKS for the
...ToGitHubPackagesRepository targets.
Consumer build.gradle (out-of-repo addon)¶
GitHub Packages requires the consumer to authenticate too (there is no anonymous read):
repositories {
mavenCentral()
maven {
url = 'https://maven.pkg.github.com/DailyStruggle/RTP'
credentials {
username = findProperty('gpr.user') ?: System.getenv('GITHUB_ACTOR')
password = findProperty('gpr.key') ?: System.getenv('GITHUB_TOKEN')
}
}
}
dependencies {
compileOnly 'io.github.dailystruggle:rtp-api:3.2.1'
compileOnly 'io.github.dailystruggle:rtp-core:3.2.1'
}
The consumer's token only needs read:packages. Because of this read-time credential
requirement, JitPack remains the friendlier choice for public addon authors; GitHub Packages
is best for internal / org-scoped consumers who already authenticate to GitHub.
Maven Central (first-party wiring, manual Portal upload)¶
Maven Central is the right destination once the rtp-api contract is stable and you expect
real third-party volume. It is more work than JitPack (one-time account + signing setup) but
gives discoverable, permanent coordinates with no per-tag build step on the consumer side.
Trust posture: this build deliberately uses only first-party Gradle tooling for the
Central path - maven-publish and the built-in signing plugin, both of which ship inside
the Gradle distribution. No third-party publish plugin is applied, so no external, auto-
updating build code ever gets access to the Central token, the GPG signing key, or the
compiled jars before they are signed. The trade-off is one manual step at release time:
Gradle signs the artifacts into a local staging directory, and that bundle is uploaded to the
Central Portal by hand (or a single curl). Sonatype's new Portal has no first-party Gradle
upload protocol, so automating that last step would mean adding a community plugin - which is
exactly the trust surface this posture avoids.
Sequencing note: ideally land Phase 2 of
docs/dev/scratch/CHECKLIST-rtp-api-devux.md(the typedrtp-apiregistration surface) before the first Central release, so the permanent public contract is not pinned tortp-coreinternals.
1. One-time account setup¶
- Create a Sonatype Central account at https://central.sonatype.com.
- Verify ownership of the
io.github.dailystrugglenamespace. For aio.github.<user>group this is automatic via a GitHub verification (Central asks you to create a short-lived public repo named after a generated code). The group already matches the GitHub org (DailyStruggle), so no group-id change is needed. - Generate a user token (username + password pair) under your Central account; these are the credentials Gradle uses to upload.
2. GPG signing key¶
Maven Central requires every artifact to be GPG-signed.
gpg --gen-key # create a key
gpg --list-secret-keys --keyid-format short
gpg --keyserver keyserver.ubuntu.com --send-keys <KEY_ID> # publish the public key
Export the secret key (in-memory ASCII-armored form is easiest for CI):
gpg --armor --export-secret-keys <KEY_ID>
3. Build-script changes (already in place)¶
The root build.gradle configure(publishedModulePaths.collect { project(it) }) block
already implements the full first-party Central wiring - you do not need to edit the build
for a release. What it does:
- Applies the built-in
signingplugin alongsidemaven-publish. - Produces the mandatory
-sourcesand-javadocjars for the published closure (withSourcesJar()/withJavadocJar()), with doclint relaxed for those javadoc tasks so a missing@param/@returnonrtp-api/rtp-corecannot fail a release. - Adds the POM metadata Central validates (name, description, url, license, developers, scm).
- Declares a local
CentralStagingMaven repository at<root>/build/central-staging/, shared by every module so the whole closure lands in one directory ready to bundle. - Wires GPG signing in two mutually exclusive modes, both gated so a credential-free
.\gradlew buildnever signs and never fails: - In-memory key from the
signingKey/signingPasswordGradle properties (best for CI: no gpg binary or keyring needed). - Local
gpgCLI, enabled with-PsignWithGpgCmd(Gradle shells out togpg). - Skips the
rtp-coretest-fixtures variant from the published component, so no stray-test-fixturesjar is published (Central rejects those) and the "cannot be mapped to Maven" POM-capability warning is gone.
Because everything is gated on the credentials being present, none of this affects a normal developer build, JitPack, or the GitHub Packages path.
Modern-GnuPG note (verified 2026-08-16): a secret key exported by a very recent GnuPG (2.4+/2.5+; this box runs 2.5.18) can be unreadable by the BouncyCastle bundled in Gradle's
signingplugin, failing the sign task withCannot perform signing task ... because it has no configured signatory. If you hit that with the in-memorysigningKeypath, switch to thegpgCLI path (-PsignWithGpgCmd), which signs with whatever keygpgitself resolves (respectingGNUPGHOME/gpg.conf) and side-steps the parse issue. The full staging + signing flow (.ascfor jar, sources, javadoc, module, and pom) was verified end-to-end via thegpgCLI path.
4. Decouple the artifact version (Phase 5.1)¶
The plugin currently versions everything at 3.0.1 (root build.gradle). For Central,
give the API artifacts a version line that moves on the rtp-api/rtp-core semver cadence
rather than the plugin release cadence, and document the policy (Phase 5.2 of
CHECKLIST-rtp-api-devux.md). A practical approach is an apiVersion ext property applied
only to the published closure.
5. Credentials (never commit)¶
The signing key + passphrase feed the build; the Portal token is used only by the upload step (not the build).
Local dev box - put the signing key in ~/.gradle/gradle.properties (never in the repo):
signingKey=-----BEGIN PGP PRIVATE KEY BLOCK----- ...
signingPassword=<key-passphrase>
CI (the recommended place to publish from) - do not write a properties file. Gradle maps
any env var named ORG_GRADLE_PROJECT_<name> to the project property <name>, so expose the
signing key as CI secrets with exactly these names:
ORG_GRADLE_PROJECT_signingKey = <ASCII-armored PGP private key>
ORG_GRADLE_PROJECT_signingPassword = <key passphrase>
That is the "repo variable" the signing step reads - no build-script change is needed to feed
it. Store the armored key as a single multi-line secret (GitHub Actions / Jenkins credentials
both support multi-line values). The Central Portal token (central-token-user /
central-token-pass) is a separate secret used only by the upload command below; keep it
out of the build entirely.
6. Publish (sign locally, upload the bundle)¶
Step 1 - sign the closure into the local staging dir (mirror the smoke-test task list,
targeting the CentralStaging repo). With the signing key present, each publish task also
runs the sign... task automatically. Do not pass -PexcludeJdk25: that flag is only for
JDK-21-only hosts like JitPack, and a release runs on a JDK-25 box, so leave the JDK-25 Loom
modules in the build graph:
In-memory-key form (CI):
.\gradlew --no-daemon `
:commands-api:publishMavenJavaPublicationToCentralStagingRepository `
:yaml-api:publishMavenJavaPublicationToCentralStagingRepository `
:metrics-api:publishMavenJavaPublicationToCentralStagingRepository `
:maps-api:publishMavenJavaPublicationToCentralStagingRepository `
:effects-api:publishMavenJavaPublicationToCentralStagingRepository `
:rtp-proxy:rtp-proxy-common:publishMavenJavaPublicationToCentralStagingRepository `
:rtp-api:publishMavenJavaPublicationToCentralStagingRepository `
:rtp-core:publishMavenJavaPublicationToCentralStagingRepository
Local gpg CLI form (recommended on a box with a modern GnuPG, e.g. this dev box) - add
-PsignWithGpgCmd and drop the signingKey properties; gpg supplies the key:
.\gradlew --no-daemon -PsignWithGpgCmd `
:commands-api:publishMavenJavaPublicationToCentralStagingRepository `
:yaml-api:publishMavenJavaPublicationToCentralStagingRepository `
:metrics-api:publishMavenJavaPublicationToCentralStagingRepository `
:maps-api:publishMavenJavaPublicationToCentralStagingRepository `
:effects-api:publishMavenJavaPublicationToCentralStagingRepository `
:rtp-proxy:rtp-proxy-common:publishMavenJavaPublicationToCentralStagingRepository `
:rtp-api:publishMavenJavaPublicationToCentralStagingRepository `
:rtp-core:publishMavenJavaPublicationToCentralStagingRepository
The signed artifacts (jar, sources, javadoc, .pom, and their .asc signatures) land under
build/central-staging/io/github/dailystruggle/....
Step 2 - zip that tree and upload it to the Portal. The Portal expects a zip whose internal
layout is the Maven repository layout (i.e. the contents of build/central-staging):
Compress-Archive -Path build/central-staging/* -DestinationPath build/central-bundle.zip -Force
$pair = '<central-token-user>:<central-token-pass>'
$auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes($pair))
curl.exe -H "Authorization: Bearer $auth" `
-F bundle=@build/central-bundle.zip `
https://central.sonatype.com/api/v1/publisher/upload
Step 3 - go to https://central.sonatype.com → Deployments, review the validated deployment, and click Publish (or drop the automatic-release query param on the upload). That manual click is the price of not handing a third-party plugin your signing key.
7. CI publishing (GitHub Actions)¶
The workflow .github/workflows/maven-central.yml automates steps 1-2 above (sign into the
CentralStaging dir, zip, and curl the bundle to the Portal). It runs only on a push to a
release-line branch (V1, V2, V3, ... and their lowercase forms), plus manual
workflow_dispatch. Because those branches are protected, a push happens only when a pull
request is merged - so publication is gated behind a PR, never an ad-hoc direct commit or a
feature branch.
Required repository secrets (Settings → Secrets and variables → Actions):
| Secret | Purpose |
|---|---|
SIGNING_KEY |
ASCII-armored PGP private key (multi-line), fed to Gradle as ORG_GRADLE_PROJECT_signingKey |
SIGNING_PASSWORD |
passphrase for the key (empty string if none) |
CENTRAL_TOKEN_USER |
Central Portal user-token username (upload only) |
CENTRAL_TOKEN_PASS |
Central Portal user-token password (upload only) |
The workflow uses the in-memory-key signing path (no gpg binary on the runner), verifies
that .asc signatures were actually produced before uploading (fail-loud per S-004), and does
not auto-release - the deployment lands VALIDATED and a human clicks Publish on the
Portal. To auto-release instead, append ?publishingType=AUTOMATIC to the upload URL in the
workflow.
Consumer build.gradle (after Central release)¶
repositories { mavenCentral() }
dependencies {
compileOnly 'io.github.dailystruggle:rtp-api:<version>'
compileOnly 'io.github.dailystruggle:rtp-core:<version>'
}
No special repository block needed - Central is in every build's default set.
Compatibility policy (to document with the first stable release)¶
- Semantic versioning for
rtp-api/rtp-coreindependent of the plugin version. - State clearly which
rtp-coretypes are contract (e.g.RTP.addShape,Shape,ConfigParser) vs. internal-and-may-break, sincertp-coreexposes more thanrtp-api(see ADR-051 andCHECKLIST-rtp-api-devux.mdPhase 2). - Link the published Javadoc from
FOR_ADDON_DEVELOPERS.md.