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

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

はじまり#

リサちゃん avatar
リサちゃん
私は、ふと思った。日々のタスクをGitHub上で管理したいと。しかし、GitHubのissuesを登録する時にいちいちオプションを選択して入力する手間が面倒くさい。そして、GitHub Projectsでも管理したいけど、その場合は尚更である。
135ml avatar
135ml
ふむふむ、どうしたどうした。
リサちゃん avatar
リサちゃん
ということで、GitHub IssuesとGitHub Projectsを一気に登録するツールとかないですか!?
135ml avatar
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公式のものみたいなので、長い期間利用できそうです。

imjohnbo
/
issue-bot
Waiting for api.github.com...
00K
0K
0K
Waiting...

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ファイル内で読み取るラベルを指定。今回は、bookblog
  2. ラベルに対応したCSVファイルを読み取る。今回で言うと、books.csvblogs.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ファイル内で読み取るラベルを指定。#

今回は、bookblogを指定したので、books.csvblogs.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などのオプションに対して、このように設定しています。

Noオプション説明
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.csvblogs.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 avatar
135ml
バックアップは大事です。
リサちゃん avatar
リサちゃん
意味深か?

以上になります!

記事を共有

この記事が役に立ったなら、ぜひ他の人と共有してください!

【GitHub】PythonとGitHub ActionsでProjectsにIssuesを作る作業を自動化する(前編:Pythonのソース)
https://endorphinbath.com/posts/github-action-automake-issue-to-projects-former/
著者
kinkinbeer135ml
公開日
2022-04-06
ライセンス
CC BY-NC-SA 4.0
関連記事 スマート
1
【GitHub】PythonとGitHub ActionsでProjectsにIssuesを作る作業を自動化する(後編:GitHub Actionの内容)
Code 日々のタスクをGitHub IssuesおよびGitHub Projectsで管理する時に登録する作業が面倒くさいと思います。いちいちオプションを選択して入力する手間が面倒くさいと思います。本記事では、その作業を省略したツールを紹介します。
2
【GitHub】Pythonでリポジトリの情報を取得するCloud Functionsを作る
Code Pythonを使用してGitHubリポジトリの情報を取得し、Cloud Functionsで処理する方法に関する記事です。PyGithubとThreadingで実装および処理を効率的にしました。
3
【GitHub】Goでリポジトリの情報を取得するCloud Functionsを作って、Pythonと比較する
Code Go言語でGitHubのリポジトリ情報を取得するCloud Functionsを開発し、Pythonで作成した同様の機能と比較した記事です。使ったツールや、並行処理の比較も行っています。
4
【Python】文字列の先頭と末尾にあるスペース、空白文字を削除する
Code Pythonで文字列の先頭と末尾にスペース(空白文字)が混じっていることがあります。そのスペースを削除するスクリプトを掲載します。
5
【GitHub】「github-readme-stats」と「github-profile-trophy」でGitHubをゴージャスに見せる
Code GitHubのプロフィール画面を豪華にするために、README.mdをいじりました。けっこう簡単にゴージャスになります。手が空いた時に設定してみて下さい。
ランダム記事 ランダム
Profile Image of the Author
kinkinbeer135ml
SIerをやめて、プログラミングを勉強しています。※Amazonアソシエイトに参加しています。
お知らせ
私のブログへようこそ!これはサンプルのお知らせです。
音楽
カバー

音楽

再生中なし

0:00 0:00
歌詞なし
カテゴリ
タグ
サイト統計
記事
287
カテゴリー
8
タグ
93
総文字数
486,174
運用日数
0
最終活動
0 日前

目次