GitHub GraphQL APIでProjectにIssuesを追加して、フィールドを更新する(Estimate, Status)

GitHub GraphQL APIでProjectにIssuesを追加して、フィールドを更新してみました。
2024.06.24

GitHub GraphQL APIでProjectにIssuesを追加してみました。 そのあと、EstimateとStatusとを設定してみます。

おすすめの方

  • GitHub GraphQL APIについて知りたい方
  • GitHub GraphQL APIでProjectにIssuesを追加したい方
  • GitHub GraphQL APIでProjectのItemのフィールドを取得・更新したい方
  • GitHub GraphQL APIで変数を使いたい方

GitHub Projectのフィールド

デフォルトのフィールドを利用します。

01_github_project_field

02_github_project_field

GitHub GraphQL APIでProjectにIssuesを追加するスクリプト

プロジェクト番号は、URLに含まれています。たとえば、「projects/4/views/1」なら、プロジェクト番号は「4」です。

また、プロジェクトが「User or Organization」のどちらにあるかによって、GraphQLのクエリ内容が異なります。 次のコードでは両方記載しています。

app.py

import requests
import json
from enum import StrEnum

ENDPOINT = "https://api.github.com/graphql"

GITHUB_TOKEN = "xxx"

GITHUB_REPOSITORY_OWNER = "yyy"
GITHUB_REPOSITORY_NAME = "zzz"

GITHUB_PROJECT_NUMBER = 4


class StatusName(StrEnum):
    TODO = "Todo"
    IN_PROGRESS = "In Progress"
    DONE = "Done"


def main():
    # リポジトリIDを取得する
    repository_id = get_repository_id(GITHUB_REPOSITORY_OWNER, GITHUB_REPOSITORY_NAME)

    # プロジェクト情報を取得する(User)
    project_id, project_fields = get_project_v2_user(
        GITHUB_REPOSITORY_OWNER, GITHUB_PROJECT_NUMBER
    )

    # プロジェクト情報を取得する(Organization)
    # project_id, project_fields = get_project_v2_organization(GITHUB_REPOSITORY_OWNER, GITHUB_PROJECT_NUMBER)

    # プロジェクトのフィールドIDとオプションIDを取得する
    for item in project_fields:
        if item.get("name", "") == "Estimate":
            estimate_id = item["id"]
        if item.get("name", "") == "Status":
            status_id = item["id"]
            status_options = array_to_dict(item["options"], "name")

    # Issueを作成する
    issue_id = create_issue(
        repository_id,
        "これがタイトルです",
        "これが本文です",
    )

    # Issueをプロジェクトに追加する
    project_item_id = add_issue_to_project(project_id, issue_id)

    # Estimateを更新する
    update_project_v2_item_field_number(project_id, project_item_id, estimate_id, 3)

    # Statusを更新する
    update_project_v2_item_field_single_select_option_id(
        project_id, project_item_id, status_id, status_options[StatusName.TODO]["id"]
    )


def get_repository_id(
    repository_owner,
    repository_name,
):
    query = """
        query getProjectLabels($repositoryOwner:String!, $repositoryName:String!) {
            repository(owner: $repositoryOwner, name: $repositoryName) {
                id
            }
        }
        """
    variables = {
        "repositoryOwner": repository_owner,
        "repositoryName": repository_name,
    }
    resp = requests.post(
        ENDPOINT,
        headers=get_headers(),
        data=json.dumps({"query": query, "variables": variables}),
    )
    body = resp.json()

    return body["data"]["repository"]["id"]


def get_project_v2_user(
    repository_owner,
    number,
):
    query = """
        query getProjectV2($repositoryOwner:String!, $number:Int!) {
            user(login: $repositoryOwner){
                projectV2(number: $number) {
                    id,
                    fields(first: 20) {
                        nodes {
                            ... on ProjectV2Field {
                                id
                                name
                            }
                            ... on ProjectV2SingleSelectField {
                                id
                                name
                                options {
                                    id
                                    name
                                }
                            }
                        }
                    }
                }
            }
        }
        """
    variables = {
        "repositoryOwner": repository_owner,
        "number": number,
    }
    resp = requests.post(
        ENDPOINT,
        headers=get_headers(),
        data=json.dumps({"query": query, "variables": variables}),
    )
    body = resp.json()

    return (
        body["data"]["user"]["projectV2"]["id"],
        body["data"]["user"]["projectV2"]["fields"]["nodes"],
    )


def get_project_v2_organization(
    repository_owner,
    number,
):
    query = """
        query getProjectV2($repositoryOwner:String!, $number:Int!) {
            organization(login: $repositoryOwner){
                projectV2(number: $number) {
                    id,
                    fields(first: 20) {
                        nodes {
                            ... on ProjectV2Field {
                                id
                                name
                            }
                            ... on ProjectV2SingleSelectField {
                                id
                                name
                                options {
                                    id
                                    name
                                }
                            }
                        }
                    }
                }
            }
        }
        """
    variables = {
        "repositoryOwner": repository_owner,
        "number": number,
    }
    resp = requests.post(
        ENDPOINT,
        headers=get_headers(),
        data=json.dumps({"query": query, "variables": variables}),
    )
    body = resp.json()

    return (
        body["data"]["organization"]["projectV2"]["id"],
        body["data"]["organization"]["projectV2"]["fields"]["nodes"],
    )


def create_issue(repository_id, title, body, labels=[]):
    mutation = """
        mutation CreateIssue($repositoryId: ID!, $title: String!, $body: String, $labelIds: [ID!]) {
            createIssue(input: {repositoryId: $repositoryId, title: $title, body: $body, labelIds: $labelIds}) {
                issue {
                    id
                    number
                    title
                    bodyText
                }
            }
        }
        """
    variables = {
        "repositoryId": repository_id,
        "title": title,
        "body": body,
        "labelIds": labels,
    }
    resp = requests.post(
        ENDPOINT,
        headers=get_headers(),
        data=json.dumps({"query": mutation, "variables": variables}),
    )
    body = resp.json()

    return body["data"]["createIssue"]["issue"]["id"]


def add_issue_to_project(project_id, issue_id):
    mutation = """
        mutation AddIssueToProject($projectId: ID!, $contentId: ID!) {
            addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) {
                item {
                    id
                }
            }
        }"""
    variables = {
        "projectId": project_id,
        "contentId": issue_id,
    }
    resp = requests.post(
        ENDPOINT,
        headers=get_headers(),
        data=json.dumps({"query": mutation, "variables": variables}),
    )
    body = resp.json()

    return body["data"]["addProjectV2ItemById"]["item"]["id"]


def update_project_v2_item_field_single_select_option_id(
    project_id, item_id, field_id, value
):
    mutation = """
        mutation updateProjectV2ItemFieldValue($projectId: ID!, $itemId: ID!, $fieldId: ID!, $value: String) {
            updateProjectV2ItemFieldValue(input: {projectId: $projectId, itemId: $itemId, fieldId: $fieldId, value: {singleSelectOptionId: $value}}) {
                projectV2Item {
                    id
                }
            }
        }"""
    variables = {
        "projectId": project_id,
        "itemId": item_id,
        "fieldId": field_id,
        "value": value,
    }
    resp = requests.post(
        ENDPOINT,
        headers=get_headers(),
        data=json.dumps({"query": mutation, "variables": variables}),
    )
    body = resp.json()


def update_project_v2_item_field_number(project_id, item_id, field_id, value):
    mutation = """
        mutation updateProjectV2ItemFieldValue($projectId: ID!, $itemId: ID!, $fieldId: ID!, $value: Float) {
            updateProjectV2ItemFieldValue(input: {projectId: $projectId, itemId: $itemId, fieldId: $fieldId, value: {number: $value}}) {
                projectV2Item {
                    id
                }
            }
        }"""
    variables = {
        "projectId": project_id,
        "itemId": item_id,
        "fieldId": field_id,
        "value": value,
    }
    resp = requests.post(
        ENDPOINT,
        headers=get_headers(),
        data=json.dumps({"query": mutation, "variables": variables}),
    )
    body = resp.json()


def get_headers():
    return {
        "Authorization": f"bearer {GITHUB_TOKEN}",
        "Accept": "application/vnd.github.bane-preview+json",
    }


def array_to_dict(array: list, key_name: str):
    return {x[key_name]: x for x in array}


if __name__ == "__main__":
    main()

実行結果

python app.py

GitHub ProjectにIssuesが追加されました。 StatusとEstimateも設定されています。

11_github_project

Issues側でも確認できます。

12_github_issues

さいごに

GitHub GraphQL APIでProjectにIssuesを追加して、フィールドを更新してみました。 フィールド情報を取得してフィールドIDを特定してから更新するのが少し手間かもしれません。 参考になれば幸いです。

参考