GitHub Apps (およびCheck Run REST API) 入門

に公開

この記事は何ですか?

僕は毎日GitHub Appsのお世話になっているのですが、その背後にある仕組みがいまいち分かっていませんでした。ソフトウェア開発者である以上、実際に作ってみることで仕組みを理解したいと思い、この記事では小さな GitHub App を自作して GitHub Apps の仕組みを学ぶことにしました。本記事で作成する GitHub App を Hello World App と呼びます。

成果物のソースコードはGitHubで公開しています。これをインストールすると、GitHubのプルリクエストに対して "Hello, world!" というチェックが実行されます。次の画像の通りです。

GitHub Appsのチェック実行結果

これは常に成功するチェックなので、実用上のメリットはありません。単にGitHub Appsの仕組みを理解するためのコードだと思ってください。

GitHub Appsとは

GitHub Appsとは何でしょう。ややこしいですが、GitHub AppsはGitHubにデプロイするアプリケーションのことではありません

GitHub Appsは、GitHubのイベントを監視して、対応するペイロードを受け取ることができるアプリケーションです。アプリケーション本体はGitHubではなく、外部のサーバーやサービス上で動作します。本記事で紹介しているGitHub AppsはCloudflare Workersで動作しています。

GitHub Appsはサブスクライブするイベントを選択できます。例えば、プルリクエストが作成されたときや、コミットがプッシュされたとき、それに対応するペイロードを受け取ることができます。

GitHub Appsにプルリクエストをサブスクライブさせる

ここではプルリクエストをサブスクライブしています。この設定のおかげで、プルリクエストが作成されると、GitHub Appsはそのイベントを受け取ることができます。

Check Runとは?

Check Runはプルリクエストやコミットに対するチェック機能です。プルリクエストにテストや静的解析の結果がGitHub上に表示されているのを見たことがあるでしょうか。これらは、GitHubの Check Run APIで実現されています。

Check Runエンドポイントに渡せるパラメーターの中でも特徴的なものが statusconclusion です。これらはチェックの状態を表すもので、例えば statuscompleted のときに conclusionsuccess であれば、チェックが成功したことを意味します。

Hello World App では、次のように Check Run API を叩いています。

const response = await octokit.rest.checks.create({
    owner,
    repo,
    name: "Hello World Check",
    head_sha: headSha,
    status: "completed", // 常に "completed"
    conclusion: "success", // 常に "success"
    output: {
        title: "Hello World Message",
        summary: `This is a simple 'Hello, world!' message from your GitHub App for PR #${pullNumber}.`,
        text: "The check was successfully created by your GitHub App for this pull request.",
    },
});

このようにして、常に Check Run が成功するようにしています。意味のあることをする場合は、statusconclusion の値を適切に設定する必要があります。例えばテストを実行中には statusin_progress に設定し、完了時に completed にする。テストが成功した場合は conclusionsuccess に、失敗した場合は failure に設定することになります。

GitHub Appsの作り方

GitHub Appsを作成するには、まずGitHubの設定画面から新しいアプリケーションを作成します。以下の手順で進めます。

  1. GitHubのウェブサイトから、右上のアイコンをクリックし、「Settings」をクリックします。
  2. 左側のメニューから「Developer settings」を選択します。
  3. 「GitHub Apps」を選択し、「New GitHub App」ボタンをクリックします。

ここで様々な設定をします。Permissions のセクションで必要なパーミッションを選択しましょう。Hello World App では、プルリクエストのイベントを受け取るために、Pull requests のパーミッションを Read & write に設定しています。Check Run を扱うアプリケーションなので Checks のパーミッションも Read & write に設定しています。

また、Hello World App の場合はWebhookのURLも設定します。プルリクエストが作成されたときにPOSTリクエストを送信してほしいからです。Webhookのシークレットもあわせて設定しましょう。

Webhook URLの設定

必須入力項目ではありますが、Homepage URL などは適当なURLを入れても構いません。

入力が完了したら「Create GitHub App」ボタンをクリックします。

Appが無事に作成できたでしょうか。ここで、もうひとつやることがあります。Generalのセクションで「Generate a private key」ボタンをクリックして、秘密鍵を生成します。この秘密鍵は、GitHub AppがAPIをコールするために必要です。なお、細かい話ですが、Hello World App が利用している octokit/auth-app.jsライブラリは、この秘密鍵を直接は利用できません。生成される秘密鍵はPKCS#1 形式ですが、octokit/auth-app.js はPKCS#8形式の秘密鍵を必要とします。そのため、生成された秘密鍵を PKCS#8 形式に変換する必要があります。次のようにします。

openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in my-original.pem -out my-hello-world-cli-app-pkcs8.pem

さらに言うと、Hello World App の場合、この秘密鍵をBase64エンコードして、環境変数 GITHUB_APP_PRIVATE_KEY に設定して、.dev.vars に設定する必要があります。なお、アプリケーションを実際に起動する方法は README.md にゆずり、ここでは単にBase64エンコードされたテキストを取得する方法だけ示します。

cat my-hello-world-cli-app-pkcs8.pem | base64

実装

実装の詳細はGitHubのリポジトリに譲りますが、雰囲気は次のような感じです。エンドポイントはHonoで実装しています。

import { Octokit } from "@octokit/rest";
import { Webhooks } from "@octokit/webhooks";

// `/webhooks` へのリクエストを処理する
app.post("/webhooks", async (c) => {
    const env = c.env;

    const webhooks = new Webhooks({
        secret: env.GITHUB_WEBHOOK_SECRET,
    });

    // Pull Requestが開かれたときのイベントを処理する
    webhooks.on("pull_request.opened", async ({ payload }) => {
        await handlePullRequest(payload, env);
    });

    return c.text("OK", 200);
});

async function handlePullRequest(payload: PullRequestPayload, env: Env) {
    const { repository, pull_request } = payload;
    const owner = repository.owner.login;
    const repo = repository.name;
    const headSha = pull_request.head.sha;

    const octokit = new Octokit({auth: "Note: トークンの取得方法は実際のソースコードを参照してください"});

    // Check Runを作成する
    await octokit.rest.checks.create({
        owner,
        repo,
        name: "Hello World Check",
        head_sha: headSha,
        status: "completed",
        conclusion: "success",
        output: {
            title: "Hello World Message",
            summary: `This is a simple 'Hello, world!' message from your GitHub App.`,
            text: "The check was successfully created by your GitHub App for this pull request.",
        },
    });
}

エラー処理などは省略していますが、雰囲気はだいぶ伝わるのではないでしょうか。

おわりに

作ってみることで、だいぶ理解が深まりました。「思ったより簡単に作れるな」というのが僕の印象です。

真面目なGitHub Appを作る場合、次のステップはマーケットプレイスにリスティングすることでしょう。Hello World Appオンラインで公開しているのですが、マーケットプレイスには載せていません。作者として、このページを開くと "Manage this apps's Marketplace listing." というリンクが表示され、マーケットプレイスに公開するための情報を入力できます。

...できるのですが、マーケットプレイスに載せる前にGitHubによる審査が入るため、ここで僕は諦めました。

GitHub Appをマケットプレイスに載せるには、GitHubによる審査が必要

Hello World App のような単純なアプリケーションが審査に通るとは思えなかったからです。もしここまで読んでくださった方で、面白いGitHub Appをマーケットプレイスに登録できた方がいらしたら、ぜひ教えてください 😊

GitHubで編集を提案

Discussion