Reusable Workflowsでポリレポでも保守しやすいトイル解消法
はじめに
モノレポがそれなりに人気?になっている昨今ですが、まだまだポリレポな開発をしている方も多いはず。弊社もマイクロサービスというほどではありませんが、10個程度のリポジトリで開発しています。
各リポジトリでは運用業務の負担軽減のために、GitHub Actionsを使った自動化を行なっています。そんな弊社で行ったGitHub Actionsのリファクタについて紹介します。
トイルは少ないほうがいい
運用する上で必要だったりチームで動く以上、避けられない作業というのはあります。
タスクやチケットのステータス管理、PRのステータス管理、Assignee管理…etc
そういった繰り返しの作業はトイルかもしれない…?
トイルとは何でしょうか。トイルとは、プロダクションサービスを動作させることに関係する作業で、手作業で繰り返し行われ、自動化することが可能であり、戦術的で長期的な価値を持たず、作業量がサービスの成長に比例するといった傾向を持つものです。
https://www.oreilly.com/library/view/sre-google/9784873117911/ch05.xhtml#:~:text=トイルとは、プロダクションサービス,傾向を持つものです。
エンジニアなら、めんどくさい作業は自動化させたいですよね。
自動化していきましょう。
どんなトイルがあるか
チケット管理を例に挙げます。
弊社ではNotionを社内のドキュメント・タスク・チケット管理ツールとして使用しています。
「チケットはお手紙」のスローガン?のもと、開発プロセスの中で、様々な役割の人がチケットを書き込み・確認をします。
弊社の開発プロセス。太線が開発フロー、破線がチケットへのアクセス
しかし問題があります。チケットとPRのステータス管理が二重で発生してしまうことです。
PMとエンジニアで普段触るところが異なります。そのため両方のステータスが同期している必要があります。ですが、毎回PRとチケット2つのステータスを変えるのは面倒ですし、人間なので変え忘れることもあります。
- PM・朝会:チケットを確認して進捗管理や議論する
- エンジニア:PR上で細かい仕様や設計についての議論する
特に弊社はPMがエンジニアに比べて少なく、PMは様々なプロジェクトを管理しており、可能な限り無駄な管理コストを下げたいという思いもあります。
GitHub Actionsで自動化しよう
そこで弊社ではGitHub Actionsを使って、PRのラベルが変更されたことをトリガーとして、Notionチケットのステータスを変更しています。これによってGitHubのステータス管理さえ徹底すれば、変え忘れは発生しません。
他にも様々な運用業務をGitHub Actionsによって効率化・自動化しています。
- PRへのプロジェクトラベル付与
- PRへのAssigneeの自動Assign
- Staging環境へのリリース時に検証担当者へSlackで通知
- リリースPR、リリースドラフトの自動作成
- Hotfix時、開発ブランチにHotfixの内容を反映するPRの自動作成
- git-secretsを使ったシークレットスキャン
- ...etc
弊社にとってGitHub Actionsはなくてはならない存在となりつつあります。
問題:リポジトリが多い
冒頭でも紹介しましたが、弊社では10個程度のリポジトリで開発を行なっています。私がジョインした2022年9月の段階では、全く同じ内容のGitHub Actionsとスクリプトを各リポジトリに配置していました。
これでは変更する時、
- すべてのリポジトリで同じ内容の変更を加え、
- すべてのリポジトリでPRを作成し、
- すべてのリポジトリでApproveをもらい
- すべてのリポジトリでマージする
という作業が発生します。
めんどくさいですね。
一応1つのリポジトリの内容を他のリポジトリにばら撒いてPRまで作ってくれるGitHubActionsまきまきくん.sh
というスクリプトファイルはありました。しかし、毎回Approveを依頼してマージするのは面倒でした。
Reusable Workflowで一箇所にまとめたいなあ……………………。
でもPublicリポジトリしかできないよね………………………………………。
えっ!?できるの!?
(宣伝失礼しました。太鳳ちゃん可愛いですね。😌
Reusable workflowsで一箇所にまとめちゃう
2022年12月くらいにPrivateリポジトリのworkflowもcallできるようになりました。(すごい👏)
「workflowをcallするって何?」って方
uses
のことです。
これによって他のリポジトリで定義されているGitHub Actionsのworkflowを再利用できるようになります。(便利😌)
steps:
- uses: actions/checkout@v4
この再利用できるworkflowのことをReusable workflows(再利用可能なworkflow)と呼びます。
Reusable workflowsは↓のようにするだけで定義できます。
on:
workflow_call:
以前までは、Privateリポジトリで定義されているworkflowはcallできませんでした。
error parsing called workflow "xxxx": Workflows in 'xxxx' cannot access remote workflows in 'xxxx'. See https://docs.github.com/en/actions/learn-github-actions/reusing-workflows#access-to-reusable-workflows for more information.
このアップデートを受けて、いろんなリポジトリに散らばっていた自動化workflowの定義を、一箇所のリポジトリ(productivity)に集約しました。これによって、GitHub Actionsの保守性がグッと高まりました。
構成図
-
call-common.yml
を各開発リポジトリに定義し、productivityのcommon.yml
をcallする -
common.yml
内で各workflowの実行条件を判定し、workflowを呼び出すか判定する - それぞれのworkflowで具体的な処理を実行する
ここからは、スクリプト実行のパターンと工夫ポイントについて話していきます。
パターン1:簡単なスクリプトしか書かない時
シェルスクリプトだけで完結する時や、GitHubホステッドランナーに元から入っているパッケージ しか使わない場合は、簡単な設定で実行できます。
例として、PRの作成時に作成者をassigneeに追加するworkflowを紹介します。
name: PR作成時にPRの作成者をassigneeにする(Reusable)
on:
workflow_call:
jobs:
add-assignee:
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
steps:
- uses: actions/checkout@v4
- name: PR作成時にPRの作成者をassigneeにする
run: |
author_login=$(gh pr view $PR_NUMBER --json author | jq -r '.author.login')
gh pr edit $PR_NUMBER --add-assignee $author_login
GitHub CLIとjqを使用しているのみで、簡潔に書けています。
※GitHub CLIを使うにはGH_TOKENが必要になるので、渡してあげましょう。
パターン2:複雑なスクリプトを実行したいとき
シェルスクリプトだけで書くのはしんどいとき、GitHubホステッドランナーに入っていないパッケージを使いたい時などは一工夫必要です。
弊社では、より複雑な処理を実行したい時にzxを活用しています。
「zx知らないよ!」って方のために簡単に説明を入れておきました。
zxとは
JavaScriptで簡単にシェルコマンドを実行できるツールです。
Bash is great, but when it comes to writing more complex scripts, many people prefer a more convenient programming language. JavaScript is a perfect choice, but the Node.js standard library requires additional hassle before using. The zx package provides useful wrappers around child_process, escapes arguments and gives sensible defaults.
https://google.github.io/zx/getting-started#overview
簡単に書き方を紹介します。
-
.mjs
を拡張子に持つファイルを作成する - 最上部にshebangを記述しする
-
$`command`
のように実行したいコマンドをバッククオートで囲む
#!/usr/bin/env zx
const name = 'foo & bar'
await $`mkdir ${name}`
これだけで、簡単にコマンドが実行できます。また、${...}
で囲まれたものは全て自動でエスケープされ、クオートで囲まれるため、インジェクションを気にせず書けることも魅力です。
作成したzxのスクリプトを実行するには下記のコマンドを実行します。
zx ./script.mjs
非常に魅力的なzxですが、Reusable Workflowで活用するには一工夫が必要です。
GitHub ActionsのReusing workflowsという章で下記の記述があります。
If you reuse a workflow from a different repository, any actions in the called workflow run as if they were part of the caller workflow. For example, if the called workflow uses actions/checkout, the action checks out the contents of the repository that hosts the caller workflow, not the called workflow.
https://docs.github.com/en/actions/using-workflows/reusing-workflows
別のリポジトリからワークフローを再利用する場合、呼び出されたワークフロー内のアクションは、呼び出されたワークフローの一部であるかのように実行されます。例えば、呼び出されたワークフローが actions/checkout を使用する場合、アクションは呼び出されたワークフローではなく、呼び出されたワークフローをホストするリポジトリのコンテンツをチェックアウトします。
つまり、workflow_call
によってactions/checkout
を実行する場合、productivity内のスクリプト群をチェックアウトできません。スクリプトがランナー上に存在しないので、スクリプトを実行しようとするとエラーになります。
これを回避するために、actions/checkoutのCheckout multiple repos (private) を活用します。
steps:
# productivityにアクセス可能なtokenを発行
- name: Generate github token
id: generate_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: "productivity"
# productivityをランナーにcheckout
- name: Check out the target repository
uses: actions/checkout@v4
with:
repository: socialdb/productivity
ref: master
token: ${{ steps.generate_token.outputs.token }}
path: productivity
# 依存パッケージをinstall
- name: Install dependencies
run: npm i --prefix ./productivity/.github/helper_scripts && npm i -g zx
これを使うことで、プライベートリポジトリのコードをランナーにチェックアウトできます。
token発行には↓を使用しました。
工夫ポイント1:workflowが失敗した時のSlack通知
運用効率化のための(=プロダクトに直接関係しない)ツールといえど、社内の業務に関わるため、workflowがバグっているときや、一時的に失敗してしまった時はSlack等に通知して問題を把握したいですよね。
ただ、workflowがネストしているため、どこでjobの失敗を判定してSlack通知を飛ばすかという問題があります。
これを解決するためにworkflowのoutputsを活用しました。
具体的には、下記のような処理でSlack通知を実現しました。
- jobの最後に、jobが失敗したらフラグを立てる処理を入れる
- jobのoutputsをworkflow_callのoutputsに渡す
-
common.yml
の最後に、各jobの成功/失敗フラグを見てSlackに通知するjobを定義する
↓実装例です。
name: PR作成時にPRの作成者をassigneeにする(Reusable)
on:
workflow_call:
outputs:
IS_FAILURE:
description: jobがfailしたかどうかのフラグ
value: ${{ jobs.add-assignee.outputs.IS_FAILURE }}
jobs:
add-assignee:
runs-on: ubuntu-latest
outputs:
IS_FAILURE: ${{ steps.is_failure.outputs.IS_FAILURE }}
steps:
###################
# メインの処理はここ #
###################
- name: 失敗したらSlack通知用のフラグを立てる
if: ${{ failure() }}
id: is_failure
run: |
echo "IS_FAILURE=true" >> $GITHUB_OUTPUT
notify-slack-if-job-failed:
runs-on: ubuntu-latest
if: ${{ always() }} # needsのjobの成功/失敗に関係なく、needsのjobがすべて完了したあとに実行する
needs: [workflowA] # 実行するjobを配列形式で全て記述する
steps:
- name: フラグ出力
env:
EVENT_CONTEXT: ${{ toJSON(needs.*.outputs.IS_FAILURE) }}
run: |
echo $EVENT_CONTEXT
- name: jobが1つでも失敗したらSlackに通知
if: ${{ contains(toJSON(needs.*.outputs.IS_FAILURE), true) }}
uses: rtCamp/action-slack-notify@v2
env:
SLACK_USERNAME: ${{ inputs.SLACK_USERNAME }}
SLACK_WEBHOOK: ${{ inputs.SLACK_WEBHOOK }}
SLACK_CHANNEL: ${{ inputs.SLACK_CHANNEL }}
SLACK_COLOR: failure
これによって、フラグを立てる処理を各workflowに定義するだけで、Slack通知自体の処理はまとめることができました。
common.yml
の呼び出し側で実行するjobを選択可能にする
工夫ポイント2:これらは、元々メインプロダクト向けに作成したworkflowでした。しかし、社内の別プロダクトの開発チームからも使いたいという要望が上がるようになりました。特にシークレットのスキャンは、セキュリティに関わる部分であるため、すべてのプロダクトでチェックできるようにしたいのもあります。
しかし、メインプロダクト独自の運用に合わせたworkflowもあります。プロダクトごとに人数規模も違うため、プロジェクトの管理方法も異なります。すべてのプロダクトの運用をメインプロダクトの運用に統合させるのは現実的ではありません。そのため、common.yml
を呼び出す側で、実行するjobを選択できるようにしました。
以下、実装例です
-
call-common.yml
側でパイプ繋ぎによって実行したいworkflowを指定workflow: "add-assignee|scan-secret"
-
common.yml
側でパイプ繋ぎされた文字列を解体しGITHUB_OUTPUT
に出力pickup-jobs: runs-on: ubuntu-latest outputs: workflows: ${{ steps.workflows.outputs.value }} steps: - name: 実行するjobを選択 if: ${{ inputs.workflow != 'all' }} id: workflows run: echo -n '${{ inputs.workflow }}'|jq -c -R 'split("|")|tojson'|xargs -0 -I {} echo "value={}"|tr -d "\n" >> $GITHUB_OUTPUT
- 各jobの実行条件に
pickup-jobs
の出力結果の判定を加える
今発生している問題:テストやデバッグの困難さ
CI/CD系に常に付きまとう問題ですが、テストやdebugが本当に面倒かつ困難です。
最近見かけた小技で多少マシにはなるかもしれませんが…。
ローカルでGitHub Actionsをテストできるライブラリもありますが、以前触った時「これできないのか〜」となった記憶があります。(何ができなかったのか忘れた)
終わりに
最近見かけた記事が良かったので。
めんどくさい作業にぶち当たった時、一気に改善してしまう人がいる。ガッと自動化したり仕組みそのものを変えたりしてしまうのだ。「めんどくさい」と心の中で思ったなら、その時スデに行動は終わっているのである。
https://konifar-zatsu.hatenadiary.jp/entry/2023/12/21/124953
ガッと自動化していくぞ、某兄貴のように…。
Discussion