7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

GitHub ActionsAdvent Calendar 2022

Day 10

GradleでGitHub PackagesにMavenレポジトリを作ってGitHub Actionが利用した話

Last updated at Posted at 2022-12-06

要旨

GitHub Packages registryにMavenレポジトリを作り、自作プロジェクト二つがMavenレポジトリを介してjarファイルを受け渡しする構成を作った。プログラミング言語はJava、ビルドツールはGradle。事例として報告します。コード一式をGitHubにあげて公開しました。

解決したい問題

Java言語でビルドツールGradle使ったプロジェクトを2つ自作した。以下で、片方を「libraryプロジェクト」、もう一つを「consumerプロジェクト」と略記する。libraryプロジェクトの成果物としてjarファイルを生成し、consumerプロジェクトがそのjarファイルを参照する、という形だ。素材としたのはTom Gregoryの記事 "How to use Gradle api vs. implementation dependencies"のサンプルコードほぼそのまま。

libraryプロジェクトはjarを生成して自PCのローカルディスク上にあるMavenレポジトリのローカルキャッシュ(mavenLocalともいう)に保存する。具体的には次のようなコマンドを実行する。

$ cd gradle-java-library-plugin-library
$ gradle publishToMavenLocal

BUILD SUCCESSFUL in 3s
5 actionable tasks: 5 executed

すると下記のようにjarファイルが出力された。

$ tree ~/.m2/repository/com/tomgregory/
/Users/kazurayam/.m2/repository/com/tomgregory/
└── gradle-java-library-plugin-library
    ├── 0.0.1-SNAPSHOT
    │   ├── gradle-java-library-plugin-library-0.0.1-SNAPSHOT.jar
    │   ├── gradle-java-library-plugin-library-0.0.1-SNAPSHOT.module
    │   ├── gradle-java-library-plugin-library-0.0.1-SNAPSHOT.pom
    │   └── maven-metadata-local.xml
    └── maven-metadata-local.xml

2 directories, 5 files

いっぽう consumerプロジェクトはmavenLocalにアクセスしてlibraryプロジェクトのjarを参照する。consumerプロジェクトのbuild.gradleにこう書いてある。

repositories {
  mavenCentral()
  mavenLocal()
}
dependencies {
  implementation group: 'com.tomgregory', name: 'gradle-java-library-plugin-library', version: '0.0.1-SNAPSHOT'
}

consumerプロジェクトのtestタスクを実行すると成功した。つまりlibraryプロジェクトのjarファイルをconsumerプロジェクトが参照することができている。

$ cd gradle-java-library-plugin-consumer
$ gradle test
Starting a Gradle Daemon (subsequent builds will be faster)

BUILD SUCCESSFUL in 9s
1 actionable task: 1 executed

二つのプロジェクトは下図のような関係にある。

diagram1

さて、わたしは某日、GitHub Actionsを使って自動化テストを実践してみようと思った。CI環境つまりリモートマシン上で gradle test コマンドを実行する。具体的にはconsumerプロジェクトに下記ファイルを追加した。

consumerプロジェクトのdevelopブランチをGitHubへpushした。するとGitHub Actionsのワークフローによりリモートのサーバー上で gradle testコマンド が実行された。おっと、失敗した。

consumer_test_on_CI_failed

なぜ失敗したのか?CI環境で起動されたconsumerプロジェクトのビルドがlibraryプロジェクトのjarを得ようとしてmavenLocalレポジトリを参照したが、CIマシン上のmavenLocalは空っぽなので、libraryプロジェクトのjarが見つからなかったからだ。この様子を図にすると次の通り。

diagram2

解決の選択肢の一つとして、Maven Centralレポジトリにlibraryプロジェクトのjarをpublishするという方法も考えられる。しかしこの方法は採用できない。第一の理由はMaven Centralは表舞台であり稽古場ではないから。Maven Centralレポジトリには不特定多数のユーザがアクセスする。自分が真剣に作ったプロジェクトを彼らに提供したいと望むならMaven Centralに上げるのが良い。しかし個人的学習のために作ったゴミプロジェクトをMaven Centralに上げるべきではない。第二の理由は長時間待たされるから。Maven Centralに向けて成果物をpublishする操作をわたしが実行すると即座にMaven Centrolの公式サイトにjarファイルが掲載されるわけではない。どういう理由かは知らないが三・四日も待たされる。仕上がったソフトウェアをお披露目する段階ならいいけれど、開発作業の途中にこんなに待たされるのはたまらない。

結局わたしはMaven Centralでもなくローカルキャッシュでもない第三のMavenレポジトリをインターネット上に作り、そこにlibraryプロジェクトのjarをあげたい。そしてconsumerプロジェクトが第三のMavenレポジトリを参照するという形をとりたい。どうすればいいか?

解決方法

GitHub Packagesを使って自作のJavaプロジェクトのためにMavenレポジトリをGradleで構築することができる。下記に公式ドキュメントがある。

だがそもそもGitHub Packagesって何?どこにあるモノのか?

自分のアカウントでGitHubにログインするとタブが並んでいる。Overview | Repositories | Projects | Packages。この中の Packages タブをクリックすると現時点でわたしが持っているGitHub Packagesの一覧が見られる。こんなふうに。

tabs

わたしのPackagesはどれもゴミなので詳細を伏せ字にしました。

GitHubが提供するサービス群の中でPackagesRepositories と同じレベルに位置付けられている。このことを理解しておきましょう。

次に個別のレポジトリに目を向けましょう。libraryプロジェクトのレポジトリ gradle-java-library-plugin-libraryを開いて、右サイドバーのちょっと下のほうを見ると Packages というタイトルが見つかる。No packages publishedと表示されている。

No packages published

GitHubのレポジトリをPackage群とを画面で結びつけている。参照するのに便利なようにしている。大多数のGitHubレポジトリはリンクするPackageを持たない。少数ながら1個のPackageとリンクしているレポジトリがある。1個のレポジトリが2つ以上のPackagesにリンクを貼ることができるのだが、実用している例は多分多くないだろう。。

gradle-java-library-plugin-libraryレポジトリのトップ画面のここに Packages 1 と表示されるようにしたい。これがわたしの達成目標だ。

説明

下記のような構成を実現しようと思う。

diagram3

これを実現するために次の三つの仕組みを実装する。

  1. libraryプロジェクトにおいて開発者は2つのレポジトリに向けてjarをpublishする。第一にMavenローカルキャッシュ。第二にGitHub Packages Registryに構築したMavenレポジトリ。これを以下で「GPRレポジトリ」と略記する。
  2. consumerプロジェクトのbuild.gradleは2つのレポジトリを探ってlibraryプロジェクトのjarを発見する。第一にMavenローカルキャッシュ。第二にGPRレポジトリ。指定したnameとversionに合致するjarが見つかりさえすればよい。どちらのレポジトリからでもかまわない。
  3. GradleがGPRレポジトリにアクセスしようとAPIをcallすると、GitHubは Personal Access Token による認証要求に応じることを求める。 (以下、 PAT と略記)。libraryプロジェクトにおいてGradleがGPRレポジトリに向けてjarをpublishするとき、実行時パラメータとして適切なUSERNAMEとKEYを宣言する必要がある。またconsumerプロジェクトにおいてGradleがGPRレポジトリを参照する時にも適切なUSERNAMEとKEYを宣言する必要がある。だから準備としてPATを作り、PATをGradleに教える必要がある。

公式ドキュメントを読みながらわたしがやったことを以下にメモする。

Personal Access Tokenを作る

Personal Access Token(以下、PATと略記する)の作り方についてはGitHubによる公式ドキュメントを参照願います。わたしはPATを作って下記のように指定した。

PAT

write:packagesをONしておく必要があるだろう。

PATをGradleプロパティとして登録する

自分のPC上の ~/.gradle/gradle.properties ファイルを下記のように手作業で修正した。

# Username and Key for GitHub Package
gpr.user=kazurayam
gpr.key=ghp_************************************

******のところに実際には皆さん各自のPersonal Access Tokenの値をここに書くべし。これで gpr.usergpr.key という名前のGradleプロパティが自分のPC上で動くすべてのGradleプロジェクトにおいて参照可能になる。だからbuild.gradleファイルの中に次のように書いて gradle buildを実行すればプロパティの値がprintされる。

println "gpr.user=" + project.findProperty('gpr.user')
println "gpr.key =" + project.findProperty('gpr.key')

こんなふうに

$ gradle build

> Configure project :
gpr.user=kazurayam
gpr.key =ghp_************************************

BUILD SUCCESSFUL in 1s
2 actionable tasks: 2 executed

libraryプロジェクトのbuild.gradleを修正する

libraryプロジェクトのbuild.gradleファイルを修正して、GPRレポジトリに向けてjarファイルをpublishできるようにした。

まず maven-publish プラグインを採用する。

plugins {
    ...
    id "maven-publish"
}

maven-publish プラグインを採用すると publishing クロージャーが使えるようになる。 publishing クロージャーにMavenレポジトリのnameとurlと認証情報を記述する。

publishing {
    repositories {
        maven {
            name = "gpr"
            url = uri("https://maven.pkg.github.com/kazurayam/gradle-java-library-plugin-library")
            credentials {
                username = project.findProperty("gpr.user") ?: System.getenv("GPR_USERNAME")
                password = project.findProperty("gpr.key") ?: System.getenv("GPR_TOKEN")
            }
        }
    }
    publications {
        mylib(MavenPublication) {
            from components.java
        }
    }
}

この記述が何をしているかというと次の二つだ。

  1. mylib という名前のpublicationを宣言している
  2. gpr という名前のrepositoryを宣言している

なおgprレポジトリに関する記述の中で

  1. urlの中にプロジェクトのownerつまり GitHubアカウント名プロジェクト名 が含まれる。

  2. Personal Access Tokenの値をusernameとpasswordパラメータとして指定している

なおここで指定するURLには注意を要する。 URLの中の プロジェクト名 の部分に別のプロジェクトの名前をうっかり書いてしまうかもしれない。別のプロジェクトのbuild.gradleファイルから repository の記述をコピペすると、URLの中の プロジェクト名 を書き換えるのをうっかり忘れてしまうかもしれない。実際わたしはこのミスをやらかした。publishコマンドを実行した時にエラーは発生しなかった。ただし本来あるべきGitHubレポジトリではなくて、間違えた別レポジトリの中にlibraryプロジェクトのPackageが作られてしまった。こういうミスをやらかしたせいで、あとで Received status code 422 from server: Unprocessable Entity というエラーメッセージに遭遇した。原因が何なのか分からなくて困った。

libraryプロジェクトでpublishコマンドを実行する

libraryプロジェクトのjarをgprレポジトリにpublishするタスクを実行した。

:~/github/gradle-java-library-plugin-library (master *)

$ gradle publishMylibPublicationToGprRepository

BUILD SUCCESSFUL in 19s
5 actionable tasks: 3 executed, 2 up-to-date

タスクの名前 publishMylibPublicationToGprRepository は build.gradleの中の publishing の記述に基づいてmaven-publishプラグインが規則的に導き出したものだ。つまり

publish + publication名 + PublicationTo + repositoryのname + Repository

がタスク名になる。ちなみに

$ gradle tasks --all

とコマンドを実行すれば全てのタスク名が列挙される。maven-publishプラグインによってどんな名前のタスクが追加されたのかを確かめることができる。

タスクを実行して成功した。libraryプロジェクトのGitHubトップページを見たら、あら目出度や、Packages 1と表示された。Packageを作ることに成功した。

one_package

consumerプロジェクトのbuild.gradleを修正する

さて、consumerプロジェクトのbuild.gradleファイルを修正して、gprレポジトリを参照するように設定しよう。

repositories {
    mavenCentral()
    maven {
        url = uri("https://maven.pkg.github.com/kazurayam/gradle-java-library-plugin-library")
        credentials {
            username = project.findProperty("gpr.user") ?: System.getenv("GPR_USERNAME")
            password = project.findProperty("gpr.key") ?: System.getenv("GPR_TOKEN")
        }
    }
    mavenLocal()
}

こんなふうにbuild.gradleファイルを修正してgit addしてgit commitしてgit pushした。GitHub Actionsで自動化テストが起動された。

ところがテストが失敗した。こういうメッセージが出力された。

> Task :compileJava FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':compileJava'.
> Could not resolve all files for configuration ':compileClasspath'.
   > Could not resolve com.tomgregory:gradle-java-library-plugin-library:0.0.1.
     Required by:
         project :
      > Could not resolve com.tomgregory:gradle-java-library-plugin-library:0.0.1.
         > Could not get resource 'https://maven.pkg.github.com/kazurayam/gradle-java-library-plugin-library/com/tomgregory/gradle-java-library-plugin-library/0.0.1/gradle-java-library-plugin-library-0.0.1.pom'.
            > Username must not be null!

Username must not be null!というメッセージから次のように推測することができる。GitHub Actionsによってconsumerプロジェクトのbuild.gradleが動いた時にGradleプロパティ gpr.usergpr.key が無いのだ。GitHub ActionsのワークフローがなんとかしてGradleプロパティ gpr.usergpr.key を作って値を設定したい。どうすればいいか?

CI環境に ~/.gradle/gradle.properties ファイルを復元する

consumerプロジェクトの .github/workflows/tests.ymlファイルに次のような行を挿入した。

      - name: Restore gradle.properties
        env:
          GP: ${{ secrets.GRADLE_PROPERTIES }}
        shell: bash
        run: |
          mkdir -p ~/.gradle/
          echo "${GP}" > ~/.gradle/gradle.properties

このコードは ${{ secrets.GRADLE_PROPERTIES }} が次のような文字列を返すことを期待している。

gpr.user=kazurayam
gpr.key=ghp_************************************

これはわたしが自分のPCで ~/.gradle/gradle.properties ファイルに書き込んだ2行と同じ。つまりGradleプロパティ gpr.usergpr.key を宣言するための記述だ。

ワークフローに挿入したbashスクリプトはCIマシン上に ~/.gradle/gradle.propertiesファイルを新しく作る。その中に 変数参照 ${{ secrets.GRADLE_PROPERTIES }} の内容をそのまま書き込む。つまりローカル環境で参照していたgradle.properitesファイルと同じ物をCI環境に再現するのだ。gradle.propertiesファイルがCI環境で再現されれば gradle testタスクが動いた時、ローカル環境と同じように、Gradleプロパティ gpr.usergpr.key が参照可能になり、Username must not be null!のエラーが解消するはずだ。

echoコマンドにご注意を

ここで注意をひとつ。ワークフロー定義の中で echo "${GRALDE_PROPERTIES}" のように二重引用符合 " " で囲っている。この" "を忘れると ~/.gradle/gradle.properties ファイルの内容がこうなってしまう。

gpr.user=kazurayam gpr.key=ghp_************************************

あちゃー。echoコマンドが$GPの内容文字列の中にある改行を空白に置換してしまうのだ。こうなるとGradleプロパティ gpr.usergpr.key の値はおかしくなってしまう。そのせいで consumerプロジェクトのCIがlibraryプロジェクトのMavenレポジトリを参照しようとした時 401 Unauthorized エラーが発生してしまう。

Personal Access Tokenの値をGitHub Actions Secretsに格納してActionsワークフローに渡す

さて大詰めです。consumerプロジェクトのGitHub Actionワークフローは ${{ secrets.GRADLE_PROPERTIES }} という記号を使って下記の文字列が参照できることを仮定しています。

gpr.user=kazurayam
gpr.key=ghp_************************************

では secretes.GRADLE_PROPERTIES をどうやって作り出せばいいのでしょうか?

(答) GitHub Actions Secretsを使え

  1. 自分のアカウントでGitHubにログインして、Secretを設定する対象となるレポジトリのページを開く。
  2. Settingsタブをクリック
  3. 左サイドメニューの中で Secrets > Actions をクリック
  4. New repository secretボタンを押す

すると /settings/secrets/secrets/actionsページ に着地した。ここでわたしは 新しいsecret GRADLE_PROPERTIES を作った。その内容は前に述べた2行だ。

Actions-secretes

secrets.GRADLE_PROPERTIESを設定したあと、consumerプロジェクトのworkflowを動かした。テストがpassした。めでたしメデタシ。

結論

GitHub Docs / Working with a GitHub Packages registry /Working with the Gradle registryを参考にしながら、自作の libraryプロジェクト のためにGitHub PackagesにMavenレポジトリを作った。ローカルPC上で gradle publishMylibPublicationToGprRepositlry コマンドを手動で実行することにより、GitHub Packages上のMaveレポジトリにjarファイルをpublishすることができた。また自作の consumerプロジェクトにGitHub Actionsワークフローによる自動化テストを実装し、libraryプロジェクトのMavenレポジトリを参照することに成功した。Packages上のレポジトリにはPersonal Access Tokenによる認証が必須だが、それを通過するための課題をクリアすることができた。

"GitHub Packages"というキーワードでネットを検索するとたくさん記事が見つかる。しかし今回わたしが試した方法を解説してくれる記事は見当たらなかった。Npm、Dockerコンテナの話が多くてMavenレポジトリの話がそもそも少ない。ビルドツールGradleを使う事例はもっと少ない。またGitHub ActionsでPackageを作る処理を自動化した事例を紹介する記事が多くて、本記事のように手動で "gradle publishXXXXX” コマンドを実行するローテクな記事は皆無だった。

こんなやり方もあるんですよ。

7
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?