Publishing to Maven Central
June 2, 2026 · View on GitHub
Ordering matters. This document covers the mechanics (GPG, bundle, upload). For the sequence a release must follow — and the rule "never advertise a version that isn't on Central yet" — see
docs/RELEASE_RUNBOOK.md. After uploading, run./gradlew checkPublishedVersionto confirm the version is resolvable before bumping the README or announcing.
Prerequisites
- Sonatype account — register at central.sonatype.com
- Namespace verified —
ai.deep-codeverified via DNS TXT record ondeep-code.ai - GPG key — for signing artifacts
GPG Key Setup
Generate a key (if you don't have one). Use a strong passphrase — the
private key gets stored on disk in ~/.gradle/gradle.properties (or in
CI secrets); the passphrase is the second factor that protects it if the
file leaks.
gpg --full-generate-key
# Prompts: RSA, 4096, 2y expiry, your name + email, then enter a passphrase.
If you need an unattended/batch flow (e.g. building inside a fresh CI
runner), pass the passphrase via Passphrase: in the batch file — never
use %no-protection:
PASSPHRASE="$(openssl rand -base64 32)" # generate; store in your password manager
gpg --pinentry-mode loopback --batch --gen-key <<EOF
Key-Type: RSA
Key-Length: 4096
Subkey-Type: RSA
Subkey-Length: 4096
Name-Real: Your Name
Name-Email: you@example.com
Expire-Date: 2y
Passphrase: $PASSPHRASE
%commit
EOF
Why not
%no-protection? An unprotected private key is a single-file compromise. If~/.gradle/gradle.properties, a CI cache, or a stray backup leaks the file, the attacker can sign artifacts as you with no further work. A passphrase forces a second factor at use-time. The small ergonomic cost (typing the passphrase once into your password manager → Gradle property) is worth it.
Upload the public key to a keyserver:
gpg --keyserver keyserver.ubuntu.com --send-keys YOUR_KEY_ID
Gradle Credentials
Create ~/.gradle/gradle.properties (chmod 600 — the file holds your
signing key and Sonatype token):
sonatypeUsername=your-token-username
sonatypePassword=your-token-password
signing.key=-----BEGIN PGP PRIVATE KEY BLOCK-----\n...\n-----END PGP PRIVATE KEY BLOCK-----
signing.password=your-gpg-passphrase
Export the key value (prompted for the passphrase):
gpg --armor --export-secret-keys you@example.com
Escape newlines as \n for the property file. The passphrase set during
key generation goes into signing.password= verbatim.
Empty-passphrase fallback. If you genuinely need an unprotected key for a constrained environment, use
--passphrase ""on the export and leavesigning.password=empty. Keep that key isolated — different identity, no upload to a keyserver bound to your real identity, scope it to one project. Avoid for anything that ships to Maven Central.
Build & Bundle
1. Publish to local Maven repo
./gradlew publishMavenCentralPublicationToMavenLocal
Artifacts land in ~/.m2/repository/ai/deep-code/agents-kt/0.7.0/.
2. Generate checksums and create bundle
SRC=~/.m2/repository/ai/deep-code/agents-kt/0.7.0
DEST=build/bundle/ai/deep-code/agents-kt/0.7.0
mkdir -p "$DEST"
for f in "$SRC"/agents-kt-*; do
fname=$(basename "$f")
cp "$f" "$DEST/$fname"
md5 -q "$f" > "$DEST/$fname.md5"
shasum -a 1 "$f" | awk '{print \$1}' > "$DEST/$fname.sha1"
done
3. Create ZIP with Maven directory structure
cd build/bundle
zip -r ../agents-kt-0.7.0-bundle.zip ai/
The ZIP must contain the full path: ai/deep-code/agents-kt/0.7.0/...
Upload to Central Portal
- Go to central.sonatype.com → Deployments → Publish Component
- Deployment Name:
ai.deep-code:agents-kt:0.7.0 - Description:
Typed Kotlin DSL framework for AI agent systems - Upload
build/agents-kt-0.7.0-bundle.zip - Wait for validation to pass
- Click Publish
Propagation to Maven Central search takes 10-30 minutes after publishing.
Bundle Contents
Each artifact needs: the file itself, .asc (GPG signature), .md5, and .sha1.
ai/deep-code/agents-kt/0.7.0/
agents-kt-0.7.0.jar
agents-kt-0.7.0.jar.asc
agents-kt-0.7.0.jar.md5
agents-kt-0.7.0.jar.sha1
agents-kt-0.7.0-sources.jar
agents-kt-0.7.0-sources.jar.asc
agents-kt-0.7.0-sources.jar.md5
agents-kt-0.7.0-sources.jar.sha1
agents-kt-0.7.0-javadoc.jar
agents-kt-0.7.0-javadoc.jar.asc
agents-kt-0.7.0-javadoc.jar.md5
agents-kt-0.7.0-javadoc.jar.sha1
agents-kt-0.7.0.pom
agents-kt-0.7.0.pom.asc
agents-kt-0.7.0.pom.md5
agents-kt-0.7.0.pom.sha1
agents-kt-0.7.0.module
agents-kt-0.7.0.module.asc
agents-kt-0.7.0.module.md5
agents-kt-0.7.0.module.sha1
Version Bump
For the next release, update version in build.gradle.kts and repeat the process.
GitHub Packages (Secondary Channel)
In addition to Maven Central, the framework publishes to GitHub Packages at https://maven.pkg.github.com/Deep-CodeAI/Agents.KT. This is a secondary channel — Maven Central remains the primary public distribution.
When to use GitHub Packages:
| Use case | Why |
|---|---|
| CI snapshots | Maven Central doesn't accept snapshots from outside Sonatype OSSRH; GitHub Packages does. |
| PR-preview builds | Reviewers can depend on a published 0.x.y-pr<NN>-<sha> artifact instead of doing a local build. |
| Sonatype outage redundancy | When Central is having a bad day (it happens), consumers can pin GitHub Packages temporarily. |
| Authenticated early-access | Private collaborators / partners consume pre-release builds with a GitHub token — no Nexus to operate. |
| Internal same-org use | Internal projects within the same org pull builds via gradle.properties auth — no waiting on the manual Sonatype publish click. |
When NOT to use it:
- Don't tell public consumers to depend on GitHub Packages for stable releases. Stable releases go to Central. GitHub Packages is for the use cases above.
Publishing locally
Set credentials in ~/.gradle/gradle.properties (NOT this repo's gradle.properties):
gpr.user=your-github-username
gpr.key=ghp_<personal-access-token-with-write-packages-scope>
Then:
./gradlew publishAllPublicationsToGitHubPackagesRepository \
:agents-kt-ksp:publishAllPublicationsToGitHubPackagesRepository
Artifacts land at the Agents.KT packages page.
Consumer-side wiring
Downstream consumers need to authenticate to GitHub Packages even for public repo packages (that's GitHub Packages' authn model; there's nothing we can do about it).
// build.gradle.kts (consumer side)
repositories {
mavenCentral()
maven {
url = uri("https://maven.pkg.github.com/Deep-CodeAI/Agents.KT")
credentials {
username = providers.gradleProperty("gpr.user").orNull
?: System.getenv("GITHUB_ACTOR")
password = providers.gradleProperty("gpr.key").orNull
?: System.getenv("GITHUB_TOKEN")
}
}
}
dependencies {
implementation("ai.deep-code:agents-kt:0.6.0-SNAPSHOT")
}
Token scopes:
- Consumer (reading packages): PAT with
read:packages. - Publisher (this repo's CI): the auto-provisioned
GITHUB_TOKENinside a GitHub Actions run already haswrite:packagesfor same-repo packages. No PAT needed.
Token storage:
- Never commit a PAT to a repository's
gradle.properties. - For local dev:
~/.gradle/gradle.properties(chmod 600). - For CI in other projects: a secrets-manager / GitHub Actions secret injected as env var.
CI workflow (not yet shipped)
The GitHub Actions workflow that auto-publishes snapshots on every push to main and releases on v*.*.* tag is not yet committed — it needs CI-environment verification on a controlled push before being enabled. The shape (for reference; commit when ready):
# .github/workflows/publish-github-packages.yml (DRAFT — not yet active)
name: Publish to GitHub Packages
on:
push:
branches: [main]
tags: ['v*.*.*']
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { distribution: temurin, java-version: 21 }
- uses: gradle/actions/setup-gradle@v3
- run: |
./gradlew \
publishAllPublicationsToGitHubPackagesRepository \
:agents-kt-ksp:publishAllPublicationsToGitHubPackagesRepository
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Snapshot-version derivation (whether to bump to <next>-SNAPSHOT on push to main, or use <next>-pr<NN>-<sha> for PR builds) is a separate decision pending discussion before the workflow goes live.
Smoke test
After publishing, validate by creating a fresh consumer project:
mkdir /tmp/agents-kt-gpr-smoke && cd /tmp/agents-kt-gpr-smoke
gradle init --type kotlin-application
# Edit build.gradle.kts to add the GitHub Packages repo + dependency above
gradle run
If the consumer compiles + runs against the published snapshot, the channel works end-to-end.