hub4j/github-apiを使ってGitHubのAPIを実行してみた

# 概要

下記の記事のように、GitLabのAPIを実行してIssueを起票するツールをJavaで実装して運用していました。ところがいろいろあってGitLabからGitHubに移行しなければならなくなりました。

同じようなことができるライブラリを探していたところ、[hub4j/github-api](https://github.com/hub4j/github-api)を見つけ、これを利用すれば同じことが実現できました。ここでは[hub4j/github-api](https://github.com/hub4j/github-api)を利用した実装について簡単にまとめます。

https://qiita.com/taka194/items/af88459a0b7a3d9f7e85

# 参考ドキュメント

下記のGitHubのリポジトリとリポジトリからリンクされている公式ドキュメントを参考にしました。

https://github.com/hub4j/github-api

https://github-api.kohsuke.org/

# 事前準備

## gradleプロジェクトの準備

下記の環境にて開発を行いました。

```
gradle : 8.10
java   : 21
```

build.gradleは下記になります。利用するライブラリは本記事執筆時点での最新のものになります。

```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`を利用しました。

https://docs.github.com/ja/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens

# 実装

ここからJavaのコードを実装していきます。ここから先に実装するGitHubのAPIを実行する処理はすべて`org.kohsuke.github.GitHub`クラスを介して行います。

今回はこれをラップする`GitHubClient`クラスを実装して進めます。

## 認証

まずはじめに、PATを使ってGitHubへの認証部分を実装します。

```java
public class GitHubClient {

    private GitHub gitHub;

    // tokenは事前に発行しておいたPAT
    public GitHubClient(String token) throws IOException {
        super();
        gitHub = new GitHubBuilder().withOAuthToken(token).build();
    }
}
```

## Repositoryオブジェクトの取得

ここでは特定のリポジトリに対しての操作を行います。そのためにはまずGitHuubクラスを介してリポジトリオブジェクトを取得する必要があります。

```java
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を実装します。

```java
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を起票します。ここでは下記の要素を指定します。

- タイトル
- 説明
- ラベル

```java
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形式の文字列を指定すれば、その通りに起票してくれます。

```java
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の一覧を取得してみます。

```java
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を実行するというのは、多くの場合ツールの一部に組み込まれているものと思います。私の場合は下記のような処理をする社内ツールの一部で利用しています。

1. 定期的にS3にアップロードされるCSVファイルをダウンロードする
1. CSVを解析し、起票すべき情報を抽出する
1. GitHubリポジトリから起票済みのIssueの一覧を取得し、重複チェックする
1. 起票すべきIssueを起票する

GitHubのAPIを実行するのは処理の後半です。動作確認をするたびに処理を最初から実行するのは効率が悪いです。GitHubのAPI実行部分を単独で実行しながら開発したいです。

こういった場合に思いつくのがUnitテストですが、ここで問題が発生します。**PATはシークレットな情報なので、ハードコーディングすることができません**。PATをリポジトリにコミットしてしまうことは情報漏洩につながります。

## NGの例

下記のコードはNGの例です。

```java
@Test
void getIssuesTest() throws Exception {
    String repositoryName = "qiita/sample";
    // PATをハードコーディングするのはNG
    GitHubClient gitHubClient = new GitHubClient("my-personal-access-token");
    gitHubClient.getIssues(repositoryName);
}
```

各開発者がローカルで動作確認するたびにテストコードを書き換え、PATのコーディング部分をコミットしないように気を付けるのもなしではないですが、もう少しスマートにやりたいです。

## 解決策の一例

私は下記のように、PATを環境変数にセットし、それを利用する形がこの問題の解決策のひとつではないかと考えています。

```java
@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を利用して動作確認することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/960294/970a4304-a3c9-6db2-f933-bb81aa9834e7.png)

`EnabledIfEnvironmentVariable`は、環境変数が指定されていなければテストを実行しないようにするアノテーションです。

GitHubの公式より、classicのPATは`ghp_`をプレフィックスとする文字列なので、環境変数の条件として`ghp_`から始まるものという条件を付けています。

https://docs.github.com/ja/authentication/keeping-your-account-and-data-secure/about-authentication-to-github

これにより、各開発者はPATを一切ハードコーディングすることなく、かつ自分のPATを利用して動作確認しながら開発することができます。