ANTIGRAVITY LABEN
記事一覧/アプリ開発
アプリ開発/2026-06-24上級

三つの経路がそれぞれ番号を振っていた — versionCode を単一の真実から決めて配信を止めない設計

AI Studio がワンプロンプトでアプリを生成し Play の内部テストへそのまま上げられるようになった結果、人手・CI・エージェントの三経路が別々に versionCode を採番して衝突するようになりました。番号を単一の真実から決め、アップロード前に照合するガードまでを実装つきで整理します。

antigravity388android23google-play3ci-cd11versioning2

プレミアム記事

ある朝、夜間に回していた配信パイプラインが「Version code 1037 has already been used. Try another version code.」で止まっていました。原因を追うと、犯人は一つではありませんでした。私が前夜に手元から内部テストへ一本上げ、CI が同じコミットで AAB を作って同じ番号を振り、さらに AI Studio から生成版の検証ビルドを内部テストトラックへ流していた。三つの経路が、それぞれ自分のやり方で versionCode を採番していたのです。

I/O 2026 以降、AI Studio はテキストのプロンプトから Kotlin/Jetpack Compose アプリを生成し、埋め込みエミュレータで動かして USB で実機に転送し、Google Play の内部テストトラックまで一画面で配信できるようになりました。「作って試して配る」の距離が一気に縮んだのは嬉しいのですが、その裏で、アプリを Play へ上げる経路が一つ増えています。人手・CI に加えて「機械が勝手に上げる経路」が常駐するようになった、ということです。

versionCode は Play にとって、そのアプリのビルドを一意に並べるための整数です。各トラックをまたいで単調増加でなければならず、一度使った番号は二度と使えません。番号を採番する主体が一つなら衝突は起きません。問題は、採番主体が三つに増えたのに、誰も他の経路がいくつまで使ったかを知らない、という状態です。

なぜ三つの経路が衝突するのか

衝突の本質は「誰が次の番号を決めるか」が分散していることです。よくある採番方式は、それぞれ別の状態を見ています。

採番方式参照している状態他経路から見えるか
手元の versionCode をエディタで +1ローカルの build.gradle見えない(手元だけ)
CI が前回ビルドの値に +1CI のキャッシュ/前回成果物見えない(CI 内部)
エージェント/AI Studio が現在時刻などで採番実行時の環境見えない(その実行限り)

どの方式も単体では正しく動きます。けれど三つが同じアプリへ並行して上げると、互いの「いくつまで使ったか」を共有していないため、同じ番号を二度振ったり、低い番号を後から振ったりします。Play は単調増加と一意性を要求するので、後から来た低い番号や重複番号は弾かれ、配信が止まります。

個人開発で長くアプリを出してきた感覚として、この種の障害は「賢さ」では防げません。各経路をもっと賢く +1 させても、参照している状態がバラバラなら衝突は残ります。効くのは、番号の決め方を一つに寄せることです。

versionCode を単一の真実から決める

私はこの場面では、versionCode を「人間が手で持つ値」から外し、誰が計算しても同じ整数になる決定的な関数にすることを選んでいます。最も扱いやすい単一の真実は、リポジトリの履歴そのものです。main に積まれたコミット数は、どのマシンの、どの経路から計算しても同じ値になります。

// app/build.gradle.kts
// versionCode を「main のコミット数」から決定的に導出する。
// 手元・CI・エージェントのどこで計算しても同じ整数になるのが要点。
import java.io.ByteArrayOutputStream
 
fun gitVersionCode(): Int {
    // CI が浅いクローン(shallow)だとコミット数が欠けるため、
    // 環境変数で上書きできる逃げ道を用意しておく。
    System.getenv("VERSION_CODE_OVERRIDE")?.toIntOrNull()?.let { return it }
 
    val out = ByteArrayOutputStream()
    val result = exec {
        commandLine("git", "rev-list", "--count", "HEAD")
        standardOutput = out
        isIgnoreExitValue = true
    }
    val count = out.toString().trim().toIntOrNull()
    require(result.exitValue == 0 && count != null && count > 0) {
        "versionCode をコミット数から導出できませんでした。" +
            "shallow clone の場合は fetch-depth: 0 にするか VERSION_CODE_OVERRIDE を渡してください。"
    }
    // ベースオフセットを足して、過去に手採番していた番号帯と衝突させない。
    return 100000 + count
}
 
android {
    defaultConfig {
        versionCode = gitVersionCode()
        versionName = "2.${gitVersionCode() - 100000}"
    }
}

ここで大事なのは二点です。一つは、CI の浅いクローンではコミット数が実数と食い違うため、fetch-depth: 0(全履歴を取得)にするか、明示的なオーバーライドを許すこと。もう一つは、過去に手採番で使っていた番号帯(たとえば 1000 番台)と新方式の番号帯を、ベースオフセットでずらしておくことです。私は移行時、旧帯の最大値より十分大きいオフセットを足して、新旧が交差しないようにしました。

CI のビルド番号を真実に使う方法もあります。GitHub Actions なら github.run_number が単調増加するので、VERSION_CODE_OVERRIDE にそれを渡せば、CI 起点の採番に寄せられます。リポジトリ履歴を真実にするか、CI のビルド番号を真実にするかは、どちらか一方に決めて全経路で揃えることが肝心です。二つの真実があれば、それはもう真実ではありません。

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
「Version code already used」で自動配信が止まっていた人が、人手・CI・エージェントの3経路のどこから上げても番号が衝突しない仕組みを今日組める
git のコミット数や CI のビルド番号から versionCode を決め、アップロード前に Play 側の最新値と照合するガードを実装できる
internal/closed/production をまたいだ採番共有や単調増加制約といった、公式に明記されていない落とし穴を踏まずに段階公開を回せるようになる
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Antigravity Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

アプリ開発2026-04-25
Play Console のデータを Antigravity で読み解く — 個人開発アプリの評価と収益を継続的に改善する方法
Google Play Console のデータをただ眺めているだけになっていませんか? Antigravity を使えば、評価・クラッシュ・収益データを自動分析し、次の改善アクションまで導いてくれます。個人開発者向けの実践的な解説です。
アプリ開発2026-06-21
戻るボタンで広告が出る/出ないが安定しない — 入れ子のifを独立ガードのリストに作り替えた記録
戻るボタン押下時のインタースティシャル表示判定が、入れ子のif文で優先度が暗黙化して壊れていました。各条件を理由つきの独立ガードに分解し、決定表からテストを生成した設計をまとめます。
アプリ開発2026-06-17
ダイアログが重なる前に止める — 課金・レビュー誘導・リワード広告を1か所のゲートで束ねる
ペイウォール・レビュー誘導・リワード広告が同じ瞬間に重なって出る不具合を、優先度つきの中央ゲートで根治した実装記録です。Antigravityのエージェントに散らばったshow()呼び出しの掃き出しを任せ、表示ポリシーは自分で握る線引きで進めました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →