チームでソフトウェア開発をするとき、一度のリリースに含まれる変更が多すぎることにより、動作検証に時間がかかったり、問題が発生した時の原因特定が難しくなることがある。これを防ぐため、tocenboughというGitHub Actionを作った。読み方はもちろん「とおせんぼう」だ。
内容は至ってシンプルで、前回のlatest releaseからのマージコミットの数を計算し、その数が閾値を上回ったら落ちるというだけのスクリプトである。かなり単純なので、動作を把握するにはソースを見てもらったほうが早いかもしれない。
# GitHubから最新のリリースのタグを取得する。これがプルリクエストの数をカウントする起点となる latest=$(curl -H "Authorization: Bearer ${{ inputs.token }}" --silent \ "https://api.github.com/repos/${{ github.repository }}/releases/latest" | jq -r .tag_name) # まだリリースがひとつもない場合、原初のコミットハッシュを設定する if [ "$latest" == "null" ]; then latest=4b825dc642cb6eb9a060e54bf8d69288fbee4904 fi log="$(git log --merges --format="%an %s" "$latest..origin/${{ github.event.pull_request.base.ref }}")" # マージコミットの数を数える。ただし、[bot]がつくものは別にカウントする。 pr_count=$(echo "$log" | grep -v 'bot]' | wc -l | xargs) bot_count=$(echo "$log" | grep 'bot]' | wc -l | xargs) # PR数の定数倍 + botによる変更数からスコアを計算する score=$(( $pr_count * ${{ inputs.multiplier }} + $bot_count )) expr="Total weight of unreleased PRs since $latest: ${{ inputs.multiplier }} * $pr_count[PRs] + $bot_count[bot] = $score" # GITHUB_STEP_SUMMARYに結果を書き込む echo -e "### tocenbough\n" >> $GITHUB_STEP_SUMMARY if [ "$score" -gt "${{ inputs.threshold }}" ]; then echo "We have too many unreleased changes! $expr" >> $GITHUB_STEP_SUMMARY if [ ${{ contains(github.event.pull_request.labels.*.name, 'cram') }} == "false" ]; then exit 1 fi else echo "$expr" >> $GITHUB_STEP_SUMMARY fi
最後のステップで参照している $GITHUB_STEP_SUMMARY は、job summary *1のパスである。ここに任意の文字列を書き込むと、GitHub ActionのSummaryページに表示できる。
より実用性を高めるため、いくつかの工夫を施している。
工夫: botによる変更の重みを調整する
dependabotやrenovateによるPRは数が嵩みやすいため、人の手による変更と同じようにカウントしたくない場合がある。そのため、 multiplier
というパラメータを設定することで、人の手によるPRの重みを調整できる。
工夫: 緊急時は制限を突破できる
「cram」という名前のラベルをPRにつけると、制限を上回っていたとしてもjobは成功する。急いで修正したいものがある場合は次のリリースにねじ込むことができる。
結果
HERP社内で最も活発なリポジトリで半年ほど運用してみた結果、リリースサイクルが目に見えて改善された(導入前の半年間に比べ、リリース間隔の平均は5.5日から4.2日、中央値は6日から3日になった)。
「変更をマージするために、まず溜まっている変更をリリースする」というインセンティブが、リリース作業自体はもちろん、プロセスの改善をも促すため、予想以上に効果がありそうだ。
リリースの頻度や粒度に悩みのあるチームはお試しあれ。.github/workflows
の下に以下のような設定を置くことで導入できる。
name: tocenbough on: pull_request: types: [opened, reopened, labeled, unlabeled] jobs: cd: runs-on: ubuntu-latest steps: - uses: fumieval/tocenbough@v0 with: token: ${{ secrets.GITHUB_TOKEN }} threshold: 10