Използваме Gradle и Github Actions за публикуване на Java проект в Sonatype Maven Central Repository

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

Реших да напиша тази статия поради липсата на нормален туториал на едно място. Цялата информация трябваше да се събира на парчета от различни източници, и то не съвсем актуални. На всеки, който се интересува, добре дошли под кат.

Създаване на хранилище в Sonatype

На първия етап трябва да създадем хранилище в Sonatype Maven Central. За целта отиваме тук, регистрираме се и създаваме нова заявка с молба да ни създадат хранилище. Въвеждаме нашия GroupId проект, Проектен URL линк към проекта и SCM url линк към системата за контрол на версиите, в която се намира проектът. GroupId тук трябва да е от вида com.example, com.example.domain, com.example.testsupport, а също така може да бъде и под формата на линк към вашия GitHub: github.com/yourusername -> io.github.yourusername. Във всеки случай ще трябва да потвърдите собствеността на този домейн или профил. Ако сте посочили профила на GitHub, ще поискат да създадете публично хранилище с необходимото име.

След известно време след потвърждението вашият GroupId ще бъде създаден и можем да преминем към следващата стъпка, конфигурация на Gradle.

Конфигурираме Gradle

Към момента на написване на статията не намерих плъгини за Gradle, които да помогнат с публикуването на артефакта. Това единственият плъгин, който намерих, обаче авторът отказа да продължи поддръжката му. Затова реших да направя всичко сам, тъй като не е твърде сложно.

Първото, което трябва да изясните, е изискванията на Sonatype за публикуване. Те са следните:

  • Наличие на изходен код и JavaDoc, т.е. трябва да присъстват -sources.jar и-javadoc.jar файлове. Както е посочено в документацията, ако не е възможно да предоставите изходен код или документация, можете да направите празно -sources.jar или -javadoc.jar с прост 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 'Някакво описание на артефакта'
            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 'Лиценз на Apache, версия 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@dev.ru'
                }
            }
        }
    }
}

След това трябва да посочим, че при изграждането ще се генерират -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 'Небольшое описание артефакта'
            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 'Лицензия Apache, версия 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@dev.bg'
                }
            }
        }
    }
}

Искам да подчертая, че версията я получаваме от променливата на средата: System.getenv('RELEASE_VERSION'). Ще я зададем по време на компилация и ще я вземем от името на тага.

Генериране на PGP ключ

Едно от изискванията на Sonatype е подписването на всички файлове с GPG/PGP ключ. За целта отиваме тук и сваляме утилитата GnuPG за операционната си система.

  • Генерираме ключова двойка: gpg --gen-key, въвеждаме име на потребителя, e-mail, както и задаваме парола.
  • Разучаваме id нашия ключ с командата: gpg --list-secret-keys --keyid-format short. Идентфикаторът ще бъде посочен след наклонената черта, например: 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 и тест, също така и при създаване на пулл реквестове.

В секцията 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: Настройка на JDK 8
        uses: actions/setup-java@v1
        with:
          java-version: 8

      - name: Подготовка за публикуване
        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: Публикуване с 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 файла. Всичко това е направено в раздела Подготовка за публикуване. Получаваме нашия ключ от променливата 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, работният процес успешно завърши, можем да влезем в Sonatype Nexus за да се уверим в това:

Използваме Gradle и Github Actions за публикуване на Java проект в Sonatype Maven Central Repository

Артефакт се появява в Staging репозитория. Веднага той е с статус Open, след което трябва ръчно да бъде преведен в статус Close, като натиснете съответния бутон. След проверка на изпълнението на всички изисквания, артефактът преминава в статус Close и повече не е достъпен за промяна. В такъв вид той ще попадне в MavenCentral. Ако всичко е наред, можете да натиснете бутона Release, като в този случай артефактът ще попадне в репозитория Sonatype.

За да попадне артефактът в MavenCentral, трябва да поискате това в задачата, която създадохме в самото начало. Това трябва да се направи само един път, тъй като публикуваме за първи път. При следващите пъти не е необходимо, всичко ще се синхронизира автоматично. Синхронизацията ми беше включена бързо, но за да стане артефактът достъпен в MavenCentral, минаха около 5 дни.

С това приключваме, публикувахме нашият артефакт в MavenCentral.

Полезни връзки

  • Подобна статия, само че публикуването е през maven
  • Staging репозиториум Sonatype
  • Jira Sonatype, в който трябва да създадете задача
  • Пример репозитория, където всичко е настроено

Източник: habr.com

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster