GitLab と Renovate で iOS のライブラリアップデートを自動化する方法 (XcodeGen)

はじめに

 カカクコムで キナリノの iOS アプリ を主に開発している奥野です。
 キナリノiOS チームでは半年ほど前、ライブラリの更新を自動化するために Renovate を導入してみました。しばらく経った今、使っているライブラリのバージョンが、最新版であるかを確認する作業が不要になり、だいぶ楽になったことを実感しています。
 ただ、Renovate の導入を検証していた際、キナリノでは現在 GitLab を使っているのですが、私が GitLab Runner の仕様を知らなかったり、iOS で XcodeGen を使っている場合は対応が必要など、情報整理に時間がかかってしまいました。
 そこで、今回は GitLab で Renovate を新たに導入したい人向けに、Renovate の概要を説明した上で、GitLab Runner のインストール方法から、XcodeGen を使っている場合の iOS の設定方法についてを紹介していきます。

Renovate の概要

 ここでは Renovate ができることについて、過去の情報も踏まえて簡単に紹介します。

Renovate とは何か

 Renovate はパッケージマネージャの依存関係を更新してくれる CLI ツールです。Github Apps や Docker イメージ、npm などで配布されています。
 サポートされているパッケージマネージャは非常に多く、SwiftPM や CocoaPods、 Bundler などもサポートされています1。ただし、バージョンを統一するためのロックファイル(Package.resolved など)は更新されないものもあるため、実際のアップデートには手作業も発生します。
 また、Renovate には regex manager という、正規表現でバージョン等の情報を抽出できる機能があります。これによって、サポートされていないパッケージマネージャであっても、自動更新の対象とすることが可能となります。XcodeGen で定義された SwiftPM のライブラリは、この regex manager で対応しています。  

(余談) Renovate の過去

 過去の記事から分かるように、かつての Renovate には有料プランもあった様子です。しかし、2019年11月に WhiteSource 社が買収して完全無料化され2、その後 2022年5月に WhiteSource が社名変更して MEND となり3、現在に至ります。割と最近の出来事でなので、Renovate に関する古い記事にはご注意ください。

実際に Renovate を実行して作成されるマージリクエス

 Renovate を実行すると、1分経たずに次のようなマージリクエストが作られます。

  • マージリクエストの概要
    ライブラリのバージョン変更やリリースノートが記載されています。
     

  • マージリクエストの差分
     ここではバージョンだけが更新されています。
     

Renovate の紹介は以上です。
次に、GitLab Runner のインストール方法に移っていきます。

GitLab Runner のインストールと設定

 ここでは GitLab Runner のインストールから Renovate に必要な設定までを紹介します。
 なお、この記事では Docker イメージの Renovate を使用するため、Docker を実行できる環境での作業を前提としています。
 また、既に GitLab Runner がインストールされている場合、executordocker になっている必要があるので、確認してください。

GitLab Runner をインストールして実行する

 次の手順で GitLab Runner の実行までを行います。これは Linux x86-64 の場合の手順 ですが、他の場合は Install GitLab Runner を参照してください。

  1. gitlab-runner をインストールする
  2. sudo curl -L --output /usr/local/bin/gitlab-runner "https://gitlab-runner-downloads.s3.amazonaws.m/latest/binariesgitlab-runner-linux-amd64"
  3. 実行権限の付与
  4. sudo chmod +x /usr/local/bin/gitlab-runner
  5. GitLab Runner ユーザー作成
  6. sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash
  7. GitLab Runner のインストールと実行
  8. sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
    sudo gitlab-runner start

以上の手順で GitLab Runner が実行された状態になるので、次にこの Runner の設定を行います。

GitLab Runner を設定する

 ここでは、GitLab Runner の GitLab への登録と、 executordocker にする設定を行います。この手順も Linux でない場合は Register a runner を参照してください。

  1. 次のコマンドを実行すると対話式で設定できるので、添付画像のように入力します

     sudo gitlab-runner register
    

    設定項目 設定する値
    Enter the GitLab instance URL 利用している GitLab の URL
    Enter the registration token GitLab で発行されたトークンを入力。特定プロジェクトのみで実行したい場合は Project runnersトークン、グループ内で使えるようにしたい場合は Group runnersトークンを入力します。
    Enter a description for the runner 適当に入力
    Enter tags for the runner 後に .gitlab-ci.yml で指定する際のタグ名
    Enter optional maintenance note for the runner 適当に入力
    Enter an executor docker に設定(必須)
    Enter the default Docker image renovate/renovate:35.89 を入力。後に .gitlab-ci.yml ファイルで指定できるため、他のイメージでも可能です。
  2. (任意)GitLab Runner は、デフォルトで Docker イメージの pull を毎回実行するため、速度改善のためにイメージがない場合のみ実行されるよう変更します。

     vim /etc/gitlab-runner/config.toml
    
     [[runners]]
         ...
         executor = "docker"
         [runners.docker]
             ...
             pull_policy = "if-not-present"  # ← 追加する
    

    詳しくは GitLab のドキュメント をご確認ください。

 以上で GitLab Runner の設定は完了です。次は iOS プロジェクトの設定に移っていきます。

iOS プロジェクトの設定

 キナリノiOS プロジェクトでは、XcodeGen を使用しています。また、ライブラリは基本的に SwiftPM で管理しており、一部が CocoaPods に残っている状態です。
 そのため、ここでは iOS プロジェクトに対する、XcodeGen を使った SwiftPM と CocoaPods に必要な Renovate の設定を紹介します。

現状の課題 Package.resolved は自動更新ができない

 SwiftPM で自動生成される Package.resolved については、Renovate では更新されません。別途手動等で更新する必要があります。

現状の課題 セマンティックバージョンではない場合、自動更新ができない可能性

※ renovate のバージョン 35.8.1 における事象であり、36.0.0 からは一部変更があります
 GitHub 上のライブラリには、バージョン管理が セマンティックバージョン ではないものがあります。例えば v3.4 というリリースは、接頭辞に v が付いていることと4、パッチバージョンがないことから5、厳密にはセマンティックバージョンではないようです。
 Renovate の regex manager はデフォルトで 厳密なセマンティックバージョン(v11.0.0 と解釈しない)(※バージョン 36.0.0 から変更 非厳密なセマンティックバージョン(v11.0.0 と解釈する)を使用してはいますが、ライブラリがセマンティックバージョンに則っていない場合、そのままでは最新版を検出できなかったり、できたとしても次の画像のような差分に v が入るなど、期待した結果にならないことがありました。

Renovate によって生成された差分がセマンティックバージョンではない場合の結果
 そのため、Renovate を動作させた後は、全てのライブラリが最新版になっているか、差分に問題がないか、確認することをお勧めします。

Podfile などバージョン指定の注意

 Podfile に記載されたライブラリはバージョン指定がない場合、 Renovate に検出されなかったり、パッチまで固定を行わないと自動更新されなかったりするので、ご注意ください。

  # × 検出されない
  pod 'ライブラリ名' 

  # △ これだとパッチのバージョンアップは検出されないことがあった
  pod 'ライブラリ名', '~> バージョン'

  # ○ パッチも検出された
  pod 'ライブラリ名', 'バージョン'

renovate.json を作成する

 結果として、次のような renovate.json ファイルを作成し、リポジトリ最上部の階層に配置しました。ここでは、最低限の設定のみを記載しており、自動マージされる機能などは割愛しています。

{
  // 基本設定
  "extends": ["config:base"],
  "timezone": "Asia/Tokyo",

   // PR(MR) の設定
  "prHourlyLimit": 20,
  "prConcurrentLimit": 99,
  "draftPR": true,

  // CocoaPods と Bundler 以外は regex manager で対応
  "enabledManagers": ["cocoapods", "bundler", "regex"],

  // yml ファイルに定義された SwiftPM と、 Makefile にバージョンを記入している XcodeGen を正規表現で対応
  "regexManagers": [
    {
      "fileMatch": ["(^|/)packages.yml$"],
      "matchStrings": ["url: https:\\/\\/github\\.com\\/(?<depName>.*?)(\\.git)?\\s*(majorVersion|minorVersion): (?<currentValue>.*?)\\s"],
      "datasourceTemplate": "github-releases",
      "versioningTemplate": "semver-coerced"
    },
    {
      "fileMatch": ["^Makefile$"],
      "matchStrings": ["XCODEGEN_LOCKED_VERSION := (?<currentValue>.*?)\\s"],
      "datasourceTemplate": "github-releases",
      "depNameTemplate": "yonaskolb/XcodeGen"
    }
  ],

  "packageRules": [
    // 自動更新の対象外としたいライブラリを定義
    {
      "matchPackageNames": [
        "fastlane",
        "jira-ruby"
      ],
      "enabled": false
    }
  ]
}

 この renovate.json を作成するにあたっては、以下の記事などを参考にさせていただきました。

 各設定について、ここでも簡単に触れたいと思います。

 まず、 extends では Renovate が用意している基本設定 config:base を引き継いでいます。

// 基本設定
"extends": ["config:base"],
"timezone": "Asia/Tokyo",

 次に、リポジトリで使用しているパッケージマネージャを設定しています。キナリノでは Bundler を CocoaPods 自体のバージョン管理などに使っています。

// CocoaPods と Bundler 以外は regex manager で対応
"enabledManagers": ["cocoapods", "bundler", "regex"],

 ここに "regex" を追加しないと、次の regexManagers を定義しても正規表現による抽出は実行されないので、ご注意ください。

 そして、次が XcodeGen で使っている yml ファイルから、SwiftPM を正規表現で抽出している箇所です。

"regexManagers": [
  {
    "fileMatch": ["(^|/)packages.yml$"],
    "matchStrings": ["url: https:\\/\\/github\\.com\\/(?<depName>.*?)(\\.git)?\\s*(majorVersion|minorVersion): (?<currentValue>.*?)\\s"],
    "datasourceTemplate": "github-releases",
    "versioningTemplate": "semver-coerced"
  },

 matchStrings が複雑そうに見えますが、やっていることは単純で、次の 3 点を renovate が分かるように定義すれば良いだけです。

 細かな書き方については Renovate の例 で示されているので、そちらを参照してください。
 "versioningTemplate": "semver-coerced" では、バージョニングを 非厳密なセマンティックバージョン (v1 → 1.0.0 と解釈する) に設定しています。キナリノでは、指定しないと自動更新できないライブラリ (Gifu) があったためです。

 また、今回は紹介しませんが、キナリノでは XcodeGen のバージョンもメンバーで揃えているので、 Makefile に定義されたバージョンを次のように確認しています。

{
  "fileMatch": ["^Makefile$"],
  "matchStrings": ["XCODEGEN_LOCKED_VERSION := (?<currentValue>.*?)\\s"],
  "datasourceTemplate": "github-releases",
  "depNameTemplate": "yonaskolb/XcodeGen"
}

 あとは、自動更新の対象から除外したいものを設定しています。

  "packageRules": [
    ...

    {
      "matchPackageNames": [
        "fastlane",
        "jira-ruby"
      ],
      "enabled": false
    }
  ]

 他にも設定できる項目は数多くあるので、ドキュメント をみて、チームに合った設定を探すのが良いと思います。

.gitlab-ci.yml を設定する

 .gitlab-ci.yml には、次のような内容で設定します。このファイルがなければ、新たにプロジェクトの最上部の階層に作成します。

stages:
    - dependency

renovate:
    image: renovate/renovate:35.89 # gitlab-runner register 時にデフォルトイメージを renovate に設定していれば不要
    tags:
        - docker-runner # gitlab-runner register 時に設定したタグ
    stage: dependency
    script:
        - renovate --platform gitlab --endpoint $CI_SERVER_URL/api/v4 $CI_PROJECT_PATH
    rules:
        - if: $IS_RENOVATE && $CI_PIPELINE_SOURCE == "schedule"

 image: renovate/renovate:35.89sudo register runner を実行した際、デフォルトイメージに設定していれば、ここでは不要です。 tags については gitlab-runner register 時に設定したタグ名を入力します。
 $CI_ から始まるものは、GitLab で定義済みの変数 です。
 $IS_RENOVATE は他のスケジュールが実行されても Renovate は実行されないようにするために用意しました。

XcodeGen を使っている場合に Podfile.lock が更新できないエラーの対処を Podfile に行う

 XcodeGen を使用している場合、Renovate を動かすと次のような Podfile.lock を更新できない旨のエラーが発生します。

 このエラーの対処法は次の記事を参考にさせていただきましたが、ここでも簡単に紹介したいと思います。

 Podfile に次のような処理を加えることで対処できます。

...

is_renovate = ENV['HOME'] !~ /^\/Users\/.*/

if is_renovate then
  install! 'cocoapods', :integrate_targets => false
end

target 'kinarino' do
  use_frameworks!

  ...

  if is_renovate then
    current_target_definition.swift_version = '5.7.2'
  end

  ...

 追加した処理がやっていることを説明すると、

if is_renovate then
  install! 'cocoapods', :integrate_targets => false
end

ここは、Renovate による pod install 実行時は、Podfile:integrated_target: false が設定されることで、Xcode の project ファイルへの組み込みを回避しています。

 また、これを追加することで、Swiftのバージョンが取得できないエラーが追加で発生してしまうため、

if is_renovate then
  current_target_definition.swift_version = '5.7.2'
end

の処理で Swift バージョンを明示することで対処しています。is_renovate については環境に合わせて定義するのが良いと思います。

 これで無事に Podfile.lock は更新されるようになると思います。
 それでは最後に、GitLab CI/CD の設定に移ります。

GitLab CI/CD の設定

 ここでは、Renovate を定期実行する上で必要な GitLab CI/CD 設定について紹介します。

Renovate が定期実行されるパイプラインスケジュールを作成する

 GitLab でパイプラインスケジュールを作成します。 IS_RENOVATEtrue を設定します。
 

プロジェクトに CI/CD の変数を用意する

 設定するプロジェクトにおいて、 CI/CD > 変数の画面で次の 2 つの変数を用意します。パイプラインスケジュールの変数として定義しても良いと思います。

  • RENOVATE_TOKEN : GitLab で発行するプロジェクトのアクセストークンの値
    • 権限には read_user, api, write_repository の 3 つが必要
    • GitLab Container registry にアクセスする場合は read_registry も追加
  • GITHUB_COM_TOKEN : GitHub で発行する読み取り専用のアクセストークンの値
    • GitHub 上にあるライブラリを読み取るために必要なトークンです
    • 詳しくはこちらを確認してください

以上の手続きで、 GitLab で Renovate が定期実行される準備が整いました。

動作確認してマージリクエストを作成

dryRun で手動実行する

 試しに、Renovate の動作を確認したい場合は、Docker が動く環境で次の --dry-run=full を加えたコマンドで実行できます。Renovate Docs > dryRun

docker run -e GITHUB_COM_TOKEN={GitHub のトークン} --rm renovate/renovate:35.89 renovate --platform gitlab --token {GitLab のトークン} --endpoint {GitLab のURL}/api/v4 {グループ名/リポジトリ名} --dry-run=full

 また、MR が作られる元のブランチは、リポジトリのデフォルトブランチとなります。もし、ブランチを指定したい場合は docker run の後ろに -e RENOVATE_BASE_BRANCHES={ブランチ名} を付け加えれば対応できます。ただ、挙動を確認したところ renovate.json ファイルの参照先はデフォルトブランチの様子なので、その点は注意が必要です。Renovate Docs > baseBranches

docker run -e RENOVATE_BASE_BRANCHES={ブランチ名} -e GITHUB_COM_TOKEN={GitHub のトークン} --rm renovate/renovate:35.89 renovate --platform gitlab --token {GitLab のトークン} --endpoint {GitLab のURL}/api/v4 {グループ名/リポジトリ名} --dry-run=full

 実行結果は次の添付画像のように、パッケージマネージャごとに検知された件数や、アップデート毎にブランチ作成など情報が出力されます。

 この結果、検出されたライブラリの数が合っていたり、エラーが表示されていなければ、 Renovate の設定は完了です!

Renovate を実行して、マージリクエストを作成する

 手動実行の方法で --dry-run=full を外して実行するか、作成したスケジュールを実行すれば、Renovate が自動で更新のあるライブラリを検出して、次のようにマージリクエストを作成してくれると思います。

おわりに

 この記事では、XcodeGen を使った iOS プロジェクトに対して、GitLab CI/CD で Renovate を定期実行し、ライブラリの最新版があれば、バージョン更新のマージリクエストが自動で作成される方法を紹介しました。
 Package.resolved が更新されないなどの課題は残りますが、キナリノでは、ライブラリの最新版のことを気にする必要がなくなり、待っていれば勝手にマージリクエストが作られるので、導入して良かったと感じています。
 導入方法についてはこの記事で紹介しましたが、導入後は 運用して得た Tips などの各社テックブログも見て、チームに合ったものへ改善していきたいですね。

カカクコムでは共にサービスをつくる仲間を募集しています

カカクコムのエンジニアリングにご興味のある方は、是非こちらをご覧ください!


カカクコム採用サイト