🤖

デフォルトブランチ以外のブランチに対して、差分があったときに定時でビルドする方法

2023/12/18に公開

この記事はCureApp Advent Calendar 2023の17日目の記事です。

みなさん、CDやってますか?
私が参加しているプロダクトでは、毎朝5時に定時ビルドを行い内部テスト向けのアプリケーションの配布やバックエンド等の更新を行う仕組みを構築しています。流れとしては

  1. Github Actions を定時で動かす
  2. bitrise をキックしてモバイルアプリをビルド
  3. Webフロントエンドやバックエンド、インフラは Github Actions でそのままビルド・デプロイ

みたいな感じでやってます。

この定時ビルドですが、 GitHub のデフォルトブランチ以外のブランチを対象としようとしたときにひと工夫必要でしたので、そのノウハウを書き留めます。

そもそもなんでそんなことが必要なのか

デフォルトブランチ以外を定時ビルドで配布するユースケースとしては、以下のようなものがあるのではないでしょうか。

  • 複数のブランチで定時ビルドを実施して、それぞれに配布するチームを設定したい
    • セールスには main ブランチのものを渡してテストチームには develop ブランチを渡す、とか
  • 定時ビルドしたい対象は一つであるが、ビジネス上の関係でデフォルトブランチと開発ブランチを別にする必要がある

私たちのチームでは後者に該当(mainブランチを更新せずに開発ブランチを伸ばしてそれをデプロイする必要があった)し、かつデフォルトブランチを変更する選択ができなかったため今回のような仕組みを構築するに至りました。

どうやるの

定時でビルドする

Github Actions で定時ビルドするときに使う手法としては、 cron を使った指定が一般的でしょう。ワークフローの yml の冒頭に以下のように書けば、毎朝日本時間5時に定時でそのワークフローが始動します(ここで指定する時間は UTC の時間であることは忘れられがちです)。

name: daily-deploy
on:
  # 毎朝5時 (=UTC20時) に実施
  schedule:
    - cron: '0 20 * * *'

ビルド対象のブランチにチェックアウトする

その後は無難に actions/checkout をしたりするわけですが、ここで一つ問題が発生します。cron で定時でビルドを発生させるとき、必ず GitHub で指定したデフォルトブランチから実施されることになります。そのため、何も設定しなければ actions/checkout は自動でデフォルトブランチが指定されることになります。それを回避するため、 actions/checkout のオプションでブランチを指定してあげます。

定時ビルドしたい対象のブランチが develop の場合、

deploy-mobile-staging-app:
    # 変更がないときにビルドをスキップする。後述します
    needs: [check-branch-updated]
    if: needs.check-branch-updated.outputs.is-updated == 'true'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          # Note: 定時でビルドしたい develop ブランチを直で指定する
          ref: 'develop'

といったイメージになります。ここは決め打ちでやるしかないかなともいます。

ビルド対象のブランチを bitrise に渡す

続いて、この先にキックする bitrise にブランチの情報を渡す必要があります。この時も通常は GITHUB_REF 等でコミットハッシュを指定して渡すことが多いと思いますが、今回はブランチ名を指定します。ここも決め打ちになるでしょうか。

curl \
  https://app.bitrise.io/app/$BITRISE_APP_ID/build/start.json \
  -H 'Content-Type: application/json' \
  -H "Authorization: token $BITRISE_API_TOKEN" \
  -d '{
  "build_params": {
    "branch": "develop",
    "commit_message": "$COMMIT_MESSAGE",
    "commit_hash": "",
    "tag": "",
    "workflow_id": "$WORKFLOW_ID"
  },
  "hook_info": {
    "type": "bitrise"
  }
}'

みたいな curl をたたくイメージです。

このとき、 commit_hash や tag に有効な文字が入っていると、そちらが優先されてブランチの設定が無視されることに注意してください。

Parameter priority
The `Git Clone`` Step has the following parameter priority:

  1. commit_hash
  2. tag
  3. branch

If you provide multiple parameters, the parameter with lower priority will be ignored.

https://devcenter.bitrise.io/en/api/triggering-and-aborting-builds.html#setting-a-branch--commit-or-tag-to-build

私が試したときは他ビルドの設定を維持するためにスクリプトにこれを残してしまっていたので、数ビルド無駄にしました。空文字で渡す分には問題なさそうでしたので上のようなスクリプトに修正しました。

デフォルトブランチでないブランチの更新有無を確認する

このままでも動くは動くのですが、このままでは更新が一切ない状態でも問答無用でビルドが開始されてしまいます。このままでは Github Actions や bitrise の枠がもったいないので、更新がなかった時にビルドをスキップするステップを作ります。今回は Actions の機能である cache を使用しました。

# 最後のデプロイからコミットハッシュが変わっていないかチェックする。
# 変わってなかったらデプロイしても無駄なので以降のビルドはやらない
check-branch-updated:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    permissions:
      id-token: write
      contents: read
    steps:
      # develop にチェックアウトする
      - uses: actions/checkout@v3
        with:
          ref: 'develop'
      - name: create Cache Dir
        run: mkdir -p cache-SHA
      - name: check-current-git-hash
        run: git show --format='%H' --no-patch
      - name: save-current-git-hash
        # 現在のコミットハッシュをファイルに書き出す
        run: git show --format='%H' --no-patch > git-hash.txt
      - name: Save or Restore Cache SHA
        id: cache_sha
        uses: actions/cache@v3
        with:
          path: ./cache-SHA
          # HACK: github.sha でのコミットハッシュの取得は cron からの定期実行だと develop のハッシュしか取得できない
          # そのため、コミットハッシュを書き出したファイルからハッシュを作成し、それをキャッシュのkeyにする
          key: cache-SHA-${{ hashFiles('git-hash.txt') }}
    outputs:
      # キャッシュヒットしない場合、現在のコミットハッシュに対してデプロイしていないので、更新ありと判断する
      is-updated: ${{ steps.cache_sha.outputs.cache-hit != 'true' }}

ちょっと面倒な記述になってますがやってることは単純で、

  1. develop にチェックアウトする
  2. git-hash.txt を作成して、そこに現在のコミットハッシュを書き出す。
  3. actions/cache でそのファイルの hashFiles() をキーにキャッシュヒットを調べる
  4. ヒットしなかったら、そのコミットハッシュでのビルドは未実施ということなので、その後のビルドを行う

という感じです

これでソースの更新がなかった時に無駄にビルドが走るのを一旦抑えることができます。
欲を言えば対象のもの(アプリ・Webフロント)の更新があったかという観点で調整できれば一番理想なんでしょうが、面倒そうなので今回はやっていません。

最終的なもの

最終的につなげるた結果を一応置いておきます。参考になれば幸いです。
本質でなかったので本文では触れてない slack の通知とかも追加してます。

yml の全容
# staging環境をデプロイするワークフロー
name: Deploy-staging-app
on:
  # 毎朝5時 (=UTC20時) に実施
  schedule:
    - cron: "0 20 * * *"
  # 手動でも実施可能
  workflow_dispatch:

jobs:
  # 最後のデプロイからコミットハッシュが変わっていないかチェックする。
  # 変わってなかったらデプロイしても無駄なので以降のビルドはやらない
  check-branch-updated:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    permissions:
      id-token: write
      contents: read
    steps:
      # develop にチェックアウトする
      - uses: actions/checkout@v3
        with:
          ref: "develop"
      - name: create Cache Dir
        run: mkdir -p cache-SHA
      - name: check-current-git-hash
        run: git show --format='%H' --no-patch
      - name: save-current-git-hash
        # 現在のコミットハッシュをファイルに書き出す
        run: git show --format='%H' --no-patch > git-hash.txt
      - name: Save or Restore Cache SHA
        id: cache_sha
        uses: actions/cache@v3
        with:
          path: ./cache-SHA
          # HACK: github.sha でのコミットハッシュの取得は cron からの定期実行だと develop のハッシュしか取得できない
          # そのため、コミットハッシュを書き出したファイルからハッシュを作成し、それをキャッシュのkeyにする
          key: cache-SHA-${{ hashFiles('git-hash.txt') }}
    outputs:
      # キャッシュヒットしない場合、現在のコミットハッシュに対してデプロイしていないので、更新ありと判断する
      is-updated: ${{ steps.cache_sha.outputs.cache-hit != 'true' }}

  deploy-mobile-staging-app:
    needs: [check-branch-updated]
    if: needs.check-branch-updated.outputs.is-updated == 'true'
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v3
        with:
          ref: "develop"
      - name: Deploy Mobile App
        # HACK: 定期実行からは GITHUB_REF からはデフォルトブランチの情報しか取得できないので、対象のブランチを決め打ちで指定する
        run: |
          ./scripts/trigger-bitrise develop "" "" "Build develop" $APP_ID staging-app
        env:
          APP_ID: ${{ secrets.APP_ID }}
          BITRISE_API_TOKEN: ${{ secrets.BITRISE_TOKEN }}

      # ビルド自体の成功/失敗通知はbitrise側から飛んでいくので、ここではそこに通知を送る時も失敗した時に備えて失敗通知だけ送るようにする
      - name: Slack Failure Notification
        if: failure()
        uses: rtCamp/action-slack-notify@v2
        env:
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
          SLACK_CHANNEL: notification
          SLACK_USERNAME: "ステージングアプリデプロイがコケてるよ!"
          SLACK_ICON_EMOJI: ":rotating_light:"
          SLACK_COLOR: "#F0741E"

発展形

ブランチ名を渡してそれをビルドするアクションを作ったら便利かも。特に、複数環境を切り分けてデプロイするようなケースのとき。

終わりに

おわり!

GitHubで編集を提案
CureApp テックブログ

Discussion