リリースされていない変更が溜まるのを防ぐGitHub Action「tocenbough」

チームでソフトウェア開発をするとき、一度のリリースに含まれる変更が多すぎることにより、動作検証に時間がかかったり、問題が発生した時の原因特定が難しくなることがある。これを防ぐため、tocenboughというGitHub Actionを作った。読み方はもちろん「とおせんぼう」だ。

github.com

内容は至ってシンプルで、前回の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