なるべくユーザー依存を減らしてGoogle App ScriptからGitHubAPIを利用する

なるべくユーザー依存を減らしてGoogle App ScriptからGitHubAPIを利用するのサムネイル

この記事はファンタアドベントカレンダー2023の7日目です。

社内の改善活動の中で色々なものが自動化されつつあります。当社の場合気軽に作れるというところもあってGoogle App Script(以降GAS)を利用するケースが多いです。

例えば、こんなスクリプトが作られています。

  • Slackのあまりアクティブではないチャンネルを定期的にリスト化するスクリプト
  • 案件のノウハウを社内共有する施策、案件共有会の準備自動化スクリプト

案件共有会については、こちらの記事をぜひ御覧ください

リモートワークにおける知の共有施策「案件共有会」のつくり方 - note

この流れに乗りつつ、Github AppsとGitHub API(REST)を活用し、最近社内で作られたリポジトリを紹介するスクリプトを書きました。

今回はこの中でやってみたことを書いてみます。

環境について

  • Node.js v18.18.0
  • @google/clasp v2.4.2
  • TypeScript v5.2.2

GitHub APIへのリクエスト処理を実装する

今回はGitHub APIからリポジトリ情報を取得しており、REST APIを利用しました。

GitHub SDKが上手く動かない

OctokitというOrganizationがあり、ここでGitHub APIのSDKが公開されています。

今回はNode.js(+TypeScript)を使うため、以下のパッケージを使うことを試みました。

octokit/rest.jsを使うと、以下のように書くことができます。

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

const octokit = new Octokit();

const response = await octokit.repos.listForOrg({
  org: '任意のorganization名',
  page: 1,
});

console.log(response.data)

認証情報は渡していないため、公開リポジトリのみが取得できます。

これをデプロイし、GASの画面から実行してみると、header周りのエラーが出てどうにも解消できませんでした。
その辺りの話を追っていくと、GASではNode.jsのネイティブAPIを使った操作などは出来ない話がよく出てきてます。またそれによって多くのパッケージが利用できないようです。
今回も利用出来ないパッケージの1つを引いてしまったのかなと思っていますが、もし解消法の分かる方がいましたら教えてください。

UrlFetchApp.fetch()でリクエストするように書き換える

GASにはUrlFetchApp.fetch()というメソッドが用意されていて、こちらを利用することで、外部APIへのリクエストが可能になります。

ちなみに、UrlFetchAppを使う際は、@types/google-apps-scriptを入れておくと型エラーを解消できます。

これらを用いてGitHub APIへのリクエスト部分を書き換えてみます。

import type { Endpoints } from '@octokit/types';

const options: GoogleAppsScript.URL_Fetch.URLFetchRequestOptions = {
  method: 'get',
  headers: {
    'X-GitHub-Api-Version': '2022-11-28',
  },
};

const params = {
  org,
  sort: 'created',
  direction: 'desc',
} satisfies Endpoints['GET /orgs/{org}/repos']['parameters'];

// NOTE: URLSearchParamsが使えず、node:urlも上手くビルドできないので手動でやっている
const queryString = (Object.keys(params) as Array<keyof typeof params>)
  .map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`)
  .join('&');

const response = await UrlFetchApp.fetch(`https://api.github.com/orgs/${org}/repos?${queryString}`, options);

querystringの組み立て部分が少し苦しいですが、こういう形でパラメーターをまとめてリクエストすることになります。SDKのありがたみを感じますね。

GitHub Appsを使ってなるべくユーザーへの依存を減らしてprivateな情報も取得する

GASを使っている時点で、最終的にはユーザーに依存してしまうのですが、それを除いてなるべくユーザーに依存しない状態を目指しました。

GitHub APIへのリクエストを行う際、GitHubのトークンを用いることでprivateリポジトリなどの情報にもアクセスできるようになります。

よく出てくる方法としてPersonal Access Token(以降PAT)を用いた方法があります。PATはGitHub側もセキュリティの兼ね合いで長期での利用は推奨しておらず、期限設定が推奨されており正直少し使いづらいです。そもそも個人に依存してしまうためあまりいい方法では無いように感じました。

GitHub Appsによるトークン取得

他の方法を調べていた所、GitHub Appsを用いる方法がありました。

GitHub Apps経由で発行したtokenを用いて、GitHub Actionsで使う - Zenn

ユーザーの代わりに権限を持ったGitHub Appsを用意し、そちらからトークンを取得して使うというものです。

この記事ではGitHub Actionsを用いていましたが、GASでも利用できます。

GitHub Appsを作る部分は上記記事を参考にしてください。また、必要に応じて権限を付与する必要がありますが、こちらのドキュメントが分かりやすかったです。
GitHub Appに必要な権限 - GitHub Docs

GitHub Apps経由でのリクエスト処理の実装

GitHub Appsが用意できたので、次はGitHub Appsからのトークンを取得します。
GitHub Apps登録時に発行される、installationIDを用いて以下のエンドポイントにリクエストすることで、トークンが取得できます。

/app/installations/{installationID}/access_tokens

UrlFetchApp.fetch()でのリクエストは大変だなと思っていたのですが、GASのライブラリを作ってくださっている方がいらっしゃいました。

GitHub Apps の Access Token を取得するGoogle Apps Script 用ライブラリーを作った - Zenn

ライブラリはこちら。

hankei6km/gas-github-app-token - GitHub

Setupの内容を辿ると無事使えるようになります。
と思いきや、claspを使っているのでコンソールからインストールするとデプロイの度に消えてしまいます。

appsscript.jsonに以下のように書いておくことでインストールまで行ってくれるようになりました。

{
  :
  "dependencies": {
    "libraries": [
      {
        "userSymbol": "GitHubAppToken",
        "libraryId": "**ここにライブラリのID**",
        "version": "1"
      }
    ]
  },
  :
}

また、ライブラリを使うとこのような形で書くことができます。

const [url, opts] = GitHubAppToken.generate({
  appId: **ここにGitHub AppsのID**,
  installationId: **ここにGitHub AppsのinstallationId**,
  privateKey: **ここにPrivate Keyを環境変数から読み込んだもの**,
});

const response = UrlFetchApp.fetch(url, opts);

Private Keyのコンバートはもうひと手間

GitHub Appsで利用するキーは形式が違っているため、GASの環境変数への設定前に対応する形式へのコンバートが必要になります。

$ openssl pkcs8 -topk8 -inform pem -in xxxxx-private-key.pem -outform pem -nocrypt -out new-private.pem

このコマンドで処理は大体終わるのですが、環境変数として登録する際に改行部分が上手くいかないため、改行部分に改行文字を入れた所無事Private Keyの正しい形式として取得できました。

この記事に出てくるフォーマットがヒントになりました。
[RS256] JWTでRSA秘密鍵を環境変数で処理したい [Javascript] - Qiita

これでGitHub Appsからトークンが取得できるようになります。
これを前述したGitHub APIへのリクエスト部分でAuthorizationヘッダーに載せてあげるとprivateな情報も取得できるようになります。

const options: GoogleAppsScript.URL_Fetch.URLFetchRequestOptions = {
  method: 'get',
  headers: {
    Authorization: `Bearer ${githubToken}`, // ここに追加
    'X-GitHub-Api-Version': '2022-11-28',
  },
};

終わりに

GitHub APIをGoogle App Scriptから叩く方法、またGitHub Appsを用いてなるべくユーザーに依存しない方法でのリクエストを試してみました。

明日は8日目。当社でnoteやXなど多くの発信をしてくれているannaさんが、入社から今までPRとして取り組んできたことを書いてくれる予定です。お楽しみに。