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

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

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

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

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

## 自動化の全体像

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

```mermaid
%%{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 を操作するための認証トークンを取得します。 
   [こちら](https://docs.github.com/ja/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) を参照して、必要な権限を持つトークンを生成してください。
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" フィールドを追加しておきます。


## スクリプトの詳細解説

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

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

```python
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 クライアントの初期化

```python
# 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` ライブラリを使用して、ログを出力するための設定を行います。

### 処理済みコミットの管理

```python
# 処理済みコミットを保存するファイル
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 の取得

```python
# プロジェクト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 の自動生成

```python
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 に追加

```python
# プロジェクトの各フィールドの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 の情報の取得

```python
    # 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 の作成

```python
    # 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 の取得

```python
    # 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 の取得

```python
# プロジェクトの各フィールドの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 への追加

```python
    # 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 に設定

```python
    # 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 の開始日と終了日を設定

```python
    # 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 を削除

```python
    # 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 を削除します。


### メイン処理

```python
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 に追加します。
        - 処理したコミットを記録します。
    - コミット日時が指定日時以前の場合はスキップします。



## 全体コード

```python

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 プロジェクト管理の効率を大幅に向上させ、開発に集中できる環境を構築できます。ぜひ、今回の内容を参考に、ご自身のプロジェクトにも導入してみてください。