GitHub App を作ってPythonからAPIを叩いてみた

GitHubで組織内のプライベートリポジトリに対してAPIを通じてIssueを立てたいと思います。方法はいくつかありますが、ここでは GitHub App を作る方法を扱います。さらにその GitHub App に対してPythonからAPIを呼び出す方法についても記載します

----

なお、この記事はタスク管理にGitHubを使う一連の記事の中の一つです。

- [情シスのタスクをGitHubのissueで管理するようにしたら捗った](https://qiita.com/itkr/items/1ae5578c1ca761dacf88)
- [プロジェクト管理ツールとしてGitHubを"普通に"使う](https://qiita.com/itkr/items/0d4c0015da28827b2bb7)
- [Google Cloud Functions (Python) を使ってみた](https://qiita.com/itkr/items/d3f8b4f8ff9becf101b4)
- **GitHub App を作ってPythonからAPIを叩いてみた** ←ここ
- [Google Apps Script から Google Cloud Functions のHTTPトリガーを実行してみた](https://qiita.com/itkr/items/6a515c5cf8c9a18f1880)

## GitHub Apps を使う理由

プライベートでAPIを叩く分には個人のアカウントに紐づけたトークンを取得すれば簡単にAPIを使用できると思いますが、組織で使う場合は担当者がやめてしまったりすると困ります。しかし GitHub Apps ならOrganizationに紐づくAppを作成し、使用したいリポジトリにインストールしてAPIを呼び出すことができます。またApp自体をプライベートにできるので組織内でのみ使うAppを作成できます。

まとめると↓のような感じです。

- 個人のアカウントのキーだと自分がやめたときに困る
- Organizationに紐付けることができる
- プライベートなアプリにすることができる
- インストール可能なリポジトリを管理者が決めることができる

## GitHub Apps でできること

GitHub Apps を作成すると例えば以下のようなことができます

- Webhookを利用してGitHubで何かアクションをしたときに特定のエンドポイントを叩く
- GitHub Apps の認証情報を利用してGitHubのAPIを叩く

つまり

- GitHubのアクション→外部のアクション
- 外部のアクション→GitHubのアクション

ということができます。
※ 今回の投稿で行ったのはGitHubのAPIを叩く方です

### OAuth Apps との違い

選択肢として GitHub Apps の他に OAuth Apps も作成できます。違いは↓がわかりやすかったです。
https://developer.github.com/apps/about-apps/#determining-which-integration-to-build

## APIを叩く準備

実際に GitHub App でIssueを作成してそれをProjectに追加するAppを作りました。
※ Organizationに紐づくAppを作成したときの手順です。

### 許可する動作を決定する

OrganizationのSettings → GitHub Apps
https://github.com/organizations/[:organization]/settings/apps

個人の場合はSettings → Developer settings → GitHub Apps
https://github.com/settings/apps

そこから New GitHub App を押します。各種設定とアクセス許可の設定が行えるので必要な項目の入力します。今回のAppではIssuesと Repository projects に Read & Write の権限を与えました。

### インストールする

Appの設定画面から Install App で許可するリポジトリを設定したりインストールしたりできます。

### 認証情報の取得

AppからAPIを叩くための認証情報を取得します。

#### App ID の取得

Appの設定画面のAbout → App ID (何桁かの数字)を取得します。

#### 秘密鍵を取得する

Appの設定画面から Private keys → Generate a private key で鍵を作成・取得します。

#### Apps ID と Instration ID を取得する

ここはわかりにくかったのですが、↓から取得しました。もしかしたら別の方法があるのかもしれないです。

Appの設定画面 → Advanced → Recent Deliveries → ... → Payload → `"installation"` → `"id"`

## 実装

ここからはPythonによる簡易的な実装を書いていきます。処理の流れとしては次のようになっています。

1. 取得した App ID と 秘密鍵によって Json Web Token (以下JWT)を作成
1. JWTと Instration ID を使ってトークンを取得
1. トークンを使ってAPIを叩く
    1. Issueの作成
    1. IssueをProjectに追加(=ProjectCardを作成)

なお、ここに書いたコードはQiitaに書くために書いたのできちんと動作検証できていないです。サンプルコードは [itkr/github-apps-python](https://github.com/itkr/github-apps-python) に書いていこうと思います。

### JWTによる認証

```sh
pip install pyjwt requests
```

```python
import jwt
import json
import requests
```


例えばこのような感じでJWTを作成します。

```python
PEM = '[pem key]'


def generate_jwt():
    utcnow = datetime.utcnow()
    alg = 'RS256'
    payload = {
        'typ': 'JWT',
        'alg': alg,
        'iat': utcnow,
        'exp': utcnow + timedelta(seconds=30),
        'iss': self._app_id,
    }
    return jwt.encode(payload, PEM, algorithm=alg).decode('utf-8')
```

そして、Installation ID を取得するためにこのJWTをヘッダーに入れて Instration API を叩きます。

```python
INSTALLATION_ID = '[Installation ID]'


def get_headers():
    jwt = generage_jwt()
    return {
        'Authorization': 'Bearer {}'.format(jwt),
        'Accept': 'application/vnd.github.machine-man-preview+json',
    }


def get_token():
    url = 'https://api.github.com/installations/{}/access_tokens'.format(
        str(INSTALLATION_ID))
    response = requests.post(url, headers=get_headers())
    return json.loads(response.text).get('token')
```

### Issueを立てる

上記の `get_token()` でようやくトークンが取得できたので、それを使ってIssueのAPIなどを叩くことができるようになりました。

```python
ORGANIZATION = '[organizaion name]'
REPOSITORY = '[repository name]'


_token = get_token()


def get_token_header():
    return {
        'Authorization': 'token {}'.format(_token),
        'Accept': 'application/vnd.github.inertia-preview+json',
    }


def create_issue(title='title', body='body', assignees=[], labels=[]):
    url = 'https://api.github.com/repos/{owner}/{repo}/issues'.format(
        owner=ORGANIZATION, repo=REPOSITORY)
    payload = json.dumps({
        'title':title,
        'body':body,
        'assignees': assignees,
        'labels': labels,
    })
    return requests.post(url, payload, headers=get_token_header()).text
```

### Issueをプロジェクトに追加する

Issueの作成ができたので、作成したIssueをProjectに追加します。その際に、Projectの Column ID が必要になります。Columnとは、Projectを[Automated kanban](https://qiita.com/itkr/items/0d4c0015da28827b2bb7#project-%E3%81%AB-to-doin-progressdone-%E3%82%92%E4%BD%9C%E3%82%8B)のテンプレートでできる「To do」などのことです。APIで取得するか、カラムの「…」から「Copy column link」を押すなどして取得できます。

```python
COLUMN_ID = '[column ID]'


def set_project_card(content_id, content_type='Issue'):
    url = 'https://api.github.com/projects/columns/{column_id}/cards'.format(
        column_id=COLUMN_ID)
    payload = json.dumps({
        'content_type': content_type,
        'content_id': content_id,
    })
    return requests.post(url, payload, headers=get_token_header()).text


def main():
    issue = create_issue(title='title', body='body', assignees=[], labels=[])
    set_project_card(issue['id'])
```