Використовуємо Gradle та Github Actions для публікації Java проекту у Sonatype Maven Central Repository

У цій статті я хочу докладно розглянути процес публікації з нуля Java артефакту через Github Actions у Sonatype Maven Central Repository, використовуючи збирач Gradle.

Цю статтю вирішив написати через відсутність нормального туторіалу в одному місці. Всю інформацію доводилося збирати по шматках із різних джерел, причому не зовсім свіжих. Кому цікаво, ласкаво просимо під кат.

Створення репозиторію в Sonatype

Першим етапом нам потрібно створити репозиторій у Sonatype Maven Central. Для цього йдемо сюди, реєструємось та створюємо нове завдання, з проханням створити нам репозиторій. Вбиваємо свій GroupId проекту, URL-адреса проекту посилання на проект та SCM url посилання систему контролю версій, у якій проект лежить. GroupId тут має бути виду com.example, com.example.domain, com.example.testsupport, а також може бути у вигляді посилання на ваш гітхаб: github.com/yourusername -> io.github.yourusername. У будь-якому випадку вам потрібно буде підтвердити володіння даним доменом або профілем. Якщо ви зазначили профіль гітхаба, попросять створити публічний репозиторій із потрібним ім'ям.

Через деякий час після підтвердження вашої GroupId буде створено і ми можемо перейти до наступного кроку, конфігурації Gradle.

Конфігуруємо Gradle

На момент написання статті я не знайшов плагінів для Gradle, які б могли допомогти з публікацією артефакту. Це єдиний плагін, який я знайшов, але автор відмовився від його подальшої підтримки. Тому я вирішив зробити все самостійно, благо зробити це не надто важко.

Перше, що потрібно з'ясувати, це Sonatype вимоги для публікації. Вони такі:

  • Наявність вихідних кодів та JavaDoc, тобто. повинні бути присутніми -sources.jar и-javadoc.jar файли. Як сказано в документації, якщо ні можливо надати вихідні коди або документацію, можна зробити пустушку -sources.jar або -javadoc.jar c простим README усередині, щоб пройти перевірку.
  • Усі файли мають бути підписані за допомогою GPG/PGP, І .asc файл, який містить підпис, має бути включений для кожного файлу.
  • Наявність pom файлу
  • Коректні значення groupId, artifactId и version. Версія може бути довільним рядком і не може закінчуватись -SNAPSHOT
  • Необхідна присутність name, description и url
  • Присутність інформації про ліцензію, розробників та систему контролю версій

Це основні правила, які мають бути дотримані під час публікації. Повна інформація доступна тут.

Реалізуємо ці вимоги build.gradle файл. Для початку додамо всю необхідну інформацію про розробників, ліцензію, систему контролю версій, а також задамо url, ім'я та опис проекту. Для цього напишемо простий метод:

def customizePom(pom) {
    pom.withXml {
        def root = asNode()

        root.dependencies.removeAll { dep ->
            dep.scope == "test"
        }

        root.children().last() + {
            resolveStrategy = DELEGATE_FIRST

            description 'Some description of artifact'
            name 'Artifct name'
            url 'https://github.com/login/projectname'
            organization {
                name 'com.github.login'
                url 'https://github.com/login'
            }
            issueManagement {
                system 'GitHub'
                url 'https://github.com/login/projectname/issues'
            }
            licenses {
                license {
                    name 'The Apache License, Version 2.0'
                    url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
                }
            }
            scm {
                url 'https://github.com/login/projectname'
                connection 'scm:https://github.com/login/projectname.git'
                developerConnection 'scm:git://github.com/login/projectname.git'
            }
            developers {
                developer {
                    id 'dev'
                    name 'DevName'
                    email '[email protected]'
                }
            }
        }
    }
}

Далі потрібно вказати, щоб при збиранні згенерувалися -sources.jar и-javadoc.jar файли. Для цього до секції java потрібно додати таке:

java {
    withJavadocJar()
    withSourcesJar()
}

Перейдемо до останньої вимоги, налаштування GPG/PGP підпису. Для цього підключимо плагін signing:

plugins {
    id 'signing'
}

І додамо секцію:

signing {
    sign publishing.publications
}

Зрештою, додамо секцію publishing:

publishing {
    publications {
        mavenJava(MavenPublication) {
            customizePom(pom)
            groupId group
            artifactId archivesBaseName
            version version

            from components.java
        }
    }
    repositories {
        maven {
            url "https://oss.sonatype.org/service/local/staging/deploy/maven2"
            credentials {
                username sonatypeUsername
                password sonatypePassword
            }
        }
    }
}

Тут sonatypeUsername и sonatypePassword змінні, що містять логін та пароль, створені при реєстрації на sonatype.org.

Таким чином, фінальний build.gradle буде виглядати наступним чином:

Повний код build.gradle

plugins {
    id 'java'
    id 'maven-publish'
    id 'signing'
}

java {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
    withJavadocJar()
    withSourcesJar()
}

group 'io.github.githublogin'
archivesBaseName = 'projectname'
version = System.getenv('RELEASE_VERSION') ?: "0.0.1"

repositories {
    mavenCentral()
}

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.5.2'
}

test {
    useJUnitPlatform()
}

jar {
    from sourceSets.main.output
    from sourceSets.main.allJava
}

signing {
    sign publishing.publications
}

publishing {
    publications {
        mavenJava(MavenPublication) {
            customizePom(pom)
            groupId group
            artifactId archivesBaseName
            version version

            from components.java
        }
    }
    repositories {
        maven {
            url "https://oss.sonatype.org/service/local/staging/deploy/maven2"
            credentials {
                username sonatypeUsername
                password sonatypePassword
            }
        }
    }
}

def customizePom(pom) {
    pom.withXml {
        def root = asNode()

        root.dependencies.removeAll { dep ->
            dep.scope == "test"
        }

        root.children().last() + {
            resolveStrategy = DELEGATE_FIRST

            description 'Some description of artifact'
            name 'Artifct name'
            url 'https://github.com/login/projectname'
            organization {
                name 'com.github.login'
                url 'https://github.com/githublogin'
            }
            issueManagement {
                system 'GitHub'
                url 'https://github.com/githublogin/projectname/issues'
            }
            licenses {
                license {
                    name 'The Apache License, Version 2.0'
                    url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
                }
            }
            scm {
                url 'https://github.com/githublogin/projectname'
                connection 'scm:https://github.com/githublogin/projectname.git'
                developerConnection 'scm:git://github.com/githublogin/projectname.git'
            }
            developers {
                developer {
                    id 'dev'
                    name 'DevName'
                    email '[email protected]'
                }
            }
        }
    }
}

Хочу зауважити, що версію ми отримуємо зі змінного середовища: System.getenv('RELEASE_VERSION'). Виставляти її ми будемо при складанні та брати з імені тега.

Генерація ключа PGP

Однією з вимог Sonatype є підписання всіх файлів за допомогою ключа GPG/PGP. Для цього йдемо сюди та качаємо утиліту GnuPG під свою операційну систему.

  • Генеруємо ключову пару: gpg --gen-key, вводимо ім'я користувача, e-mail, а також задаємо пароль.
  • З'ясовуємо id нашого ключа командою: gpg --list-secret-keys --keyid-format short. Id буде вказано після слішу, наприклад: rsa2048/9B695056
  • Публікуємо публічний ключ на сервер https://keys.openpgp.org командою: gpg --keyserver [https://keys.openpgp.org](https://keys.openpgp.org/) --send-keys 9B695056
  • Експортуємо секретний ключ у довільне місце, він нам знадобиться надалі: gpg --export-secret-key 9B695056 > D:\gpg\9B695056.gpg

Налаштовуємо Github Actions

Перейдемо до завершального етапу, налаштуємо збірку та авто публікацію, використовуючи Github Actions.
Github Actions – функціонал, що дозволяє автоматизувати робочий процес, реалізувавши повний цикл CI/CD. Складання, тестування та деплою можуть бути викликані різними подіями: пушинг коду, створення релізу або issues. Цей функціонал абсолютно безкоштовний для громадських репозиторіїв.

У цьому розділі я покажу як налаштувати збірку і пуше коду і деплою в Sonatype репозиторій під час випуску релізу, а також налаштування секретів.

Задаємо секрети

Для автоматичного складання та деплою нам знадобиться ряд секретних значень, таких як id ключа, пароль, який ми вводили при генерації ключа, безпосередньо сам PGP ключ, а також логін/пароль Sonatype. Задати їх можна у спеціальному розділі у налаштуваннях репозиторію:

Використовуємо Gradle та Github Actions для публікації Java проекту у Sonatype Maven Central Repository

Задаємо наступні змінні:

  • SONATYPE_USERNAME/SONATYPE_PASSWORD — логін/пароль, який ми вводили під час реєстрації у Sonatype
  • SIGNING_KEYID/SIGNING_PASSWORD — ID PGP ключа та пароль, встановлений під час генерації.

На змінній GPG_KEY_CONTENTS хочу зупинитися детальніше. Справа в тому, що для публікації нам необхідний закритий ключ PGP. Для того, щоб розмістити його в секретах, я скористався інструкцією і додатково зробив низку дій.

  • Зашифруємо наш ключ за допомогою gpg: gpg --symmetric --cipher-algo AES256 9B695056.gpg, ввівши пароль. Його слід помістити у змінну: SECRET_PASSPHRASE
  • Перекладемо отриманий зашифрований ключ у текстову форму за допомогою base64: base64 9B695056.gpg.gpg > 9B695056.txt. Вміст розмістимо у змінній: GPG_KEY_CONTENTS.

Налаштування складання при пуші коду та створення PR

Для початку потрібно створити папку в корені вашого проекту: .github/workflows.

У ній розмітити файл, наприклад, gradle-ci-build.yml з наступним вмістом:

name: build

on:
  push:
    branches:
      - master
      - dev
      - testing
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      - name: Set up JDK 8
        uses: actions/setup-java@v1
        with:
          java-version: 8

      - name: Build with Gradle
        uses: eskatos/gradle-command-action@v1
        with:
          gradle-version: current
          arguments: build -PsonatypeUsername=${{secrets.SONATYPE_USERNAME}} -PsonatypePassword=${{secrets.SONATYPE_PASSWORD}}

Цей робочий процес буде виконуватися при пуші в гілки master, dev и testing, також під час створення пулл реквестов.

У секції jobs вказані кроки, які мають виконатись за вказаними подіями. У даному випадку збирати ми будемо на останній версії ubuntu, використовувати Java 8, а також використовувати плагін для Gradle eskatos/gradle-command-action@v1, який використовуючи останню версію збирача запустить команди, вказані в arguments. Змінні secrets.SONATYPE_USERNAME и secrets.SONATYPE_PASSWORD це секрети, які ми поставили раніше.

Результати складання будуть відображені у вкладці Actions:

Використовуємо Gradle та Github Actions для публікації Java проекту у Sonatype Maven Central Repository

Автодеплой під час випуску нового релізу

Для автодеплою створимо окремий файл робочого процесу gradle-ci-publish.yml:

name: publish

on:
  push:
    tags:
      - 'v*'

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      - name: Set up JDK 8
        uses: actions/setup-java@v1
        with:
          java-version: 8

      - name: Prepare to publish
        run: |
          echo '${{secrets.GPG_KEY_CONTENTS}}' | base64 -d > publish_key.gpg
          gpg --quiet --batch --yes --decrypt --passphrase="${{secrets.SECRET_PASSPHRASE}}" 
          --output secret.gpg publish_key.gpg
          echo "::set-env name=RELEASE_VERSION::${GITHUB_REF:11}"

      - name: Publish with Gradle
        uses: eskatos/gradle-command-action@v1
        with:
          gradle-version: current
          arguments: test publish -Psigning.secretKeyRingFile=secret.gpg -Psigning.keyId=${{secrets.SIGNING_KEYID}} -Psigning.password=${{secrets.SIGNING_PASSWORD}} -PsonatypeUsername=${{secrets.SONATYPE_USERNAME}} -PsonatypePassword=${{secrets.SONATYPE_PASSWORD}}

Файл практично ідентичний попередньому за винятком події, за якої він спрацьовуватиме. У цьому випадку ця подія створення тега з ім'ям, що починається на v.

Перед деплоєм нам потрібно витягнути PGP ключ із секретів та розмістити його в корені проекту, а також розшифрувати його. Далі нам потрібно виставити спеціальне змінне середовище RELEASE_VERSION до якої ми звертаємось у gradle.build файл. Все це зроблено у розділі Prepare to publish. Ми отримуємо наш ключ зі змінної GPG_KEY_CONTENTS, переводимо його в файл gpg, потім розшифровуємо його, поміщаючи у файл secret.gpg.

Далі ми звертаємось до спеціальної змінної GITHUB_REF, з якої можемо дістати версію, яку ми поставили під час створення тега. Ця змінна у разі має значення refs/tags/v0.0.2 з якої ми відрізаємо перші 11 символів, щоб дістати версію. Далі стандартно використовуємо команди Gradle для публікації: test publish

Перевірка результатів деплою в Sonatype репозиторій

Після створення релізу повинен запустити робочий процес, описаний у попередньому розділі. Для цього створюємо реліз:

Використовуємо Gradle та Github Actions для публікації Java проекту у Sonatype Maven Central Repository

у своїй ім'я тега має починатися з v. Якщо після натискання Publish release робочий процес успішно відпрацює, ми можемо зайти в Сонатип Nexus щоб у цьому переконатися:

Використовуємо Gradle та Github Actions для публікації Java проекту у Sonatype Maven Central Repository

Артефакт виник у Staging репозиторії. Відразу він з'являється у статусі Open, далі його необхідно вручну перевести у статус Close, натиснувши відповідну кнопку. Після перевірки виконання всіх вимог артефакт переходить у статус Close і більше не доступний для зміни. У такому вигляді він потрапить до MavenCentral. Якщо все гаразд, можна натиснути кнопку ВідпустітьПри цьому артефакт потрапить до репозиторію Sonatype.

Для того, щоб артефакт потрапив до MavenCentral, потрібно попросити про це завдання, яке ми створили на самому початку. Зробити це потрібно лише один раз, тому ми публікуємо вперше. У наступні рази це робити не потрібно, все синхронізуватиметься автоматично. Включили мені синхронізацію швидко, але щоб артефакт став доступним у MavenCentral пройшло близько 5 днів.

На цьому все, ми опублікували наш артефакт у MavenCentral.

Корисні посилання

  • схожа стаття, тільки публікація через maven
  • Інсценування репозиторій Сонатип
  • Jira Sonatype, в якій необхідно створити завдання
  • Приклад репозиторія, де все це налаштовано

Джерело: habr.com