GitHubで組織内のプライベートリポジトリに対してAPIを通じてIssueを立てたいと思います。方法はいくつかありますが、ここでは GitHub App を作る方法を扱います。さらにその GitHub App に対してPythonからAPIを呼び出す方法についても記載します
なお、この記事はタスク管理にGitHubを使う一連の記事の中の一つです。
- 情シスのタスクをGitHubのissueで管理するようにしたら捗った
- プロジェクト管理ツールとしてGitHubを"普通に"使う
- Google Cloud Functions (Python) を使ってみた
- GitHub App を作ってPythonからAPIを叩いてみた ←ここ
- Google Apps Script から Google Cloud Functions のHTTPトリガーを実行してみた
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による簡易的な実装を書いていきます。処理の流れとしては次のようになっています。
- 取得した App ID と 秘密鍵によって Json Web Token (以下JWT)を作成
- JWTと Instration ID を使ってトークンを取得
- トークンを使ってAPIを叩く
- Issueの作成
- IssueをProjectに追加(=ProjectCardを作成)
なお、ここに書いたコードはQiitaに書くために書いたのできちんと動作検証できていないです。サンプルコードは itkr/github-apps-python に書いていこうと思います。
JWTによる認証
pip install pyjwt requests
import jwt
import json
import requests
例えばこのような感じでJWTを作成します。
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 を叩きます。
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などを叩くことができるようになりました。
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のテンプレートでできる「To do」などのことです。APIで取得するか、カラムの「…」から「Copy column link」を押すなどして取得できます。
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'])