Англоязычная версия статьи на Medium
Начиная с 12 марта 2024 года регистрация на OSSRH портале теперь недоступна. Большинство существующих туториалов в интернете описывают как раз опыт публикации через OSSRH на Maven Central из-за чего после марта 2024 года эти инструкции стали не актуальны для публикации проектов новых авторов.
Disclaimer: я не смогу провести вас по этому процессу шаг за шагом, потому что разные проекты работают по разному. Ниже - не пошаговая инструкция, а руководство к действию. Вдумчиво выполняйте этапы публикации и не забывайте про секцию Troubleshoting в конце статьи
Процесс публикации можно разбить на следующие шаги:
Регистрация на Central Portal и верификация namespace
Создания GPG ключа для подписи артефактов
Локальная публикация проекта для теста
Подключение JReleaser к проекту и публикация локально
Настройка Github Actions для автоматической публикации
Регистрация на Central Portal и верификация namespace
Зарегистрироваться на портале можно через главную страницу - https://central.sonatype.com/. Кнопка для регистрации справа сверху

После регистрации верифицируйте ваш email и войдите в аккаунт. Далее нужно верифицировать namespace - это будет ваша первая часть проекта. Например, в моем случае я владею доменом kulikov.uk, а значит я смогу использовать в качестве namespace uk.kulikov
Для верификации владения домена вам нужно добавить TXT запись в DNS. У Maven Central есть гайд как это сделать тут.
Если у вас нет своего домена, то вы сможете воспользоваться одним из публичнодоступных из списка:
Github:
io.github.myusernameGitlab:
io.gitlab.myusernameGitee:
io.gitee.myusernameBitbucket:
io.bitbucket.myusername
Всегда актуальную инструкцию по верификации namespace можно найти в документации Maven Central: https://central.sonatype.org/register/namespace/#for-code-hosting-services-with-personal-groupid
В результате на странице с namespace’ами у вас должен быть как минимум один верифицированный namespace как у меня на скриншоте:

Создания GPG ключа для подписи артефактов
Прежде чем приступать к загрузке артефакта необходимо его подписать своим ключем. Чтобы каждый, кто скачивает вашу библиотеку, мог быть уверен что это вы
Устанавливаем утилиту GPG
На MacOS с помощью brew: Введите в терминале brew install gnupg
На Linux с помощью apt-get: Введите в терминале sudo apt-get install gnupg
Генерируем PGP ключ:
В терминале вводим и затем заполняем все требуемые поля:
gpg --generate-keyЗапомните или запишите passphrase, который использовали при настройке! Не используйте пустой!
Публикуем наш публичный ключ
Невероятно важно опубликовать публичный ключ на один из поддерживаемых Maven Central'ом серверов. ��олько так Central Portal сможет понять что артефакт пришел от вас.
Для публикации я решил использовать keyserver.ubuntu.com и опубликовал публичный ключ с помощью команды ниже.
gpg --keyserver keyserver.ubuntu.com --send-keys BF81E10590D4EBF590D00F911D41D36F7A67A07CЗамените BF81E10590D4EBF590D00F911D41D36F7A67A07C своим ID ключа из вывода команды gpg --generate-key. В случае если вы не нашли ID ключа, то можете получить все ваши ключи с помощью команды:
gpg --list-secret-keys --keyid-format LONGПодойдут оба формата записи ID ключа - в моем случае это 1D41D36F7A67A07C и BF81E10590D4EBF590D00F911D41D36F7A67A07C
Локальная публикация Maven проекта
Конфигурация этого шага очень сильно зависит от того какой стек вы используете. Итоговым результатом будет опубликованный пакет в локальном maven-репозитории. Локальный Maven-репозиторий находится в:
Windows:
C:\Users\<User_Name>\.m2Linux:
/home/<User_Name>/.m2Mac:
/Users/<user_name>/.m2
Для публикации вам следует подключить Gradle Plugin
maven-publish:
plugins {
...
id("maven-publish")
...
}Для дальнейшей публикации через JReleaser мы должны добавить публикацию в локальную папку в
buildпапке проекта
publishing {
...
repositories {
maven {
setUrl(layout.buildDirectory.dir("staging-deploy"))
}
}
}Для Java проекта не забудьте добавить java-компонент:
publishing {
publications {
create<MavenPublication>("release") {
from(components["java"])
...
}
...
}
...
}Для Android проекта добавьте Android-компонент:
android {
publishing {
singleVariant("release") {
withSourcesJar()
withJavadocJar()
}
}
}
publishing {
publications {
create<MavenPublication>("release") {
afterEvaluate {
from(components["release"])
}
...
}
...
}
...
}Для примера вы можете взять настроенную Maven публикацию из моих репозиториев:
Пример для публикации Android-библиотеки можете найти тут
Пример для публикации чистой Java-библиотеки можете найти тут
Проверьте что ваша библиотека собрана и подключается корректно с помощью команды:
./gradlew publishToMavenLocalЗатем в другом вашем проекте добавьте в Maven repositories локальный репозиторий:
repositories {
google()
mavenCentral()
...
mavenLocal()
}И можно подключать к проекту библиотеку. В моем случае для groupId = "uk.kulikov.detekt.decompose" , artifactId = "decompose-detekt-rules" добавление библиотеки выглядит так:
implementation("uk.kulikov.detekt.decompose:decompose-detekt-rules:0.1")Подключение JReleaser к проекту и публикация локально
Добавляем JReleaser плагин к проекту. Инструкцию как это сделать можно найти тут
Извлекаем публичный и приватный ключ из хранилища. Замените
BF81E10590D4EBF590D00F911D41D36F7A67A07Cна свой ID ключа:
gpg --output public.pgp --armor --export BF81E10590D4EBF590D00F911D41D36F7A67A07C
gpg --output private.pgp --armor --export-secret-key BF81E10590D4EBF590D00F911D41D36F7A67A07CЭти команды создадут два файла - public.pgp и private.pgp
Генерируем токены доступа к Central Portal для публикации на странице аккаунта по кнопке “Generate User Token”:

Сгенерируем токен для GitHub. Это нужно для публикации релиза в GitHub. Если вы не хотите этого, пропустите этот шаг. Токен генерируется по этой ссылке: https://github.com/settings/tokens/new
Для публикации релиза нужен доступ на запись
Добавляем конфиг JReleaser со всеми необходимыми параметрами для публикации. Мы используем toml потому-что там удобнее указывать мультистрочные параметры, поэтому локально конфиг-файл храниться по пути
~/.jreleaser/config.toml. Мой конфиг выглядит примерно так (я вырезал свои токены в целях безопасности):
JRELEASER_GITHUB_TOKEN="EMPTY"
JRELEASER_GPG_PASSPHRASE="supersecretpassword"
JRELEASER_MAVENCENTRAL_SONATYPE_TOKEN="Maven Central Portal Token"
JRELEASER_MAVENCENTRAL_SONATYPE_USERNAME="Maven Central Portal Password/Username"
JRELEASER_GPG_PUBLIC_KEY="""-----BEGIN PGP PUBLIC KEY BLOCK-----
...
-----END PGP PUBLIC KEY BLOCK-----"""
JRELEASER_GPG_SECRET_KEY="""-----BEGIN PGP PRIVATE KEY BLOCK-----
...
-----END PGP PRIVATE KEY BLOCK-----"""
Добавьте в проект минимальную конфигурацию jReleaser:
jreleaser {
release {
github {
skipRelease = true
skipTag = true
}
}
}Запустите
./gradlew jreleaserConfigчтобы проверить что все настроено правильно. Мой вывод выглядит так:
hooks:
enabled: false
active: NEVER
command:
enabled: false
active: NEVER
script:
enabled: false
active: NEVER
project:
name: detekt-decompose-rule
version: 0.2
versionPattern: SEMVER
snapshot:
enabled: false
pattern: .*-SNAPSHOT
label: early-access
fullChangelog: false
description: Detekt ruleset for Decompose project
longDescription: Detekt ruleset for Decompose project
stereotype: NONE
links:
license: https://github.com/LionZXY/detekt-decompose-rule/blob/main/LICENSE
bugTracker: https://{{repoHost}}/{{repoOwner}}/{{repoName}}/issues
vcsBrowser: https://{{repoHost}}/{{repoOwner}}/{{repoName}}
extraProperties:
versionMajor: 0
versionMinor: 2
versionNumber: 0.2
versionWithUnderscores: 0_2
versionWithDashes: 0-2
versionNumberWithUnderscores: 0_2
versionNumberWithDashes: 0-2
effectiveVersionWithUnderscores: 0_2
effectiveVersionWithDashes: 0-2
java:
enabled: true
version: 8
groupId: uk.kulikov.detekt.decompose
artifactId: detekt-decompose-rule
multiProject: false
release:
github:
enabled: true
host: github.com
owner: LionZXY
name: detekt-decompose-rule
username: LionZXY
token: ************
uploadAssets: ALWAYS
artifacts: true
files: true
checksums: true
catalogs: true
signatures: true
repoUrl: https://{{repoHost}}/{{repoOwner}}/{{repoName}}
repoCloneUrl: https://{{repoHost}}/{{repoOwner}}/{{repoName}}.git
commitUrl: https://{{repoHost}}/{{repoOwner}}/{{repoName}}/commits
srcUrl: https://{{repoHost}}/{{repoOwner}}/{{repoName}}/blob/{{repoBranch}}
downloadUrl: https://{{repoHost}}/{{repoOwner}}/{{repoName}}/releases/download/{{tagName}}/{{artifactFile}}
releaseNotesUrl: https://{{repoHost}}/{{repoOwner}}/{{repoName}}/releases/tag/{{tagName}}
latestReleaseUrl: https://{{repoHost}}/{{repoOwner}}/{{repoName}}/releases/latest
issueTrackerUrl: https://{{repoHost}}/{{repoOwner}}/{{repoName}}/issues
tagName: v{{projectVersion}}
releaseName: Release {{tagName}}
branch: main
branchPush: main
commitAuthor:
name: jreleaserbot
email: jreleaser@kordamp.org
sign: false
skipTag: false
skipRelease: false
overwrite: false
update:
enabled: false
apiEndpoint: https://api.github.com
connectTimeout: 20
readTimeout: 60
changelog:
enabled: true
append:
enabled: false
links: false
skipMergeCommits: false
formatted: NEVER
hide:
contributors:
milestone:
name: {{tagName}}
close: true
issues:
enabled: false
comment: 🎉 This issue has been resolved in `{{tagName}}` ([Release Notes]({{releaseNotesUrl}}))
label:
name: released
color: #FF0000
description: Issue has been released
prerelease:
enabled: false
draft: false
releaseNotes:
enabled: false
checksum:
name: checksums.txt
individual: false
algorithms:
SHA_256
artifacts: true
files: true
catalog:
enabled: false
active: NEVER
sbom:
enabled: false
active: NEVERДобавьте блок
signingдля подписи в Central Portal:
jreleaser {
...
signing {
active = Active.ALWAYS
armored = true
verify = true
}
}Проверьте что подпись успешна с помощью команды: ./gradlew jreleaserSign
Добавьте как минимум одного автора и год публикации. Это нужно для генерации лицензии jReleser плагином:
jreleaser {
project {
inceptionYear = "2024"
author("@LionZXY")
}
...
}Добавляем публикацию в Maven Central:
jreleaser {
...
deploy {
maven {
mavenCentral.create("sonatype") {
active = Active.ALWAYS
url = "https://central.sonatype.com/api/v1/publisher"
stagingRepository(layout.buildDirectory.dir("staging-deploy").get().toString())
setAuthorization("Basic")
}
}
}
}Если вы публикуете Android-проект, то вам придется выключить верификацию POM до исправления этого issue:
jreleaser {
...
deploy {
maven {
mavenCentral.create("sonatype") {
...
applyMavenCentralRules = false // Wait for fix: https://github.com/kordamp/pomchecker/issues/21
sign = true
checksums = true
sourceJar = true
javadocJar = true
...
}
}
}
}Таймаут по умолчанию слишком маленький, я увеличил его с помощью изменения параметра retryDelay :
jreleaser {
...
deploy {
maven {
mavenCentral.create("sonatype") {
...
retryDelay = 60
...
}
}
}
}Все! Готово! Мы можем сделать первую публикацию в Central Portal
Мой итоговый файл для Android-библиотеки выглядит в итоге так:
jreleaser {
project {
inceptionYear = "2024"
author("@LionZXY")
}
gitRootSearch = true // I added this parameter because my project is in a subfolder
signing {
active = Active.ALWAYS
armored = true
verify = true
}
release {
github {
skipRelease = true
skipTag = true
}
}
deploy {
maven {
mavenCentral.create("sonatype") {
active = Active.ALWAYS
url = "https://central.sonatype.com/api/v1/publisher"
stagingRepository(layout.buildDirectory.dir("staging-deploy").get().toString())
setAuthorization("Basic")
applyMavenCentralRules = false // Wait for fix: https://github.com/kordamp/pomchecker/issues/21
sign = true
checksums = true
sourceJar = true
javadocJar = true
retryDelay = 60
}
}
}
}Процесс публикации теперь будет выглядеть так:
./gradlew jreleaserConfig build publish
./gradlew jreleaserFullReleaseВнимание: Убедитесь что в названии версии нет постфикса -SNAPSHOT !
Напоминаю что вы всегда можете свериться с уже настроенной публикацией из примеров:
Публикация в GitHub Actions
Для удобства публикации я предлагаю вам настроить автоматический CI/CD. Публикация будет происходить по назначению тега в репозитории.
Самый простой способ начать - просто скопировать файл ниже по пути .github/workflows/release.yml:
name: Publish to mavencentral
on:
push:
tags:
- '*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 21
uses: actions/setup-java@v2
with:
java-version: '21'
distribution: 'temurin'
- name: Build and publish with Gradle
uses: gradle/gradle-build-action@3
with:
arguments: --no-daemon -i jreleaserConfig build test publish
env:
JRELEASER_GPG_SECRET_KEY: ${{ secrets.JRELEASER_GPG_SECRET_KEY }}
JRELEASER_GPG_PASSPHRASE: ${{ secrets.JRELEASER_GPG_PASSPHRASE }}
JRELEASER_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.JRELEASER_MAVENCENTRAL_SONATYPE_USERNAME }}
JRELEASER_MAVENCENTRAL_TOKEN: ${{ secrets.JRELEASER_MAVENCENTRAL_SONATYPE_TOKEN }}
JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.JRELEASER_GPG_PUBLIC_KEY }}
- name: Release with gradle
uses: gradle/gradle-build-action@3
with:
arguments: --no-daemon -i jreleaserFullRelease
env:
JRELEASER_GPG_SECRET_KEY: ${{ secrets.JRELEASER_GPG_SECRET_KEY }}
JRELEASER_GPG_PASSPHRASE: ${{ secrets.JRELEASER_GPG_PASSPHRASE }}
JRELEASER_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.JRELEASER_MAVENCENTRAL_SONATYPE_USERNAME }}
JRELEASER_MAVENCENTRAL_TOKEN: ${{ secrets.JRELEASER_MAVENCENTRAL_SONATYPE_TOKEN }}
JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.JRELEASER_GPG_PUBLIC_KEY }}Д��лее добавить в Settings → Secrets and Variables → Actions секреты для публикации:
JRELEASER_GPG_PASSPHRASEJRELEASER_GPG_PUBLIC_KEYJRELEASER_GPG_SECRET_KEYJRELEASER_MAVENCENTRAL_SONATYPE_TOKENJRELEASER_MAVENCENTRAL_SONATYPE_USERNAME

Необходимо запушить все изменения в GitHub и попробовать создать тег - я делаю это через создание нового релиза

Если все пройдет хорошо, то в запущенных Actions появиться ваш run и спустя какое-то время вы сможете найти свою публикацию в Maven Central
https://central.sonatype.com/
Troubleshooting
При сборке Gradle ошибка типа
release.github.token must not be blank. Configure a value using the Gradle DSL, or define a System property jreleaser.github.token, or define a JRELEASER_GITHUB_TOKEN environment variable, or define a key/value pair in /Users/lionzxy/.jreleaser/config.toml with a key named JRELEASER_GITHUB_TOKENРешение: Проверьте что вы config файл задан верно или в переменных окружения есть этот параметр
В Central Portal ошибка типа:
Invalid signature for file: slf4j2-timber-0.1.pomРешение: Проверьте что вы подписываете артефакт и загрузили его в keyserver.ubuntu.com
При публикации gpg ключа ошибка:
gpg: keyserver send failed: No route to hostРешение: Выполните команду host keyserver.ubuntu.com чтобы узнать IP адреса сервера Ubuntu:
keyserver.ubuntu.com has address 185.125.188.27
keyserver.ubuntu.com has address 185.125.188.26
keyserver.ubuntu.com has IPv6 address 2620:2d:4000:1007::d43
keyserver.ubuntu.com has IPv6 address 2620:2d:4000:1007::70cЗамените URL одним из IP. Например:
gpg --keyserver 185.125.188.27 --send-keys 1D41D36F7A67A07CВ Central Portal ошибка типа:
Invalid 'md5' checksum for file: slf4j2-timber-0.2-javadoc.jar.ascРешение: Проверьте что вы подписываете ваши файлы с помощью команды ./gradlew jreleaserConfig . Вывод должен быть такой:
deploy:
...
maven:
mavenCentral:
sonatype:
....
sign: true
checksums: true
sourceJar: true
javadocJar: true
...Флаг applyMavenCentralRules = false автоматически отключает подпись. Поэтому подпись нужно включить насильно:
jreleaser {
...
deploy {
maven {
mavenCentral.create("sonatype") {
...
sign = true
checksums = true
sourceJar = true
javadocJar = true
...
}
}
}
}Или подписывать своими силами:
plugins {
id("signing")
}
signing {
val signingSecretKey = System.getenv("JRELEASER_GPG_SECRET_KEY")
val signingPasskey = System.getenv("JRELEASER_GPG_PASSPHRASE")
useInMemoryPgpKeys(signingSecretKey, signingPasskey)
sign(publishing.publications.getByName("release"))
}При сборке jreleaser ошибка типа:
No release provider has been configuredРешение: Вам нужно иметь хотя бы один releaser - проще всего использовать GitHub
jreleaser {
release {
github {
enabled = true
}
}
}При сборке jreleaser ошибка типа:
Could not determine git HEADРешение: Скорее всего вы пытаетесь выполнять конфигурацию jReleaser из сабдиректории. Для исправления этого вам нужно передать специальный флаг - gitRootSearch:
jreleaser {
gitRootSearch = true
}При публикации ошибка:
<description> is not defined in POM. Will use value from parent: Решение: Проверьте что вы корректно задали description в POM файле
Когда я ввожу
./gradlew jreleaserFullReleaseпубликации не происходит
Решение: Убедитесь что в названии вашей версии нет постфикса -SNAPSHOT
При публикации ошибка:
cannot be uploaded to Maven Central due to the following reasons: * <version> can not be -SNAPSHOT.Решение: Если вы уже удалили постфикс -SNAPSHOT, то попробуйте выполнить ./gradlew clean
На любом этапе выполнения операций с gradle plugin jreleaser:
Execution failed for task ':jreleaserFullRelease'.
> Unexpected errorРешение: Проверьте файл build/jreleaser/trace.log на наличие дополнительных ошибок
При публикации в
trace.logjreleaser ошибка 403.
Решение: Проверьте правильность credentials. Попробуйте поменять их местами
12. Version unspecified does not follow the semver spec
Решение: убедитесь что ваша версия следует https://semver.org/
Так же убедитесь что вы задали version внутри build.gradle вашего приложения:
version = "1.1"