PRがmergeされるまでの時間をGitHubActionsで取得してGoogle Spread Sheetに追記していく
こんにちは、@okuxxbos20です。普段はRailsなどを業務で触りつつ、レガシーシステムの改善やDevOpsっぽいこともやります。自分が関わった改善プロジェクトでチームの開発パフォーマンスが上がっていくとテンションも上がりますよね。
最近よく FourKeys という言葉を聞くようになりました。これはGoogleのDevOps Research&Assessmentチームが提唱している、ソフトウェア開発のパフォーマンスを測定するための以下の4つの指標のことです。
- リードタイム
- デプロイ頻度
- 変更失敗率
- 復元時間
この中の「リードタイム」についてですが、Googleでは 「commitから本番環境稼働までの所要時間」 と定義されています。つまり、あるリリースアイテムがfirst commitされた時間から本番稼働するまでの時間ですね。リードタイムが短いチームはユーザに早く価値を届けることができ、パフォーマンスの高い開発プロセスを実行できていることになります。このリードタイムを計測することで、チームの開発パフォーマンスを測定し、改善プロセスをスタートさせることができそうです。
しかしfour keysに関してのリポジトリを見るとBigQueryのテーブルを作成したり、dashboardにデータを連携するパイプラインを作成したりとちょっと大変そうです。
ですがリードタイムを取るだけであれば、GitHubActionsとSpreadsheetという馴染みがあるツールで代替することで改善プロセスをスモールスタートすることができそうです。本番稼働するまでの時間...?🤔
改めて「本番稼働するまでの時間」とはなんでしょうか。例えば一台のEC2インスタンスで稼働しているサービスであればそのインスタンスへのデプロイが完了したタイミングですし、CodeDeployなどでBlue/Greenデプロイを採用しているサービスであれば、Taskセットを終了した時間でしょうか。本番稼働するまでの時間を正確に取得するためにはそのサービスのリリース完了タイミングを知る必要があり、なかなか難しいです。
このように本番稼働するまでの時間を含める場合、リードタイムとして出てくる値はそのリリース方法に依存します。仮に実装からmergeまでが早い場合でもリリース時間が伸びた場合、リードタイムは長くなってしまいます。
そこからどうやって早く本番稼働まで持っていくかという視点で議論もできそうですが、今回の場合はチームの実装スピードやレビュー体制などの変化を可視化したかったため、Googleの定義からは少し変える必要がありました。
代替案
ということで、今回は本番稼働するまでの時間ではなく、「PRがmergeされるまでの時間」を記録することにします。なので今回定義するリードタイムは以下で計算することにしました。[1]
リードタイム = PRがmergeされた時刻 - first_commitの時刻
PRがmergeされたらGitHubActionsで定義したworkflowを開始します。そのPRのfirst_commitの時刻とmergeされた時刻を抽出し、Spreadsheetに追記することで可視化していくことをゴールにします。
Actionを定義していく
そのPRがmergeされたらActionを開始します。「PRがmergeされたら」という条件でworkflowを走らせたかったのですが、GitHubActions上では pull_request
の中にmergeのhookは見当たりませんでした。なので以下のようにして対応します。
on:
pull_request:
types:
- closed
jobs:
fetch_pr_data:
runs-on: ubuntu-latest
# GitHubActionsのトリガーにmergeは存在しないため, closeをトリガーにworkflowをstartする.
# closeされた && mergeされたPRに対してworkflowを走らせる.
if: github.event.pull_request.merged == true
PRのデータの取得方法
PRのデータの取得はGitHubAPIを使います。PRのauthorを取得することで、チームごとのリードタイムの傾向を調査できるようにしておきます。またPRのURLを取得してSpreadsheetに記録しておけばすぐにそのPRへ飛んで原因分析できそうです。いい感じにjqで欲しい値を抽出します。
$ gh api /repos/<OWNER_NAME>/<REPO_NAME>/pulls/<PR_NUMBER> \
-H "Accept: application/vnd.github+json" \
| jq -r "{
merged_at: .merged_at, \
author: .user.login, \
html_url: .html_url
}"
# =>
# {
# "merged_at": "2022-12-23T02:48:02Z",
# "author": "<AUTHOR_NAME>",
# "html_url": # "https://github.com/<OWNER_NAME>/<REPO_NAME>/pull/<PR_NUMBER>"
# }
またfirst_commitは上記のAPIのレスポンスには含まれていなかったため、こちらのAPIでcommitの配列を取得し、先頭のcommitの配列の時刻を記録します。
$ gh api /repos/<OWNER_NAME>/<REPO_NAME>/pulls/<PR_NUMBER>/commits \
-H "Accept: application/vnd.github+json" \
| jq -r ".[0]".commit.author.date
# => 2022-11-21T02:48:52Z
以上をGitHubActionsのstepにするとこんな感じでしょうか[2]。ここで取得した値を後のstepで使えるように $GITHUB_OUTPUT
に値を格納します。
- id: pr_data
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.number }}
run: |
JSON_RESPONSE=$(gh api /repos/<OWNER_NAME>/${{ github.event.repository.name }}/pulls/${{ env.PR_NUMBER }} \
-H "Accept: application/vnd.github+json" \
| jq -r "{
merged_at: .merged_at, \
author: .user.login, \
html_url: .html_url
}")
echo "merged_at=$(echo $JSON_RESPONSE | jq ".merged_at")" >> $GITHUB_OUTPUT
echo "author=$(echo $JSON_RESPONSE | jq ".author")" >> $GITHUB_OUTPUT
echo "html_url=$(echo $JSON_RESPONSE | jq ".html_url")" >> $GITHUB_OUTPUT
- id: first_commit_at
name: Fetch first commit at
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.number }}
run: |
FIRST_COMMIT_AT=$(gh api /repos/<OWNER>/${{ github.event.repository.name }}/pulls/${{ env.PR_NUMBER }}/commits \
--header "Accept: application/vnd.github+json" \
| jq -r '.[0]'.commit.author.date)
echo "first_commit_at=$(echo $FIRST_COMMIT_AT)" >> $GITHUB_OUTPUT
Google Platformに認証
データが取得できたらSpreadsheetにデータを書き込んでいくのですが、Sheet APIを使うためにはGoogle Cloud上で認証が必要です。GitHubActionではOIDCトークンが使えるため、Google CloudのWorkloadIdentity連携を使ってGoogle Cloudに対して認証できます[3]。わざわざAPIキーを管理し、定期的にローテートするなどの対応が必要ないのは運用面でもとても楽ですね。
そのためのGCP純正のgoogle-github-actions/authというGitHubActionsを使っていきます。その前にGoogle Cloudで
- Projectの作成
- Service Accountの作成
- Workload Identity Poolの作成
- OIDCプロバイダ設定
などの事前セットアップが必要です。詳しくはSetting up Workload Identity Federationを参考にしてください。リソースが作成できたらauthに情報を渡して準備完了です。
- id: auth
uses: google-github-actions/auth@v1
with:
workload_identity_provider: <WORKLOAD_IDENTITY_PROVIDER>
service_account: <SERVICE_ACCOUNT>
token_format: access_token
access_token_scopes: https://www.googleapis.com/auth/spreadsheets
この認証フローによってworking directoryのtopにcredential情報のjsonファイルを保存し、認証を行います。workflow終了後にこのjsonファイルは自動で削除されますが、誤ってcommitしてしまわないように .gitignore
設定をしておきます。
# Ignore generated credentials from google-github-actions/auth
gha-creds-*.json
Spreadsheetに書き込み
いよいよ取得したデータをSpreadsheetに書き込んでいきます。Google Sheets APIのドキュメントを見るとNode.jsやRubyなど様々な言語をサポートしています。
しかしGitHubAction上でこれらを使う場合、例えばNode.jsやRubyなどを使うときは npm i
や bundle install
などを実行して環境をセットアップする必要があります。今回はシンプルにGitHubActionでPOSTリクエストを送信したいだけなので、curlを使ってリクエストしてみます。ここでHeaderにBearerトークンとして auth
で取得したtokenを指定することでリクエストが可能です。
- name: Append PR data to spreadsheet
env:
SHEET_NAME: sample
FIRST_COMMIT_AT: ${{ steps.first_commit_at.outputs.first_commit_at }}
MERGED_AT: ${{ steps.pr_data.outputs.merged_at }}
AUTHOR: ${{ steps.pr_data.outputs.author }}
PR_URL: ${{ steps.pr_data.outputs.html_url }}
run: |
curl -X POST https://sheets.googleapis.com/v4/spreadsheets/${{ secrets.SPREADSHEET_ID }}/values/${{ env.SHEET_NAME }}:append?valueInputOption=USER_ENTERED \
-H "Authorization: Bearer ${{ steps.auth.outputs.access_token }}" \
-H "Content-Type:application/json" \
-d \
'
{
"majorDimension": "ROWS",
"values": [
[${{ env.FIRST_COMMIT_AT }}, ${{ env.MERGED_AT }}, ${{ env.AUTHOR }}, ${{ env.PR_URL }}]
]
}
'
書き込みが成功すると以下のような感じになります(GitHubAPIで取得できる時刻はISO8601形式なので、lead_timeを計算するときはSpreadsheet側で頑張って整形しました)。
GitHubActions全体図
以下が全体図です。何回かworkflowを走らせてみた結果、10数秒程度でSpreadsheetの追記まで完了していました。Timeout設定やbranch設定は適宜変更してください。
name: four-keys lead time
on:
pull_request:
types:
- closed
branches:
- main
jobs:
four_keys_lead_time:
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true
permissions:
contents: read
pull-requests: read
id-token: write
steps:
- name: Check out code
uses: actions/checkout@v3
- id: pr_data
name: Fetch PR data
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.number }}
run: |
JSON_RESPONSE=$(gh api /repos/<OWNER_NAME>/${{ github.event.repository.name }}/pulls/${{ env.PR_NUMBER }} \
-H "Accept: application/vnd.github+json" \
| jq -r "{
merged_at: .merged_at, \
author: .user.login, \
html_url: .html_url
}")
echo "merged_at=$(echo $JSON_RESPONSE | jq ".merged_at")" >> $GITHUB_OUTPUT
echo "author=$(echo $JSON_RESPONSE | jq ".author")" >> $GITHUB_OUTPUT
echo "html_url=$(echo $JSON_RESPONSE | jq ".html_url")" >> $GITHUB_OUTPUT
- id: first_commit_at
name: Fetch first commit at
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.number }}
run: |
FIRST_COMMIT_AT=$(gh api /repos/<OWNER_NAME>/${{ github.event.repository.name }}/pulls/${{ env.PR_NUMBER }}/commits \
-H "Accept: application/vnd.github+json" \
| jq -r '.[0]'.commit.author.date)
echo "first_commit_at=$(echo $FIRST_COMMIT_AT)" >> $GITHUB_OUTPUT
- id: auth
name: Authenticate to Google Cloud
uses: google-github-actions/auth@v1
with:
workload_identity_provider: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ secrets.SERVICE_ACCOUNT }}
token_format: access_token
access_token_scopes: https://www.googleapis.com/auth/spreadsheets
- name: Append PR data to spreadsheet
env:
SHEET_NAME: sample
FIRST_COMMIT_AT: ${{ steps.first_commit_at.outputs.first_commit_at }}
MERGED_AT: ${{ steps.pr_data.outputs.merged_at }}
AUTHOR: ${{ steps.pr_data.outputs.author }}
PR_URL: ${{ steps.pr_data.outputs.html_url }}
run: |
curl -X POST https://sheets.googleapis.com/v4/spreadsheets/${{ secrets.SPREADSHEET_ID }}/values/${{ env.SHEET_NAME }}:append?valueInputOption=USER_ENTERED \
-H "Authorization: Bearer ${{ steps.auth.outputs.access_token }}" \
-H "Content-Type:application/json" \
-d \
'
{
"majorDimension": "ROWS",
"values": [
[${{ env.FIRST_COMMIT_AT }}, ${{ env.MERGED_AT }}, ${{ env.AUTHOR }}, ${{ env.PR_URL }}]
]
}
'
まとめ
以上four keysのメトリクスの一つである、リードタイムを計測して可視化するまでをやってみました。計測したいリポジトリに対して、ymlファイルを一つ追加するだけで簡単に計測ができ、改善プロセスをスタートすることができました。今後の展開としてはPRが作成された時間やapproveが付いた時間なども計測することで、より詳細な分析ができそうです。
-
mercariなどでもリードタイムに関しては「PRの作成時間からmainブランチにmergeされるまでの時間」と独自定義したものを使っているそうです。https://engineering.mercari.com/blog/entry/20221116-souzoh-productivity/ ↩︎
-
GitHubActionsのrunner-imageで
ubuntu-latest
を指定すると2022/12月現在はUbuntu 22.04.1 LTSが使われますが、最初からgithub apiやjqが使えるので特にinstallのstepは不要です。便利ですね。https://github.com/actions/runner-images/blob/main/images/linux/Ubuntu2204-Readme.md ↩︎ -
WorkloadIdentity連携などで使用するリソースは基本的に無料で使えます。https://cloud.google.com/iam/pricing ↩︎
Discussion