🎫

タスク管理基盤をGitHub Projectsで再構築する時の実装パターン

に公開

はじめに

Luupに所属している、ぐりもお(@gr1m0h)です。

私たちのチームでは、スクラム的なアプローチを採用してタスク管理しています。プロダクトオーナーが不在であるため、チーム全員でタスクの優先順位付けやリファインメントを行い、スプリント単位で開発を進めているという特徴があります。

https://zenn.dev/luup_developers/articles/sre-gr1m0h-20250605

これまでNotionDBを使ってタスク管理していましたが、開発ワークフローとの分断が課題となっていたため、GitHub Projectsへの移行を決断しました。

本記事では、移行の背景から具体的な実装内容、そして移行後の運用方法までタスク管理基盤の刷新プロジェクトについて紹介します。Issue Template、GitHub Actions、AIツールを組み合わせることで、タスクの質を維持しながら自動化を実現した取り組みについてお話します。

移行の背景

これまでは、NotionDBを使ってタスク管理していました。ドキュメント管理場所とタスク管理場所を同一にすることは分かりやすさがありましたが、以下のような課題が生じていました。

  • 開発ワークフローとの分断
    • IssueやPull Requestとの紐付けが難しい
      • GitHubのコード変更とタスクの関連性が見えにくい
    • プレビューの非対称性
      • GitHubのIssue URLをNotionに貼り付けるとステータスがプレビューされるのに対し、NotionDB内のタスクはNotion上でプレビューできない
  • 自動化の限界
    • Notionにもスクラム機能はあるが、高度な自動化やカスタマイズが難しい
    • GitHub Actionsなどの開発ツールチェーンと連携しにくい
  • AI活用の障壁
    • DevinやClaude CodeなどのAIツールを活用するにあたり、GitHub Issueを起点としたAI連携を強化したい
    • Issueを読み込ませてコードを生成したり、ブレインストーミングを行ったりといったことを実現したい

これらの課題を解決するため、タスク管理をGitHubに移行する決断をしました。タスクの「質」と「自動化」を維持・向上させることを目標に移行プロジェクトを進めることにしました。

タスクのライフサイクル

まず、GitHub Projectsでのタスク管理の全体像を理解していただくため、タスクのステータスとライフサイクルについて説明します。
私たちのチームでは、タスクのステータスを以下のように定義しています。

グループ ステータス 説明
未着手 Idea まだ実行の優先度が低かったり、具体的に着手するタイミングが定まっていないアイデアや要望。基本的に担当者のアサインはなしだが、リファインメントのために担当者をアサインすることがある。
未着手 Backlog 今後取り組む可能性が高いタスクのリスト。優先度付けや見積もりがある程度進んだ状態。担当者の設定はなし。
未着手 To Do 今まさに着手準備が整っており、次に取り掛かるべきタスク。担当者とスプリントが設定されている。
進行中 In Progress 現在、作業が進行中のタスク。担当者がアサインされ、実際に手を動かしている段階。
完了 Done 完了したタスク。必要な作業やテストがすべて終わり、プロダクトやサービスへの反映が完了した状態。
完了 Won't 何らかの理由で「対応しない(着手しない)」と判断したタスク(タスクの重複、他の対応で解決された等)。廃案扱いだが、将来的に復活させる可能性は基本的に低い。

タスクのステータスに合わせて、ライフサイクルは以下のようになります。

通常のフローでは、「Idea」からリファインメントを経て「Backlog」に移動し、スプリントプランニングを経て「To Do」に入ります。一方で、外部依頼やチーム内部の割り込みタスクは、優先度に応じて直接「To Do」に投入されることもあります。また、リファインメント完了後にチーム確認済みで優先度が高いタスクは、「Idea」から「To Do」に直接移動することもあります。

タスクのライフサイクルを図で整理すると以下のようになります。

image1

このように、通常フローと割り込みフローの2つの経路を明確にすることで、スプリント計画と実際の運用のバランスを保っています。

Issue Templateによるタスクテンプレート

移行にあたって最初に取り組んだのが、タスクの目的と内容を明確にするためのIssue Templateの定義です。タスク作成時に必要な情報が必ず入力されるようにすることで、タスクの質を担保したいと考えました。

以下の2種類のIssue Templateを定義しました。

1.定常タスク

定常タスクでは、Why(背景と目的)、Goals(達成目標)、Non Goals(対応しないこと)、To-do(具体的な作業項目)、Notes(補足情報)という5つのセクションを設けました。特にWhyとGoalsは必須項目とし、タスクの目的が不明確なまま作業が始まることを防いでいます。

name: 定常タスク
description: task template
title: "xxx"

body:
  - type: textarea
    id: why
    attributes:
      label: Why (Background & Purpose)
      description: このタスクを行う理由を明確に記述してください。何故この対応が必要なのか?ビジネス的・技術的背景は?何を改善・解決したいのか?
    validations:
      required: true
  - type: textarea
    id: goals
    attributes:
      label: Goals (Objectives)
      description: 何を達成すれば、このタスクが完了したとみなせるか明確に記述してください。定量的・定性的な基準があると良いです。
    validations:
      required: true
  - type: textarea
    id: non-goals
    attributes:
      label: Non Goals (Out of Scope)
      description: 誤解を防ぐため、このタスクでは対応しないことを明記してください。
    validations:
      required: false
  - type: textarea
    id: todo
    attributes:
      label: To-do (Action Items)
      description: このタスクで対応する内容を具体的に記載してください。
      value: "- [ ]"
    validations:
      required: false
  - type: textarea
    id: notes
    attributes:
      label: Notes (Additional Information)
      description: その他関連情報、参考リンク、議論の経緯などを記載してください。
    validations:
      required: false

このテンプレートを使うことで、タスクの背景や目的が明確になり、チームメンバー全員ががタスクの意味を理解しやすくなりました。

2.割り込みタスク

割り込みタスクは、スプリント途中で発生する緊急対応や予期せぬ作業を記録するためのテンプレートです。チームメンバーが活動の中で気づいた改善活動や細かい対応、チーム外とのコミュニケーションで必要になった対応をまとめて記載するために使用しています。

定常タスクと比べてシンプルな構成にしており、To-doとNotesのみを記載する形にしています。スプリント計画時には想定していなかった作業を素早く記録し、後から振り返れるようにすることが目的です。

name: 割り込みタスク
description: task template
title: "${スプリント}: 割り込み対応 ${担当者名}"

body:
  - type: textarea
    id: todo
    attributes:
      label: To-do (Action Items)
      description: このタスクで対応する内容を具体的に記載してください。
      value: "- [ ]"
    validations:
      required: false
  - type: textarea
    id: notes
    attributes:
      label: Notes (Additional Information)
      description: その他関連情報、参考リンク、議論の経緯などを記載してください。
    validations:
      required: false

割り込みタスクは緊急性が高いことが多いため、必要最小限の情報で素早く作成できるようにしました。タイトルにスプリント番号と担当者名を含めることで、後から振り返る際にも整理しやすくなっています。

なお、チーム外からの割り込み(対応リクエスト)については、別途Slack Workflowからタスクを作成できるようにしています。これにより、チーム外のメンバーからの依頼を受けた際も、スムーズにタスク化できる体制を整えました。

Issueの運用状態チェック

GitHub Projectsでタスクを適切に運用するため、想定されていない状態のIssueを定期的に検出し、Slackに通知するGitHub Actionsを導入しました。タスク管理の品質を保ちつつ、手動でのチェック作業を削減することが目的です。

この仕組みでは、以下の2つの状態をチェックしています。

  1. Priorityが設定されていないIssue: タスクの優先順位が不明確な状態を防ぐため
  2. マイルストーンとステータスの不整合: マイルストーン(スプリント)が設定されているのにステータスがIdea/Backlogになっているタスクを検出

1.チェック処理の実装

以下はIssueを分析してチェック対象を抽出する処理です。実装にはPythonを使用し、GraphQL APIを利用してIssueとProject情報を取得しています。

def analyze_issues(issues):
    no_priority_issues = []
    milestone_with_wrong_status_issues = []

    # RenovateのDependency Dashboardなど対象外とするIssueを定義しています
    EXCLUDED_ISSUE_NUMBERS = [123]

    for issue in issues:
        if issue['number'] in EXCLUDED_ISSUE_NUMBERS:
            continue

        if not issue['projectItems']['nodes']:
            issues_without_projects += 1
            continue

        for project_item in issue['projectItems']['nodes']:
            has_priority = False
            status = None
            milestone = None

            for field_value in project_item['fieldValues']['nodes']:
                if field_value and 'field' in field_value and field_value['field']:
                    field_name = field_value['field'].get('name')

                    if field_name == 'Priority':
                        has_priority = True

                    if field_name == 'Status' and 'name' in field_value:
                        status = field_value['name']

                if field_value and 'milestone' in field_value and field_value['milestone']:
                    milestone = field_value['milestone']['title']

            if not has_priority:
                no_priority_issues.append({
                    'number': issue['number'],
                    'title': issue['title'],
                    'url': issue['url']
                })

            if milestone and status in ['Idea', 'Backlog']:
                milestone_with_wrong_status_issues.append({
                    'number': issue['number'],
                    'title': issue['title'],
                    'url': issue['url'],
                    'status': status,
                    'milestone': milestone
                })

    return no_priority_issues, milestone_with_wrong_status_issues

この処理では、各IssueのProject情報を取得し、PriorityフィールドやStatusフィールドの値を確認しています。特定のIssue番号は除外リストで管理できるようにしています。これは、Renovate Dependency Dashboardなどタスクと関係ないIssueを除外するためです。

2.GitHub Actionsワークフロー

GitHub Actionsのワークフローは、毎週月曜日の夕方(日本時間18時)にチェック処理を自動実行し、問題が見つかった場合のみSlackに通知されます。

name: Check GitHub Project Issues

on:
  schedule:
    - cron: "0 9 * * 1"
  workflow_dispatch:

jobs:
  check-issues:
    runs-on: ubuntu-latest
    steps:
      - name: Generate GitHub App Token
        id: generate-token
        uses: actions/create-github-app-token@v2.1.4
        with:
          app-id: ${{ secrets.APP_ID }}
          private-key: ${{ secrets.PRIVATE_KEY }}

      - name: Checkout repository
        uses: actions/checkout@v4.3.0

      - name: Setup Python
        uses: actions/setup-python@v5.6.0
        with:
          python-version: "3.11"

      - name: Check Project Issues
        id: check
        env:
          GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
          REPO_OWNER: ${{ github.repository_owner }}
          REPO_NAME: ${{ github.event.repository.name }}
        run: python .github/scripts/check-issues.py

      - name: Send Slack notification
        if: steps.check.outputs.has_issues == 'true'
        uses: slackapi/slack-github-action@v2.0.0
        with:
          webhook: ${{ secrets.SLACK_WEBHOOK_ISSUE_NOTIFICATION }}
          webhook-type: incoming-webhook
          payload: |
            {
              "text": "GitHub Project運用チェック結果",
              "blocks": [
                {
                  "type": "header",
                  "text": {
                    "type": "plain_text",
                    "text": "⚠️ GitHub Project運用チェック結果"
                  }
                },
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "${{ steps.check.outputs.message }}"
                  }
                }
              ]
            }

この仕組みにより、タスク管理の品質を保ちつつ、手動でのチェック作業を削減しています。また、workflow_dispatchを設定することで、必要に応じて手動実行も可能にしました。

スプリントの自動引き継ぎ

スプリントをマイルストーンで管理しているため、マイルストーンがクローズされた際に未完了のIssueを別のマイルストーン(次のスプリント)に自動で移動させる仕組みを導入しました。これにより、スプリントの終わりに手作業でタスクを整理する手間を削減できます。

実装では、マイルストーン名がSprint-<number>というパターンに従っていることを前提に、次のスプリント番号を計算して移動先マイルストーンを特定しています。

name: Move Issues to Next Milestone on Milestone Close

on:
  milestone:
    types: [closed]

jobs:
  move-overdue-issues:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      issues: write
    steps:
      - name: Move issues from closed milestone to next milestone
        uses: actions/github-script@v7.0.1
        with:
          script: |
            const { owner, repo } = context.repo;
            const closedMilestone = context.payload.milestone;

            try {
              const sprintMatch = closedMilestone.title.match(/^Sprint-(\d+)$/);
              if (!sprintMatch) {
                console.log(`Closed milestone "${closedMilestone.title}" doesn't match Sprint-<number> pattern. No action needed.`);
                return;
              }

              console.log(`Processing closed milestone: "${closedMilestone.title}"`);

              const issues = await github.rest.issues.listForRepo({
                owner,
                repo,
                milestone: closedMilestone.number,
                state: 'open'
              });

              if (issues.data.length === 0) {
                console.log(`No open issues found in closed milestone "${closedMilestone.title}"`);
                return;
              }

              console.log(`Found ${issues.data.length} open issues in closed milestone "${closedMilestone.title}"`);

              const currentSprintNumber = parseInt(sprintMatch[1]);
              const nextSprintNumber = currentSprintNumber + 1;
              const nextMilestoneName = `Sprint-${nextSprintNumber}`;

              const milestones = await github.rest.issues.listMilestones({
                owner,
                repo,
                state: 'open'
              });

              const nextMilestone = milestones.data.find(m => m.title === nextMilestoneName);
              if (!nextMilestone) {
                console.log(`Next milestone "${nextMilestoneName}" not found. Cannot move issues from "${closedMilestone.title}"`);
                return;
              }

              console.log(`Moving issues from "${closedMilestone.title}" to "${nextMilestoneName}"`);

              for (const issue of issues.data) {
                try {
                  await github.rest.issues.update({
                    owner,
                    repo,
                    issue_number: issue.number,
                    milestone: nextMilestone.number
                  });

                  console.log(`Moved issue #${issue.number} "${issue.title}" from "${closedMilestone.title}" to "${nextMilestoneName}"`);
                } catch (error) {
                  console.error(`Failed to move issue #${issue.number}:`, error.message);
                }
              }

              console.log(`Completed moving issues from closed milestone "${closedMilestone.title}" to "${nextMilestoneName}"`);

            } catch (error) {
              console.error('Error processing closed milestone:', error);
              throw new Error(`Failed to process closed milestone: ${error.message}`);
            }

この処理では、クローズされたマイルストーンに紐づく未完了Issueを取得し、次のスプリント番号のマイルストーンに自動的に移動させています。

この他にも、タスクが作成された時にタスクのタイトルと作成者をSlackに通知する仕組みも導入しており、チーム全体でタスクの動きを把握しやすくしています。

AIによるタスクの効率化

タスクの検討段階でAIを活用するため、Claude Code Actionを導入しました。
これにより、Issue内でClaudeを直接利用し、タスクの背景整理や具体的な対応内容のブレインストーミングを効率的に行えるようになりました。

私たちのチームではプロダクトオーナーが不在のため、スクラム的なアプローチでチーム全員がタスクの優先順位付けやリファインメントを行っています。このプロセスにAIを組み込むことで、リファインメントプロセスを加速させ、より迅速かつ効果的な意思決定が可能になると期待しています。

さいごに

NotionDBからGitHub Projectsへのタスク管理移行について紹介しました。Issue Template、GitHub Actions、Claude Codeを組み合わせることで、手作業を削減できました。

今回の移行では、自作のマイグレーションツールを使ってNotionからGitHubへデータを移行しました。

https://github.com/gr1m0h/notion-to-github-migrator

Notionは引き続きドキュメント管理ツールとして活用し、GitHubをタスク管理と開発の中心に据えることで、チーム全体の生産性を向上できたと感じています。今後も、GitHub Actionsによる自動化の拡充やAI活用の深化など、継続的な改善を進めていきたいと思います。

弊社のSREに興味を持っていただけた方は、以下のリンクからお気軽にご連絡ください!私のX(Twitter)アカウントでもDMを受け付けています。
副業や転職をお考えの方だけでなく、気軽に話を聞きたいという方も大歓迎です!

https://recruit.luup.sc/

Luup Developers Blog

Discussion