滅入るんるん

何か書きます

dependabotにGradleプロジェクトで使っているライブラリーをいい感じに自動更新させる

さて皆さん、dependabotをご存知でしょうか

dependabot.com

Node.jsやRubyのプロジェクトをGitHubで管理している人なら、ある日唐突に使ってるライブラリーの脆弱性が発見されたからバージョンアップしろよな!PRを出してくるあのbotです

本来の用途としてはライブラリーのアップデートがあったらアップデートするPullRequestを自動で作成してくれるbotです。それがGitHubに買収かなにかされたらしくGitHubとの統合が進んでいろいろと便利にしてくれてるみたいな感じです

f:id:meilcli:20200621161727p:plain

さて、そのdependabotですが現在様々な言語やプラットフォームに対応していたり対応途中だったりします。gradleも対応中のプラットフォームということでBETA版になっています
そして、public・private repository問わずに無料で使えるみたいです(private repositoryの場合はGitHubへデータの提供を許可しないといけないです)

というわけでこれを使わないわけにはいかないですよね?使っていきましょう

Setup

f:id:meilcli:20200621162630p:plain

GitHubのRepository上でInsightsのDependency graphのところからdependabotを有効にすることができます。ここのEnableボタンを押すと.github/dependabot.ymlを作成することになります。この.github/dependabot.ymlがdependabotの設定なのでここを各々好きなように設定すればいいのです

version: 2
updates:
  - package-ecosystem: "gradle"
    directory: "/"
    schedule:
      interval: "daily"
    reviewers:
      - "MeilCli"
    open-pull-requests-limit: 20

MeilCliの場合は試しにこのように設定しました。ここのオプションについては↓にドキュメントへのリンクを貼っておくのでそれを参考にすればいいでしょう

help.github.com

.github/dependabot.ymlをコミットすれば初回としてdependabotが作動します。それ以降はscheduleに設定したとおりに自動で動作するようになります

f:id:meilcli:20200621163309p:plain もし更新できるライブラリーが見つかれば画像のようにdependabotが自動でPullRequestを作成してくれます

PullRequest

dependabotが作成してくれたPullRequestはbase branchが更新されたら自動でrebaseしてくれます。その他にもコメントで@dependabotにコマンドを送り付けると様々なアクションを取ってくれます

f:id:meilcli:20200621163653p:plain

コマンド一覧を見る限りは実運用に耐えれそうな機能を持っている様子でした

Advanced

さてdependabotですが、ソースコードを読む限りはGradleプロジェクトをビルドしているわけではなさそうです

自分が読み取れた限りでは以下のように動作しているようです

  1. settings.gradleからモジュール情報を抜き取る
  2. 抜き取ったモジュール情報からモジュールのbuild.gradleを探し出す
  3. 見つけたbuild.gradleから正規表現などでMaven Repository URLと依存の宣言(example:example:1.0みたいなやつ)を抜き取る
    • extなどの変数定義にはできる限り対応させている様子
  4. あとはMaven Repository URLに対して依存のアップデートがないか確認する

このような動作をしているため、ここから外れたbuild.gradleをしているとdependabotに検知してくれません。あとbuild.gradle.ktsに対応してないのでKotlinで書いてたら問答無用で検知してくれません
自分の場合はbuildSrcにまとめて依存情報の定義を行っていたため検知してくれませんでした

dependabotが検知してくれないので諦めるのか?いえ、そうではありません。検知してくれないのなら検知してくれるように変えればいいのです。そして正規表現で抜き出してるのならbuild.gradleとして完全体である必要もありません

f:id:meilcli:20200621170415p:plain

さてdependabotを騙すわけですが、その構成としては画像のように行っていきます

実際に依存を定義するのはdependenciesモジュールのbuild.gradleで行うのですが、このモジュールはこれのためだけに用意する空のモジュールにします。dependenciesモジュールを使うことによってdependabotに検知されるファイルパスにするわけです

そしてbuildSrc/build.gradle.ktsからdependencies/build.gradleを読み取って依存の定義ファイルを生成します。ここの依存の定義ファイルを自動生成する部分に関してはちょうどよさそうなgmazo/gradle-buildconfig-pluginを使います。このPluginはAndroidモジュールで言うところのbuildConfigFieldBuildConfigファイルを自動生成してくれるみたいな動作をしてくれます。自動生成された依存の定義ファイルを各モジュールのbuild.gradleでimportすればいいという仕掛けです

では、画像の構成を実現していきましょう。root直下のbuild.gradlegmazo/gradle-buildconfig-pluginをclasspathに通します

buildscript {
    repositories {
        maven { url "https://plugins.gradle.org/m2/" }
    }
    dependencies {
        classpath "com.github.gmazzo:gradle-buildconfig-plugin:2.0.1"
    }
}

その次にdependencies/build.gradleに依存を定義します

apply plugin: "com.github.gmazzo.buildconfig"

buildConfig {
    packageName("net.meilcli.librarian.gradle.dependencies") // Packageは各々buildSrcのpackageに合わせて変更してくれ
    className("Dependencies")
    buildConfigField("String", "test", "\"test_value\"") // ここで他に必要な値を自動生成できる
}

ext {
    configUrl = ""
    configName = ""
}

@SuppressWarnings("GrMethodMayBeStatic")
void repositories(Closure closure) {
    closure()
}

@SuppressWarnings("GrMethodMayBeStatic")
void maven(Closure closure) {
    closure()
    buildConfig.forClass("Repositories") {
        buildConfigField("String", configName, "\"$configUrl\"")
    }
    ext.set("repository_$configName", configUrl)
}

@SuppressWarnings("GrMethodMayBeStatic")
void url(String url) {
    ext.configUrl = url
}

@SuppressWarnings("GrMethodMayBeStatic")
void name(String name) {
    ext.configName = name
}

@SuppressWarnings("GrMethodMayBeStatic")
void dependencies(Closure closure) {
    closure()
}

@SuppressWarnings("GrMethodMayBeStatic")
void library(String notice, String name, String className) {
    buildConfig.forClass(className) {
        buildConfigField("String", name, "\"$notice\"")
    }
    ext.set("library_${className}_${name}", notice)
}

repositories {
    // mave { の後ろにはurl ""が先に来る必要がある
    maven {
        url "https://maven.google.com/"
        name "google"
    }
    maven {
        url "https://jcenter.bintray.com/"
        name "jcenter"
    }
    maven {
        url "https://plugins.gradle.org/m2/"
        name "gradle"
    }
}

dependencies {
    ext.kotlinGroup = "org.jetbrains.kotlin"
    ext.kotlinVersion = "1.3.70"
    library "$kotlinGroup:kotlin-gradle-plugin:$kotlinVersion", "gradle", "Kotlin"
    library "$kotlinGroup:kotlin-stdlib-jdk7:$kotlinVersion", "stdlib", "Kotlin"
    library "$kotlinGroup:kotlin-serialization:$kotlinVersion", "gradle", "KotlinSerialization"
    library "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0", "runtime", "KotlinSerialization"
}

そしてこのファイルをbuildSrc/build.gradle.ktsで利用すればいいわけです

plugins {
    `kotlin-dsl`
}

apply(from = "../dependencies/build.gradle")

buildscript {
    repositories {
        maven("https://plugins.gradle.org/m2/")
    }
    dependencies {
        classpath("com.github.gmazzo:gradle-buildconfig-plugin:2.0.1")
    }
}

あとはgradle-buildconfig-pluginが定義ファイルを自動生成してくれるので各々使いたいところでimportすればいいです
ただ、buildSrc/build.gradle.ktsだけはブートストラップ問題により以下の制限がかかります

  • buildscriptブロックでは定義情報を参照できない
  • それ以外ではext(ktsだとextra)から参照する
    • たとえば先ほどの例だとval kotlinGradle = extra["library_Kotlin_gradle"] as Stringのようにして参照します

その他細かいところは↓のリポジトリーで実際に運用しているのでそちらのコードを読んでください

github.com

最後に

dependabotを活用したらライブラリー管理が非常に楽になると思いますが、PullRequestが自動で作られてもそのPullRequestがビルドに成功するかとかテストに成功するかとかを自動でチェックするCIがなければあまり労力は変わらないでしょう。なのでdependabotを導入するか悩んでいてCIによる自動化が不完全なリポジトリーではまずCIから導入してみましょう