こんにちは!学習動画・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つ作成できます。
作成した 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 }}
まとめ
今回はチームで導入したライブラリアップデートの自動化の仕組みとその経緯について紹介しました。 特別なものではありませんが、少しの工夫で日々の時間を節約できたのは良い成果だと感じています。
今後もこのような開発・運用を改善する取り組みについて紹介していきたいと思います。