【GitHub API Tips】GitHub APIを使ってissueを削除する方法

# はじめに
　GitHub APIを使って、リポジトリのissueを削除する方法を紹介する。

# 動作確認環境

* Ubuntu 22.04 x86_64
* Python 3.10.12

# 環境構築

## Bash
 BashでJSONを扱うためにjqをインストールする。

```bash
sudo apt update
sudo apt install -y jq
```

## Python

 Pythonの環境を準備しておく。[requests](https://pypi.org/project/requests/)インストールする。

```bash
pip install requests
```

# 方法

　GitHub APIは、[REST API](https://docs.github.com/en/rest?apiVersion=2022-11-28)と[GraphQL API](https://docs.github.com/en/graphql)が用意されている。REST APIは簡便で扱いやすいが、今回行いたいissueの削除は直接サポートされていない（以下のAPIリストの画像を参照）。

 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/638977/919028a1-d1ee-85bc-0f0d-6ff22b51696a.png)

　そこで本記事では、REST APIとGraphQL APIを組み合わせて、issueを削除する方法を紹介する。

基本的な流れは以下である。

1. APIのアクセストークンを取得し設定する
1. REST APIで、削除したいissueのissueIdを取得する
1. GraphQL APIで、上記issueIdを指定して、issueを削除する

## APIのアクセストークンを取得し設定する

　[Personal access tokens (classic)](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#personal-access-tokens-classic)、または、[Fine-grained personal access tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#fine-grained-personal-access-tokens)を使い、削除したいリポジトリへの書き込み権限を与える。詳細は、[こちらの記事](https://qiita.com/suin/items/1ce9e11bd1c203fb1167)を参照。本記事では、[Personal access tokens (classic)](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#personal-access-tokens-classic)で動作確認を行った。

## REST APIで、削除したいissueのissueIdを取得する

REST APIの[List repository issues](https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#list-repository-issues)を使い、リポジトリ内の全てのissueIdを取得する。

以下のコマンドでAPIの動作確認が可能。

```bash
curl -L \
  -H "Accept: application/vnd.github+json" \
  -H "Authorization: Bearer <YOUR_TOKEN>" \
  -H "X-GitHub-Api-Version: 2022-11-28" \
  https://api.github.com/repos/OWNER/REPO/issues
```
* <YOUR_TOKEN> : 上記設定したアクセストークン
* OWNER : リポジトリ所有者名 (Organizationsまたは個人)
* REPO : リポジトリ名

結果例

```json
[
  {
    "id": 1,
    "node_id": "MDU6SXNzdWUx",
    "url": "https://api.github.com/repos/octocat/Hello-World/issues/1347",
    "repository_url": "https://api.github.com/repos/octocat/Hello-World",
    "labels_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347/labels{/name}",
    "comments_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347/comments",
    "events_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347/events",
    "html_url": "https://github.com/octocat/Hello-World/issues/1347",
    "number": 1347,
    "state": "open",
    "title": "Found a bug",
    "body": "I'm having a problem with this.",
    "user": {
      "login": "octocat",
      "id": 1,
      "node_id": "MDQ6VXNlcjE=",
      "avatar_url": "https://github.com/images/error/octocat_happy.gif",
      "gravatar_id": "",
      "url": "https://api.github.com/users/octocat",
      "html_url": "https://github.com/octocat",
      "followers_url": "https://api.github.com/users/octocat/followers",
      "following_url": "https://api.github.com/users/octocat/following{/other_user}",
      "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
      "organizations_url": "https://api.github.com/users/octocat/orgs",
      "repos_url": "https://api.github.com/users/octocat/repos",
      "events_url": "https://api.github.com/users/octocat/events{/privacy}",
      "received_events_url": "https://api.github.com/users/octocat/received_events",
      "type": "User",
      "site_admin": false
    },
    "labels": [
      {
        "id": 208045946,
        "node_id": "MDU6TGFiZWwyMDgwNDU5NDY=",
        "url": "https://api.github.com/repos/octocat/Hello-World/labels/bug",
        "name": "bug",
        "description": "Something isn't working",
        "color": "f29513",
        "default": true
      }
    ],
    "assignee": {
      "login": "octocat",
      "id": 1,
      "node_id": "MDQ6VXNlcjE=",
      "avatar_url": "https://github.com/images/error/octocat_happy.gif",
      "gravatar_id": "",
      "url": "https://api.github.com/users/octocat",
      "html_url": "https://github.com/octocat",
      "followers_url": "https://api.github.com/users/octocat/followers",
      "following_url": "https://api.github.com/users/octocat/following{/other_user}",
      "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
      "organizations_url": "https://api.github.com/users/octocat/orgs",
      "repos_url": "https://api.github.com/users/octocat/repos",
      "events_url": "https://api.github.com/users/octocat/events{/privacy}",
      "received_events_url": "https://api.github.com/users/octocat/received_events",
      "type": "User",
      "site_admin": false
    },
    "assignees": [
      {
        "login": "octocat",
        "id": 1,
        "node_id": "MDQ6VXNlcjE=",
        "avatar_url": "https://github.com/images/error/octocat_happy.gif",
        "gravatar_id": "",
        "url": "https://api.github.com/users/octocat",
        "html_url": "https://github.com/octocat",
        "followers_url": "https://api.github.com/users/octocat/followers",
        "following_url": "https://api.github.com/users/octocat/following{/other_user}",
        "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
        "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
        "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
        "organizations_url": "https://api.github.com/users/octocat/orgs",
        "repos_url": "https://api.github.com/users/octocat/repos",
        "events_url": "https://api.github.com/users/octocat/events{/privacy}",
        "received_events_url": "https://api.github.com/users/octocat/received_events",
        "type": "User",
        "site_admin": false
      }
    ],
    "milestone": {
      "url": "https://api.github.com/repos/octocat/Hello-World/milestones/1",
      "html_url": "https://github.com/octocat/Hello-World/milestones/v1.0",
      "labels_url": "https://api.github.com/repos/octocat/Hello-World/milestones/1/labels",
      "id": 1002604,
      "node_id": "MDk6TWlsZXN0b25lMTAwMjYwNA==",
      "number": 1,
      "state": "open",
      "title": "v1.0",
      "description": "Tracking milestone for version 1.0",
      "creator": {
        "login": "octocat",
        "id": 1,
        "node_id": "MDQ6VXNlcjE=",
        "avatar_url": "https://github.com/images/error/octocat_happy.gif",
        "gravatar_id": "",
        "url": "https://api.github.com/users/octocat",
        "html_url": "https://github.com/octocat",
        "followers_url": "https://api.github.com/users/octocat/followers",
        "following_url": "https://api.github.com/users/octocat/following{/other_user}",
        "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
        "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
        "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
        "organizations_url": "https://api.github.com/users/octocat/orgs",
        "repos_url": "https://api.github.com/users/octocat/repos",
        "events_url": "https://api.github.com/users/octocat/events{/privacy}",
        "received_events_url": "https://api.github.com/users/octocat/received_events",
        "type": "User",
        "site_admin": false
      },
      "open_issues": 4,
      "closed_issues": 8,
      "created_at": "2011-04-10T20:09:31Z",
      "updated_at": "2014-03-03T18:58:10Z",
      "closed_at": "2013-02-12T13:22:01Z",
      "due_on": "2012-10-09T23:39:01Z"
    },
    "locked": true,
    "active_lock_reason": "too heated",
    "comments": 0,
    "pull_request": {
      "url": "https://api.github.com/repos/octocat/Hello-World/pulls/1347",
      "html_url": "https://github.com/octocat/Hello-World/pull/1347",
      "diff_url": "https://github.com/octocat/Hello-World/pull/1347.diff",
      "patch_url": "https://github.com/octocat/Hello-World/pull/1347.patch"
    },
    "closed_at": null,
    "created_at": "2011-04-22T13:33:48Z",
    "updated_at": "2011-04-22T13:33:48Z",
    "closed_by": {
      "login": "octocat",
      "id": 1,
      "node_id": "MDQ6VXNlcjE=",
      "avatar_url": "https://github.com/images/error/octocat_happy.gif",
      "gravatar_id": "",
      "url": "https://api.github.com/users/octocat",
      "html_url": "https://github.com/octocat",
      "followers_url": "https://api.github.com/users/octocat/followers",
      "following_url": "https://api.github.com/users/octocat/following{/other_user}",
      "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
      "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
      "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
      "organizations_url": "https://api.github.com/users/octocat/orgs",
      "repos_url": "https://api.github.com/users/octocat/repos",
      "events_url": "https://api.github.com/users/octocat/events{/privacy}",
      "received_events_url": "https://api.github.com/users/octocat/received_events",
      "type": "User",
      "site_admin": false
    },
    "author_association": "COLLABORATOR",
    "state_reason": "completed"
  }
]
```

上記の、```node_id```が、issueに割り当てられているユニークIDであるissueIdである。削除したいissueIdをメモしておく。また、```number```は、GitHubリポジトリから参照できるissue番号なので、こちらもメモしておく。

## GraphQL APIで、上記issueIdを指定してissueを削除する

 GraphQL APIの、[deleteIssue](https://docs.github.com/en/graphql/reference/mutations#deleteissue)で、上記のissueIdを指定してissueを削除する。deleteIssue APIの入力の詳細は[DeleteIssueInput](https://docs.github.com/en/graphql/reference/input-objects#deleteissueinput)を参照。

　以下にissueIdからissueを削除するBashスクリプトの例を示す。リポジトリ名、issue番号は、削除には不要だがわかりやすさのために記載している。

```bash
#!/bin/bash

ISSUE_ID=<ISSUE_ID> # issueId
TOKEN=<YOUR_TOKEN> # アクセストークン
REPO=<REPO> # リポジトリ名 (API呼び出しには不要)
ISSUE_NUMBER=<ISSUE_NUMBER> # issue番号 (API呼び出しには不要)

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

# GraphQLクエリ
QUERY=$(cat <<EOF
{
  "query": "mutation { deleteIssue(input: {issueId: \\"$ISSUE_ID\\"}) { repository { id } } }"
}
EOF
)

# curlコマンドでGraphQLクエリを実行
RESPONSE=$(curl -s -H "Authorization: bearer $TOKEN" -H "Content-Type: application/json" -X POST -d "$QUERY" $URL)

# ステータスコードの取得
STATUS_CODE=$(echo $RESPONSE | jq -r '.status_code')

# 結果の確認
if [ "$STATUS_CODE" -eq 200 ]; then
  echo "$REPO issue #$ISSUE_NUMBER deleted successfully."
  exit 0
else
  echo "Failed to delete $REPO issue #$ISSUE_NUMBER: $STATUS_CODE"
  echo $RESPONSE | jq
  exit 1
fi
```

# 例: リポジトリ内の全てのissueを削除する(Python)

　Pythonを使って、あるリポジトリ内の全てのissueを削除する例を示す。

:::note warn
警告
下記スクリプトを実行する場合は、テスト用リポジトリで試すことを推奨
:::

```python:リポジトリ内の全てのissueを削除する例 (Python)
#!/usr/bin/python3
# -*- coding: utf-8 -*-

OWNER="<OWNER>"
REPO="<REPO>"
TOKEN="<TOKEN>"

# リポジトリ内の全てのGitHub issueを削除する関数
def delete_all_github_issues(self):
    headers = {'Authorization': f'token {TOKEN}'}
    issues_url = f'https://api.github.com/repos/{OWNER}/{REPO}/issues'
    response = requests.get(issues_url, headers=headers)
    issues = response.json()
    print(issues)
    if isinstance(issues, list):
        print(f'{REPO} issues count {len(issues)}')
    else:
        print(f'{REPO} issues are not found: {issues}')
        return False

    success = True
    for issue in issues:
        issue_number = issue['number']
        issue_id = issue['node_id']
        # issueIdからissueを削除するスクリプトを呼び出す
        success = success & self._delete_github_issue(REPO, issue_number, issue_id)
    return success

# リポジトリ内の、あるGitHub issueを削除する関数
def _delete_github_issue(self, repo_name: str, issue_number: int, issue_id: int):
    url = 'https://api.github.com/graphql'
    headers = {
        'Authorization': f'bearer {TOKEN}',
        'Content-Type': 'application/json'
    }
    query = """
    mutation {
    deleteIssue(input: {issueId: "%s"}) {
        repository {
        id
        }
    }
    }
    """ % issue_id

    response = requests.post(url, headers=headers, json={'query': query})

    if response.status_code == 200:
        print(f'{repo_name} issue #{issue_number} deleted successfully.')
        return True
    else:
        print(f'Failed to delete {repo_name} issue #{issue_number}: {response.status_code}')
        print(response.json())
        return False

def main():
    delete_all_github_issues()

if __name__ == '__main__':
    main()
```

# まとめ

　GitHub APIを使って、リポジトリのissueを削除する方法を紹介した。リポジトリの移行などでissueの削除を自動化したい場合に有用。

# 参考

https://github.com/orgs/community/discussions/46529

https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#list-repository-issues

https://docs.github.com/en/graphql/reference/mutations#deleteissue

https://gist.github.com/eggplants/569bd6de7023442f275659c1aa9cc3d6