GitHub Appsの登録からアクセストークンの発行をJavaScriptとGolangでやってみる

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは。齋藤です。

はじめに

GitHub Apps を登録して自動化したい気持ちがあるので、やってみます。 今回はひとまず、GitHub Apps を登録してインストールした後、アクセストークンを取得するまでをやってみます。

今回は以下の流れでやってみようと思います。

  • コールバックのLambda関数を作成
  • GitHub Appsの登録
  • アプリを利用したいリポジトリにアプリをインストールする
  • アクセストークンの発行のために private keyの発行
  • イベントの配信履歴から installations_id を調べる
  • アクセストークンの発行をしてみる
    • まずはJSでやってみる
    • golangで書き直す

また、Github Appsの基本的な流れは以下のような流れになるはずです。

  • WebHookのためのLambda関数を作成
  • 作成したLambdaを呼び出すAPI Gatewayのトリガーを設定
  • JWTを生成して、アクセストークンを生成
  • アクセストークンを使ってGitHubのAPIを呼び出し

今回は、「JWTを生成して、アクセストークンを生成する」の部分に着目して 記事を書いていきます。

WebHookのためのLambda関数を作成

WebHookのためのLambda関数を作ります。

今回は簡易的にマネージメントコンソールから作ります。 また、今回はここの部分にはほぼ何も書きません。

ただeventをdumpしてレスポンス返すコードですね。

exports.handler = async (event) => {
    console.log(JSON.stringify(event))
    return {
        statusCode: 200,
        body: JSON.stringify({
            "test": "test"
        })
    }
};

↑のコードを使って AWSのマネージメントコンソールから関数の作成をしていきます。

設定を入力していって

ロールも作成して

コードをコピペして

保存したらLambda関数自体は完成です。

今回は学習用途にダンプするだけのWebHook用のエンドポイントを用意しました。

作成したLambdaを呼び出すAPI Gatewayのトリガーを設定

作成したLambda関数をWebHook用のエンドポイントにするには API Gatewayのインテグレーションが必要なので設定をしていきます。 画面左にある、トリガーの追加からAPI Gatewayのトリガーを追加します。

いい感じに設定を入力します。今回はWebHook用のエンドポイントなのでひとまず、オープンにしておきました。

GitHub Appsの登録用にURLをメモするため、API Gatewayの画面を開きます。

作成したAPI(トリガー)の適切なステージを選択してください。

ステージを選択するとURLが確認できるので こちらのURLをメモしておいてください。 GitHub Appsの登録に必要です。

GitHub Appsの登録

登録画面を開いていきます。 Settings --> Developers Settings --> Github Apps --> New Github App の流れです。

URL周りはひとまず、上記で作成したAPI Gatewayで確認できるURLを設定しておきます。

今回はサンプルアプリなので WebHookのURL以外もlambdaのURLにします。

リポジトリの内容を読み取りたい + Pull Requestの作成がしたいので こんな感じで設定します。 今回の用途だとRepository webhookに関しては必要ないと思います。 要件に合わせて選択してください。

通知してほしいWebHookを選択します。 ここではMilestoneの通知が欲しいので、Milestoneを選びました。

今回はお試し、ということで only on this accountです。

下のほうに登録ボタンがあると思うのでそれを押下すると登録はおしまいです。

アクセストークンの発行のために private keyの発行

今度は JWTの署名に必要なpemファイルを生成します。 GitHub Appをインストールしたリポジトリを操作するアクセストークンの発行には、JWTが必要です。 そのため、Github Appsにprivate keyの発行・登録をする必要があります。

登録後の画面下部に以下のような画面が存在すると思います。

表示されているボタンを押下すると GitHub Appに公開鍵が登録されて 手元にpemファイルがダウンロードされてくるはずです。

これでGitHub Appの設定は終わりになります。

アプリを利用したいリポジトリにアプリをインストールする

作ったGitHub Appsの設定の画面左側の 「Install App」を押下して 作ったアプリをインストールしてみましょう。

今回は 選択したリポジトリだけにしました。

イベントの配信履歴から installations_id を調べる

インストールした後、作ったアプリの画面から、イベントの配信履歴が確認できます。 そのイベントの配信履歴から installations_id を調べます。 なぜ調べるかというと、アクセストークンの発行に必要になるからです。

ヘッダでどのリソースの通知かが分かります。

payloadを見るとaccess_tokens_urlというのがあります。 /installations/{installation_id}/access_tokensinstallation_id をメモしておきます。

アクセストークンの発行をする

それでは本題のアクセストークンの発行をしてみましょう。 今回はGolangで署名検証をしたかったのですが ひとまず慣れているJSで、QiitaでGitHub Appsの紹介をしている方のPythonのコードを見ながら 書いていきました。

JSで書いた後にgolangで書き直します。

JSでJWTを生成し、アクセストークンを発行する

今回は jsonwebtokenaxios を使います。

$ npm i jsonwebtoken axios

アクセストークンが取得できるところまでを書きました。 your-apps-idとyour-installation-idを置き換えて使ってください。

const jwt = require("jsonwebtoken")
const fs = require("fs")
const axios = require("axios")

const payload = {
    exp: Math.floor(Date.now() / 1000) + 60,  // JWT expiration time
    // ちょっとだけ時間を手前にしておくとアクセストークンの発行に失敗し辛いらしい。
    // https://qiita.com/icoxfog417/items/fe411b94b8e7ae229e3e#github-apps%E3%81%AE%E8%AA%8D%E8%A8%BC
    iat: Math.floor(Date.now() / 1000) - 10,       // Issued at time 
    iss: <your-apps-id>
}

const cert = fs.readFileSync("./test.pem").toString()
const token = jwt.sign(payload, cert, { algorithm: 'RS256'});

axios.default.post("https://api.github.com/installations/<your-installation-id>/access_tokens", null, {
    headers: {
        Authorization: "Bearer " + token,
        Accept: "application/vnd.github.machine-man-preview+json"
    }
})
.then(res => console.log(res.data.token)) // とりあえずログに吐く
.catch(console.log) // とりあえずログに吐く

これで終わりです。

これをgolangで書き直していきます。

golangでJWTを作成し、アクセストークンを発行する

以下のイメージで、goのリポジトリを作ってライブラリを追加しておきます。

dep init
vim main.go # main関数だけ作っておきます。
dep ensure -add github.com/dgrijalva/jwt-go # jwtの操作のために使う

書いたコードはこんな感じになりました。

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "os"
    "time"

    jwt "github.com/dgrijalva/jwt-go"
)

func generateToken(appID string) (string, error) {
    b, err := ioutil.ReadFile("./test.pem")
    if err != nil {
        return "", err
    }

    c := &jwt.StandardClaims{
        Issuer:    appID,
        ExpiresAt: time.Now().Unix() + 60,
        IssuedAt:  time.Now().Unix() - 10,
    }
    token := jwt.NewWithClaims(jwt.GetSigningMethod("RS256"), c)

    // pemから秘密鍵を取り出して
    key, err := jwt.ParseRSAPrivateKeyFromPEM(b)
    if err != nil {
        return "", err
    }

    // 秘密鍵でjwtを署名
    t, err := token.SignedString(key)
    return t, err
}

func mainInternal() (err error) {
    token, err := generateToken("<your-app-id>")
    if err != nil {
        return
    }

    client := new(http.Client)
    req, _ := http.NewRequest("POST", "https://api.github.com/installations/<your-installation-id>/access_tokens", nil)
    req.Header.Add("Authorization", "Bearer "+token)
    req.Header.Add("Accept", "application/vnd.github.machine-man-preview+json")

    res, err := client.Do(req)
    if err != nil {
        return
    }

    defer res.Body.Close()

    body, err := ioutil.ReadAll(res.Body)
    if err != nil {
        return
    }

    // bodyの .token (jq expression) にトークンが入っています。
    fmt.Println(string(body)) 
    return
}

func main() {
    if err := mainInternal(); err != nil {
        fmt.Println(err.Error())
        os.Exit(1)
    }
}

これでひとまず、動くようにはできました。

生成したアクセストークンからPull Requestを作ってみる

curlでgithubのAPIを叩いてみます。

curl -XPOST -H "Authorization: Bearer $TOKEN" https://api.github.com/repos/wreulicke/automatic-release-sandbox/pulls -d '{
    "title": "Test",
    "head": "master",
    "base": "develop",
    "body": "## 概要 \r\n\r\nCreated by github-apps token"
}'

これが上記呼び出しで作ったPRです。BOTの文字があることが分かります。

まとめ

今回はGitHub Appsへの導入としてアクセストークンの発行とそれに伴うJWTの作成について記事に書きました。 その結果、アクセストークンの発行が出来て、curlでお試しですが、Pull Requestの作成が出来ることまで確認しました。

今回は probotを使わなかった理由としては、以下の理由がメインです。

  • Lambdaで動かしたかった
    • AWS Lambdaでprobotを動かせるような仕組みとして Serverless Frameworkのインテグレーションも検討されているようだが・・・ https://github.com/probot/probot/issues/149
    • まだWIPの模様 https://github.com/probot/serverless-lambda
  • golangで書きたかった
    • こちらで書いたコードを使いたかった
    • JSでもisomorphic-gitというのがあるのでそちらを使えば出来るのかもしれない
  • 隠蔽されてて見えないのが気になった

というわけで今回は最終的にはgolangで書きましたが githubのAPIクライアントなどもJSのほうが揃ってそうではあるので そちらを使うのほうが良さそうですね。 また、isomorphic-gitも試して記事を書いてみたいと思います。

それではまた次の記事でお会いしましょう。

関連リンク