tubone

tubone

Boyaki makes a new world


 Recent posts  6 / 73

FebMarAprMayJunElixirでパラレルな負荷試験ツールを作るNEW GAME!から見る新入社員の心得その1NEW GAME!から見る新入社員の心得その2Jetson Nanoを触る報告 ~立派なタワーPCみたいだろ? 小型なんだぜ?~tiny_yolov2_onnx_camを使って物体検知するJetson nano + SainSmart IMX219でアニメ風自撮り動画を作るStyleGANとStyleGAN2を使って美少女キャラを無限増殖させるRaspberry PIを使って植物の水やり監視システムを作る2020年のデスクトップはこれだ!かっこいいあつもりを始めてしまいましたクロスルート証明書について考えてみるReact Iframeを使ってPortfolioサイトにSoundCloudのメディアプレーヤーをつけるMonWedFri
React Iframeを使ってPortfolioサイトにSoundCloudのメディアプレーヤーをつけるクロスルート証明書について考えてみるあつもりを始めてしまいました2020年のデスクトップはこれだ!かっこいいRaspberry PIを使って植物の水やり監視システムを作るStyleGANとStyleGAN2を使って美少女キャラを無限増殖させる


 SearchBox

Search your interesting by Algolia in this blog.


この記事は91文字約10分で読めます

システムのリリース実績を取る必要がでてきたものの、管理表にはしてなかったので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件未満だったので特にベージネーションをしてませんが次はベージネーションも実装しようかと思います。

˚