GitHubのOrganization配下のIssue・PRからアサインを外すスクリプトを書きました

2021.08.25

サーモン大好き横山です。

とある事情で、「Organizationを抜ける前に、それぞれのリポジトリのアサインをOpen・Closeに関わらず抜けておいてね」という作業を依頼されました。
そこでまず、Issuesで archived:false is:closed assignee:tututen user:xxxxxx を検索で検索して件数を調べることにしました

はい、1120件見つかりました。 一つのリポジトリだったらIssuesの画面開いて手で地道にアサインを外していくということもできたでしょうが、複数リポジトリまたいでこの数でしたので、GitHub APIを用いてアサインを外していくことにしました。

前提

GitHub APIを叩くためのPersonalTokenを取得します。
権限はRepo権限フルアクセスのものを作成します。

そのtokenは、任意の場所のファイルに保存しておきます。

$ vim ~/.github_token/github_unassignees_token
$ chmod 0600 ~/.github_token/github_unassignees_token

また、pythonの requests を使うので、利用する仮想環境にrequestsを入れてください

$ pip3 install requests

処理内容

Search issues and pull requests でアサインされてるIssue・PRを取得します。
取得した情報に、 Get an issue を呼び出せる url があるので、その末尾に /assignees を連結し、 Remove assignees from an issue を1件1件呼び出してアサインを外します

{
  "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",
  :
}

コード

書いたコードは以下になります。

import requests
from requests.auth import HTTPBasicAuth
from typing import List

import os
import json


def get_assign_issue_and_pr_urls(session: requests.Session, username: str) -> List[str]:
    """
    アサインしているIssue/PRのAPIURLのリストを返す(最大1000件)
    参考: https://docs.github.com/en/rest/reference/search#search-issues-and-pull-requests
    """
    per_page = 100
    gh_org_user = "prismatix-jp"
    url_fn = (
        lambda n: f"https://api.github.com/search/issues?q=archived:false+assignee:{username}+user:{gh_org_user}&per_page={per_page}&page={n}"
    )
    resp = session.get(url_fn(1))

    resp_data = json.loads(resp.text)
    if not resp_data["items"]:
        return []

    unassignee_urls = [f'{item["url"]}/assignees' for item in resp_data["items"]]
    total_paging_count = resp_data["total_count"] // per_page + 1
    # 合計1000件超えると怒られるので、1000件以上取得するときは1000件までに留める
    total_paging_count = min(1000 // per_page, total_paging_count)
    for i in range(2, total_paging_count + 1):
        resp = session.get(url_fn(i))
        resp_data = json.loads(resp.text)
        if resp_data["items"]:
            unassignee_urls.extend(
                [f'{item["url"]}/assignees' for item in resp_data["items"]]
            )

    return unassignee_urls


def unassign_issue(session: requests.Session, api_url: str, username: str) -> None:
    """
    引数のAPIURLを元にusernameのアサインを外す
    参考: https://docs.github.com/en/rest/reference/issues#remove-assignees-from-an-issue
    """
    body_data = {"assignees": [username]}
    session.delete(api_url, data=json.dumps(body_data))


if __name__ == "__main__":
    # token
    with open(os.path.expanduser("~/.github_token/github_unassignees_token")) as f:
        gh_token = f.read().strip()

    target_username = "tututen"
    session = requests.Session()
    session.auth = HTTPBasicAuth(target_username, gh_token)
    session.headers["Accept"] = "application/vnd.github.v3+json"
    session.headers["Content-Type"] = "application/x-www-form-urlencoded"

    prev_urls = None
    while True:
        unassignee_urls = get_assign_issue_and_pr_urls(session, target_username)
        # 取得したurlsが0もしくは前回と同一の場合loopを抜ける
        if not unassignee_urls or prev_urls == unassignee_urls:
            break

        for url in unassignee_urls:
            unassign_issue(session, url, target_username)

        # 結果保持
        prev_urls = unassignee_urls

結果

1120件 -> 23件まで減りました。
残った23件はなぜアサインが外せなかったといいますと、リポジトリが既に凍結(更新停止)のリポジトリでアサインが外せなかったIssue・PRでした。 GitHub APIをcurlで叩いても結果は404が返ってきます。

$ curl -i -H "Accept: application/vnd.github.v3+json" -u tututen:$(cat ~/.github_token/github_unassignees_token) https://api.github.com/repos/xxxxxxx/hoge-repositories/issues/33/assignees -XDELETE -d '{"assignees": ["tututen"]}'
HTTP/2 404
server: GitHub.com
date: Wed, xx Aug 2021 xx:00:47 GMT
content-type: application/json; charset=utf-8
content-length: 132
x-oauth-scopes: repo
x-accepted-oauth-scopes:
github-authentication-token-expiration: 2021-xx-xx 07:01:01 UTC
x-github-media-type: github.v3; format=json
x-ratelimit-limit: 5000
x-ratelimit-remaining: 4988
x-ratelimit-reset: 162xxxx626
x-ratelimit-used: 12
x-ratelimit-resource: core
access-control-expose-headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, Deprecation, Sunset
access-control-allow-origin: *
strict-transport-security: max-age=31536000; includeSubdomains; preload
x-frame-options: deny
x-content-type-options: nosniff
x-xss-protection: 0
referrer-policy: origin-when-cross-origin, strict-origin-when-cross-origin
content-security-policy: default-src 'none'
vary: Accept-Encoding, Accept, X-Requested-With
x-github-request-id: ppp

{
  "message": "Not Found",
  "documentation_url": "https://docs.github.com/rest/reference/issues#remove-assignees-from-an-issue"
}

まとめ

GitHub API+PythonでOrganization配下に存在する1100件弱のIssue・PRのアサインをはずすことができました。

凍結したIssue・PRがWebからは見えるのに、APIから見ると 404 Not Found が返ってくるのには少し腑に落ちてませんがそういうものなのでしょう。

この記事がだれかのお役に立てば幸いです。