Англоязычная версия статьи на 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"