SEN PRODUCT BLOG

千株式会社のエンジニアによるブログ

GitHub Projects のカスタムフィールドを半自動で更新したい

この記事は SEN Product Blog アドベントカレンダー 2024 の17日目の記事です。

こんにちは!千株式会社ものづくり部のおはぎです。

アドベントカレンダー2024、2回目の執筆です。前回は目指すチーム像のお話でしたが、今回はもう少し技術寄りの話として、GitHub Projectsのカスタムフィールド更新を自動化した話をしようと思います。

productblog.sencorp.co.jp

本記事では動機から実現したことの大枠の設計に焦点を当てます。そのため、あまり詳細には実装や各種設定を解説しませんが、関連するドキュメントへのリンクを貼っているので、気になる方はそちらで補完いただけたらと思います。

なぜやろうと思ったのか

私のチームではスクラムの開発手法を取り入れており、プロダクトバックログやスプリントバックログをGitHub Projectsで管理しています。

デイリースクラムでは、スプリントバックログを見ながらスプリントゴールに対する進捗を検査し、作業を調整している"つもり"だったのですが、スプリントの後半で忙しなくなることが多々あり。進捗を正しく捉えられておらず、楽観的に調整してしまっているのではないか?と感じていました。

しかし、作業実績を逐一記録するのは開発者の負担が大きいですし、そこにあまりコストを割きたくないといった事情から具体的なデータは収集しておらず、あくまでも「そんな気がする」に留まっている状況。

そこで、作業の開始/終了を自動で保存できれば、開発者への負担を抑えながらも実績を把握し、開発プロセスの改善を検討できるのではないかと考えました。

やりたいこと

前述の背景から、スプリントバックログに含まれる計画に対して想定通りの規模感だったかを把握したいです。

スクラムガイドに依ると、多くの場合で計画は1日以内の作業アイテムに分解されるとのこと。この目安に沿いたいので、時間までは厳密に取る必要はなく、開始/終了日が蓄積できれば良さそうです。

開発者は、選択したプロダクトバックログアイテムごとに、完成の定義を満たすインクリメントを作成するために必要な作業を計画する。これは多くの場合、プロダクトバックログアイテムを 1 ⽇以内の⼩さな作業アイテムに分解することによって⾏われる。

https://scrumguides.org/docs/scrumguide/v2020/2020-Scrum-Guide-Japanese.pdf

そのため、作業アイテムの開始/終了日を半自動的に入力する仕組みを作ります。

仕組みと構成図

まずは現状のバックログ管理をご説明すると、バックログアイテム用に GitHub Repository を用意しており、そこに作成した Issue をバックログ管理用の GitHub Projects に紐付けています。

Projects にアイテムの進捗を表す Status というカスタムフィールドを設定しており、このフィールドを変更することでアイテムの状態を管理しています。

このカスタムフィールドの変更は Project Item の変更であって Issue の変更ではないというのが課題でした。Issue の変更をトリガーにできれば GitHub Actions で気軽に自動化できたのですが、Project Item をトリガーにすることは Actions のワークフローでは今のところできません。

よって、GitHub Webhook をトリガーに API Gateway + Lambda で実装した API へリクエストし、Lambda から GitHub API で Project Item を更新するという仕組みにしました。

構成図

設定と実装

Lambda 関数と API Gateway で API を作成する

基本的にAWSの開発ガイド通りで問題ありません。必要に応じて関数名を設定したり、お好みでREST API やNode.js以外のランタイムとして読み替えて作成してください。

docs.aws.amazon.com

作成した API の URL はこの後の Webhook の設定で使用するので、書き留めておきます。

Webhook を設定する

Project Item の変更をトリガーにしたいので、projects_v2_item*1 イベントを利用します。

Repository ではなく Organization レベルのイベントなので、Organization の Settings から設定を行います。

docs.github.com

権限によっては Settings にアクセスできませんので、その場合は上長に確認しましょう。

docs.github.com

Settings から Webhooks を選択。Add Webhook ボタンから以下のような画面が表示されるはずです。

設定必須なのは以下です。

  • Payload URL
    • 前工程で作成した API のURLを入力する
  • Which events would you like to trigger this webhook?
    • Let me select individual eventsprojects_v2_itemを選択

お好みで以下も設定してください。

  • Content type
    • 私は application/jsonとしました。
  • Secret
    • 今回の記事では割愛しますので、必要に応じて以下のページなどをご参照ください。

docs.github.com

Add webhook ボタンを押して設定は完了です。

GitHub で Personal Access Token を発行する

Lambda 関数で GitHub API を使用するので、PATを発行します。

トークンの発行や取り扱いは組織ごとの方針に従って設定してください。今回の実装では organization projects に対して Read/Write の権限が必要です。

docs.github.com

Lambda 関数を実装する

リクエストを判別する

Organization レベルの Webhook なので、Organization 内の全ての Project item の変更がトリガーになります。

弊社では Organization 内に複数の Project が存在しており、自チームの Project に対してのみ処理を行いたいので、リクエストの内容を見て判別を行います。

const PROJECT_NODE_ID = 'dummy';

export const handler = async (event) => {
  if (event.body.projects_v2_item.project_node_id !== PROJECT_NODE_ID) {
    return {
        statusCode: 200,
        body: JSON.stringify('対象のプロジェクトではないので処理をスキップ'),
    };
  }
}

なお、本記事ではPROJECT_NODE_IDのような値を一律定数として表現しますが、必要に応じて環境変数としたり Secret Manager を用いて管理いただく方が好ましいと思います。

また、以下のドキュメントでevent内の定義を確認できます。コンソールログに吐き出せばCloudWatchで実際の値を確認することも可能ですので、うまく動作しない場合はご確認ください。

docs.github.com

これで他の Project へ影響することがなくなりました。

モジュールを使用できる状態にする

今回は GitHub API を用いて Projects を変更したいので octokit/graphql を使用します。

Lambda で Node.js のパッケージやモジュールを扱う方法はいくつかありますが、今回はレイヤーを作成する形で実装しました。

docs.aws.amazon.com

まずは zip で固めて、

$ mkdir nodejs && cd ./nodejs
$ npm install @octokit/graphql
$ cd ../ && zip -r nodejs.zip ./nodejs

レイヤーの作成と関数への追加は以下の通りです。

docs.aws.amazon.com

docs.aws.amazon.com

これにて Lambda 関数で octokit/graphql をインポートできるようになりました。

GitHub API でカスタムフィールドを更新する

更新対象を判定する

今回はStatusがTodoからIn Progressに移行した際に開始日を、元のステータスは問わずDoneに移行した際に終了日を入力したいので、以下のように Status の変更であるかどうかを判定するロジックと Status の変更内容を判定するロジックを実装します。

const isStatusUpdated = (event) => {
  return event.body.changes.field_value?.field_node_id === STATUS_FIELD_ID;
}

const isStatusUpdatedToInProgressFromTodo = (event) => {
  return event.body.changes.field_value?.from?.id === STATUS_TODO_ID
    && event.body.changes.field_value?.to?.id === STATUS_IN_PROGRESS_ID;
}

const isStatusUpdatedToDone = (event) => {
  return event.body.changes.field_value?.to?.id === STATUS_DONE_ID;
}

これで更新対象を判別する準備が整いました。

更新する

今回は一度しかリクエストしないので実行時に設定しても良いのですが、デフォルトでヘッダーに authorization を設定するようにしておきます。

import {graphql} from "@octokit/graphql";

const GITHUB_TOKEN = 'dummy'; // 前工程で作成したPAT

const graphqlWithAuth = graphql.defaults({
  headers: {
    authorization: `token ${GITHUB_TOKEN}`,
  }
});

クエリは別ファイルとして切り出した方が管理しやすいので、graphqlファイルを読み込む形で利用します。

updateProjectV2ItemFieldValue を用いてカスタムフィールドの値を更新するのですが、Date型のカスタムフィールドを更新するという点では開始/終了日で共通なので、今回は引数で fieldId と fieldName を渡す形で共通化してみました。

mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $fieldName: String!, $date: Date!) {
    updateProjectV2ItemFieldValue(input: {projectId: $projectId, itemId: $itemId, fieldId: $fieldId, value: {date: $date}}) {
        projectV2Item {
            id
            content {
                ... on Issue {
                    title
                    url
                }
            }
            fieldValueByName(name: $fieldName) {
                ... on ProjectV2ItemFieldDateValue {
                    id
                    date
                }
            }
        }
    }
}

docs.github.com

JS側の実装な以下のようなイメージです。 シンプルに例示するために開始日のみの更新処理ですが、実際には先に実装した判定ロジックで分岐させて利用します。

import {fileURLToPath} from 'url';
import fs from "fs";
import path from "path";

const PROJECT_NODE_ID = 'dummy';
const START_DATE_FIELD_ID = 'dummy';
const START_DATE_FIELD_NAME = 'dummy';

<!-- 中略 -->

const dirName = path.dirname(fileURLToPath(import.meta.url));
const query = fs.readFileSync(path.resolve(dirName, 'update_date_field.graphql'), 'utf8');

try {
  return await graphqlWithAuth(query, {
    projectId: PROJECT_NODE_ID,
    itemId: event.body.projects_v2_item.node_id,
    fieldId: START_DATE_FIELD_ID,
    fieldName: START_DATE_FIELD_NAME,
    date: new Date().toISOString()
  });
} catch (error) {
  console.error(error.message);
}

なお、フィールドのIDがわからない場合は ProjectV2SingleSelectField を用いて確認することができます。

docs.github.com

これで開始/終了日に値が更新されるはずです。

まとめ

今回は GitHub Webhook + GitHub API + API Gateway + Lambda で GitHub Projects の Item に対する更新の一部を半自動化してみました。

まずはデータの蓄積をするという観点で取り組みましたが、今後はそのデータを参照しやすい形で取得したり、データ活用に向けての仕組みを整備していこうと考えています。

いずれ機会があれば、その後のお話もできたらなと思います。

明日は daitasu さんの インフラ知見がないチームで新規システムを AWSで構築・Terraform 化してみんなでInfra 頑張ってるよの話 です!

*1:記事公開時点では Projects の Webhook イベントはパブリックプレビュー版です。最新の情報はGitHub Doc等をご参照ください。