Renovateを使ってフロントエンドのバージョンアップを改善した話

  • URLをコピーしました!

こんにちは、フロントエンドエンジニアの小張です。Renovateを使ってフロントエンドのパッケージやライブラリのバージョンアップを改善したことについて紹介します。

PR TIMESではReactに関するコードを、monorepoとしてprtimes-frontendという1つのリポジトリで管理しています。

このリポジトリは作成されてから2年ほどしか経っておらず、使っているライブラリも比較的新しいため、今までバージョンアップの仕組みを特に整備していませんでした。

ただフロントエンドのライブラリはバージョンアップの頻度が多く、異なるライブラリ間でバージョンの依存関係があることもあり、将来のことを考えればライブラリのバージョンを更新する仕組みを作ることはほぼ必須でした。

また、monorepoであるためライブラリのバージョンを大きくあげようとした際の対応コストも大きく、最新との差が小さいうちに細かくバージョンアップしていきたかったことも理由の一つでした。

目次

バージョンアップデートを検知する

バージョンアップの仕組みを作るに当たって、以下の2点を決める必要がありました。

  1. バージョンアップデートをどのように検知するか
  2. バージョンアップデートをどのようにリリースするか

Renovateを利用するとリポジトリのpackage.jsonやDockerfileを見て、ライブラリを新しいバージョンに上げるPull Requestを自動で立てることができるため、1についてはRenovateを使用することにしました。

Renovateのインストール方法については以下をご覧ください。

あわせて読みたい
Installing & Onboarding - Renovate Docs Renovate documentation.

Dependabotとの比較(2024-02-16時点)

GitHubの提供するDependabotでも同じくバージョンアップのPull Requestを自動作成する事ができますが、いくつかの箇所で機能的に不足を感じたので採用を見送りました。具体的には以下の部分です。

  • コンフリクト時にだけrebaseする設定ができない
    • Renovateでは“rebaseWhen”: conflicted で可能
  • Pull Requestをdraftで作成できない
    • Renovateでは“draftPR”: true で可能
  • ^1.0.0 などの形式を禁止し、1.0.0 のようにバージョンを固定値に自動修正できない
  • react-query@tanstack/react-query に修正するような、別のパッケージとして配布されるようになったライブラリを自動修正できない

バージョンアップデートのリリース方法

上記の2については月1回のリファクタリングデーに合わせて、1ヶ月間にアップデートされたライブラリたちをまとめてバージョンアップすることにしました。

リファクタリングデーについて詳しくはこちらをご覧下さい。

あわせて読みたい
PR TIMESにおけるリファクタリングデー こんにちは、業務委託でPR TIMESにJOINしているuzulla (”うずら”, twitter, GitHub)です。本エントリではPR TIMESで行っているリファクタリングデーについてお話したい...

背景としては、特にmajorアップデートを含む場合、リグレッションテストを行うべきQA範囲はとても広くなるため、このリグレッションテストをライブラリ毎に行うことは、QAのリソースを考えると非現実的だったことがあります。

実際に運用しているRenovateの設定

renovate.jsonの設定はこちらです。

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "group:allNonMajor", // minorとpatchアップデートを1つのPRにまとめる
    "config:best-practices",
    ":disableRateLimiting" // 1時間単位のPR数と同時に存在可能なPR数を無制限にする
  ],
  "enabledManagers": ["npm", "dockerfile", "github-actions"],
  "baseBranches": ["release-renovate"], // release-renovateブランチがあるときにだけrenovateを起動する
  "draftPR": true, // CIの実行時間を抑えるため、renovateがdraftPRを作成するようにする
  "timezone": "Asia/Tokyo",
  // デフォルトではtests配下のpackage.jsonが無視されてしまうため、デフォルトを上書き
  "ignorePaths": [
    "**/node_modules/**",
    "**/bower_components/**",
    "**/vendor/**",
    "**/examples/**",
    "**/__fixtures__/**"
  ],
  // CIの実行時間を抑えるため、rebaseをconflict時のみにする
  "rebaseWhen": "conflicted",
  "github-actions": {
    "fileMatch": [
      "^(workflow-templates|\\.(?:github|gitea|forgejo)/workflows)/[^/]+\\.ya?ml$",
      "(^|/)action\\.ya?ml$",
      // デフォルトでは.github/actions配下は管理対象にならないため追加
      "\\.github/actions/.+\\.ya?ml$"
    ]
  },
  "packageRules": [
    {
      "matchPackageNames": ["node"],
      "matchManagers": ["dockerfile"],
      // Node.jsは偶数バージョンのみアップデートする
      "allowedVersions": "/^[0-9]*[02468]([.-].*)?$/"
    },
    {
      // Playwrightのnpmパッケージとdocker imageのバージョンを揃えるため、PRをまとめる設定にする
      "matchPackagePatterns": ["playwright"],
      "groupName": "playwright"
    },
	{
	  // ViteとVitestのPRをまとめる
      "matchPackagePatterns": ["vite"],
      "groupName": "vite"
    },
    {
	  // majorアップデートにはタグをつける
      "matchUpdateTypes": ["major"],
      "labels": ["major"]
    },
    {
	  // minorアップデートにはタグをつける
      "matchUpdateTypes": ["minor"],
      "labels": ["minor"]
    },
    {
	  // patchアップデートにはタグをつける
      "matchUpdateTypes": ["patch"],
      "labels": ["patch"]
    }
  ]
}

設定で工夫している点をいくつか紹介します。

baseBranchesの設定

月1回のリファクタリングデーでmainブランチにリリースする前に、RenovateのPull RequestをまとめてQAできるブランチが必要です。

そこでrelease-renovate というブランチを起点に、RenovateがPull Requestを作成するようにbaseBranches を設定しています。

{
  "baseBranches": ["release-renovate"],
}

作成されたPull Requestは手動でrelease-renovateにマージしていきます。ブランチ運用は以下のようなイメージです。

Groupingの設定

デフォルトの設定でRenovateを動かしたところ、約70件のPull Requestが作成されたのですが、これらを全て手動でマージするのが大変なのと、これらに対してGitHub Actionsが回ることでbillable timeを消費してしまう問題がありました。

GitHub Docs
GitHub Actions billing - GitHub Docs Learn how usage of GitHub Actions is measured against your free allowance and how to pay for additional use.

そこで以下のようにgroup:allNonMajor というpresetをextendsして、minorとpatchアップデートを1つのPull Requestにまとめる(= Groupingする)ようにしました。

ただconfig:best-practices より後にgroup:allNonMajor をextendsしてしまうと、デフォルトでGroupingされているNext.jsなどのminor・patchアップデートもまとめて1つのPull Requestに入ってしまい、extendsする順番を以下のように調整する必要がありました。

config:best-practicesgroup:nextjsMonorepo というNext.jsに関するアップデートをGroupingするruleを内包しています。 このruleがgroup:allNonMajor と競合する場合、手元で確認したところrenovate.json内で後にextendsした方のruleが優先されるようでした。

{
  // OK
  "extends": [
    "group:allNonMajor", // minorとpatchアップデートを1つのPRにまとめる
    "config:best-practices",
  ],

  // NG(config:best-practicesに含まれるGrouping設定が上書きされてしまう)
  "extends": [
    "config:best-practices",
    "group:allNonMajor", // minorとpathcアップデートを1つのPRにまとめる
  ]
}

またデフォルトのGrouping設定以外でGroupingしたい場合には、以下のようにして独自のGroupingを作ることができます。弊社ではそこまで複雑なルールは使ってないですが、matchPackagePatterns ではライブラリ名に正規表現でマッチさせることができるので、柔軟なGrouping設定が可能です。

{
  {
    // Playwrightのnpmパッケージとdocker imageのバージョンを揃えるため、PRをまとめる設定にする
    "matchPackagePatterns": ["playwright"],
    "groupName": "playwright"
  },
  {	
    // ViteとVitestのPRをまとめる
    "matchPackagePatterns": ["vite"],
    "groupName": "vite"
  },
}

CI実行時間の削減を目的とした設定

Groupingの設定の他にも、CI実行時間の削減を目的にRenovateの設定を調整しています。

{
  "rebaseWhen": "conflicted", // CIの実行時間を抑えるため、rebaseをconflict時のみにする
  "draftPR": true, // CIの実行時間を抑えるため、renovateがdraftPRを作成するようにする
}

Renovateのデフォルト設定では、baseBranches で設定したブランチに1つでもPull Requestがマージされたら、Renovateが作成した他のPull Requestのブランチを全てrebaseして、変更に追従しようとします。

弊社のCIはrebaseされることによっても発火する設定になっており、かなりの実行時間を消費してしまうので、rebaseWhen を設定してRenovateのrebase回数を減らすようにしました。

さらに、弊社のCI実行はdraft PRを対象外とするような設定になっているので、”draftPR”: true として開発者が手動でdraftを解除しない限り、RenovateのPull RequestではCIが実行されないようにしました。

これらの設定を行ったことで1ヶ月間のRenovateによるGitHub Actions使用時間(billable time換算)が、約14000分削減できました。

設定前設定後
1ヶ月間のRenovateのPR上でのbillable time14459分321分

ライブラリごとの個別設定

prtimes-frontendではNode.jsの最新LTS(偶数バージョン)を使用するようにしているので、Node.jsの奇数バージョンにアップデートするPull Requestはマージする必要がありません。

そこで以下のようにallowedVersions で許可するNode.jsのバージョンを正規表現で指定することで、偶数バージョンへのアップデートのみ、自動でPull Requestが作成されるように設定しました。

{
    {
      "matchPackageNames": ["node"],
      "matchManagers": ["dockerfile"],
      // Node.jsは偶数バージョンのみアップデートする
      "allowedVersions": "/^[0-9]*[02468]([.-].*)?$/"
    },
}

設定ファイルのバリデーション

上記の設定を行ったrenovate.jsonが、正しい設定になっているかをバリデーションするrenovate-config-validator というCLIがRenovate公式から提供されています。

そこで、prtimes-frontendでは以下のようなGitHub Actionsのworkflowを用意して、renovate.jsonに変更があれば自動でバリデーションを行うようにしています。

name: Renovate Config Validator

on:
  workflow_dispatch:
  pull_request:
    paths:
      - 'renovate.json'
      - '.github/workflows/renovate-config-validator-ci.yaml'

jobs:
  renovate-config-validator:
    timeout-minutes: 10
    runs-on: ubuntu-22.04

    steps:
      - name: Checkout
        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
      - name: validate renovate.json
        run: npx --yes --package renovate@latest -- renovate-config-validator --strict

結果

Renovateを導入し、リファクタリングデーを使ったリリースフローを整備したことで、今では1ヶ月平均10個のライブラリをメジャーアップデートすることができるようになりました。

またライブラリの新しい機能がリリースされてすぐに使えるため、開発者体験も向上しました。例えば、TypeScript v4.9で導入されたsatisfiesですが、当時最新だったVite v3では使うことができませんでした。しかし、Viteのバージョンアップを最新に保っていたため、v4がリリースされてすぐにv4に上げることができ、satisfiesを使った開発ができるようになりました。

さらに最新バージョンを使って開発することが当たり前になったことで、バージョンアップできていないライブラリのバージョンを上げようとする意識が自然と高くなり、例えばエディターチームではTiptapのバージョンを上げるための取り組みが進められています。

We are hiring!

フロントエンドはもちろん、各種ポジションで採用を行っています!

あわせて読みたい
株式会社PR TIMES
02.開発部 の求人一覧 - 株式会社PR TIMES 株式会社PR TIMESが公開している、02.開発部 の求人一覧です
  • URLをコピーしました!

この記事を書いた人

2021卒でフロントエンド開発を担当しています。

目次