SMARTCAMP Engineer Blog

スマートキャンプ株式会社(SMARTCAMP Co., Ltd.)のエンジニアブログです。業務で取り入れた新しい技術や試行錯誤を知見として共有していきます。

バージョンアップしんどい!!って思ったから仕組み化した話

どうも、職人です!

スマートキャンプでBOXIL SaaSのエンジニアをやってます職人こと袴田です!
最近は趣味のサウナが好きすぎて、熱波とアウフグースに目覚めました。
気がついたらとある温浴施設で熱波師としてデビューしたのをお知らせいたします。

そんなことはさておき、弊社サービスのBOXIL SaaSはRuby on Railsを使用して開発しています。
サービス保守運用に欠かせない作業といえばバージョンアップですよね。
皆さん大好きバージョンアップ作業を少し効率化した話を書かせていただきます!

バージョンアップ?なにそれおいしいの?

「バージョンアップとは、ソフトウェアのバージョンを上げることです。」ってCopilotが教えてくれました。

なんでバージョンアップなんかしないといけないんや!
って思いますが、バージョンアップをしないとセキュリティリスクがあったり、新しい機能を使えなかったり、開発体験も悪くなる一方です。
デメリットしかないですね。
大変な作業ですが、サービスを保守運用していくためには必要な作業です。

バージョンアップの何が辛い?

バージョンアップは一般的に後回しにされるイメージがありますが、何がそんなに辛いのか考えてみました。

メインタスクとの兼ね合い

もちろんバージョンアップタスクのために、メインタスクをおざなりにすることはできません。
効率化をしなければどうしてもバージョンアップタスクは後回しになってしまいがちです。

そのライブラリがどこで使用されているか

バージョンアップ対象のライブラリがサービスのどこで使用されているかを確認する必要があります。 使用箇所が確認できなければ、動作確認対象もわかりません。

バージョンアップをして問題ないだろうか

バージョンアップをしたらok!リリースしまーす!
なんてことはもちろんできません。
バージョンアップをしたことによって、サービスに影響がないか、バグがないかを確認する必要があります。

バージョンアップするときの面倒な作業

まず何がバージョンアップのネックになっているか、面倒な作業を考えてみました。

  • どのライブラリがバージョンアップできるのか調査する

単純に調査自体が面倒です。

  • 使用箇所を特定する

バージョンアップしたいライブラリの使用箇所を確認する必要があります。

  • 動作確認をする

ライブラリをバージョンアップし、ライブラリ同士の依存関係の解決します。
それができたらライブラリを使用している箇所の機能が正常に動作するか確認する必要があります。

  • PRを作成する

弊社はGitHubを使用しているので、バージョンアップのPRを作成する必要があります。

  • レビューする

バージョンアップのPRをレビューする(してもらう)必要があります。

  • リリースする

バージョンアップしたライブラリをリリースします。

どこを効率化できるだろうか

まず

  • どのライブラリがバージョンアップできるのか調査する

作業をどうにかしたいので、GitHubが提供しているDependabotを使用してみることにしました。

Dependabot

DependabotはライブラリをバージョンアップするPRを自動で作成してくれるbotです。

BOXIL SaaSはRuby on Railsを使用しているため、DependabotがGemfile.lockを監視し、バージョンアップが必要なgemのPRを自動で作成してくれます。

こちらを参考に、.github/dependabot.ymlを作成します。

version: 2
updates:
  - package-ecosystem: "bundler"
    directory: "/"
    open-pull-requests-limit: 5 # 作成するPRの最大数を5に設定
    schedule:
      interval: "daily"
      time: "10:00"
      timezone: "Asia/Tokyo"
    labels:
      - "レビュー求む"
    ignore:
      - dependency-name: "*"
        update-types: [ "version-update:semver-major" ] # メジャーアップデートは無視する

これで、毎日10時に5個のバージョンアップのPRが作成されるようになりました。
いきなりメジャーアップデートのPRが連続して作成されると、大変なのでいったん無視するように設定しました。

Dependabotを導入した結果

  • どのライブラリがバージョンアップできるのか調査する
  • PRを作成する

という作業はDependabotに任せることができました。
しかし、PRは上がってきたものの、そのPRのライブラリはどこで使用されているのかがわかりません。
こればかりはどうしようもないので、重い腰を上げて片っ端から確認することにしました。
確認をしていくうえで、PRのコメントに使用箇所や動作確認に必要な情報を書き込んでいくことにしました。

あれ、このライブラリ既視感あるな...

同じライブラリのバージョンアップPRが何度か上がってくることがあり、その度に「あれ?これ前にもあったような?」と思いながら確認していることに気づきました。
過去に対応した内容はPRのコメントに残しているので、それを確認すればいいのですが
「これ毎回過去のPRを漁りにいくのしんどいな・・・」と思ったので、この作業自体をGitHub Actionsで自動化できないか考えてみました。

GitHub Actionsで過去のPRを漁る

実現したいことは

  • DependabotがPRをオープンしたタイミングでGitHub Actionsを実行する
  • Dependabotが作成したPRのライブラリのバージョンアップを過去に行っているかを過去のPRを漁って確認する
  • 過去に行っている場合は、そのPRのリンクをコメントする

になります。

GitHub Actionsで過去のPRを漁るためには、GitHubのAPIを使用する必要があります。
こちらのページに使えそうなAPIがあったので、活用してみます。

そして作ったのがこちらのGitHub Actionsです。

.github/workflows/fishing-for-past-upgraded-prs.yml

name: Fishing for past upgraded PRs

on:
  pull_request:
    types:
      - "opened"
      - "reopened"

permissions:
  pull-requests: write

jobs:
  fishing_for_past_upgraded_prs:
    runs-on: ubuntu-latest
    steps:
      - name: Add Links Comment
        uses: actions/github-script@v6
        with:
          script: |
            const { owner, repo, number } = context.issue;
            const pr_title = context.payload.pull_request.title;
            const regex = /Bump (.*) from (.*) to (.*)/;
            const match = pr_title.match(regex);

            if (match) {
              const gem_name = match[1];
              let previous_prs = [];
              for (let page_number = 1; page_number < 11; page_number++) {
                const { data: prs } = await github.rest.pulls.list({
                  owner,
                  repo,
                  state: "closed",
                  sort: "updated",
                  direction: "desc",
                  per_page: 100,
                  page: page_number
                });
                const target_prs = prs.filter(
                  (pr) =>
                    pr.title.includes(`Bump ${gem_name}`)
                )
                previous_prs = previous_prs.concat(target_prs)
              }

              if (previous_prs.length > 0) {
                let comment = "過去の関連PR:\n";
                previous_prs.forEach((pr) => {
                  comment += `- [#${pr.number}](${pr.html_url})\n`;
                });
                await github.rest.issues.createComment({
                  owner,
                  repo,
                  issue_number: number,
                  body: comment,
                });
              }
            }

過去PRを釣り上げるということで、fishing-for-past-upgraded-prsという名前にしました。

やっていることを簡単に説明すると

  • Dependabotが作成したPRのタイトルからライブラリ名を取得する
  • 過去に作成したPRを更新日時の降順で取得する
  • 過去に作成したPRのタイトルにライブラリ名が含まれているものを抽出する
  • 抽出したPRのリンクをコメントする

ということをしています。

このコードのポイントは2つあります。

1つ目はGitHub APIが1回のリクエストで100件のPRしか返さないことです。

BOXIL SaaSを管理しているGitHubリポジトリではPRが約10000件あります。
それらのPRをすべて取得してしまうと、1回のアクションで約100回リクエストを送信することになり、時間がかなりかかってしまいます。
これでは現実的ではないので、リクエストの回数は10回に制限し、1000件のPRを取得するようにしています。

1000件というのは「1000件くらい漁れば、過去のPR見つかるっしょ!」くらいの感覚です。
仮に1000件を超えたところにPRがあったとしても、PRがコメントを通じて数珠つなぎになって辿れるので、問題はなさそうです。
(健全にバージョンアップ作業を続けられているのであれば、1000件より少なく、500件でも300件でも良さそうな気はしています)

2つ目はpermissionsの設定です。

permissions:
  pull-requests: write

この部分です。 こちらを参考にpermissionsの設定をしないと、

RequestError [HttpError]: Resource not accessible by integration
Error: Unhandled error: HttpError: Resource not accessible by integration

このようにGitHubのAPIを実行したときにエラーが発生してしまいます。
レスポンスのHTTPステータスコードは403なので、閲覧権限がないようです。
permissionsの設定をwriteとすることで、PRへの閲覧権限を付与するようにしています。 (readだとPRにコメントを書くときに、権限がないと怒られます)

こちらを導入した結果、DependabotがPRを上げると、過去に作成したPRのリンクがコメントされるようになりました。
あくまで過去の参考情報なので鵜呑みにするのはよくありませんが、

  • 使用箇所を特定する

作業に関して、少し負荷の軽減ができた実感があります。

その結果

前期ではバージョンアップのためのノウハウを蓄積するという意味でひたすらにDependabotが上げたPRを対応していました。 その結果6ヶ月間で40PRほど対応できたのですが、今期が始まって2ヶ月ほどの間にすでに40PRほど対応できています。

直近のバージョンアップの状況(バージョンに関わる部分は黒塗りにさせていただきました。)

とはいえ、

  • Dependabotが上げたPRのみGitHub Actionsが実行されるようにする
  • ラベルでGitHub Actionsの実行対象のPRかを判別する

などまたまだ改善の余地はありそうです。

ちなみに

  • Dependabotが上げたPRのみGitHub Actionsが実行されるようにする

に関しては、Dependabotではない誰かがPRを上げた場合にも実行するようにあえて、DependabotによるPRの制限は行っていません。

まとめ

明らかに状況が変わってきている実感があり、バージョンアップ作業の定常化に向けて大きな前進になったと思います。
引き続きバージョンアップ作業を進めて、常に最新のバージョンを維持できるようにもっと効率化していきたいと思っています。