GitHubプロジェクトとPythonを連携!コミットからIssue自動生成&ボード管理を完全自動化!

プロジェクト管理

GitHub でのプロジェクト管理、もっと効率化したいと思いませんか?

本記事では、Python と GitHub API を駆使して、コミット情報から自動的に Issue を作成し、指定のプロジェクトボードへ追加するだけでなく、 ステータス変更や期日設定までをも自動化する 方法を徹底解説します。

しかも、GitHub Project の Draft Issue 機能を使って、より安全に Issue を管理するテクニックも紹介します。

この自動化により、面倒な作業ログ記録や進捗管理の手間を大幅に削減し、開発に集中できる環境を手に入れましょう!

自動化の全体像

今回の自動化スクリプトでは、以下の流れで処理を行います。

%%{init: { 'theme': 'base', 'themeVariables': { 'primaryColor': '#024959', 'primaryTextColor': '#F2C12E', 'primaryBorderColor': '#024959', 'lineColor': '#A1A2A6', 'secondaryColor': '#F2AE30', 'tertiaryColor': '#593E25', 'textColor': '#A1A2A6', 'fontSize': '20px' } } }%% graph TD A[コミット取得] --> B{コミットが処理済み?} B -- Yes --> C[処理スキップ] B -- No --> D{コミット日時が指定日時以降?} D -- Yes --> E[Draft Issueを自動生成] E --> F[Draft IssueをProjectに追加] F --> G[Draft IssueをIssueに変換] G --> H[Issueのステータス、開始日、終了日を更新] H --> I[ProjectからDraft Issueを削除] I --> J[処理済みコミットを記録] D -- No --> C
  1. GitHub リポジトリから最近のコミットを取得する
  2. 各コミットについて、過去に処理済みかどうかを確認する
  3. 未処理のコミットの場合、コミット日時が指定日時以降かどうかを判定する
  4. 判定を満たすコミットの場合、コミット情報に基づいて Draft Issue を自動生成する
  5. 生成した Draft Issue を指定の GitHub Project へ追加する
  6. Draft Issue を正式な Issue に変換する
  7. Issue のステータスを "Done" に設定し、開始日と終了日をコミット日時に設定する
  8. Project から Draft Issue を削除する
  9. 処理済みのコミットとして記録する

準備

自動化スクリプトを実行する前に、以下の準備が必要です。

  1. Python 環境のセットアップ: Python がインストールされ、必要なライブラリをインストールします。 bash pip install requests github3.py python-dotenv loguru csv
  2. GitHub Personal Access Token の取得: GitHub API を操作するための認証トークンを取得します。 こちら を参照して、必要な権限を持つトークンを生成してください。
  3. .env ファイルの作成: 取得した GitHub Personal Access Token やリポジトリ名、プロジェクト番号などの情報を .env ファイルに記述します。 GITHUB_TOKEN=your_github_token REPO_NAME=your_username/your_repository_name PROJECT_NUMBER=your_project_number
  4. GitHub Project の準備: Issue を追加する対象の GitHub Project を作成し、"Status", "Start date", "End date" フィールドを追加しておきます。

スクリプトの詳細解説

それでは、スクリプトの各部分について詳しく見ていきましょう。

ライブラリのインポートと環境変数の読み込み

import requests
from github import Github
from datetime import datetime, timedelta
from loguru import logger
import json
from dotenv import load_dotenv
import os
import csv

# .envファイルから環境変数を読み込む
load_dotenv()

# GitHub認証情報
github_token = os.getenv("GITHUB_TOKEN")
repo_name = os.getenv("REPO_NAME", "Sunwood-ai-labs/Yukihiko")
project_number = os.getenv("PROJECT_NUMBER", "9")

最初に、必要なライブラリをインポートします。

  • requests: HTTP リクエストを送信して GitHub API と通信するために使用します。
  • github: GitHub API を Python から簡単に利用するためのライブラリです。
  • datetime: 日付と時刻を扱うためのライブラリです。コミット日時を取得したり、指定日時と比較したりする際に使用します。
  • loguru: ログ出力を行うためのライブラリです。処理状況を分かりやすく記録することができます。
  • json: JSON 形式のデータを扱うためのライブラリです。GitHub API との通信では、JSON 形式でデータのやり取りを行います。
  • dotenv: .env ファイルから環境変数を安全に読み込むためのライブラリです。
  • os: OS レベルの操作を行うためのライブラリです。ファイルの有無を確認する際に使用します。
  • csv: CSV ファイルを読み書きするためのライブラリです。処理済みコミットの情報を CSV ファイルに保存し、次回以降の処理で重複を防ぎます。

次に、.env ファイルから GitHub のアクセストークン、リポジトリ名、プロジェクト番号を読み込みます。これらの情報は、GitHub API を利用する際に必要となります。

GitHub API クライアントの初期化

# GitHubクライアントの初期化
g = Github(github_token)
repo = g.get_repo(repo_name)

# GraphQL APIエンドポイント
api_url = "https://api.github.com/graphql"

# リクエストヘッダー
headers = {
    "Authorization": f"Bearer {github_token}",
    "Content-Type": "application/json"
}

# loguruの設定
logger.add("github_project_sync.log", rotation="500 MB")

ここでは、GitHub API を利用するための準備を行います。

  • g = Github(github_token): GitHub API を利用するためのクライアントオブジェクトを作成します。
  • repo = g.get_repo(repo_name): 操作対象のリポジトリオブジェクトを取得します。
  • api_url: GitHub API の GraphQL のエンドポイントを定義します。
  • headers: GitHub API リクエストに必要なヘッダー情報を定義します。Authorization には、取得した GitHub Personal Access Token を設定します。
  • logger.add(...): loguru ライブラリを使用して、ログを出力するための設定を行います。

処理済みコミットの管理

# 処理済みコミットを保存するファイル
PROCESSED_COMMITS_FILE = "processed_commits.csv"

# 処理済みコミットを読み込む関数
def load_processed_commits():
    processed_commits = set()
    if os.path.exists(PROCESSED_COMMITS_FILE):
        with open(PROCESSED_COMMITS_FILE, 'r', newline='') as f:
            reader = csv.reader(f)
            next(reader)  # ヘッダーをスキップ
            for row in reader:
                processed_commits.add(row[0])  # SHA列を読み込む
    return processed_commits

# 処理済みコミットを保存する関数
def save_processed_commit(sha, issue_url):
    file_exists = os.path.exists(PROCESSED_COMMITS_FILE)
    with open(PROCESSED_COMMITS_FILE, 'a', newline='') as f:
        writer = csv.writer(f)
        if not file_exists:
            writer.writerow(['SHA', 'Issue URL', 'Processed At'])  # ヘッダーを書き込む
        writer.writerow([sha, issue_url, datetime.now().isoformat()])

ここでは、一度処理したコミットを記録することで、スクリプトを再実行した際に同じコミットから Issue が重複して作成されるのを防ぎます。

  • PROCESSED_COMMITS_FILE: 処理済みコミットの情報を保存する CSV ファイル名を定義します。
  • load_processed_commits(): CSV ファイルから処理済みコミットの SHA を読み込み、セットとして返します。
  • save_processed_commit(sha, issue_url): 新たに処理したコミットの SHA と、作成した Issue の URL を CSV ファイルに追記します。

GitHub Project ID の取得

# プロジェクトIDを取得
def get_project_id():
    logger.info("プロジェクトIDを取得しています...")
    query = f"""
    query {{
      user(login: "{repo.owner.login}") {{
        projectV2(number: {project_number}) {{
          id
        }}
      }}
    }}
    """
    response = requests.post(api_url, json={"query": query}, headers=headers)
    project_id = response.json()['data']['user']['projectV2']['id']
    logger.info(f"プロジェクトID: {project_id}")
    return project_id

get_project_id 関数は、GitHub Project の番号をもとに、そのプロジェクトの一意な ID を取得します。 Issue をプロジェクトに追加するには、この ID が必要となります。

Draft Issue の自動生成

def escape_string(s):
    """GraphQLクエリ用に文字列をエスケープする"""
    return json.dumps(s)[1:-1]  # json.dumpsでエスケープし、前後のダブルクォートを除去

# Draft Issueを作成してProjectに追加
def create_draft_issue(project_id, title, body):
    logger.info(f"Draft Issue を作成しています: {title}")

    # タイトルと本文をエスケープ
    escaped_title = escape_string(title)
    escaped_body = escape_string(body)

    mutation = f"""
    mutation {{
      addProjectV2DraftIssue(input: {{projectId: "{project_id}", title: "{escaped_title}", body: "{escaped_body}"}}) {{
        projectItem {{
          id
        }}
      }}
    }}
    """
    logger.debug(f"mutation:\n{mutation}")
    response = requests.post(api_url, json={"query": mutation}, headers=headers)
    logger.debug(f"response.json:\n{response.json()}")

    if 'errors' in response.json():
        logger.error(f"GraphQL APIからエラーレスポンスを受け取りました: {response.json()['errors']}")
        return None

    draft_issue_id = response.json()['data']['addProjectV2DraftIssue']['projectItem']['id']
    logger.info(f"Draft Issue を Project に追加しました: {title} (ID: {draft_issue_id})")
    return draft_issue_id

create_draft_issue 関数は、指定されたコミット情報から Draft Issue を作成し、指定の GitHub Project に追加します。 Draft Issue を作成することで、正式な Issue として公開する前に、内容を確認したり、誤って作成した場合でも簡単に削除したりすることができます。

Draft Issue を Issue に変換し、Project に追加

# プロジェクトの各フィールドのIDを取得する関数
def get_project_fields(project_id):
    # ... (後述)

# Draft IssueをIssueに変換し、Projectに追加
def convert_to_issue(project_id, draft_issue_id, commit_date):
    logger.info(f"Draft Issue (ID: {draft_issue_id}) を Issue に変換しています...")

    # 1. Draft Issueの情報を取得
    # ... (後述)

    # 2. Issueを作成
    # ... (後述)

    # 3. 作成したIssueのノードIDを取得 (GraphQL を使用)
    # ... (後述)

    # 4. Status フィールドのID、doneの値、Start dateとEnd dateフィールドのIDを取得
    status_field_id, done_value_id, start_date_field_id, end_date_field_id = get_project_fields(project_id)

    if not status_field_id or not done_value_id or not start_date_field_id or not end_date_field_id:
        logger.error("必要なフィールドのIDを取得できませんでした。")
        return

    # 5. Issue を Project に追加
    # ... (後述)

    # 6. Issue の Status を Done に設定
    # ... (後述)

    # 7. Issue の Start date と End date を設定
    # ... (後述)

    # 8. ProjectからDraft Issueを削除
    # ... (後述)

    issue_url = created_issue.html_url
    logger.info(f"Issueに変換しました: {issue_url}")
    return issue_url

convert_to_issue 関数は、Draft Issue を正式な Issue に変換し、GitHub Project に追加します。

Draft Issue の情報の取得

    # 1. Draft Issueの情報を取得
    query = f"""
    query {{
        node(id: "{draft_issue_id}") {{
            ... on ProjectV2Item {{
                content {{
                    ... on DraftIssue {{
                        title
                        body
                    }}
                }}
            }}
        }}
    }}
    """
    response = requests.post(api_url, json={"query": query}, headers=headers)
    issue_data = response.json()['data']['node']['content']

Draft Issue のタイトルと本文を取得します。

Issue の作成

    # 2. Issueを作成
    issue_title = issue_data['title']
    issue_body = issue_data['body']
    created_issue = repo.create_issue(title=issue_title, body=issue_body)
    logger.debug(f"created_issue: {created_issue}")

取得した Draft Issue の情報をもとに、repo.create_issue を使用して正式な Issue を作成します。

作成した Issue のノード ID の取得

    # 3. 作成したIssueのノードIDを取得 (GraphQL を使用)
    query = f"""
    query {{
        repository(owner: "{repo.owner.login}", name: "{repo.name}") {{
            issue(number: {created_issue.number}) {{
                id # node_id は id と同じです 
            }}
        }}
    }}
    """
    response = requests.post(api_url, json={"query": query}, headers=headers)
    issue_node_id = response.json()['data']['repository']['issue']['id']

作成した Issue のノード ID を取得します。ノード ID は、GitHub 上のオブジェクトを一意に識別するためのものです。

Project フィールド ID の取得

# プロジェクトの各フィールドのIDを取得する関数
def get_project_fields(project_id):
    logger.info("プロジェクトフィールドのIDを取得しています...")
    query = f"""
    query {{
      node(id: "{project_id}") {{
        ... on ProjectV2 {{
          fields(first: 20) {{
            nodes {{
              ... on ProjectV2SingleSelectField {{
                id
                name
                options {{
                  id
                  name
                }}
              }}
              ... on ProjectV2Field {{
                id
                name
              }}
            }}
          }}
        }}
      }}
    }}
    """
    response = requests.post(api_url, json={"query": query}, headers=headers)

    fields = response.json()['data']['node']['fields']['nodes']

    status_field_id = None
    done_value_id = None
    start_date_field_id = None
    end_date_field_id = None

    for field in fields:
        if field['name'] == 'Status':
            status_field_id = field['id']
            for option in field['options']:
                if option['name'] == 'Done':
                    done_value_id = option['id']
        elif field['name'] == 'Start date':
            start_date_field_id = field['id']
        elif field['name'] == 'End date':
            end_date_field_id = field['id']

    logger.info(f"Status フィールド ID: {status_field_id}")
    logger.info(f"Done 値 ID: {done_value_id}")
    logger.info(f"Start date フィールド ID: {start_date_field_id}")
    logger.info(f"End date フィールド ID: {end_date_field_id}")
    return status_field_id, done_value_id, start_date_field_id, end_date_field_id

get_project_fields 関数は、Project 内の "Status", "Done", "Start date", "End date" フィールドの ID を取得します。これらの ID は、Issue を Project に追加したり、ステータスや日付を更新したりする際に使用します。

Issue の Project への追加

    # 5. Issue を Project に追加
    mutation = f"""
    mutation {{
      addProjectV2ItemById(input: {{
        projectId: "{project_id}",
        contentId: "{issue_node_id}"
      }}) {{
        item {{
          id
        }}
      }}
    }}
    """
    response = requests.post(api_url, json={"query": mutation}, headers=headers)

    # 追加された Issue の ID を取得
    added_item_id = response.json()['data']['addProjectV2ItemById']['item']['id']
    logger.debug(f"addProjectV2ItemById(input: {added_item_id}")

取得した Issue のノード ID を使用して、Issue を Project に追加します。

Issue のステータスを Done に設定

    # 6. Issue の Status を Done に設定
    mutation = f"""
    mutation {{
    updateProjectV2ItemFieldValue(
        input: {{
        projectId: "{project_id}"
        # 追加された Issue の ID を指定
        itemId: "{added_item_id}"
        fieldId: "{status_field_id}"
        value: {{
            singleSelectOptionId: "{done_value_id}"
        }}
        }}
    ) {{
        projectV2Item {{
        id
        fieldValues(first: 10) {{
            nodes {{
            ... on ProjectV2ItemFieldSingleSelectValue {{
                field {{
                ... on ProjectV2FieldCommon {{
                    name
                }}
                }}
                name
            }}
            }}
        }}
        }}
    }}
    }}
    """
    response = requests.post(api_url, json={"query": mutation}, headers=headers)

    # レスポンスの詳細をログ出力
    logger.debug(f"updateProjectV2ItemFieldValue response: {response.json()}")

追加した Issue のステータスを "Done" に更新します。

Issue の開始日と終了日を設定

    # 7. Issue の Start date と End date を設定
    formatted_date = commit_date.strftime("%Y-%m-%d")
    mutation = f"""
    mutation {{
      updateProjectV2ItemFieldValue(
        input: {{
          projectId: "{project_id}"
          itemId: "{added_item_id}"
          fieldId: "{start_date_field_id}"
          value: {{
            date: "{formatted_date}"
          }}
        }}
      ) {{
        projectV2Item {{
          id
        }}
      }}
    }}
    """
    response = requests.post(api_url, json={"query": mutation}, headers=headers)
    logger.debug(f"Set Start date response: {response.json()}")

    mutation = f"""
    mutation {{
      updateProjectV2ItemFieldValue(
        input: {{
          projectId: "{project_id}"
          itemId: "{added_item_id}"
          fieldId: "{end_date_field_id}"
          value: {{
            date: "{formatted_date}"
          }}
        }}
      ) {{
        projectV2Item {{
          id
        }}
      }}
    }}
    """
    response = requests.post(api_url, json={"query": mutation}, headers=headers)
    logger.debug(f"Set End date response: {response.json()}")

Issue の開始日と終了日をコミット日時と同じ日付に設定します。

Project から Draft Issue を削除

    # 8. ProjectからDraft Issueを削除
    mutation = f"""
    mutation {{
      deleteProjectV2Item(input: {{projectId: "{project_id}", itemId: "{draft_issue_id}"}}) {{
        deletedItemId
      }}
    }}
    """
    response = requests.post(api_url, json={"query": mutation}, headers=headers)
    logger.info(f"Project から Draft Issue (ID: {draft_issue_id}) を削除しました")

正式な Issue が作成され、Project に追加されたため、Draft Issue は不要になります。 deleteProjectV2Item を使用して Draft Issue を削除します。

メイン処理

if __name__ == "__main__":
    logger.info("処理を開始します。")

    # GitHub tokenが設定されていることを確認
    if not github_token:
        logger.error("GITHUB_TOKEN が設定されていません。.env ファイルを確認してください。")
        exit(1)

    # 最近のコミットを取得 (例: 過去30日間)
    commits = repo.get_commits(since=datetime.now() - timedelta(days=30))

    # Issueとして投稿するコミットの開始日時
    since_datetime = datetime(2023, 12, 1)  # 例: 2023年12月1日以降のコミット

    # Project IDを取得
    project_id = get_project_id()

    # 処理済みコミットを読み込む
    processed_commits = load_processed_commits()

    # 各コミットに対して処理
    for commit in commits:
        logger.info(f"コミット {commit.sha} を処理しています...")

        # コミットがすでに処理済みの場合はスキップ
        if commit.sha in processed_commits:
            logger.info(f"コミット {commit.sha} は既に処理済みのためスキップします。")
            continue

        # コミット日時が指定日時以降の場合のみIssueを作成
        if commit.commit.author.date >= since_datetime:
            body = "\n".join(commit.commit.message.splitlines()[1:])
            issue_title = f"{commit.commit.message.splitlines()[0]}"
            if ("Merge" in issue_title):
                continue

            # Issueのタイトルと本文を作成
            issue_body = f"""
            {body}

            # info

            - コミットSHA: {commit.sha}
            - コミット日時: {commit.commit.author.date}
            - コミット作成者: {commit.commit.author.name} <{commit.commit.author.email}>

                    """

            # Draft Issueを作成してProjectに追加
            draft_issue_id = create_draft_issue(project_id, issue_title, issue_body)

            # Draft IssueをIssueに変換し、Projectに追加
            issue_url = convert_to_issue(project_id, draft_issue_id, commit.commit.author.date)

            # 処理したコミットを記録
            save_processed_commit(commit.sha, issue_url)
        else:
            logger.info(f"コミット {commit.sha} は指定日時以前のためスキップします。")

    logger.info("処理が完了しました。")

メイン処理では、以下の流れでコミットを処理します。

  1. GitHub トークンが設定されていることを確認します。
  2. 最近のコミットを取得します。
  3. Issue として投稿するコミットの開始日時を指定します。
  4. Project ID を取得します。
  5. 処理済みコミットを読み込みます。
  6. 各コミットに対して以下の処理を行います。
    • コミットがすでに処理済みの場合はスキップします。
    • コミット日時が指定日時以降の場合のみ Issue を作成します。
      • Issue のタイトルと本文を作成します。
      • Draft Issue を作成して Project に追加します。
      • Draft Issue を Issue に変換し、Project に追加します。
      • 処理したコミットを記録します。
    • コミット日時が指定日時以前の場合はスキップします。

全体コード


import requests
from github import Github
from datetime import datetime, timedelta
from loguru import logger
import json
from dotenv import load_dotenv
import os
import csv

# .envファイルから環境変数を読み込む
load_dotenv()

# GitHub認証情報
github_token = os.getenv("GITHUB_TOKEN")
repo_name = os.getenv("REPO_NAME", "Sunwood-ai-labs/Yukihiko")
project_number = os.getenv("PROJECT_NUMBER", "9")

# GitHubクライアントの初期化
g = Github(github_token)
repo = g.get_repo(repo_name)

# GraphQL APIエンドポイント
api_url = "https://api.github.com/graphql"

# リクエストヘッダー
headers = {
    "Authorization": f"Bearer {github_token}",
    "Content-Type": "application/json"
}

# loguruの設定
logger.add("github_project_sync.log", rotation="500 MB")

# 処理済みコミットを保存するファイル
PROCESSED_COMMITS_FILE = "processed_commits.csv"

# 処理済みコミットを読み込む関数
def load_processed_commits():
    processed_commits = set()
    if os.path.exists(PROCESSED_COMMITS_FILE):
        with open(PROCESSED_COMMITS_FILE, 'r', newline='') as f:
            reader = csv.reader(f)
            next(reader)  # ヘッダーをスキップ
            for row in reader:
                processed_commits.add(row[0])  # SHA列を読み込む
    return processed_commits

# 処理済みコミットを保存する関数
def save_processed_commit(sha, issue_url):
    file_exists = os.path.exists(PROCESSED_COMMITS_FILE)
    with open(PROCESSED_COMMITS_FILE, 'a', newline='') as f:
        writer = csv.writer(f)
        if not file_exists:
            writer.writerow(['SHA', 'Issue URL', 'Processed At'])  # ヘッダーを書き込む
        writer.writerow([sha, issue_url, datetime.now().isoformat()])

# プロジェクトIDを取得
def get_project_id():
    logger.info("プロジェクトIDを取得しています...")
    query = f"""
    query {{
      user(login: "{repo.owner.login}") {{
        projectV2(number: {project_number}) {{
          id
        }}
      }}
    }}
    """
    response = requests.post(api_url, json={"query": query}, headers=headers)
    project_id = response.json()['data']['user']['projectV2']['id']
    logger.info(f"プロジェクトID: {project_id}")
    return project_id

def escape_string(s):
    """GraphQLクエリ用に文字列をエスケープする"""
    return json.dumps(s)[1:-1]  # json.dumpsでエスケープし、前後のダブルクォートを除去

# Draft Issueを作成してProjectに追加
def create_draft_issue(project_id, title, body):
    logger.info(f"Draft Issue を作成しています: {title}")

    # タイトルと本文をエスケープ
    escaped_title = escape_string(title)
    escaped_body = escape_string(body)

    mutation = f"""
    mutation {{
      addProjectV2DraftIssue(input: {{projectId: "{project_id}", title: "{escaped_title}", body: "{escaped_body}"}}) {{
        projectItem {{
          id
        }}
      }}
    }}
    """
    logger.debug(f"mutation:\n{mutation}")
    response = requests.post(api_url, json={"query": mutation}, headers=headers)
    logger.debug(f"response.json:\n{response.json()}")

    if 'errors' in response.json():
        logger.error(f"GraphQL APIからエラーレスポンスを受け取りました: {response.json()['errors']}")
        return None

    draft_issue_id = response.json()['data']['addProjectV2DraftIssue']['projectItem']['id']
    logger.info(f"Draft Issue を Project に追加しました: {title} (ID: {draft_issue_id})")
    return draft_issue_id

# プロジェクトの各フィールドのIDを取得する関数
def get_project_fields(project_id):
    logger.info("プロジェクトフィールドのIDを取得しています...")
    query = f"""
    query {{
      node(id: "{project_id}") {{
        ... on ProjectV2 {{
          fields(first: 20) {{
            nodes {{
              ... on ProjectV2SingleSelectField {{
                id
                name
                options {{
                  id
                  name
                }}
              }}
              ... on ProjectV2Field {{
                id
                name
              }}
            }}
          }}
        }}
      }}
    }}
    """
    response = requests.post(api_url, json={"query": query}, headers=headers)

    fields = response.json()['data']['node']['fields']['nodes']

    status_field_id = None
    done_value_id = None
    start_date_field_id = None
    end_date_field_id = None

    for field in fields:
        if field['name'] == 'Status':
            status_field_id = field['id']
            for option in field['options']:
                if option['name'] == 'Done':
                    done_value_id = option['id']
        elif field['name'] == 'Start date':
            start_date_field_id = field['id']
        elif field['name'] == 'End date':
            end_date_field_id = field['id']

    logger.info(f"Status フィールド ID: {status_field_id}")
    logger.info(f"Done 値 ID: {done_value_id}")
    logger.info(f"Start date フィールド ID: {start_date_field_id}")
    logger.info(f"End date フィールド ID: {end_date_field_id}")
    return status_field_id, done_value_id, start_date_field_id, end_date_field_id

# Draft IssueをIssueに変換し、Projectに追加
def convert_to_issue(project_id, draft_issue_id, commit_date):
    logger.info(f"Draft Issue (ID: {draft_issue_id}) を Issue に変換しています...")
    # 1. Draft Issueの情報を取得
    query = f"""
    query {{
        node(id: "{draft_issue_id}") {{
            ... on ProjectV2Item {{
                content {{
                    ... on DraftIssue {{
                        title
                        body
                    }}
                }}
            }}
        }}
    }}
    """
    response = requests.post(api_url, json={"query": query}, headers=headers)
    issue_data = response.json()['data']['node']['content']

    # 2. Issueを作成
    issue_title = issue_data['title']
    issue_body = issue_data['body']
    created_issue = repo.create_issue(title=issue_title, body=issue_body)
    logger.debug(f"created_issue: {created_issue}")

    # 3. 作成したIssueのノードIDを取得 (GraphQL を使用)
    query = f"""
    query {{
        repository(owner: "{repo.owner.login}", name: "{repo.name}") {{
            issue(number: {created_issue.number}) {{
                id # node_id は id と同じです 
            }}
        }}
    }}
    """
    response = requests.post(api_url, json={"query": query}, headers=headers)
    issue_node_id = response.json()['data']['repository']['issue']['id']

    # 4. Status フィールドのID、doneの値、Start dateとEnd dateフィールドのIDを取得
    status_field_id, done_value_id, start_date_field_id, end_date_field_id = get_project_fields(project_id)

    if not status_field_id or not done_value_id or not start_date_field_id or not end_date_field_id:
        logger.error("必要なフィールドのIDを取得できませんでした。")
        return

    # 5. Issue を Project に追加
    mutation = f"""
    mutation {{
      addProjectV2ItemById(input: {{
        projectId: "{project_id}",
        contentId: "{issue_node_id}"
      }}) {{
        item {{
          id
        }}
      }}
    }}
    """
    response = requests.post(api_url, json={"query": mutation}, headers=headers)

    # 追加された Issue の ID を取得
    added_item_id = response.json()['data']['addProjectV2ItemById']['item']['id']
    logger.debug(f"addProjectV2ItemById(input: {added_item_id}")

    # 6. Issue の Status を Done に設定
    mutation = f"""
    mutation {{
    updateProjectV2ItemFieldValue(
        input: {{
        projectId: "{project_id}"
        # 追加された Issue の ID を指定
        itemId: "{added_item_id}"
        fieldId: "{status_field_id}"
        value: {{
            singleSelectOptionId: "{done_value_id}"
        }}
        }}
    ) {{
        projectV2Item {{
        id
        fieldValues(first: 10) {{
            nodes {{
            ... on ProjectV2ItemFieldSingleSelectValue {{
                field {{
                ... on ProjectV2FieldCommon {{
                    name
                }}
                }}
                name
            }}
            }}
        }}
        }}
    }}
    }}
    """
    response = requests.post(api_url, json={"query": mutation}, headers=headers)

    # レスポンスの詳細をログ出力
    logger.debug(f"updateProjectV2ItemFieldValue response: {response.json()}")

    # ... (既存のコードはそのまま) ...

    # 6. ProjectからDraft Issueを削除
    mutation = f"""
    mutation {{
      deleteProjectV2Item(input: {{projectId: "{project_id}", itemId: "{draft_issue_id}"}}) {{
        deletedItemId
      }}
    }}
    """
    response = requests.post(api_url, json={"query": mutation}, headers=headers)
    logger.info(f"Project から Draft Issue (ID: {draft_issue_id}) を削除しました")

    # 7. Issue の Start date と End date を設定
    formatted_date = commit_date.strftime("%Y-%m-%d")
    mutation = f"""
    mutation {{
      updateProjectV2ItemFieldValue(
        input: {{
          projectId: "{project_id}"
          itemId: "{added_item_id}"
          fieldId: "{start_date_field_id}"
          value: {{
            date: "{formatted_date}"
          }}
        }}
      ) {{
        projectV2Item {{
          id
        }}
      }}
    }}
    """
    response = requests.post(api_url, json={"query": mutation}, headers=headers)
    logger.debug(f"Set Start date response: {response.json()}")

    mutation = f"""
    mutation {{
      updateProjectV2ItemFieldValue(
        input: {{
          projectId: "{project_id}"
          itemId: "{added_item_id}"
          fieldId: "{end_date_field_id}"
          value: {{
            date: "{formatted_date}"
          }}
        }}
      ) {{
        projectV2Item {{
          id
        }}
      }}
    }}
    """
    response = requests.post(api_url, json={"query": mutation}, headers=headers)
    logger.debug(f"Set End date response: {response.json()}")

    issue_url = created_issue.html_url
    logger.info(f"Issueに変換しました: {issue_url}")
    return issue_url

# Status フィールドのIDとdoneの値を取得する関数
def get_status_field_and_done_value(project_id):
    logger.info("Status フィールドと Done 値のIDを取得しています...")
    query = f"""
    query {{
      node(id: "{project_id}") {{
        ... on ProjectV2 {{
          fields(first: 10) {{
            nodes {{
              ... on ProjectV2SingleSelectField {{
                id
                name
                options {{
                  id
                  name
                }}
              }}
            }}
          }}
        }}
      }}
    }}
    """
    response = requests.post(api_url, json={"query": query}, headers=headers)

    # エラーハンドリング
    if 'errors' in response.json():
        print(response.json()['errors'])
        return None, None

    print(response.json())
    fields = response.json()['data']['node']['fields']['nodes']

    status_field_id = None
    done_value_id = None

    for field in fields:
        # 'name' キーの存在を確認する
        if 'name' in field and field['name'] == 'Status':
            status_field_id = field['id']
            for option in field['options']:
                if option['name'] == 'Done':
                    done_value_id = option['id']
                    break
            break

    logger.info(f"Status フィールド ID: {status_field_id}")
    logger.info(f"Done 値 ID: {done_value_id}")
    return status_field_id, done_value_id

# ------------------------
# メイン処理
#
if __name__ == "__main__":
    logger.info("処理を開始します。")

    # GitHub tokenが設定されていることを確認
    if not github_token:
        logger.error("GITHUB_TOKEN が設定されていません。.env ファイルを確認してください。")
        exit(1)

    # 最近のコミットを取得 (例: 過去30日間)
    commits = repo.get_commits(since=datetime.now() - timedelta(days=30))

    # Issueとして投稿するコミットの開始日時
    since_datetime = datetime(2023, 12, 1)  # 例: 2023年12月1日以降のコミット

    # Project IDを取得
    project_id = get_project_id()

    # 処理済みコミットを読み込む
    processed_commits = load_processed_commits()

    # 各コミットに対して処理
    for commit in commits:
        logger.info(f"コミット {commit.sha} を処理しています...")

        # コミットがすでに処理済みの場合はスキップ
        if commit.sha in processed_commits:
            logger.info(f"コミット {commit.sha} は既に処理済みのためスキップします。")
            continue

        body = "\n".join(commit.commit.message.splitlines()[1:])
        issue_title = f"{commit.commit.message.splitlines()[0]}"
        if ("Merge" in issue_title):
            continue

        # Issueのタイトルと本文を作成
        issue_body = f"""
{body}

# info

- コミットSHA: {commit.sha}
- コミット日時: {commit.commit.author.date}
- コミット作成者: {commit.commit.author.name} <{commit.commit.author.email}>

        """

        # Draft Issueを作成してProjectに追加
        draft_issue_id = create_draft_issue(project_id, issue_title, issue_body)

        # Draft IssueをIssueに変換し、Projectに追加
        issue_url = convert_to_issue(project_id, draft_issue_id, commit.commit.author.date)

        # 処理したコミットを記録
        save_processed_commit(commit.sha, issue_url)

    logger.info("処理が完了しました。")

まとめ

本記事では、Python と GitHub API を活用して、コミット情報から Issue を自動生成し、GitHub Project へ追加する方法を、Draft Issue を利用したより安全な方法を含めて紹介しました。

この自動化により、GitHub プロジェクト管理の効率を大幅に向上させ、開発に集中できる環境を構築できます。ぜひ、今回の内容を参考に、ご自身のプロジェクトにも導入してみてください。

コメント

タイトルとURLをコピーしました