🔄

社内向け CLI ツール開発で Notion × GitHub 連携の小さな不満を解消する

に公開

はじめに

わたしの所属するチームでは Notion を使ってかんばん運用しており、GitHub インテグレーションを用いて Pull Request のステータスと Notion 上のタスクのステータスを同期させる運用を行っています。 この方法は Pull Request を中心とした開発ワークフローに Notion の強力なドキュメンテーション機能をシームレスに繋げることができ便利ですが、タスク着手時にタスクと Pull Request を紐づけるための定型作業を毎回行う必要があり、面倒に感じていました。

具体的には、タスクに着手する際に以下の一連の操作が必要でした。

  1. ブランチを作成する
  2. 空コミットを作成する
  3. リモートリポジトリにプッシュする
  4. Pull Request を作成する
  5. Notion のタスクと Pull Request を紐づける

これらの作業自体は難しくないものの、繰り返し行うには面倒です。 ブランチ名はある程度ヒューマンリーダブルなものが望ましいですがタスクごとに適切な命名を毎回考えるのは地味に面倒ですし、Pull Request とタスクの紐づけをうっかり忘れてしまうケースもありました。 この部分を自動化すべく社内向けの CLI ツールを開発しました。

技術スタック

以下の技術スタックを選定しました。

パッケージ 用途
TypeScript Notion 公式 SDK が JavaScript のみの実装であることと、チームの技術スタックとの親和性から選択
tsx TypeScript ファイルを直接実行する
zx シェルコマンドを扱いやすくする
minimist コマンドライン引数のパース
prompts 対話型の CLI インターフェースを実現する
@notionhq/client Notion API の公式クライアント
openai ブランチ名を自動生成する

実装とワークフロー

以下のシーケンスで処理します。

Pull Request タイトルに [<Notion のタスク ID>] を含めることで Notion のタスクと紐づけされます。

実装の詳細

主要な部分のコードを紹介します。

Notion タスクの取得

Notion SDK を使用して現在のスプリントの未完了タスク一覧を取得します。 Notion SDK は複数条件を AND, OR で繋げられるためかなり自由度高く抽出できて便利でした。

const fetchCurrentSprintTasks = async () => {
  const currentSprint = await fetchCurrentSprint();
  const notion = getNotionClient();

  const response = await notion.databases.query({
    database_id: process.env.NOTION_KANBAN_DATABASE_ID!,
    filter: {
      and: [
        {
          property: "スプリント",
          relation: { contains: currentSprint.id },
        },
        ...["Done", "Archived"].map((status) => ({
          property: "ステータス",
          status: { does_not_equal: status },
        })),
      ],
    },
    sorts: [{ property: "ステータス", direction: "ascending" }],
  });

  const tasks = response.results as PageObjectResponse[];
  return tasks;
};

タスクの選択

prompts を用いて対話的にオプションを指定できるようにしています。 これによりツール利用者はタスクに着手する際にとりあえず CLI ツールを叩いておけばよく、要求される事前知識を最小化することができます。

const selectTask = async (tasks: PageObjectResponse[]) => {
  if (tasks.length === 0) {
    return null;
  }
  
  const answer = await prompts(
    {
      type: "select",
      name: "task",
      message: "チケットを選択してください",
      choices: tasks.map((task) => {
        const simpleTask = toSimpleTask(task);
        return { title: simpleTask.styledLabel, value: task };
      }),
    },
    {
      onCancel: () => {
        echo`Aborted`;
        process.exit();
      },
    },
  );

  const task = answer.task as PageObjectResponse;
  return task;
};

AI を使ったブランチ名の生成

AI を活用しタスクタイトルから適切なブランチ名を自動生成します。 ある程度 AI に出力の方向性を示すために Few-shot プロンプティングを用いて意図した形式の出力を促し、さらに後処理で英数字とハイフン以外の文字を除去し、実用的な出力になるよう調整しています。

const suggestBranchName = async (taskTitle: string) => {
  const openaiClient = getOpenAiClient();

  const completion = await openaiClient.chat.completions.create({
    messages: [
      { role: "system", content: "与えられたタスクタイトルから GitHub のブランチ名を生成してください。 ブランチ名は英小文字、数字、ハイフンのみを使用し、短く簡潔にします。" },
      { role: "user", content: " ログイン機能の実装" },
      { role: "assistant", content: "implement-login-feature" },
      { role: "user", content: "パフォーマンス改善: 画像の遅延読み込み" },
      { role: "assistant", content: "improve-performance-lazy-loading" },
      { role: "user", content: taskTitle },
    ],
    model: "gpt-4o-mini",
  });

  const answer = completion.choices.at(0)?.message.content;
  if (!answer) {
    throw new Error("Failed to get branch name suggestion");
  }

  return answer.toLowerCase().replace(/[^a-z0-9-]/g, "");
};

Pull Request 作成

各開発者の環境に依存する部分(Git 操作や GitHub 操作)については、既存のコマンドライン(gitgh)を利用することで、追加のセットアップを最小限に抑えました。 これを実現するために、シェルコマンドを直感的に扱える zx を採用しました。

const createPr = async ({ base, head, title, body, labels = [], draft = false, }: CreatePrParams) => {
  const flags = [
    "--base", base,
    "--head", head,
    "--title", title,
    "--body", body,
    ...labels.flatMap((label) => ["--label", label]),
    "--fill",
    "--assignee", "@me",
  ];
  if (draft) {
    flags.push("--draft");
  }

  await $`gh pr create ${flags}`.catch(() => process.exit(1));
};

デモ

実際に CLI ツールを使用してタスクから Pull Request を作成するまでの流れを以下の動画で確認できます。 ここでは取得したリストから「ログインを実装する(サンプル)」というタスクを選択し、Pull Request を作成しています。

デモ動画

導入効果

以下のような効果が得られました。

作業開始時間が短縮された

タスク着手時の定型作業が数秒で完了するようになりました。 また、Pull Request と Notion タスクの紐づけ忘れなどのミスもなくなりました。

ワークフロー構築が容易になった

Pull Request を自動作成する際に、Notion タスクに予め登録されているタスク種別 (e.g. story, chore など) やタスク ID (e.g. TSK-1234) をブランチ名の prefix として付与するようにしています (e.g. story/TSK-1234/implement-login)。 またタスク種別はラベルとしても付与します。
これらは GitHub の Branch Rulesets や GitHub Actions による制御の条件として活用でき、自動化の条件分岐を容易に書くことができるようになりました。 実際に特定のラベルのときのみ QA チームに試験実施を依頼するなどの自動化に活用しています。

メトリクス測定が容易になった

チームで共通の CLI ツールを使うことで、着手時に Pull Request を作成するというフローが定着しました。 作業手順が標準化されたことでタスクの開始日時と Pull Request の作成日時が一致し、タスクのリードタイムなどの各種メトリクスの測定が容易になりました。

まとめ

Notion × GitHub 連携を効率化する CLI ツールの開発を通じて、日々の開発ワークフローが改善されました。 TypeScript とオープンソースライブラリを組み合わせ、比較的シンプルな実装でありながら、実用的なツールを作ることができたと思います。

単に時間を節約するだけでなく、作業手順を標準化したことによる副次的な効果も得られました。 日々の小さな面倒を解消することで、より創造的な作業に集中できる環境を整えることができると実感しました。

あしたのチーム Tech Blog

Discussion