Выкарыстоўваны Gradle і Github Actions для публікацыі Java праекта ў Sonatype Maven Central Repository

У дадзеным артыкуле я жадаю падрабязна разгледзець працэс публікацыі з нуля Java артэфакта праз Github Actions у Sonatype Maven Central Repository выкарыстоўваючы зборшчык Gradle.

Гэты артыкул вырашыў напісаць з прычыны адсутнасці нармальнага тутарыялу ў адным месцы. Усю інфармацыю даводзілася збіраць па кавалках з розных крыніц, пры тым не зусім свежых. Каму цікава, сардэчна запрашаем пад кат.

Стварэнне рэпазітара ў Sonatype

Першым этапам нам трэба стварыць рэпазітар у Sonatype Maven Central. Для гэтага ідзем сюды, рэгіструемся і ствараем новую задачу, з просьбай стварыць нам рэпазітар. Забіваем свой GroupId праекту, Project 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, працоўны працэс паспяхова адпрацуе, мы можам зайсці ў Sonatype Nexus каб у гэтым пераканацца:

Выкарыстоўваны Gradle і Github Actions для публікацыі Java праекта ў Sonatype Maven Central Repository

Артэфакт з'явіўся ў Staging рэпазітары. Адразу ён з'яўляецца ў статуце Open, далей яго неабходна ўручную перавесці ў статут Close, націснуўшы адпаведную кнопку. Пасля праверкі выканання ўсіх патрабаванняў, артэфакт пераходзіць у статут Close і больш не даступны для змены. У такім выглядзе ён патрапіць у MavenCentral. Калі ўсё добра, можна націснуць кнопку Адпусціце, пры гэтым артэфакт патрапіць у рэпазітар Sonatype.

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

На гэтым усё, мы апублікавалі наш артэфакт у MavenCentral.

Карысныя спасылкі

  • Падобная артыкул, толькі публікацыя праз maven
  • Пастаноўка рэпазітар Санатып
  • Jira Sonatype, у якой неабходна стварыць задачу
  • Прыклад рэпазітара, дзе гэта ўсё наладжана

Крыніца: habr.com