Magicode logo
Magicode
4
7 min read

[GitHub Actions] Dependabotのプルリクを集約する

https://cdn.apollon.ai/media/notebox/6f5e2367-f9c9-499e-9fc2-bd2c952f9891.jpeg

概要

  • ライブラリのアップデートに追従するのにDependabotは非常に便利
  • Dependabotのプルリクは設定の仕方によって集約することが出来る

Dependabotとは

  • プロジェクトの依存関係が持つ脆弱性の特定・報告をしてくれる
  • 依存関係を持つライブラリのバージョンアップデートをしてくれる
等々、OSSのライブラリを使うことがもはや当たり前になった昨今のシステム開発においては入れておいて損はないでしょう。
https://docs.github.com/ja/code-security/dependabot

導入方法

GitHubであれば .github/dependabot.yml を作っておくだけ。非常に簡単。
https://docs.github.com/ja/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file

問題点

デフォルトブランチに対するプルリクが作られた際にCIを行うGitHub Actionsのジョブを実装するのは多々ある。
Dependabotをデフォルトの設定のまま導入すると、プルリクのターゲットブランチがデフォルトブランチとなるため、Dependabotがプルリクを作成した時点で実装したGitHub Actionsのジョブが走ることになる。
Dependabotは基本的に1ライブラリごとにバージョンアップデートのプルリクを作ってくるので、仮にDependabotが10個のライブラリのバージョンアップデートをしようとすると10本のプルリクが作られる。
そうするとプルリクが作られた時点で10回はGitHub Actionsのジョブが実行されることになる。
しかもDependabotは賢いのでデフォルトブランチが更新された場合は自動的にリベースを試みるため、リベースが実行されると再度GitHub Actionsのジョブが走ることになる。
つまり10本のプルリクが作成された場合、トータルで走るGitHub Actionsのジョブは10 + 9 + 8 + 7 + 6 + 5 + 4 + 3 + 2 + 1 = 55回となり、1回あたりのジョブの実行時間が5分だとすると55 × 5 = 275分になる。
GitHub Actionsの実行時間には無料枠があり、プランによって上限は異なるがGitHub Freeの場合は2000分/月である。
先程の例でいけば1回のDependabotのバージョンアップデートの実行でひと月の15%近くを消費してしまう。
JavaScript界隈のライブラリなんかは数も多く、更新頻度もそれなりに高いので何もしていないとDependabotのプルリクだけで無料枠をほぼ使い切ってしまう、なんてことも起きてくる。

本題

ブランチ戦略を立てる

とは言いつつもDependabotの自動バージョンアップデートの恩恵は受けたい、でもライブラリのバージョンアップデートをしたことでデフォルトブランチが壊れる(動かなくなる)ようなことは避けたい。それではどうするか?
そう、答えは「Dependabotのプルリクをデフォルトブランチではないブランチに集約する」である。
つまり以下のようにする。
  • Dependabotが作るプルリクのターゲットブランチは集約用のブランチに向ける
  • 集約用のブランチに対するプルリクではGitHub Actionsのジョブを走らせないようにする
  • 集約用のブランチからデフォルトブランチに対するプルリクではGitHub Actionsのジョブを走らせる
こうすることでDependabotの恩恵は受けつつも、デフォルトブランチに取り込む際には事前にCIを挟むことでデフォルトブランチを保護することが出来る。

Dependabotの設定

3つのモジュールを持つリポジトリを例にする。
dependabot-sample
    ├── .github
    │   ├── dependabot.yml
    │   └── workflows
    │       ├── create-dependabot-branch.yml
    │       ├── on-pull-request-moduleA.yml
    │       ├── on-pull-request-moduleB.yml
    │       └── on-pull-request-moduleC.yml
    ├── moduleA
    │   ├── package.json
    │   └── yarn.lock
    ├── moduleB
    │   ├── package.json
    │   └── yarn.lock
    └── moduleC
        ├── package.json
        └── yarn.lock
Dependabotの設定( .github/dependabot.yml )を以下のように設定する。
version: 2
updates:
  - package-ecosystem: npm
    directory: /moduleA
    schedule:
      internal: weekly
      day: 'mondy'
      time: '09:00'
      timezone: 'Asia/Tokyo'
    open-pull-requests-limit: 10
    commit-message:
      prefix: chore
      prefix-development: chore
      include: scope
    target-branch: dependencies/moduleA
  - package-ecosystem: npm
    directory: /moduleB
    schedule:
      internal: weekly
      day: 'mondy'
      time: '09:00'
      timezone: 'Asia/Tokyo'
    open-pull-requests-limit: 10
    commit-message:
      prefix: chore
      prefix-development: chore
      include: scope
    target-branch: dependencies/moduleB
  - package-ecosystem: npm
    directory: /moduleC
    schedule:
      internal: weekly
      day: 'mondy'
      time: '09:00'
      timezone: 'Asia/Tokyo'
    open-pull-requests-limit: 10
    commit-message:
      prefix: chore
      prefix-development: chore
      include: scope
    target-branch: dependencies/moduleC
ポイントは2つ。
  • Dependabotの起動タイミングを毎週月曜9:00に固定する
    • どこかしら固定出来る曜日・時間にするのがポイント
  • ターゲットブランチをデフォルトブランチではなく各モジュールの集約用ブランチに向ける

Dependabot用のGitHub Actionsの実装

Dependabot向けの集約ブランチを自動生成するGitHub Actionsを実装する( .github/workflows/create-dependabot-branch.yml )。
name: 'Create Branch for Dependabot'

on:
  schedule:
    # Monday 8:00 at JST
    - cron: '0 23 * * SUN'

jobs:
  create-branch:
    runs-on: ubuntu-latest
    timeout-minutes: 3
    strategy:
      fail-fast: false
      matrix:
        target-branch:
          - 'dependencies/moduleA'
          - 'dependencies/moduleB'
          - 'dependencies/moduleC'
    steps:
      - uses: actions/checkout@v3
      - name: create branch if not exists
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        shell: bash -x {0}
        run: |
          git branch -a | grep remotes/origin/${{ matrix.target-branch }}
          if [ "$?" -eq 0 ]; then
            echo "${{ matrix.target-branch }} branch is exists. will not create branch."
          else
            set -eo pipefail
            git switch -c ${{ matrix.target-branch }}
            git push origin ${{ matrix.target-branch }}
          fi
ポイントは2つ。
  • GitHub Actionsのcronで毎週月曜8:00にジョブを起動する
    • Dependabotの起動タイミングの前になるように時間を調整する
  • Dependabot向けの集約用ブランチが存在しなければデフォルトブランチからブランチを作成する

CI用のGitHub Actionsの実装

各モジュールのCIを行うGitHub Actionsを実装する。
.github/workflows/on-pull-request-moduleA.yml
name: '[moduleA] CI'

on:
  pull_request:
    branches-ignore:
      - 'dependencies/moduleA'
    paths:
      - 'moduleA/**'

jobs:
  # (省略)CIの実装
.github/workflows/on-pull-request-moduleB.yml
name: '[moduleB] CI'

on:
  pull_request:
    branches-ignore:
      - 'dependencies/moduleB'
    paths:
      - 'moduleB/**'

jobs:
  # (省略)CIの実装
.github/workflows/on-pull-request-moduleC.yml
name: '[moduleC] CI'

on:
  pull_request:
    branches-ignore:
      - 'dependencies/moduleC'
    paths:
      - 'moduleC/**'

jobs:
  # (省略)CIの実装
ポイントは2つ。
  • branches-ignore を使い、Dependabotが作成するプルリクはCI対象外とする
  • paths を使い、各モジュールのディレクトリ配下のファイル更新を起動トリガーにする

運用方法

  • 毎週月曜にデフォルトブランチからDependabot用の集約ブランチを自動的に作成し、集約ブランチに対して各モジュールの依存ライブラリのバージョンアップデートを取り込んでいく
  • 各集約ブランチへのバージョンアップデートの取込が完了したら、集約ブランチからデフォルトブランチへのプルリクを作成する
  • 集約ブランチに対するCIが通ったらデフォルトブランチへのマージを行う

Discussion

コメントにはログインが必要です。