Как публиковать библиотеку в Maven Central Portal в 2024 году

от автора

Англоязычная версия статьи на Medium

Начиная с 12 марта 2024 года регистрация на OSSRH портале теперь недоступна. Большинство существующих туториалов в интернете описывают как раз опыт публикации через OSSRH на Maven Central из-за чего после марта 2024 года эти инструкции стали не актуальны для публикации проектов новых авторов.

Disclaimer: я не смогу провести вас по этому процессу шаг за шагом, потому что разные проекты работают по разному. Ниже — не пошаговая инструкция, а руководство к действию. Вдумчиво выполняйте этапы публикации и не забывайте про секцию Troubleshoting в конце статьи

Процесс публикации можно разбить на следующие шаги:

  1. Регистрация на Central Portal и верификация namespace

  2. Создания GPG ключа для подписи артефактов

  3. Локальная публикация проекта для теста

  4. Подключение JReleaser к проекту и публикация локально

  5. Настройка 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 ключа для подписи артефактов

Прежде чем приступать к загрузке артефакта необходимо его подписать своим ключем. Чтобы каждый, кто скачивает вашу библиотеку, мог быть уверен что это вы

  1. Устанавливаем утилиту GPG

На MacOS с помощью brew: Введите в терминале brew install gnupg

На Linux с помощью apt-get: Введите в терминале sudo apt-get install gnupg

  1. Генерируем PGP ключ:

В терминале вводим и затем заполняем все требуемые поля:

gpg --generate-key

Запомните или запишите passphrase, который использовали при настройке! Не используйте пустой!

  1. Публикуем наш публичный ключ

Невероятно важно опубликовать публичный ключ на один из поддерживаемых 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-репозиторий находится в:

  1. Windows: C:\Users\<User_Name>\.m2

  2. Linux: /home/<User_Name>/.m2

  3. 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 к проекту и публикация локально

  1. Добавляем JReleaser плагин к проекту. Инструкцию как это сделать можно найти тут

  2. Извлекаем публичный и приватный ключ из хранилища. Замените BF81E10590D4EBF590D00F911D41D36F7A67A07C на свой ID ключа:

gpg --output public.pgp --armor --export BF81E10590D4EBF590D00F911D41D36F7A67A07C gpg --output private.pgp --armor --export-secret-key BF81E10590D4EBF590D00F911D41D36F7A67A07C

Эти команды создадут два файла — public.pgp и private.pgp

  1. Генерируем токены доступа к Central Portal для публикации на странице аккаунта по кнопке “Generate User Token”:

  1. Сгенерируем токен для GitHub. Это нужно для публикации релиза в GitHub. Если вы не хотите этого, пропустите этот шаг. Токен генерируется по этой ссылке: https://github.com/settings/tokens/new

Для публикации релиза нужен доступ на запись

  1. Добавляем конфиг 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-----""" 
  1. Добавьте в проект минимальную конфигурацию jReleaser:

jreleaser {     release {         github {         skipRelease = true         skipTag = true         }     } }
  1. Запустите ./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
  1. Добавьте блок signing для подписи в Central Portal:

jreleaser { ...     signing {         active = Active.ALWAYS         armored = true         verify = true     } }

Проверьте что подпись успешна с помощью команды: ./gradlew jreleaserSign

  1. Добавьте как минимум одного автора и год публикации. Это нужно для генерации лицензии jReleser плагином:

jreleaser {     project {         inceptionYear = "2024"         author("@LionZXY")     } ... }
  1. Добавляем публикацию в 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

  1. При сборке 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 файл задан верно или в переменных окружения есть этот параметр

  1. В Central Portal ошибка типа:

Invalid signature for file: slf4j2-timber-0.1.pom

Решение: Проверьте что вы подписываете артефакт и загрузили его в keyserver.ubuntu.com

  1. При публикации 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
  1. В 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")) }
  1. При сборке jreleaser ошибка типа:

No release provider has been configured

Решение: Вам нужно иметь хотя бы один releaser — проще всего использовать GitHub

jreleaser {     release {         github {        enabled = true         }     } }
  1. При сборке jreleaser ошибка типа:

Could not determine git HEAD

Решение: Скорее всего вы пытаетесь выполнять конфигурацию jReleaser из сабдиректории. Для исправления этого вам нужно передать специальный флаг — gitRootSearch:

jreleaser {     gitRootSearch = true }
  1. При публикации ошибка:

<description> is not defined in POM. Will use value from parent: 

Решение: Проверьте что вы корректно задали description в POM файле

  1. Когда я ввожу ./gradlew jreleaserFullRelease публикации не происходит

Решение: Убедитесь что в названии вашей версии нет постфикса -SNAPSHOT

  1. При публикации ошибка:

cannot be uploaded to Maven Central due to the following reasons: * <version> can not be -SNAPSHOT.

Решение: Если вы уже удалили постфикс -SNAPSHOT, то попробуйте выполнить ./gradlew clean

  1. На любом этапе выполнения операций с gradle plugin jreleaser:

Execution failed for task ':jreleaserFullRelease'. > Unexpected error

Решение: Проверьте файл build/jreleaser/trace.log на наличие дополнительных ошибок

  1. При публикации в trace.log jreleaser ошибка 403.

Решение: Проверьте правильность credentials. Попробуйте поменять их местами
12) Version unspecified does not follow the semver spec
Решение: убедитесь что ваша версия следует https://semver.org/
Так же убедитесь что вы задали version внутри build.gradle вашего приложения:

version = "1.1"


ссылка на оригинал статьи https://habr.com/ru/articles/830630/