Как публиковать библиотеку в Maven Central Portal в 2024 году
Англоязычная версия статьи на 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.myusername
Gitlab:
io.gitlab.myusername
Gitee:
io.gitee.myusername
Bitbucket:
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>\.m2
Linux:
/home/<User_Name>/.m2
Mac:
/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_PASSPHRASE
JRELEASER_GPG_PUBLIC_KEY
JRELEASER_GPG_SECRET_KEY
JRELEASER_MAVENCENTRAL_SONATYPE_TOKEN
JRELEASER_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.log
jreleaser ошибка 403.
Решение: Проверьте правильность credentials. Попробуйте поменять их местами
12. Version unspecified does not follow the semver spec
Решение: убедитесь что ваша версия следует https://semver.org/
Так же убедитесь что вы задали version
внутри build.gradle
вашего приложения:
version = "1.1"