jCenterで公開していたKotlinライブラリをMaven Centralで公開する

**[Into the Sunset on May 1st: Bintray, JCenter, GoCenter, and ChartCenter](https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/)**

jCenterが閉鎖するってことなので、jCenterで公開していたKotlinのライブラリをMaven Centralに登録することにしました。やり方はわかりやすく解説している記事がたくさんありますが、Kotlin特有のところで詰まったりしたので自分なりにまとめてみようかと思います。

やり方はいくつかあるらしいのですが、Sonatype経由で登録するのが簡単らしいです。

作業に当たり以下の記事を参考にさせていただきました。 :pray:

- [Maven Central Repository への公開手順(Gradle版)](https://blog1.mammb.com/entry/2020/02/19/090000)

ほとんど同じ内容なのとこっちの方が丁寧な説明をされているので、こちらを参照されるのが良いかと思います。

また、実際に対応したコミットの一例はこちらになります
https://github.com/ohmae/color-chooser/commit/d88c909b309b96bb32588d544705eade1cb69c3d

# 1. sonatype にサインアップする

プロジェクト作成はsonatypeのJIRAで依頼することになります。まずはここにサインアップします。

https://issues.sonatype.org/secure/Signup!default.jspa

特に難しくないと思いますが、以下のようにパスワードの条件が結構厳しい点に注意ですね。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/8c3d50db-76a5-0343-2376-c3455c730b85.png)

サインアップ完了後はアバターの設定や言語設定を行います。アバターはなしでもOK、言語設定は日本語を選択可能です。

# 2. プロジェクトを作成する

後悔したいライブラリのプロジェクトを作成します。GroupIDごとにプロジェクトが必要です。同一のGroupIDであれば、一つのプロジェクトで複数のライブラリをホスティングできます。
JIRAで依頼します。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/8619459b-004a-815a-9ff6-b5661f4e6eb4.png)

画面上部の「作成」を押すと「課題の作成」ができます。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/4b2d9ac9-5b17-ee21-38f2-f828016d034a.png)

こんな感じで設定します。特に難しく考える必要は無いですね。英語も必要最低限でOKです。

|項目|内容|
|---|---|
|プロジェクト|Community Support - Open Source Project Repository Hosting (OSSRH) を選択|
|課題タイプ|New Project を選択|
|要約|プロジェクト名を入力、問い合わせのタイトルという認識でOK|
|説明|プロジェクトの説明を入力、未入力でも良いかは分からないけど、中身を読んで何かを判断される分けでもなさそうで、GithubのREADME一行目とかの簡単な説明程度で問題無かった|
|添付ファイル|不要|
|Group Id|ライブラリのGroupIdを指定、パッケージ名と同様にドメイン名を逆順にしたものをベースに命名しますが、後でドメインの所有者であることの確認がある。io.github.<username>みたいなGithubのアカウントベースの名前なら確認なしOKらしい|
|Project URL|GithubのURL|
|SCM url|GithubのリポジトリURL、SSHではだめ、HTTPSのURLを指定|
|Username(s)|自分以外にプロジェクトの管理者を指定したい場合に指定、一人のプロジェクトなら空で良い|
|Already Synced to Central|No|

課題を作成して数分で以下のコメントがつきます。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/1b8a4234-8e62-ca8e-bfe9-74397afc5352.png)

ドメインの所有権の確認です。DNSの設定ができるのならTXTレコードにチケット名を設定するのが早いらしいですね。
私の場合はValue DomainのDNSを使っているので以下のように設定しました。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/771a11a7-381d-bda9-5b1d-03df4f23d3bd.png)

設定できたら、設定したよーとコメントをします。
すると、10分程度で準備ができたコメントが来ます。

![2021-02-07_15h25_25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/a7e46bda-affe-c55f-6a4d-2c76caa854e0.png)

なお、チケット作成後すぐにTXTレコードの設定を終わらせておくと確認フェーススキップしていきなり準備完了の連絡が来ます。DNSの反映に時間がかかるので間に合った場合はですが。

書かれたとおり、
https://oss.sonatype.org/service/local/staging/deploy/maven2
にアクセスすると
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/7e4a64f3-47fc-8043-dbe2-a0ac0293338a.png)
となり戸惑いますが、JIRAと同じアカウント名/パスワードでログインすればアクセスできます。
https://oss.sonatype.org/
で良いかと思います。右上からログインできます。

Stating Profilesで作成された各プロジェクトを確認できます。
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/4a30b635-a39d-e62f-4f1b-8d72248f6bd6.png)

# 3. GPG署名の準備

GnuPGをインストールします。
Linuxならapt/yumなど、Macならhomebrew、Windowsならhttps://www.gnupg.org/download/ からダウンロードしましょう。

```
gpg --full-gen-key
```

- キータイプ: RSA and RSA (default)
- キー長さ: 4096
- 期限: 0 (期限なし)

で作成しました。

```
gpg --list-keys
C:/Users/xxxxxx/AppData/Roaming/gnupg/pubring.kbx
--------------------------------------------------
pub   rsa4096 2021-02-06 [SC]
      XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
uid           [  究極  ] OHMAE Ryosuke <XXXX@mm2d.net>
sub   rsa4096 2021-02-06 [E]
```

鍵サーバに登録（Xでぼかしてますが上記で確認した鍵IDを指定）

```
gpg --send-keys XXXXXXX
```

秘密鍵のエクスポート、Gradleのsingingプラグインで署名するためエクスポートしておく必要があります。

```
gpg --export-secret-keys -o .gnupg/secring.gpg
```

鍵を作成したときのパスワードを聞かれます。
これで、Windowsなら `C:\Users\<ユーザ名>\.gnupg\secring.gpg` にエクスポートされます。

signingプラグインで鍵を指定するためには、ショート形式の鍵IDを指定する必要がある（フルで指定するとエラーになる）

```
gpg --list-secret-keys --keyid-format short
sec   rsa4096/XXXXXXXX 2021-02-06 [SC]
```
`rsa4096/`の後ろの8文字がそのIDです。これを覚えておきましょう。


# 4.Gradleの設定を行う

署名用にsingingプラグイン、パッケージ作成とアップロード用にmaven-publishプラグインを利用します。
jCenterへアップロードしていたのであればmaven-publishプラグインは使っていたと思います。
また、maven centralで公開するにはjavadoc.jarを含める必要があります。
Kotlinの場合はdokkaプラグインを使ってKDocからjavadocを作成します。

```kotlin
plugins {
...
    `maven-publish`
    signing
    id("org.jetbrains.dokka")
...
}
```

javadoc.jarとsources.jarを作成するタスクを作成（なければ）

```kotlin
tasks.named<DokkaTask>("dokkaJavadoc") {
    outputDirectory.set(File(buildDir, "docs/javadoc"))
}

tasks.create("javadocJar", Jar::class) {
    dependsOn("dokkaJavadoc")
    archiveClassifier.set("javadoc")
    from(File(buildDir, "docs/javadoc"))
}

tasks.create("sourcesJar", Jar::class) {
    dependsOn("classes")
    archiveClassifier.set("sources")
    from(sourceSets["main"].allSource)
}
```


gradleのグローバル設定にgpg鍵とsonatypeのアカウント設定を書いておく、
Windowsなら `C:\Users\<ユーザ名>\.gradle\gralde.properties` です。
平文なのでこういう情報を置くのはあまり良くないかもですが、以下のように設定しておきます

```
signing.keyId=XXXXXXXX
signing.password=XXXXXXXX
signing.secretKeyRingFile=C:\\Users\\<ユーザ名>\\.gnupg\\secring.gpg

sonatype_username=
sonatype_password=
```

publishタスクの設定、この辺もbintrayと特に変わりませんが、maven centralではpomファイルへ記述が必須の項目が決まっているため、それらを設定する必要があります。
不足しているとNexus repository managerでcloseする際にエラーになります。エラーメッセージを見て何が不足しているかを確認して対応しましょう。私の場合、project name、project description、project url、developerが不足していると出て対応しました。

```kotlin
publishing {
    publications {
        create<MavenPublication>("bintray") {
        create<MavenPublication>("mavenJava") {
            artifact("$buildDir/outputs/aar/${base.archivesBaseName}-release.aar")
            artifact(tasks["sourcesJar"])
            artifact(tasks["javadocJar"])
            groupId = ProjectProperties.groupId
            artifactId = base.archivesBaseName
            version = ProjectProperties.versionName
            pom.withXml {
                val node = asNode()
                node.appendNode("name", ProjectProperties.name)
                node.appendNode("description", ProjectProperties.description)
                node.appendNode("url", ProjectProperties.Url.site)
                node.appendNode("licenses").appendNode("license").apply {
                    appendNode("name", "The MIT License")
                    appendNode("url", "https://opensource.org/licenses/MIT")
                    appendNode("distribution", "repo")
                }
                node.appendNode("developers").appendNode("developer").apply {
                    appendNode("id", ProjectProperties.developerId)
                    appendNode("name", ProjectProperties.developerName)
                }
                node.appendNode("scm").apply {
                    appendNode("connection", ProjectProperties.Url.scm)
                    appendNode("developerConnection", ProjectProperties.Url.scm)
                    appendNode("url", ProjectProperties.Url.github)
                }
                val dependencies = node.appendNode("dependencies")
                configurations.api.get().dependencies.forEach {
                    appendDependency(
                        dependencies,
                        groupId = it.group ?: "",
                        artifactId = it.name,
                        version = it.version ?: "",
                        scope = "compile"
                    )
                }
                configurations.implementation.get().dependencies.forEach {
                    appendDependency(
                        dependencies,
                        groupId = it.group ?: "",
                        artifactId = it.name,
                        version = it.version ?: "",
                        scope = "runtime"
                    )
                }
            }
        }
    }
    repositories {
        maven {
            url = URI("https://oss.sonatype.org/service/local/staging/deploy/maven2")
            credentials {
                username = project.findProperty("sonatype_username") as? String ?: ""
                password = project.findProperty("sonatype_password") as? String ?: ""
            }
        }
    }
    signing {
        sign(publishing.publications["mavenJava"])
    }
}
private fun appendDependency(
    parentNode: Node,
    groupId: String,
    artifactId: String,
    version: String,
    scope: String
) {
    parentNode.appendNode("dependency").apply {
        appendNode("groupId", groupId)
        appendNode("artifactId", artifactId)
        appendNode("version", version)
        appendNode("scope", scope)
    }
}
```

ProjectPropertiesは定数を集めた自作クラスなので、それぞれ必要に応じて設定してください。

```kotlin
object ProjectProperties {
    const val groupId: String = "xxx.xxxxx.xxxxxx"
    const val name: String = "xxxxx"
    const val description: String = "xxxxxxx"
    const val developerId: String = "xxxx"
    const val developerName: String = "xxxx"

    private const val versionMajor: Int = 0
    private const val versionMinor: Int = 0
    private const val versionPatch: Int = 1
    const val versionName: String = "$versionMajor.$versionMinor.$versionPatch"

    object Url {
        const val site: String = "https://github.com/xxxx/xxxx"
        const val github: String = "https://github.com/xxxx/xxxx"
        const val scm: String = "scm:git@github.com:xxxx/xxxx.git"
    }
}
```

# 5.アップロードと公開

前項の設定を行っていればpublishタスクを実行すればアップロードまで行われます。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/d4f7e6bc-1503-9ba4-4ecf-d59678043eb4.png)

repository managerにはstaging状態で表示されます。
Closeを実行するとパッケージ内容のチェックが行われ、Releaseができるようになります。
Releaseを実行すると公開されます。

これで完了ですが、初回リリースでは最初のJIRAにコメントしておきます。これでmaven centralに同期が行われ、パッケージは10分ほどで反映されます、検索できるようになるまでには2時間ぐらいかかるらしいです。

以上です。