【GitHub】PythonとGitHub ActionsでProjectsにIssuesを作る作業を自動化する(前編:Pythonのソース)

Code

はじまり

リサちゃん
リサちゃん

私は、ふと思った。日々のタスクをGitHub上で管理したいと。

しかし、GitHubのissuesを登録する時にいちいちオプションを選択して入力する手間が面倒くさい。そして、GitHub Projectsでも管理したいけど、その場合は尚更である。

135ml
135ml

ふむふむ、どうしたどうした。

リサちゃん
リサちゃん

ということで、GitHub IssuesとGitHub Projectsを一気に登録するツールとかないですか!?

135ml
135ml

ほうほうほう・・・、よーし、分かった。やってやろうじゃないか!

では、今回は、そんなことが出来るツールの詳細について紹介するぞ!

まずは、第1弾だ!

リサちゃん
リサちゃん

往け〜!!

今回、取り組んだこと

今回は、自分がリポジトリ内に用意したCSVのタイトルと内容を読み取って、その内容をGitHub Projects内にカードとして登録させるツールです。

例えば、このようにCSVを編集しておいて、・・・

このGitHub Actionsが動くと、CSV内のレコード数と同じ数のワークフローファイルが作成されます。

そして、色々動いた後に、このようにkanban形式内でcardが登録されます。Issueも登録されています。

システム構成

ざっと、今回のツールのシステム構成を書きます。

概要図はこんな感じです。

ざっくりした流れは以下の感じです。

  1. Issueを作るために、Pythonファイルを動かすGitHub Actionsが動く。(夜中の4時に設定)
  2. Pythonファイルが動いて、Issueを作るためのGitHub Actionsのワークフローファイルが作成される。
  3. そのワークフローファイルは、そのPythonファイルが動いた10分後に起動する。
  4. ワークフローファイルが起動。Issueに登録したと同時にGitHub Projectsにもカードを追加する。
  5. その日に登録した分のワークフローファイルとCSVの内容を削除するために、Pythonファイルを動かすGitHub Actionsが動く。(1. の30分後に設定。設定処理は、2. で行っていた。)
  6. Pythonファイルが動いて、ワークフローファイルとCSVの内容を削除する。

流れと概要図を対応させるとこんな感じです。

今回作ったツールの肝となるGitHub Actionsがこれです。GitHub公式のものみたいなので、長い期間利用できそうです。

GitHub - imjohnbo/issue-bot: GitHub Actions powered Issue Bot 🦾
GitHub Actions powered Issue Bot 🦾. Contribute to imjohnbo/issue-bot development by creating an account on GitHub.

Pythonスクリプトの中身

今回は、前編として、Pythonの中身を見てみたいと思います。

Pythonを実行するディレクトリ内の構成は、以下の感じになっています。「img」と「README.md」は使いません。requirements.txtでは、僕用に色々な処理をまとめたライブラリだけをインストールします。

Pythonファイルその1:generate_workflow_to_make_issue.py

以下がソースです。

# Library by default
from distutils.log import error
from time import time
import traceback
from pathlib import Path
# Library by third party
# nothing
# Library in the local
# nothing
# Library in landmasterlibrary
from landmasterlibrary.generaltool import get_str_by_zero_padding, get_src_path_from_test_path, read_txt_lines, read_csv_lines, get_indcies_containing_words, append_items, generate_cron_from_datetime_now


def rewrite_cron_of_remove_workflow(is_there_issue_to_generate : bool, minutes_scheduled_later : int, time_difference : int):
    file_name = "removeWfForIssuesAndCsvContent.yml"
    dir_having_file = ".github/workflows"
    file_full_name = get_src_path_from_test_path(__file__, file_name, dir_having_file, isChecking=False)

    read_wf_lines = read_txt_lines(__file__, [file_name], dir_having_file)[0]
    mark_of_schedule = "  schedule:"
    index_of_workflow_schedule = get_indcies_containing_words(read_wf_lines, [mark_of_schedule])[0]
    mark_of_cron = "  - cron: '"
    index_of_workflow_cron = get_indcies_containing_words(read_wf_lines, [mark_of_cron])[0]
    cron = generate_cron_from_datetime_now(minutes_scheduled_later, time_difference)

    read_wf_lines[index_of_workflow_schedule] = "  schedule:"
    read_wf_lines[index_of_workflow_cron] = f"  - cron: '{cron}'  # https://crontab.guru"

    read_wf_lines_str = "\n".join(map(str, read_wf_lines))
    with open(file_full_name, "w") as fw:
        fw.write(read_wf_lines_str)
    print(read_wf_lines_str)
    print("==================================")

def generate_workflow():
    labels = ["book", "blog"]
    read_files = []
    count_of_zero_length = 0
    is_there_issue_to_generate = True
    for i in range(0, len(labels)):
        read_files.append(f"{labels[i]}s.csv")
    txt_lines = read_csv_lines(__file__, read_files, "generateIssueWf")
    for i in range(0, len(txt_lines)):
        txt_lines[i].pop(0) # remove "title,milestone" row.
        if len(txt_lines[i]) == 0:
            count_of_zero_length += 1
    print(txt_lines)

    if count_of_zero_length == len(labels):
        txt_lines = [[["dummytitle", 0, "dummybody"]]]
        is_there_issue_to_generate = False

    minutes_scheduled_later = 10
    time_difference = 0
    cron = generate_cron_from_datetime_now(minutes_scheduled_later, time_difference)
    rewrite_cron_of_remove_workflow(is_there_issue_to_generate, minutes_scheduled_later + 20, time_difference)

    cron_lines = []
    if is_there_issue_to_generate:
        cron_lines.append("  schedule:")
        cron_lines.append(f"  - cron: '{cron}'  # https://crontab.guru")
    else:
        cron_lines.append("#   schedule:")
        cron_lines.append(f"#   - cron: '{cron}'  # https://crontab.guru")
        print("No generated workflows to make issues today.")
        # return False

    str_of_workflow_dispatch = "  workflow_dispatch:"
    read_wf_lines = read_txt_lines(__file__, ["generateIssues.yml"], ".github/workflows")[0]
    index_of_workflow_dispatch = read_wf_lines.index(str_of_workflow_dispatch)
    read_wf_lines = append_items(read_wf_lines, cron_lines, index_of_workflow_dispatch + 1)

    str_of_scheduled_issue = "    - name: Scheduled Issue to landmaster135"
    index_of_name_of_scheduled_issue = read_wf_lines.index(str_of_scheduled_issue)

    src_file_name = "generateIssues.yml"
    generated_files = []
    for i in range(0, len(txt_lines)):
        for j in range(0, len(txt_lines[i])):
            # write contents
            read_wf_lines[index_of_name_of_scheduled_issue + 3] = f"        title: {txt_lines[i][j][0]}" # title
            label = str(labels[i])
            read_wf_lines[index_of_name_of_scheduled_issue + 5] = f"        labels: \"{label}\"" # labels
            read_wf_lines[index_of_name_of_scheduled_issue + 8] = f"        template: \".github/ISSUE_TEMPLATE/custom.md\"" # template
            read_wf_lines[index_of_name_of_scheduled_issue + 9] = f"        project: 1" # project
            read_wf_lines[index_of_name_of_scheduled_issue + 10] = f"        column: Todo" # column
            milestone = ""
            initial_of_milestone = "        milestone: "
            if txt_lines[i][j][1] == "0":
                pass
            else:
                milestone = f"{initial_of_milestone}{str(txt_lines[i][j][1])}"
            read_wf_lines[index_of_name_of_scheduled_issue + 11] = milestone # milestone
            read_wf_lines[index_of_name_of_scheduled_issue + 12] = f"        body: {txt_lines[i][j][2]}" # body

            # decide file name
            j_padded = get_str_by_zero_padding(j, 2)
            src_file_name = f"generateIssues_{label}_{j_padded}.yml"
            generated_file_full_name = get_src_path_from_test_path(__file__, src_file_name, ".github/workflows", isChecking=False)

            # generate workflow files
            read_wf_lines_str = "\n".join(map(str, read_wf_lines))
            with open(generated_file_full_name, "w") as fw:
                fw.writelines(read_wf_lines_str)
            generated_files.append(src_file_name)

    # genereate text memo file.
    generated_files_str = "\n".join(map(str, generated_files))
    file_name_written_file_to_remove = "generatedWfFiles.txt"
    generated_file_full_name = get_src_path_from_test_path(__file__, file_name_written_file_to_remove, ".github/workflows", isChecking=False)
    with open(generated_file_full_name, "w") as fw:
        fw.write(generated_files_str)
    for i in read_wf_lines:
        print(i)

    return True

def main():
    generate_workflow()

if __name__ == "__main__":
    main()

処理の流れとしては、

  1. Pythonファイル内で読み取るラベルを指定。今回は、「book」と「blog」。
  2. ラベルに対応したCSVファイルを読み取る。今回で言うと、「books.csv」と「blogs.csv」を読み取る。
  3. issueを登録するワークフローファイルのcronを実行時の10分後に設定する。(まだファイル生成は行わない。)
  4. もう一つのPythonファイル(「remove_workflow_files_making_issue.py」:登録したIssueの元情報(CSVとかワークフローファイル)を消すためのファイル)を実行するワークフローファイルのcronを3.の20分後に設定する。こっちはここでファイル編集を行う。
  5. 登録したいissueがCSVになかったら、ワークフローファイル用にダミーの内容を1つ用意する。
  6. 登録する個々のIssueに対して、色々なオプションを設定して、設定するためのワークフローファイルを作成する。

それでは、以下で詳しく解説していきます。

1.Pythonファイル内で読み取るラベルを指定。

今回は、「book」と「blog」を指定したので、「books.csv」と「blogs.csv」を作成しておいて、Pythonファイルに読み取らせます。

2.ラベルに対応したCSVファイルを読み取る。

次に、CSVファイルを読み取ります。取得する内容は、ヘッダーの行以外になります。

3.issueを登録するワークフローファイルのcronを実行時の10分後に設定する。

これらのIssueを登録するためのワークフローの実行時間を設定します。

ワークフローファイルを作った直後はこんなファイル構成になります。(この工程ではまだ作りません。5.の工程での判定を超えてから作ります。)

これらのワークフローファイルにcronを設定します。例えば、AM4:00にこのPythonスクリプトが実行されたら、AM4:10にこれらのワークフローファイルが実行されるように設定します。

cron設定の詳細は、この記事でも紹介していますので、よければご参考ください。

4.もう一つのPythonファイルを実行するワークフローファイルのcronを3.の20分後に設定する。

次に、「remove_workflow_files_making_issue.py」という、登録したIssueの元情報(CSVとかワークフローファイル)を消すためのPythonファイルを実行するワークフローファイルのスケジュールを設定します。

そのワークフローファイルの名前が、「removeWfForIssuesAndCsvContent.yml」と言います。

このワークフローファイルにcronを設定します。

例えば、AM4:00にこのPythonスクリプトが実行されたら、先程はAM4:10にこれらのワークフローファイルが実行されるように設定しましたが、

このファイルでは、AM4:30に実行されるように設定します。

先程も含めて、10分と20分と間を空けていますが、この時間に特別な意味はありません。大体、そのくらいの時間でGitHub Actionsが終わるだろうという見込みの数値です。

5.登録したいissueがCSVになかったら、ダミーの内容でワークフローファイルを1つ作成する。

日によっては、登録したいissueが無いことがあると思います。その場合は、ダミーの内容で、6.の処理を行います。

元々は、6.の処理を行わずに、Pythonスクリプトを終了させるようにしていたのですが、そうするとgit addする際にファイルがないので、GitHub Actions実行中にエラーが発生します。

continue-on-error: trueにして、GitHub Actionsのエラーを無視することも試したのですが、エラーが起きた旨の通知メールは届いてしまうので、git addを空振りさせない方法に落ち着きました。

これがそのメール。これが毎日届くのは煩わしい・・・。

6.登録する個々のIssueに対して、色々なオプションを設定して、設定するためのワークフローファイルを作成する。

5.の壁を超えたら、Issueを設定するためのワークフローファイルを作成する処理に入ります。

作成するための元のワークフローファイル「generateIssues.yml」を読み取ってそのオプション値を変えていきます。

まず、Issue、ProjectおよびMarkdownテンプレートがある画面と照らし合わせてみてみます。

これがIssueのページになります。

これがProjectのページ。

これがMarkdownテンプレートがある画面になります。

そのIssueなどのオプションに対して、このように設定しています。

#オプション説明
1titleIssuesのタイトルです。今回は、CSVのtitleフィールドの値を入れています。
2labelsラベルです。デフォルトだと、「bug」とか「enhanced」とかあるやつです。
今回は、「book」とか「blog」とかを指定しています。
3templatesIssues作成時に利用できるテンプレートMarkdownを指定します。
ぶっちゃけ、登録する内容が毎回異なるので、指定はしていますが実際のところ使っていません。
4projectプロジェクト。IDで指定します。
5columnプロジェクトのkanbanのカラム。「To do」とか「Done」とか。
(カラム名は空白文字が無いほうが良いです。結局設定の仕方が分かりませんでした。)
6milestoneプロジェクトごとに設定できるマイルストーン
7bodyIssuesの内容(Description)です。

そして、色々設定したら、ワークフローファイルを書き出して、git pushします。

ワークフローファイル作成前のフォルダ構成は、こんな感じですが、

ワークフローファイル作成後は、こうなります。

以上で、「generate_workflow_to_make_issue.py」の解説になります。

Pythonファイルその2:remove_workflow_files_making_issue.py

次に、ワークフローでIssuesが登録された後に行う処理の解説になります。

# Library by default
from distutils.log import error
import traceback
from pathlib import Path
import os
# Library by third party
# nothing
# Library in the local
# nothing
# Library in landmasterlibrary
from landmasterlibrary.generaltool import get_src_path_from_test_path, read_txt_lines, get_files_by_extensions

def remove_workflow():
    # remove workflow files.
    file_names_written_file_to_remove = ["generatedWfFiles.txt"]
    target_dir = ".github/workflows"
    file_names = read_txt_lines(__file__, file_names_written_file_to_remove, target_dir)[0]
    for i in range(0, len(file_names)):
        file_full_name = get_src_path_from_test_path(__file__, file_names[i], target_dir)
        os.remove(file_full_name)
    file_full_name_written_file_to_remove = get_src_path_from_test_path(__file__, file_names_written_file_to_remove[0], target_dir)
    with open(file_full_name_written_file_to_remove, "w") as fw:
        fw.write("")

    # delete issue titles from csv files.
    extension = [".csv"]
    csv_files = get_files_by_extensions("./", extension)
    print(csv_files)
    for i in range(0, len(csv_files)):
        with open(csv_files[i], "r") as fr:
            read_lines = fr.readlines()
            print(read_lines)
        with open(csv_files[i], "w") as fw:
            fw.writelines(read_lines[0])
        with open(csv_files[i], "r") as fr:
            read_lines = fr.readlines()
            print(read_lines)

def main():
    remove_workflow()

if __name__ == "__main__":
    main()

処理の流れとしては、

  1. Issuesを作成したワークフローファイルの名前の一覧が載っている「generatedWfFiles.txt」を読み取る。
  2. 読み取ったファイル名のファイルを削除する。
  3. 「generatedWfFiles.txt」の内容を削除する。
  4. 「books.csv」や「blogs.csv」に記載されているレコード部分の内容を削除する。

それでは、以下で詳しく解説していきます。

1.Issuesを作成したワークフローファイルの名前の一覧が載っている「generatedWfFiles.txt」を読み取る。

まず、Issuesを作成したワークフローファイルを削除します。

そのために、「generatedWfFiles.txt」に書いてあるファイル名を取得します。このファイルの内容は、実は先程、「generate_workflow_to_make_issue.py」で記述していたのです。

例えば、このフォルダ構成になっている場合、

この内容が「generatedWfFiles.txt」に記載されています。

2.読み取ったファイル名のファイルを削除する。

そして、読み取ったファイルを元に、ファイルを削除します。

3.「generatedWfFiles.txt」の内容を削除する。

ファイルの削除が完了したら、「generatedWfFiles.txt」に記載されている内容を削除します。

4.「books.csv」や「blogs.csv」に記載されているレコード部分の内容を削除する。

CSVの内容を削除して、また同じタイトルのIssuesが登録されることに起こらないようにします。

以下のような状態になっているCSVに対して、

このようにヘッダー部が残るようにレコード部分だけ削除します。

そして、色々設定したら、編集もしくは削除したファイルに対してgit pushします。

以上で、「remove_workflow_files_making_issue.py」の解説になります。

おしまい

135ml
135ml

はい!今回は、Pythonファイルに対しての説明になりました!

リサちゃん
リサちゃん

はい〜、次回後編ではGitHub Actionsに関して解説お願いします〜!

補足

ペンギン
ペンギン

GitHub Actionsの中身の方を解説した記事はこちらになっているので、よければ見てみてください!

ペンギン
ペンギン

今回のツールのソースを載せたリポジトリが、以下にあります。

GitHub - landmaster135/generateIssueWf_Dev
Contribute to landmaster135/generateIssueWf_Dev development by creating an account on GitHub.

以上になります!

コメント

タイトルとURLをコピーしました