• トップ
  • ブログ一覧
  • 【Android】GitHub Actionsでアプリのバージョン情報を更新する
  • 【Android】GitHub Actionsでアプリのバージョン情報を更新する

    こー(エンジニア)こー(エンジニア)
    2025.02.07

    IT技術

    はじめに

    こんにちは!株式会社ライトコードの福岡本社でモバイルエンジニアやってる こー です!

    featureImg2021.11.05YOUは何しにライトコードへ?〜こーくん編〜プロジェクト内で安心感を与えられる存在になりたい!今回は、弊社のエンジニアである高さんにフィーチャー!技術力に定評のあ...

    現在、個人開発しているAndroidアプリでFirebase App Distributionへの自動デプロイする際に利用している、GitHub Actionsを利用したアプリバージョン情報の更新 について解説していこうと思います。

    次の記事では、実際に今回の方法と組み合わせて 1クリックでバージョン更新 & Firebase App Distributionにアプリをデプロイする方法 について執筆予定です。

    それでは早速見ていきましょう!

    どうしてやったの?

    Androidアプリを継続リリースするためには、リリースAPKをビルドする前にアプリのバージョン情報を更新する必要があります。

    バージョン情報は「バージョンコード」「バージョン名」の2つで構成されています。

    用途
    バージョンコード整数
    • アプリのアップデート管理に利用される内部識別子
    • 新しいバージョンをリリースする度にインクリメントが必要
    • ユーザーには表示されない
    バージョン名文字列

    プロジェクトのビルドファイルapp/build.gradle.ktsにあるversionCode/versionNameがそれぞれ「バージョンコード」/「バージョン名」に対応します。

    ここを開発者が編集しリリースAPKをビルドする、というのが一般的です。

    1/// app/build.gradle.kts
    2android {
    3    /* ~中略~ */
    4    defaultConfig {
    5        /* ~中略~ */
    6        versionCode = 12
    7        versionName = "0.10.0"
    8    }
    9    /* ~中略~ */
    10}

    この部分を GitHub Actionsを利用して自動化する ことで、タイムコストを削減しつつ手動更新によるヒューマンエラーを防ぐことができます。

    また、GitHub上で実行するため開発者の環境に依存せず実行できるというメリットもあります。

    何をやったの?

    今回、この仕組みを作るにあたって行なったことは大きく分けて2つです。

    1. バージョン情報を更新するGradleタスクの実装
    2. 1を実行するGitHub Actionsのワークフローの実装

    それでは、それぞれで具体的に何をやったのか、詳しく見ていきましょう。

    ちなみに、今回Gradleファイルを扱いますが、Kotlin DSLでの実装を前提としています。

    1. バージョン情報を更新するGradleタスクの実装

    まず、versionCodeversionNameからバージョンコード/バージョン名を操作するクラスを実装します。

    1/// app/build.gradle.kts
    2// バージョンを取り扱うクラス
    3class Version(private var code: Int, version: String) {
    4    private var major: Int
    5    private var minor: Int
    6    private var patch: Int
    7
    8    init {
    9        // "0.0.0"で表現されるバージョン名を、メジャー/マイナー/パッチに分割 & 整数として操作できるように管理する。
    10        val (major, minor, patch) = version.split(".").map { it.toInt() }
    11        this.major = major
    12        this.minor = minor
    13        this.patch = patch
    14    }
    15
    16    // バージョン情報を操作するメソッドをコマンドから実行するために、各メソッド名を保持。
    17    val functionsByName = listOf(::bumpMajor, ::bumpMinor, ::bumpPatch).associateBy { it.name }
    18
    19    @SuppressWarnings("unused")
    20    fun bumpMajor() {
    21        major += 1
    22        minor = 0
    23        patch = 0
    24
    25        code += 1
    26    }
    27
    28    @SuppressWarnings("unused")
    29    fun bumpMinor() {
    30        minor += 1
    31        patch = 0
    32
    33        code += 1
    34    }
    35
    36    @SuppressWarnings("unused")
    37    fun bumpPatch() {
    38        patch += 1
    39
    40        code += 1
    41    }
    42
    43    fun getName(): String = "$major.$minor.$patch"
    44    fun getCode(): Int = code
    45}

    バージョン番号/バージョン情報それぞれの文字列から、メジャー/マイナー/パッチを抽出して保持し、それぞれを操作するメソッドを実装しています。

    次にGradleタスクのコマンドでそれぞれのメソッドを実行するために、メソッド名のリストをfunctionsByNameで保持しておきます。

    次に実際にバージョン情報を操作するGradleタスクのコマンドを実装していきます。

    1/// app/build.gradle.kts
    2tasks.addRule("Pattern: bumpVersion") {
    3    if (this.matches(Regex("bump(Major|Minor|Patch)Version"))) {
    4        task(this) {
    5            doLast {
    6                // コマンド名から実行タイプを抽出
    7                val type = this@addRule
    8                    .replace(Regex("bump"), "")
    9                    .replace(Regex("Version"), "")
    10
    11                println("Bumping ${type.lowercase(Locale.getDefault())} version...")
    12
    13                // 旧バージョンを取得
    14                val oldVersionCode = android.defaultConfig.versionCode ?: return@doLast
    15                val oldVersionName = android.defaultConfig.versionName ?: return@doLast
    16                val version = Version(oldVersionCode, oldVersionName)
    17
    18                // メソッド名を取得しそのメソッドを実行(実行不可であればエラー)
    19                val methodName = "bump${type}"
    20                version.functionsByName[methodName]?.invoke() ?: error("Unknown method: $methodName")
    21
    22                // 更新したバージョンを取得
    23                val newVersionCode = version.getCode()
    24                val newVersionName = version.getName()
    25
    26                println("${oldVersionName}($oldVersionCode) -> ${newVersionName}($newVersionCode)")
    27
    28                // ビルドファイルに更新したversionName・versionCodeを書き込み
    29                var updated = buildFile.readText()
    30                updated = updated.replaceFirst(
    31                    "versionName = \"${oldVersionName}\"",
    32                    "versionName = \"${newVersionName}\""
    33                )
    34                updated = updated.replaceFirst(
    35                    "versionCode = $oldVersionCode",
    36                    "versionCode = $newVersionCode"
    37                )
    38                buildFile.writeText(updated)
    39            }
    40        }
    41    }
    42}
    43
    44// バージョン情報を出力するコマンド群
    45tasks.register("printVersionCode") {
    46    doLast {
    47        println(android.defaultConfig.versionCode)
    48    }
    49}
    50
    51tasks.register("printVersionName") {
    52    doLast {
    53        println(android.defaultConfig.versionName)
    54    }
    55}

    今回は、

    1. メジャー/マイナー/パッチそれぞれを操作するbumpMajorVersion/bumpMinorVersion/bumpPatchVersionの3コマンド
    2. バージョンコード/バージョン名それぞれを標準出力する2コマンド

    の合計5コマンドを実装しています。

    個々の処理が何を行っているかはコード上のコメントを参照していただけると幸いですが、重要な部分を抜粋して解説します。

    1-1. 旧バージョンの取得

    1    // 旧バージョンを取得
    2    val oldVersionCode = android.defaultConfig.versionCode ?: return@doLast
    3    val oldVersionName = android.defaultConfig.versionName ?: return@doLast
    4    val version = Version(oldVersionCode, oldVersionName)

    プロジェクトに現在設定されているバージョン情報はandroid.defaultConfigから参照することができます。

    ここからバージョンコード/バージョン名を抽出し、先程実装したVersionクラスのインスタンスを作成し、バージョン情報の操作を可能にします。

    1-2. バージョン情報の操作

    1    // メソッド名を取得しそのメソッドを実行(実行不可であればエラー)
    2    val methodName = "bump${type}"
    3    version.functionsByName[methodName]?.invoke() ?: error("Unknown method: $methodName")
    4
    5    // 更新したバージョンを取得
    6    val newVersionCode = version.getCode()
    7    val newVersionName = version.getName()
    8
    9    println("${oldVersionName}($oldVersionCode) -> ${newVersionName}($newVersionCode)")
    10
    11    // ビルドファイルに更新したversionName・versionCodeを書き込み
    12    var updated = buildFile.readText()
    13    updated = updated.replaceFirst(
    14        "versionName = \"${oldVersionName}\"",
    15        "versionName = \"${newVersionName}\""
    16    )
    17    updated = updated.replaceFirst(
    18        "versionCode = $oldVersionCode",
    19        "versionCode = $newVersionCode"
    20    )
    21    buildFile.writeText(updated)

    実行したコマンド名から操作するバージョン番号のタイプを抽出し、それに対応したVersionクラスのメソッドを実行することで次のバージョン情報を取得します。

    実際のビルドファイルはbuildFileで参照することができ、ここからバージョンコード/バージョン名の記載部分を読み取り、更新した次のバージョン情報に置き換え書き込むことでビルドファイルへ反映させます。

    これでGradleタスクの実装は完了です。

    これによりプロジェクトのルートディレクトリでターミナルから./gradlew bump(Major|Minor|Patch)Versionコマンドを実行することで、バージョン情報の更新を行うことができるようになりました!

    ここで一旦実際にターミナルからコマンドを実行し、正しくバージョン情報が更新されるか動作確認を行なっておきましょうね。

    次はこのコマンドをGitHub Actionsで実行するためのワークフローを実装していきます。

    2. Gradleタスクを実行するワークフローの実装

    メジャー/マイナー/パッチそれぞれの更新用ワークフローを実装する必要がありますが、叩くGradleタスクのコマンドを変更するだけなので、今回はメジャーバージョン更新用のワークフローに絞って見ていきましょう。

    1# /.github/workflows/increment-major-version.yaml
    2name: Increment Major Version
    3
    4# ワークフローは手動実行
    5on: [ workflow_dispatch ]
    6
    7jobs:
    8  build:
    9    runs-on: ubuntu-latest
    10
    11    steps:
    12      # releaseブランチのチェックアウト
    13      - uses: actions/checkout@v4
    14        with:
    15          ref: release
    16          fetch-depth: 0
    17
    18      # JDKのセットアップ
    19      - name: set up JDK 17
    20        uses: actions/setup-java@v2
    21        with:
    22          java-version: '17'
    23          distribution: 'temurin'
    24          cache: gradle
    25
    26      # gradlewの実行権限の付与
    27      - name: Grant Permission gradlew
    28        run: chmod +x gradlew
    29
    30      # メジャーバージョンの更新
    31      - name: Bump Major Version
    32        run: ./gradlew bumpMajorVersion
    33
    34      # 更新したバージョンを取得し出力変数に保存
    35      - name: Get Version
    36        run: |
    37          echo "::set-output name=VERSION_CODE::$(./gradlew -q printVersionCode)"
    38          echo "::set-output name=VERSION_NAME::$(./gradlew -q printVersionName)"
    39        id: version
    40
    41      # バージョン更新処理をコミット & Pull Requestを作成
    42      - name: Create Pull Request
    43        uses: peter-evans/create-pull-request@v6
    44        with:
    45          token: ${{ secrets.GITHUB_TOKEN }}
    46          branch: update-app-major-version-${{ steps.version.outputs.VERSION_NAME }}-${{ steps.version.outputs.VERSION_CODE }}
    47          base: release
    48          title: "chore: 🤖 versionName: ${{ steps.version.outputs.VERSION_NAME }} / versionCode: ${{ steps.version.outputs.VERSION_CODE }}"
    49          commit-message: "chore: 🤖 versionName: `${{ steps.version.outputs.VERSION_NAME }}` / versionCode: `${{ steps.version.outputs.VERSION_CODE }}`"

    2-1. gradlewの実行権限の付与

    1      - name: Grant Permission gradlew
    2        run: chmod +x gradlew

    GitHub ActionsのLinux環境ではgradlewの実行権限が付与されていないことがあるため、明示的にchmodコマンドで実行権限を付与しておきます。

    2-2. メジャーバージョンの更新

    1      - name: Bump Major Version
    2        run: ./gradlew bumpMajorVersion

    実行権限を付与したら、1で作成したバージョン更新のコマンドを実行します。

    2-3. 更新したバージョンを取得し出力変数に保存

    1      - name: Get Version
    2        run: |
    3          echo "::set-output name=VERSION_CODE::$(./gradlew -q printVersionCode)"
    4          echo "::set-output name=VERSION_NAME::$(./gradlew -q printVersionName)"
    5        id: version

    次で行うPull Requestの自動作成で、Pull Requestタイトルやコミット名にバージョン情報を記載するため、更新後のバージョン情報をこのステップに保存します。

    echo "::set-output name=A::B"というコマンドで、Aという出力変数にBというコマンドの出力結果を保存することができます。

    この出力変数をPull Requestの自動作成ステップで利用するため、id: versionで本ステップに参照名を指定し、出力変数を参照できるようにしています。

    2-4. バージョン更新処理をコミット & PR作成

    1      - name: Create Pull Request
    2        uses: peter-evans/create-pull-request@v6
    3        with:
    4          token: ${{ secrets.GITHUB_TOKEN }}
    5          branch: update-app-major-version-${{ steps.version.outputs.VERSION_NAME }}-${{ steps.version.outputs.VERSION_CODE }}
    6          base: release
    7          title: "chore: 🤖 versionName: ${{ steps.version.outputs.VERSION_NAME }} / versionCode: ${{ steps.version.outputs.VERSION_CODE }}"
    8          commit-message: "chore: 🤖 versionName: `${{ steps.version.outputs.VERSION_NAME }}` / versionCode: `${{ steps.version.outputs.VERSION_CODE }}`"

    バージョン更新で生じた差分をGitHub Actions上でコミット & Pull Requestを作成します。

    ここでは、カスタムアクションとして公開されている peter-evans/create-pull-requestを利用します。

    このカスタムアクションでは、読み書き可能なGitHubトークンや各プロパティを指定するだけで、リポジトリにPull Requestを作成してくれます。

    GitHubトークンの取得方法は割愛しますが、秘匿情報になるため、必ずSecretsを通して取得するようにしてくださいね。

    今回設定しているプロパティを軽くまとめておきます。

    token
    • 読み書き可能なGitHubトークンを設定する
    • トークンは秘匿情報になるため、必ずSecretsを通して取得する
    branch
    • Pull Requestを作成する元のブランチ
    base
    • Pull Requestのマージ先となるブランチ
    title
    • Pull Requestのタイトル名
    commit-message
    • 差分コミットのメッセージ

    2-3で保存した出力変数はsteps.version.outputs.~で参照することができ、これをPull Requestのタイトルやコミットメッセージで利用しています。

    これで、GitHub Actionsからアプリのバージョン更新を自動で行う仕組みを作成することができました!

    最後に実装全体を記載しますので、間違いや足りないところがないかの確認に利用してくださいね。

    実装全体

    1/// app/build.gradle.kts
    2class Version(private var code: Int, version: String) {
    3    private var major: Int
    4    private var minor: Int
    5    private var patch: Int
    6
    7    init {
    8        val (major, minor, patch) = version.split(".").map { it.toInt() }
    9        this.major = major
    10        this.minor = minor
    11        this.patch = patch
    12    }
    13
    14    val functionsByName = listOf(::bumpMajor, ::bumpMinor, ::bumpPatch).associateBy { it.name }
    15
    16    @SuppressWarnings("unused")
    17    fun bumpMajor() {
    18        major += 1
    19        minor = 0
    20        patch = 0
    21
    22        code += 1
    23    }
    24
    25    @SuppressWarnings("unused")
    26    fun bumpMinor() {
    27        minor += 1
    28        patch = 0
    29
    30        code += 1
    31    }
    32
    33    @SuppressWarnings("unused")
    34    fun bumpPatch() {
    35        patch += 1
    36
    37        code += 1
    38    }
    39
    40    fun getName(): String = "$major.$minor.$patch"
    41    fun getCode(): Int = code
    42}
    43
    44tasks.addRule("Pattern: bumpVersion") {
    45    if (this.matches(Regex("bump(Major|Minor|Patch)Version"))) {
    46        task(this) {
    47            doLast {
    48                // コマンドから実行タイプを抽出
    49                val type = this@addRule
    50                    .replace(Regex("bump"), "")
    51                    .replace(Regex("Version"), "")
    52
    53                println("Bumping ${type.lowercase(Locale.getDefault())} version...")
    54
    55                // 旧バージョンを取得
    56                val oldVersionCode = android.defaultConfig.versionCode ?: return@doLast
    57                val oldVersionName = android.defaultConfig.versionName ?: return@doLast
    58                val version = Version(oldVersionCode, oldVersionName)
    59
    60                // メソッド名を取得しそのメソッドを実行(実行不可であればエラー)
    61                val methodName = "bump${type}"
    62                version.functionsByName[methodName]?.invoke() ?: error("Unknown method: $methodName")
    63
    64                // 更新したバージョンを取得
    65                val newVersionCode = version.getCode()
    66                val newVersionName = version.getName()
    67
    68                println("${oldVersionName}($oldVersionCode) -> ${newVersionName}($newVersionCode)")
    69
    70                // ビルドファイルに更新したversionName・versionCodeを書き込み
    71                var updated = buildFile.readText()
    72                updated = updated.replaceFirst(
    73                    "versionName = \"${oldVersionName}\"",
    74                    "versionName = \"${newVersionName}\""
    75                )
    76                updated = updated.replaceFirst(
    77                    "versionCode = $oldVersionCode",
    78                    "versionCode = $newVersionCode"
    79                )
    80                buildFile.writeText(updated)
    81            }
    82        }
    83    }
    84}
    85
    86tasks.register("printVersionCode") {
    87    doLast {
    88        println(android.defaultConfig.versionCode)
    89    }
    90}
    91
    92tasks.register("printVersionName") {
    93    doLast {
    94        println(android.defaultConfig.versionName)
    95    }
    96}
    1# /.github/workflows/increment-major-version.yaml
    2name: Increment Major Version
    3
    4on: [ workflow_dispatch ]
    5
    6jobs:
    7  build:
    8    runs-on: ubuntu-latest
    9
    10    steps:
    11      - uses: actions/checkout@v4
    12        with:
    13          ref: release
    14          fetch-depth: 0
    15
    16      - name: set up JDK 17
    17        uses: actions/setup-java@v2
    18        with:
    19          java-version: '17'
    20          distribution: 'temurin'
    21          cache: gradle
    22
    23      - name: Grant Permission gradlew
    24        run: chmod +x gradlew
    25
    26      - name: Bump Major Version
    27        run: ./gradlew bumpMajorVersion
    28
    29      - name: Get Version
    30        run: |
    31          echo "::set-output name=VERSION_CODE::$(./gradlew -q printVersionCode)"
    32          echo "::set-output name=VERSION_NAME::$(./gradlew -q printVersionName)"
    33        id: version
    34
    35      - name: Create Pull Request
    36        uses: peter-evans/create-pull-request@v6
    37        with:
    38          token: ${{ secrets.GITHUB_TOKEN }}
    39          branch: update-app-major-version-${{ steps.version.outputs.VERSION_NAME }}-${{ steps.version.outputs.VERSION_CODE }}
    40          base: release
    41          title: "chore: 🤖 versionName: ${{ steps.version.outputs.VERSION_NAME }} / versionCode: ${{ steps.version.outputs.VERSION_CODE }}"
    42          commit-message: "chore: 🤖 versionName: `${{ steps.version.outputs.VERSION_NAME }}` / versionCode: `${{ steps.version.outputs.VERSION_CODE }}`"

    さいごに

    さて、今回はGitHub Actionsを利用したAndroidアプリのバージョン情報の更新について見ていきました。

    Kotlin DSLでGradleタスクのコマンドを実装できるため、Kotlinを利用したスクリプトをプロジェクト上に簡単に書けるのは嬉しいですね。

    GitHub ActionsのLinux環境でも比較的高速に動き相性も良いため、こと自動化においては非常に強力なツールだと思います。

    皆さんもこれを足がかりに様々な自動化に挑戦してみてはいかがでしょうか?

    次回は冒頭でも述べたように、今回の内容を踏まえた1クリックでバージョン更新 & Firebase App Distributionへのアプリのデプロイ方法について書いていこうと思います!

    どちらかというと今回は前座で次回が本番です(笑)

    興味を持ってくださった方もそうでない方も楽しみにしていただけると嬉しいです!

    今回の記事も最後までご覧いただき、本当にありがとうございました!

    皆さんのスキルアップに少しでも貢献できていれば幸いです。

    それではまた!!

    ライトコードでは、エンジニアを積極採用中!

    ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。

    採用情報へ

    こー(エンジニア)
    こー(エンジニア)
    Show more...

    おすすめ記事

    エンジニア大募集中!

    ライトコードでは、エンジニアを積極採用中です。

    特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

    また、フリーランスエンジニア様も大募集中です。

    background