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