Septeni Engineer's Blog

セプテーニ・オリジナルのエンジニアが綴る技術ブログ

退屈なことは GitLab API にやらせよう

みなさんこんにちは、もう少しで入社から 1 年が経ちそうな新卒の清水です。 ところで自動化って気持ちいいですよね。今回は GitLab API面倒な 温かみのある手作業から解放されてみた話について書いてみました。

私のジョインしたチームでは GitLab の Issue Boards でチケット管理をしています。毎週木曜のセレモニーでスプリント中に消化したチケットの weight 合計を算出するのですが、当時はカラム上部に weight 合計の表示がされておらず、メンバーのプロの Emacs 使いによる鮮やかな手計算で算出されていました*1

ちょうどその頃、業務で Twitter API について齧り始めていたこともあり、API というものについて結構な興味がありました。あるとき会社近くのラーメン屋で「API 叩いて weight 合計求められたら面白そう」とつけ麺を食べながら考え始めたことが全ての始まりです。

本題に入る前に

GitLab API を叩くためには Personal access tokens が必須です。詳細はドキュメントに従えば間違いありませんが、Expires at は空欄にすれば無期限で Scopes は api にチェックをつけておけば OK です。jq コマンドも使います。

次に示す一覧はシェルスクリプトで共通して使われている環境変数です。.env ファイルに切り出し source コマンドで読み込まれます。ただし次章「API から weight 合計を算出してみた」では適宜、以下の変数が穴埋めされているものとしてお読みください。

  • PRIVATE_TOKENPersonal access tokens で取得したトーク
  • BASE_URLhttps://gitlab.example.com/api/v4 など
  • GROUP_ID:対象としたい Group の ID
  • PROJECT_ID:対象としたい Project の ID
  • SLEEP_TIMEsleep コマンドの引数(手元では 0.3 で設定しています)

API から weight 合計を算出してみた

フロントエンドやサーバーサイドなどの複数の Project を束ねる形で Group があり、チームではこの Group を Issue Boards で管理しています。スプリント中に達成したチケットが分かりやすいように Closed の前段に Done というカラムが用意されており、weight が振られるチケットは PBI ラベルの付いたものと定めています。ですので、Group 単位で Done ラベルと PBI ラベルの付いた Closed ではないチケットに振られた weight を取得して合計を算出すればよいということになります。

Issues APIList group issues の以下のエンドポイントを叩きます。

GET /groups/:id/issues

実際のコマンドは以下の通りです。

$ curl -s --header "PRIVATE-TOKEN: ${PRIVATE_TOKEN}" \
  "${BASE_URL}/groups/${GROUP_ID}/issues?labels=Done,PBI&state=opened" |
  jq '[.[].weight] | add'

小ネタ

なお、この weight 合計を API から取得するという試みの寿命は非常に短かったです。というのも、このコマンドが完成したのが 2018 年 8 月 1 日、Issue Boards の各カラム上に weight の合計値が表示されるようになった GitLab 11.2 のリリースが 2018 年 8 月 22 日、週 1 回のセレモニーで叩くので有効活躍回数が 3 回しかなかったからです(ポジティブに考えると時代の 3 週間先をいっていたとも)。

Done チケットの Close を自動化

API から weight 合計を算出できたことに味を占めた私は、これまで手作業で行われていた Done カラムから Closed カラムへの移動も自動化してみようと思いました。そしてこれが私にとって初めての自作シェルスクリプトとなったわけです(なので汚いコードかも知れません)。

Issues APIList group issuesEdit issue の以下のエンドポイントを叩きます。

GET /groups/:id/issues
PUT /projects/:id/issues/:issue_iid

Group に紐づく複数 Project に跨ってエンドポイントを叩くので、対象の Project ID と Project 内での Issue ID がペアで必要です。①〜④でこれらの配列を用意しています*2。⑤では、Project ID の配列の要素分ループしながら Project ID と Issue ID をペアで 1 個ずつ取り出して Closed に更新をかけています。なお、Project ID および Issue ID の配列の長さは同一なのでどちらでループしても問題ありません。

[@] は配列の全要素を取得、! を先頭に付すとインデックスが取得可能です。また、curl-s オプションをつけると進捗が表示されなくなるので Closed にされたチケットのタイトルだけが並ぶのも個人的にはこだわりポイントだったりします。

#!/bin/sh
source ./.env

# ① Group 配下の Done ラベルのついたチケットから Project ID をスペース区切りで取得
_PROJECT_ID=$(curl --header "PRIVATE-TOKEN: ${PRIVATE_TOKEN}" \
  "${BASE_URL}/groups/${GROUP_ID}/issues?labels=Done&state=opened" |
  jq '[.[].project_id] | @sh' | sed "s/\"//g")

# ② Group 配下の Done ラベルのついたチケットから Issue ID をスペース区切りで取得
_ISSUE_ID=$(curl --header "PRIVATE-TOKEN: ${PRIVATE_TOKEN}" \
  "${BASE_URL}/groups/${GROUP_ID}/issues?labels=Done&state=opened" |
  jq '[.[].iid] | @sh' | sed "s/\"//g")

# ③ ①で取得した Project ID を配列化
for id in ${_PROJECT_ID}; do
  PROJECT_ID_ARRAY+=("$id")
done

# ④ ②で取得した Issue ID を配列化
for id in ${_ISSUE_ID}; do
  ISSUE_ID_ARRAY+=("$id")
done

# ⑤ ③の配列のインデックスを取ってループし、③④からインデックスに対応する ID を展開して Closed に更新
for index in ${!PROJECT_ID_ARRAY[@]}; do
  curl -s --request PUT --header "PRIVATE-TOKEN: ${PRIVATE_TOKEN}" \
  "${BASE_URL}/projects/${PROJECT_ID_ARRAY[index]}/issues/${ISSUE_ID_ARRAY[index]}" \
  -d "state_event=close" | jq '.title'
  
  sleep $SLEEP_TIME
done

リリースフローのテンプレート作成と対象チケットの紐付けを自動化

弊チームでは可能な限り週次リリースを行なっていますが、リリース作業に慣れたころに(致命的なミスではないが)手順をすっぽかしたことがありました。この対策としてリリースフローをチェックボックス形式で羅列したリリースチケットを私が作り始めました。そのうち、どのチケットがリリースされるのかが分かると嬉しいという要望を受け、リリース対象チケットを Related issues にポチポチ紐付けるようになり、しばらく私の手作業で運用されていました。もちろんこれも自動化したくなりますよね。

Issues APINew issue と Issue links APICreate an issue link の以下のエンドポイントを叩きます。

POST /projects/:id/issues
POST /projects/:id/issues/:issue_iid/links

リリース対象のチケットには Goal ラベルが付けられるものとします。①でクエリパラメータの description 以降にリリースフローを含めたリリースチケットのテンプレートを作成します。②は「Done チケットの Close を自動化」の章の①〜④で、対象とするラベルが Goal ラベルに変わっただけなので割愛します。③は②で取得した Project ID と Project 内での Issue ID のペアで指定し、Create an issue link のエンドポイントから①で作成した Issue に対してぶら下げています。

#!/bin/sh
source ./.env

DESCRIPTION="description=(略)"

# ① Description を含めたリリースチケットのテンプレート作成
CREATED_ISSUE_ID=$(curl --request POST --header "PRIVATE-TOKEN: ${PRIVATE_TOKEN}" \
     "${BASE_URL}/projects/${PROJECT_ID}/issues" \
      -d "labels=ToDo&title=Release" \
      --data-urlencode "${DESCRIPTION}" |
      jq '.iid | @sh' | sed "s/\"//g")

sleep $SLEEP_TIME

# ② Goal ラベルの付いたチケットの Project ID, Issue ID を全件取得し配列化
_PROJECT_ID=$(curl --header "PRIVATE-TOKEN: ${PRIVATE_TOKEN}" \
  "${BASE_URL}/groups/${GROUP_ID}/issues?labels=Goal&state=opened" |
  jq '[.[].project_id] | @sh' | sed "s/\"//g")

_ISSUE_ID=$(curl --header "PRIVATE-TOKEN: ${PRIVATE_TOKEN}" \
  "${BASE_URL}/groups/${GROUP_ID}/issues?labels=Goal&state=opened" |
  jq '[.[].iid] | @sh' | sed "s/\"//g")

for id in ${_PROJECT_ID}; do
  PROJECT_ID_ARRAY+=("$id")
done

for id in ${_ISSUE_ID}; do
  ISSUE_ID_ARRAY+=("$id")
done

# ③ ②で取得した Goal ラベルの付いたチケットを①の Related issues に紐付ける
for index in ${!PROJECT_ID_ARRAY[@]}; do
  curl -s --request POST --header "PRIVATE-TOKEN: ${PRIVATE_TOKEN}" \
  "${BASE_URL}/projects/${PROJECT_ID}/issues/${CREATED_ISSUE_ID}/links" \
  -d "target_project_id=${PROJECT_ID_ARRAY[index]}" \
  -d "target_issue_iid=${ISSUE_ID_ARRAY[index]}" |
  jq '.target_issue.title' | sed "s/\"//g"

  sleep $SLEEP_TIME
done

アップデートの必要なライブラリを取得しチケット作成を自動化

弊チームではフロントエンドのライブラリアップデートを可能な限り毎スプリント行うようにしています。フロントエンドのライブラリは特にバージョンの進みが速いので定期的に追従していかないと痛い目を見るのが目に見えているからです(パッケージマネージャには Yarn を使っています)。これも毎度毎度同じような作業が発生するので自動化してしまいましょう。

Issues APINew issue の以下のエンドポイントを叩きます。すでに前章で登場しており、やっていることも yarn outdated の結果を description に指定して issue を立てているだけなので、スクリプト自体の説明はあまりしません。

POST /projects/:id/issues

①でフロントエンドのディレクトリまで移動したあと、②で yarn outdated を実行します。出力は以下のようになっていますが欲しいのは上から 7 行目〜最終行を除いた行だけです。sed '$d' で末尾を削除したあとtail -n +6 で上から 6 行目以降だけを返すようにしています(何気にこういう整形に一番時間をかけてしまう)。

yarn outdated v1.13.0
info Color legend :
 "<red>"    : Major Update backward-incompatible updates
 "<yellow>" : Minor Update backward-compatible features
 "<green>"  : Patch Update backward-compatible bug fixes
Package  Current  Wanted  Latest Package  Type  URL
...      ...      ...     ...    ...      ...   ...
✨  Done in 1.58s.

③で②の出力と合わせて ToDo, PBI, Goal ラベルと weight 2 が振られたチケットが作成されます。

#!/bin/sh
source ./.env

# ① .env ファイルに定義されたフロントエンドのディレクトリに移動
cd $DIR_PATH

# ② yarn outdated の結果をトリミングして description へ
DESCRIPTION="description=\`\`\`\n"$(yarn outdated | sed '$d' | tail -n +6)""

# ③ ToDo, PBI, Goal ラベルと weight 2 を付してチケットを作成
curl --request POST --header "PRIVATE-TOKEN: ${PRIVATE_TOKEN}" \
      "${BASE_URL}/projects/${PROJECT_ID}/issues" \
      -d "labels=ToDo,Goal,PBI&weight=2&title=ライブラリアップデート" \
      --data-urlencode "${DESCRIPTION}" |
      jq '.title'

さいごに

GitLab API のドキュメントを読んでやりたいことが実現できそうなエンドポイントを探して実際に叩いてみたり、初めてのシェルスクリプトで書き方から調べてつどつど挙動を確かめながら少しずつ形にしていきましたが、考えたものが形になったときはやっぱり嬉しいですね。

最後までお読みいただきありがとうございました!

*1:超高性能電卓という愛称

*2:①③を一緒にできる書き方があればこっそり教えてください