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.
- Sonatype account — register at central.sonatype.com
- Namespace verified —
ai.deep-codeverified via DNS TXT record ondeep-code.ai - GPG key — for signing artifacts
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
EOFWhy 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_IDCreate ~/.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-passphraseExport the key value (prompted for the passphrase):
gpg --armor --export-secret-keys you@example.comEscape 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.
./gradlew publishMavenCentralPublicationToMavenLocalArtifacts land in ~/.m2/repository/ai/deep-code/agents-kt/0.7.0/.
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"
donecd 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/...
- 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.
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
For the next release, update version in build.gradle.kts and repeat the process.
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.
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:publishAllPublicationsToGitHubPackagesRepositoryArtifacts land at the Agents.KT packages page.
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.
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.
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 runIf the consumer compiles + runs against the published snapshot, the channel works end-to-end.