概要
下記の記事のように、GitLabのAPIを実行してIssueを起票するツールをJavaで実装して運用していました。ところがいろいろあってGitLabからGitHubに移行しなければならなくなりました。
同じようなことができるライブラリを探していたところ、hub4j/github-apiを見つけ、これを利用すれば同じことが実現できました。ここではhub4j/github-apiを利用した実装について簡単にまとめます。
参考ドキュメント
下記のGitHubのリポジトリとリポジトリからリンクされている公式ドキュメントを参考にしました。
事前準備
gradleプロジェクトの準備
下記の環境にて開発を行いました。
gradle : 8.10
java : 21
build.gradleは下記になります。利用するライブラリは本記事執筆時点での最新のものになります。
apply plugin: "java"
repositories {
mavenCentral()
}
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
compileJava.options.encoding = 'UTF-8'
compileTestJava.options.encoding = 'UTF-8'
test {
useJUnitPlatform()
}
dependencies {
// hub4j/github-api
implementation "org.kohsuke:github-api:2.0.0-alpha-2"
// for unit test
testImplementation "org.junit.jupiter:junit-jupiter:5.11.3"
testImplementation "org.junit.jupiter:junit-jupiter-api:5.11.3"
testImplementation "org.hamcrest:hamcrest-core:3.0-rc1"
testImplementation "org.mockito:mockito-core:5.14.2"
testImplementation "org.mockito:mockito-inline:5.2.0"
testImplementation "org.mockito:mockito-junit-jupiter:5.14.2"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.11.3"
testRuntimeOnly "org.junit.platform:junit-platform-launcher:1.9.3"
}
PATの準備
下記を参考に、Personal Access Token(PAT)を発行します。トークンにはFine-grained
とclassic
がありますが、ここではclassic
を利用しました。
実装
ここからJavaのコードを実装していきます。ここから先に実装するGitHubのAPIを実行する処理はすべてorg.kohsuke.github.GitHub
クラスを介して行います。
今回はこれをラップするGitHubClient
クラスを実装して進めます。
認証
まずはじめに、PATを使ってGitHubへの認証部分を実装します。
public class GitHubClient {
private GitHub gitHub;
// tokenは事前に発行しておいたPAT
public GitHubClient(String token) throws IOException {
super();
gitHub = new GitHubBuilder().withOAuthToken(token).build();
}
}
Repositoryオブジェクトの取得
ここでは特定のリポジトリに対しての操作を行います。そのためにはまずGitHuubクラスを介してリポジトリオブジェクトを取得する必要があります。
private GHRepository getRepository(String repositoryName) throws IOException {
return gitHub.getRepository(repositoryName);
}
引数のrepositoryNameはGitHubRepositoryのowner/name
の文字列を渡します。
例えばhttps://github.com/qiita/sample
というリポジトリの場合はqiita/sample
を指定します。
Issueの一覧の取得
指定のGitHubのリポジトリに作成されたIssueの一覧を取得するAPIを実装します。
public List<GHIssue> getIssues(String repositoryName) throws IOException {
GHRepository repository = getRepository(repositoryName);
List<GHIssue> result = repository.getIssues(GHIssueState.ALL);
result.stream().forEach(issue -> {
System.out.println(issue.getNumber());// issueの番号
System.out.println(issue.getTitle());// issueのタイトル
System.out.println(issue.getBody());// issueの本文
issue.getLabels().forEach(e -> {
System.out.println(e.getName());// issueのラベル
});
});
return result;
}
引数にGHIssueState.ALL
を指定しているので、すべてのIssueを取得しています。
Issueの作成
指定のGitHubのリポジトリにIssueを起票します。ここでは下記の要素を指定します。
- タイトル
- 説明
- ラベル
public GHIssue createIssue(String repositoryName, String title, String body, String label) throws IOException {
GHRepository repository = getRepository(repositoryName);
GHIssue createdIssue = repository.createIssue(title).body(body).label(label).create();
System.out.println(createdIssue.getNumber());// issueの番号
System.out.println(createdIssue.getTitle());// issueのタイトル
System.out.println(createdIssue.getBody());// issueの本文
createdIssue.getLabels().forEach(e -> {
System.out.println(e.getName());// issueのラベル
});
return createdIssue;
}
このAPIはIssueの起票に成功すると、起票したIssueのオブジェクトを返してくれます。
Issueの説明は、下記のようにMarkdown形式の文字列を指定すれば、その通りに起票してくれます。
String getBody() {
StringBuilder sb = new StringBuilder();
sb.append("# Overview");
sb.append(System.lineSeparator());
sb.append(System.lineSeparator());
sb.append("|COLUMN1|COLUMN2|COLUMN3|");
sb.append(System.lineSeparator());
sb.append("|---|---|---|");
sb.append(System.lineSeparator());
sb.append("|hoge|fuga|piyo|");
sb.append(System.lineSeparator());
sb.append("|foo|bar|baz|");
return sb.toString();
}
Branchの一覧を取得する
指定のGitHubのリポジトリに存在するBranchの一覧を取得してみます。
public Map<String, GHBranch> getBranches(String repositoryName) throws IOException {
GHRepository repository = getRepository(repositoryName);
Map<String, GHBranch> result = repository.getBranches();
repository.getBranches().forEach((branchName, branchObj) -> {
System.out.println(branchName);// ブランチ名
});
return result;
}
このAPIはBranch名をキー、BranchオブジェクトをバリューとしたMapを返します。
UNITテストを使って簡単に動作確認しながら開発する
GitHubにIssueを起票するAPIを実行するというのは、多くの場合ツールの一部に組み込まれているものと思います。私の場合は下記のような処理をする社内ツールの一部で利用しています。
- 定期的にS3にアップロードされるCSVファイルをダウンロードする
- CSVを解析し、起票すべき情報を抽出する
- GitHubリポジトリから起票済みのIssueの一覧を取得し、重複チェックする
- 起票すべきIssueを起票する
GitHubのAPIを実行するのは処理の後半です。動作確認をするたびに処理を最初から実行するのは効率が悪いです。GitHubのAPI実行部分を単独で実行しながら開発したいです。
こういった場合に思いつくのがUnitテストですが、ここで問題が発生します。PATはシークレットな情報なので、ハードコーディングすることができません。PATをリポジトリにコミットしてしまうことは情報漏洩につながります。
NGの例
下記のコードはNGの例です。
@Test
void getIssuesTest() throws Exception {
String repositoryName = "qiita/sample";
// PATをハードコーディングするのはNG
GitHubClient gitHubClient = new GitHubClient("my-personal-access-token");
gitHubClient.getIssues(repositoryName);
}
各開発者がローカルで動作確認するたびにテストコードを書き換え、PATのコーディング部分をコミットしないように気を付けるのもなしではないですが、もう少しスマートにやりたいです。
解決策の一例
私は下記のように、PATを環境変数にセットし、それを利用する形がこの問題の解決策のひとつではないかと考えています。
@Test
@EnabledIfEnvironmentVariable(named = "GITHUB_PAT", matches = "ghp_.*")
void getIssuesTest() throws Exception {
String repositoryName = "qiita/sample";
GitHubClient gitHubClient = new GitHubClient(System.getenv("GITHUB_PAT"));
gitHubClient.getIssues(repositoryName);
}
環境変数は、Unitテストの実行構成の環境変数にセットします。こうすれば、PATをハードコーディングすることなく、各開発者が自分のPATを利用して動作確認することができます。
EnabledIfEnvironmentVariable
は、環境変数が指定されていなければテストを実行しないようにするアノテーションです。
GitHubの公式より、classicのPATはghp_
をプレフィックスとする文字列なので、環境変数の条件としてghp_
から始まるものという条件を付けています。
これにより、各開発者はPATを一切ハードコーディングすることなく、かつ自分のPATを利用して動作確認しながら開発することができます。