「厳密なテストは書きたくないけど微妙なルール」ってあるじゃないですか?
たとえば私はAndroidアプリをよく作ってますが、
- Fragmentにコンストラクタが実装されてない場合は気づけるようにしたい
- Activityのクラス名は必ず「〜〜Activity」、Fragmentのクラス名は必ず「〜〜Fragment」であってほしい
のように、「守って無くてもビルドは通るけど、守ってもらわないと困る」てきなやつです。
Circle CIとかで、そういうチェックスクリプトを走るよう記述すればぶっちゃけ十分なんですが、
「CircleCIつかえます( ー`дー´)キリッ」っていうよりも「CircleCIとは別に自前でCI建てれます」っていうほうが格好がつくので、今回は自前でCIを建ててみました。
まえおき:GitHubのPull Requestレビューの仕組み
こんなかんじで、WebHookを受けて、StatusをPOSTするだけの単純な仕組みで動いているみたいです。
Python+Bottle on Heroku でオレオレCIを実装する
WebHookを受ける
#どんなJSONが飛んできているかはrequestbin で観察しながら実装しました。
from bottle import Bottle, run, request
import os
app = Bottle()
@app.post('/event_handler')
def handle_event():
payload = request.json
event_name = request.get_header('X-GitHub-Event')
if event_name == 'pull_request':
if payload['action'] == 'opened' or payload['action'] == 'reopened':
return handle_pull_request_created(payload['pull_request'])
elif payload['action'] == 'synchronize':
return handle_pull_request_updated(payload['pull_request'])
return 'dame'
def handle_pull_request_created(pull_request):
return 'ok: created'
def handle_pull_request_updated(pull_request):
return 'ok: updated'
run(app, host='0.0.0.0', port=os.environ['PORT'])
これを適当にHerokuにデプロイして、テストしたいソースコードのリポジトリにWebHook設定を追加すれば、Pull Request追加時に handle_pull_request_created
が実行されるようになります。
セキュリティのため、WebHookのSecretをちゃんと考慮する
request.get_header('X-Hub-Signature')
で sha1=xxxxxxxxxxxxx
みたいな文字列が取れるはずです。
リクエストボディをSHA1ハッシュとったものらしいので、
import hashlib
import hmac
digest = hmac.new(b'hogehoge123', request.body.read(), hashlib.sha1).hexdigest()
if "sha1="+digest == request.get_header('X-Hub-Signature'):
# 正しいリクエスト
else:
# 不正なリクエスト
のようなコードをかけば、検証ができます。 (hogehoge123
がWebHook設定のときに指定したシークレット)
いろいろチェックをして、ステータスPOSTのAPIをたたく
from rq import Queue
from worker import conn
from reviewer import review
import json
def handle_pull_request_created(pull_request):
Queue(connection=conn).enqueue(review, json.dumps(pull_request))
return 'ok: created'
from time import sleep
import json
import requests
import os
def review(pull_request_json):
pull_request = json.loads(pull_request_json)
repo_name = pull_request['base']['repo']['full_name']
sha = pull_request['head']['sha']
set_github_status(repo_name, sha, 'pending', 'now checking OREORE')
# ここで Pull Requestのチェックをいろいろやる
check_coding_style(pull_request)
another_check(pull_request)
set_github_status(repo_name, sha, 'success', 'OREORE: success')
def set_github_status(repo_name, sha, status, description):
data = {
'state': status,
'context': 'continuous-integration/oreore_ci',
'description': description
}
url = 'https://api.github.com/repos/{repo_name}/statuses/{sha}'.format(repo_name=repo_name, sha=sha)
requests.post(url, data=json.dumps(data), headers={'Authorization': 'token {token}'.format(token=os.environ['GITHUB_ACCESS_TOKEN'])})
def check_coding_style(pull_request):
sleep(3)
def another_check(pull_request):
sleep(10)
Redis Queueを使って、非同期実行するようにしてるので、 Queue(・・・).enqueue(review, ・・・)
みたいなことを書いています。
(Herokuだとこのへん に事細かに手順が書かれていて、かなり参考になります)
GitHubのAPI用トークンは、本来はOAuthとかで取るべきなんですが、今回は面倒なので、Private Access Tokenを取ってきて環境変数に入れる形にしてあります。
これで、適当なWebHook設定済みのリポジトリにPull Requestを出すと・・・
↓13秒後
キタ━━━━(゚∀゚)━━━━!!
ってなります。
まとめ
とりあえず、オレオレCIの大枠は作れました。
https://github.com/YusukeIwaki/oreore_ci にトライアルのコードを置いておきます。
今回は構造理解が目的なので、Python+Bottleのような超ライトなCIサーバにしましたが、
ちゃんとやるならRailsのAPIモードとかを使ったほうがいいかと思います。