メールでGitHubに新規issueを追加する


メールの受け口にAmazon SESを使うようリファクタリングしました [2020/04/26] → 記事はこちら

はじめに

メールしか使えない環境からGitHubに新規issueを追加できるようになると便利、というニッチな個人的ニーズがあり、やってみました。

方針

まず、メールで何らかのプログラムをキックする方法を考えます。自由に使えるドメインとメールサーバがあれば何でもできそうですが、そうでない場合、メール受信を起点とするWEBサービスを作る選択肢ってそうたくさんはなさそうです。
というわけで、今回は、IFTTTに用意されている「Email」サービスを使ってみます。IFTTTのトリガーとして使うことができて、IFTTTアカウントに紐付けられたメールアドレスからIFTTTの所定のメールアドレス宛にメールを送ることで、IFTTTのアクションをキックできます。

IFTTTの「GitHub」サービスを使ったissue追加の課題

IFTTTには「GitHub」サービスも用意されているので、トリガーを「Email」、アクションを「GitHub」にすれば確かに目的のレシピは作れます。この場合、IFTTTに送りつけたメールのタイトルと本文で新規issueを追加できます。

が、、、これを実際に使ってみると、追加された新規issueにはメール本文の改行がなぜか反映されません。
GitHubのissueはMarkdownが使えるところが便利ポイントなので、改行が消えてしまうこのレシピでは使い物になりません。

というわけで「Webhooks」サービス

IFTTTのサービスの中にニーズを満たすものがないときは「Webhooks」サービスの出番です。新規issueの追加はGitHub REST API v3を使えばできるので、トリガーを「Email」、アクションを「Webhooks」にして、自作のWEBサービスでGitHubへissueを追加することにします。
IFTTTのレシピにするとこんな感じ。

実装

小さなWEBサービスなので、Amazon API GatewayAWS Lambdaと、この環境向けのPythonフレームワークであるchaliceを使って簡単に作ってしまいます。

説明をすっ飛ばして、コードは以下だけ。
中で使っているいくつかの環境変数(os.environ['hoge'])をchaliceの設定ファイル(.chalice/config.json)で設定する必要はありますが、chalice deployのコマンド一発でWEBサービスが動き出します。API GatewayやLambdaの管理コンソールに触る必要は全くありません。

app.py
from chalice import Chalice
import logging, os, json
import urllib.request

# setup logger
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

# setup chalice
app = Chalice(app_name='hook2issue')


@app.route('/', methods=['POST'])
def index():
    json_body = app.current_request.json_body
    logger.info("request body: {}".format(json_body))

    key = json_body['key']
    subject = json_body['subject']
    body = json_body['body']

    if key != os.environ['IFTTT_WEBHOOKS_KEY']:
        logger.warning("wrong key: {}".format(key))

    else:
        url = 'https://api.github.com/repos/{}/{}/issues?access_token={}'.format(
            os.environ['GITHUB_OWNER'],
            os.environ['GITHUB_REPOSITORY'],
            os.environ['GITHUB_ACCESS_TOKEN']
        )
        headers = {
            'Content-Type': 'application/json'
        }
        body = {
            'title': subject,
            'body': body
        }
        req = urllib.request.Request(
            url,
            data=json.dumps(body).encode('utf-8'),
            method='POST',
            headers=headers
        )
        try:
            with urllib.request.urlopen(req) as res:
                logger.info(res.read().decode("utf-8"))
        except Exception as e:
            logger.exception("Error in request: %s", e)

    # 200OK
    return {}

ちなみに、上記の自作WEBサービスに合わせて、IFTTTの「Webhooks」は以下のような感じで設定します。

これで所望の動きをしてくれるのですが、ハマリポイントが一点だけ。
「Body (optional)」の注意書きに、Surround any text with "<<>>" to escape the contentとあるのですが、きちんと文字列をエスケープするには"<<<>>>"で囲ってあげる必要があります。運良くこのことが書かれたWEB記事を見つけられたので良かったですが、もう少しで諦めるところでした。。。

おわりに

メールはもはやレガシーな手段になってしまった感がありますが、それでもなお、メールしかまともに使えない環境もあるにはあります。メールを起点にしたWEBサービス連携の一つのパターンとして、他にも使えるものが何か作れそうです。

参考