この記事は1893文字約5分で読めます

システムのリリース実績を取る必要がでてきたものの、管理表にはしてなかったのでGitHub API v4から取得し、まとめましたというお話.

Table of Contents

背景

どうしてこんなことしなきゃいけなくなったかというとちょっとした頼まれごときっかけです。

ふつうシステムをリリースするとリリース管理というリリース日やリリースの内容などをまとめておく管理表があって然るべきなんですが、それがまとまっていなくて困った!なんとかしてというものでした。

困りましたね…。

ただ、我々のソースはGitHubで管理していて、かならず**Git-flow**というブランチ戦略を取っているのでPRやコミットの履歴を集めればええやんけ!となったわけです。

Git-flow

上記の通りProduction環境へのリリースはmasterブランチから行ないます。

GitHub API v4を使う

このブログだと2回目の取り上げになりますが、GitHubのAPIには2種類あります。

GitHub API v3 (RestAPI)v4(GraphQL)です。

もちろん使い慣れたRestAPIを使ってもいいのですが、GraphQLくらい使えないと会社で怒られそうなのでGraphQLを使うことにします。

なぜGraphQLを使わないと会社で怒られそうにされるのか

ちょっと閑話休題。

最近流行ってますね。GraphQL。

こういった新しい技術は会社の若い人たちがどんどん使っていくので追いつくのに必死です。

APIサービスを提供する人も使う人両者からみてRESTよりはいいところがあるから流行るわけですがどういった点でしょうか?

リソースごとにAPIをコールしなくていい

利用側からするとこれが一番大きいのではないでしょうか?

ちょっと複雑なAPIになるとアルアルなのが、

記事情報APIからコメントIDを取得して、コメントIDを使ってコメントAPIを叩いて、結果からユーザーIDをとってユーザーIDを使ってユーザーAPIから……

どんだけリクエストするんや…と。

GraphQLなら自分が必要な項目を必要なだけ取得できます。

BFFとして使うことでたくさんのエンドポイントを作らなくてよい

Backends For Frontendsな世界では、フロントエンドが使いやすい形式でさまざまな業務ロジックをGatewayしてあげることが求められます。

GraphQLはまさに呼び手、つまりフロントエンドがデータの形式を決められるためにBFFとの親和性が高いと思います。

PythonではRequestsが便利だよねっていう話

タイトルの通り面倒なことはPythonに任せようなのでPythonで実装していくわけですが、PythonにはRequestsという超便利ライブラリがあるのでこちらを使ってGraphQLコール部を作っていきます。

次のような形で作るとGraphQLのリクエストができ、PythonのDictで結果を受け取ることができます。

def post(query):
    headers = {"Authorization": "bearer " + token}
 # tokenはbearer
    res = requests.post(endpoint, json=query, headers=headers)
    return res.json() # res.json()でJSONをPytnon Dict形式に

それでは早速、作っていきましょう。

リリース実績に必要な項目は?

今回のリリース実績取得に当たって下記のものがリリース実績としてカウントするものとします。

  • masterブランチへのPRで
    • マージされてるものが対象
    • マージされずにCloseしているものは対象外
    • マージ日がリリース日とする
    • どんな変更が当たっているか後追いしたいのでPRに含まれるCommitのメッセージも添付する
    • 結果は標準出力とCSVで出す

さて、こちらを実現したソースがこちら。

import requests
import json
import csv
import pytz
from datetime import datetime

import os from os.path import join, dirnamefrom dotenv import load_dotenvdotenv_path = join(dirname(__file__), "../.env")load_dotenv(dotenv_path) # dotenv経由で環境変数取得TOKEN = os.environ.get("TOKEN")
ENDPOINT = os.environ.get("ENDPOINT")

# token
token = TOKEN

# endpoint
endpoint = ENDPOINT


# If you want to search repo's in organization `org:hoge` instead of user:hogeget_master_pr = {    "query": """  query {  search(query: "user:tubone24", type: REPOSITORY, first: 100) {      pageInfo {      endCursor      startCursor    }    edges {      node {        ... on Repository {          name          url          pullRequests (first: 100){            edges {            node {              baseRefName              createdAt              closedAt              merged              mergedAt              mergedBy {                login              }              commits (first: 45){                nodes {                  commit {                    message                  }                }              }              title              url              headRefName              }            }            }          }        }    }  }  }  """
}


def post(query):
    headers = {"Authorization": "bearer " + token}
    res = requests.post(endpoint, json=query, headers=headers)
    if res.status_code != 200:
        raise Exception("failed : {}".format(res.status_code))
    return res.json()
def iso_to_jst(iso_str): # ISO8601(RFC3339)形式をJST %Y/%m/%d %H:%M:%Sに変換    dt = None    try:        dt = datetime.strptime(iso_str, "%Y-%m-%dT%H:%M:%SZ")        dt = pytz.utc.localize(dt).astimezone(pytz.timezone("Asia/Tokyo"))    except ValueError:        try:            dt = datetime.strptime(iso_str, "%Y-%m-%dT%H:%M:%Sz")            dt = dt.astimezone(pytz.timezone("Asia/Tokyo"))        except ValueError:            pass    if dt is None:
        return ""
    return dt.strftime("%Y/%m/%d %H:%M:%S")

def create_csv_header():
    with open("master_pr.csv", "w", encoding="utf_8_sig") as f:
 # BOM付UTF-8
        writer = csv.writer(f)
        writer.writerow(
            [
                "Repository",
                "Repository URL",
                "PR#",
                "PR Title",
                "Target Branch",
                "Merged By",
                "Merged at",
                "Created at",
                "PR URL",
                "Commit Msgs",
            ]
        )


def main():
    create_csv_header()
    res = post(get_master_pr)
    print("{}".format(json.dumps(res)))
    for node in res["data"]["search"]["edges"]:
        repo_name = node["node"]["name"]
        repo_url = node["node"]["url"]
        pr_count = 0
        for pr in node["node"]["pullRequests"]["edges"]:
            base_ref_name = pr["node"]["baseRefName"]
            if base_ref_name != "master":
                continue
            head_ref_name = pr["node"]["headRefName"]
            created_at = iso_to_jst(pr["node"]["createdAt"])
            if pr["node"]["merged"]:
                pr_count += 1
                merged_at = iso_to_jst(pr["node"]["mergedAt"])
                merged_by = pr["node"]["mergedBy"]["login"]
                pr_title = pr["node"]["title"]
                pr_url = pr["node"]["url"]
                commit_list = [
                    x["commit"]["message"] for x in pr["node"]["commits"]["nodes"]
                ]
                if pr_count == 1:
                    print("\n")
                    print(
                        "{repo_name}:  {repo_url}".format(
                            repo_name=repo_name, repo_url=repo_url
                        )
                    )
                print(
                    "  #{pr_count} {pr_title} for {head_ref_name} by {merged_by} at {merged_at}".format(
                        pr_count=pr_count,
                        pr_title=pr_title,
                        head_ref_name=head_ref_name,
                        merged_by=merged_by,
                        merged_at=merged_at,
                    )
                )
                print("        {pr_url}".format(pr_url=pr_url))
                with open("master_pr.csv", "a", encoding="utf_8_sig") as f:
                    writer = csv.writer(f)
                    writer.writerow(
                        [
                            repo_name,
                            repo_url,
                            pr_count,
                            pr_title,
                            head_ref_name,
                            merged_by,
                            merged_at,
                            created_at,
                            pr_url,
                            "\n".join(commit_list),
                        ]
                    )


if __name__ == "__main__":
    main()

TOKENとENDPOINTは.envファイルから取得するようにしてます。

また、GitHubのAPIから帰ってくる時刻は**ISO8601(RFC3339)**形式なのでJSTに変換してます。

細かいですが、CSVはUTF-8で作成するところ、Excelで開けるようにBOM付UTF-8にしてます。

print文が残念なかんじですね。今回は急ぎ作ってしまいましたが、今後ちょっとリファクタします。

結論

急ぎの依頼でしたが楽しくプログラミングできました。

Terminalでこんな感じで出ます。

terminal

集計用のCSVもばっちりです。

csv

今回はレポジトリが100件未満だったので特にベージネーションをしてませんが次はベージネーションも実装しようかと思います。

tubone24にラーメンを食べさせよう!

ぽちっとな↓

Buy me a ramen
hatena bookmark