Classi開発者ブログ

教育プラットフォーム「Classi」を開発・運営するClassi株式会社の開発者ブログです。

ライブラリのアップデートを自動化した仕組みの紹介

こんにちは!学習動画・Webテストの開発を行っています エンジニアの daichi (id:da1chi24) です。 この記事では、最近チームで導入したライブラリアップデートを自動化した仕組みとその経緯について紹介します。

なぜ自動化しようと思ったか

サービスを開発するだけではなく、日々の運用も必要です。

その運用業務の1つとして、ライブラリのアップデートがあります。 これはサービスを運用する上では大切なことではありますが、日々ライブラリアップデートのPRをさばき続けるのも大変です。

その時間をできるだけ減らし、その分空いた時間をユーザーへの価値提供や将来の投資に充てるために、今回の自動化の仕組みを作成しました。

この辺りの話は以前勉強会でLTしたことがありますので、興味があればご覧ください。

作ったもの

前置きは長くなりましたが、凝ったものを作ったわけではありません。

作成したものはライブラリアップデートの PR を 自動マージする GitHub Acitons の workflow です。 既に稼働しており、今まで20件以上のライブラリが自動でマージされました。

導入したリポジトリはバックエンドアプリとフロントエンドアプリをモノレポで管理しています。 それぞれのアプリで自動マージする要件を変えていますが、やることは大体同じです。 プロダクションに影響がないライブラリに対して、CI が通れば github-actions[bot] で Approve してからマージするワークフローになっています。

このCIの要件はバックエンドアプリ と フロントエンドアプリで以下のように決めています。

  • バックエンドアプリの workflow
  • 対象:dependabot が作成する PR
  • 自動マージの要件:lintとテスト が通り development dependency group に属すること

バックエンドアプリの workflow のソースコード(一部抜粋)

name: Dependabot Automation
on:
  pull_request:
    paths:
      - 'Gemfile.lock'

permissions:
  pull-requests: write
  contents: write
  issues: write
  repository-projects: write
  packages: read

jobs:
  ci_and_automerge:
    if: ${{ github.actor == 'dependabot[bot]' }}
    runs-on: 'ubuntu-latest'
    timeout-minutes: 30
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      - name: Install Ruby and gems
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: 3.1
          bundler-cache: true
      - name: Run lint
        run: bundle exec rubocop -P
      - name: Run tests
        run: bundle exec rspec
      - name: Dependabot metadata
        id: metadata
        uses: dependabot/fetch-metadata@v1.6.0
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
      - name: Approve & enable auto-merge for Dependabot PR
        if: |
          (steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor') &&
          steps.metadata.outputs.dependency-type == 'direct:development'
        run: |
          gh pr review --approve "$PR_URL"
          gh pr edit "$PR_URL" -t "(auto merged) $PR_TITLE"
          gh pr merge --auto --merge "$PR_URL"
        env:
          PR_URL: ${{ github.event.pull_request.html_url }}
          PR_TITLE: ${{ github.event.pull_request.title }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  • フロントエンドアプリの workflow
  • 対象:renovatebot が作成するPR
  • 自動マージの要件:main と PR のブランチで build 結果の差分を比較し差分がないこと

フロントエンドアプリの workflow のソースコード(一部抜粋)

name: Renovatebot Automation
on:
  pull_request:
    paths:
      - 'yarn.lock'
permissions:
  pull-requests: write
  contents: write
  issues: write
  repository-projects: write

jobs:
  compare-static-assets:
    timeout-minutes: 10
    if: ${{ github.actor == 'renovate[bot]' }}
    runs-on: ubuntu-latest
    steps:
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
      - name: Checkout current branch
        uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - name: Install dependencies
        run: yarn
      - name: Build on current branch
        run: |
          yarn build:production
          rm -rf public/fonts/
          rm -rf public/img/
      - name: Upload output file on current branch
        uses: actions/upload-artifact@v3
        with:
          name: ${{ github.sha }}-current
          path: public
      - name: Checkout main branch
        uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.base.sha }}
          fetch-depth: '0'
      - name: Reinstall dependencies
        run: yarn
      - name: Build on main branch
        run: |
          yarn build:production
          rm -rf public/fonts/
          rm -rf public/img/
      - name: Download current export file
        uses: actions/download-artifact@v3
        with:
          name: ${{ github.sha }}-current
          path: out-current
      - name: Compare build assets
        run: |
          diff -rq out-current public > result.txt || true
      - name: Comment results of diff
        uses: actions/github-script@v6
        with:
          github-token: ${{secrets.GITHUB_TOKEN}}
          script: |
            const fs = require('fs')
            const result = fs.readFileSync('result.txt', 'utf8')
            const commentBody = result ?
              `静的アセットのビルド結果に差分があります👀 <p>${result}</p>`
              : '静的アセットのビルド結果に差分はありません🎉'
            await github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: commentBody
            })
      - name: Approve & enable auto-merge for Renovatebot PR
        run: |
          diff -rq out-current public
          gh pr review --approve "$PR_URL"
          gh pr edit "$PR_URL" -t "(auto merged) $PR_TITLE"
          gh pr merge --auto --merge "$PR_URL"
        env:
          PR_URL: ${{ github.event.pull_request.html_url }}
          PR_TITLE: ${{ github.event.pull_request.title }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

補足 GitHub Actions のワークフローで PR の作成や Approve を行うためには、事前に Organization で許可をする必要があります。

GitHub Actions による pull request の作成または承認を回避する

フロントエンドアプリに 「development dependency group に属すること」という要件を入れていないのは、ライブラリがdevDependenies に属してもプロダクションに影響与える場合があるためです。 例えば babel や webpack などのビルドツールはバージョンが上がることで、ビルド結果が変わる可能性があります。

そのためプロダクションに影響がないのはビルド結果の差分を検知することだと判断しました。

作る際に詰まったところ

ここではこのワークフローを稼働させるまでに詰まったところについて共有します。

code owner が指定されていると自動マージができない

branch protection rules で code owner のレビューを必須にしている場合は、github-actions[bot] で Approve しても要件を満たすことができないため、マージできません。 また 2023年10月現在では bot を code owner に登録することもできません。

GitHub Apps can’t be used in CODEOWNERS – that’s not supported. https://github.com/orgs/community/discussions/23064

この問題は code owner を使わないことで解決しました。

code owner は ディレクトリやファイルごとにレビュワーを制限することができますが、導入したリポジトリでは運用するチームが限定されているため必要ありません。 リポジトリ単位でレビュワーを制限するなら Collaborators の設定 で 運用チームだけにApprove できる権限を絞れば良いと言う判断になりました。

どうしても code owner を使いたい場合は、machine user を利用する方法があります。 machine user は Organization に対して1つ作成できます。

GitHub アカウントの種類

作成した machine user を code owner に指定し、PATの権限を利用し gh コマンドで Apporve をすると code owner の要件を満たすことができます。

どのような方法を使うかは各チームやリポジトリの要件に応じて決めると良いでしょう。

権限が不足して GitHub Container Repository からイメージを pull できない

チームではGitHub Actions のワークフロー上でテスト環境を準備するために、 GitHub Container Repository から専用のイメージを pull して使用しています。 使用するイメージは社内でプライベートに公開しているイメージなので、アクセストークンで認証する必要がありました。

このアクセストークンに GITHUB_TOKEN を使ったのですが、イメージが pull できませんでした。 原因は GITHUB_TOKEN の permission の scope が不足していることでした。

workflow で使い捨ての GITHUB_TOKEN という認証トークンが発行されますが、ワークフローを動かすためには permissions キーで必要な scope を設定する必要があります。

以下のように指定することで解決しました。

permissions:
  packages: read

jobs:
  ci:
    services:
      db:
        image: ghcr.io/classi/xxx
        credentials:
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

まとめ

今回はチームで導入したライブラリアップデートの自動化の仕組みとその経緯について紹介しました。 特別なものではありませんが、少しの工夫で日々の時間を節約できたのは良い成果だと感じています。

今後もこのような開発・運用を改善する取り組みについて紹介していきたいと思います。

© 2020 Classi Corp.