RORK LABEN
BUILD — Rork Max はクラウド上の実機 Mac に Xcode と iOS SDK を載せ、SwiftUI を書き、ビルドし、エラーを読んで直す反復を回します。コードを吐いて終わりではない点が生成物の質に効いていますNATIVE — 生成されるのは React Native ではなく純粋な Swift / SwiftUI です。AR や Metal、ウィジェットなど React Native からは届かない機能に手が届くことが、他ビルダーとの実質的な差になりますPLATFORMS — 対応は iPhone・iPad・Apple Watch・Apple TV・Vision Pro に加えて iMessage まで含みます。ウォッチや拡張から入る設計を試すのに向いていますCOMPANION — Rork Companion により、有料の Apple Developer アカウントなしで生成したアプリを実機の iPhone で確認できます。最初の一本を試す段階の障壁が一段下がりましたPRICING — 無料で開始でき有料プランは月額25ドルから、Rork Max は月額200ドルの Max プランです。回収できる本数と規模を先に見積もっておく価値がありますDEADLINE — Google Play は2026年8月31日以降、新規アプリと更新の双方に対象 API レベル36 以上を要求します。残り10日で、生成物の targetSdkVersion は自分で確認が必要ですBUILD — Rork Max はクラウド上の実機 Mac に Xcode と iOS SDK を載せ、SwiftUI を書き、ビルドし、エラーを読んで直す反復を回します。コードを吐いて終わりではない点が生成物の質に効いていますNATIVE — 生成されるのは React Native ではなく純粋な Swift / SwiftUI です。AR や Metal、ウィジェットなど React Native からは届かない機能に手が届くことが、他ビルダーとの実質的な差になりますPLATFORMS — 対応は iPhone・iPad・Apple Watch・Apple TV・Vision Pro に加えて iMessage まで含みます。ウォッチや拡張から入る設計を試すのに向いていますCOMPANION — Rork Companion により、有料の Apple Developer アカウントなしで生成したアプリを実機の iPhone で確認できます。最初の一本を試す段階の障壁が一段下がりましたPRICING — 無料で開始でき有料プランは月額25ドルから、Rork Max は月額200ドルの Max プランです。回収できる本数と規模を先に見積もっておく価値がありますDEADLINE — Google Play は2026年8月31日以降、新規アプリと更新の双方に対象 API レベル36 以上を要求します。残り10日で、生成物の targetSdkVersion は自分で確認が必要です
記事一覧/開発ツール
開発ツール/2026-08-18中級

「Version code 1 has already been used」の番号は、app.json と EAS のどちらが持っているか

Play へのアップロードが versionCode で止まったとき、番号を闇雲に上げる前に確かめる場所と、app.json・EAS・build.gradle のどれが実際の値を持っているかを整理します。

Google Play27EAS Build16Expo175リリース10troubleshooting26

壁紙アプリの更新を出そうとして、Play Console のアップロード画面で止まりました。返ってきたのは一行です。

Version code 1 has already been used. Try another version code.

手元の app.json は、たしかに versionCode を 2 に上げてありました。ですので最初は Play 側の表示が追いついていないのだろうと考え、しばらく待ってから同じファイルを上げ直しました。結果は変わりません。

原因は Play ではなく、私の手元にありました。上げたはずの値が、そもそもビルドに入っていなかったのです。

この番号は、置き場所が複数あります。どこに書いたかではなく、どこが持ち主になっているかで結果が決まります。今日はその見分け方を、実際に動かせる形で整理します。

その番号は、公開していなくても使われています

まず Play 側の事実を確かめます。versionCode は Android アプリの更新順序を決める整数で、Play は一度受け取った番号を再利用させません。ここで見落としやすいのは、公開していなくても消費されることです。

  • 製品版に出していないドラフトのリリースに添付しただけ
  • 内部テストやクローズドテストのトラックに上げただけ
  • アップロード後に「破棄」せず、リリースを保存したまま放置した

いずれの場合も、その番号は使用済みとして扱われます。「まだ誰にも配っていないのだから空いているはず」という感覚は通りません。

現状は Play Console の リリース > アプリバンドルエクスプローラ で確認できます。ここには受け取り済みの AAB が版ごとに並んでいますので、いま何番まで消費しているのかが一目で分かります。エラーに驚いて番号を適当に上げる前に、まずこの画面を開くのが早道です。

対処は二択です。

  1. ドラフトのリリースに添付された AAB を外し、アプリバンドルエクスプローラ側でも削除して、その番号を空ける
  2. 番号を上げて、ビルドし直す

2 を選ぶ場合、番号は連番である必要がありません。2 の次が 10 でも 100 でも、前より大きければ受け付けられます。私は原因調査で番号を何度か無駄にしたとき、切りのよい数まで飛ばして仕切り直すようにしています。あとから履歴を見たときに「ここで何かあった」と分かる方が、詰めて並べるより読みやすいためです。

なお、番号を変えたら必ずビルドし直しますversionCode は AAB の中に焼き込まれているため、設定ファイルを書き換えただけの状態で同じ成果物を上げ直しても、Play には古い番号が届きます。私が最初にはまったのは、まさにこの往復でした。

上げたはずの番号が、ビルドに入っていない

ここからが本題です。Expo のプロジェクトでは、versionCode を決めうる場所が三つあります。

  • app.json(または app.config.js)の expo.android.versionCode
  • EAS のサーバー側に保管された値(eas.jsoncli.appVersionSourceremote のとき)
  • android/app/build.gradleversionCodeandroid ディレクトリをリポジトリに置いているとき)

三つのうちどれが効くかは、プロジェクトの構成で変わります。書いた場所と効く場所がずれていると、「上げたのに同じ番号で提出される」が起こります。

宣言した値と実際にビルドへ入る値が割れる現象は、versionCode に限りません。同じ日に扱った targetSdkVersion 36 を通すまでに直した3か所 でも、原因の構造はよく似ています。設定ファイルの記述は宣言であって、ビルドが読む値そのものではない、という一点です。

目で追うと間違えますので、読み取りを機械にやらせます。依存を増やさずに済むよう、Node の標準モジュールだけで書きました。

#!/usr/bin/env node
// 提出前に「Play へ実際に届く versionCode」を1コマンドで確かめる
import { readFileSync, existsSync } from "node:fs";
import { join } from "node:path";
 
const root = process.argv[2] ?? ".";
const readJson = (p) => (existsSync(p) ? JSON.parse(readFileSync(p, "utf8")) : null);
 
const appJson = readJson(join(root, "app.json"));
const easJson = readJson(join(root, "eas.json"));
const gradlePath = join(root, "android", "app", "build.gradle");
const hasNativeDir = existsSync(gradlePath);
 
const expo = appJson?.expo ?? {};
const declared = expo.android?.versionCode ?? null;
const versionName = expo.version ?? null;
 
let gradleCode = null;
if (hasNativeDir) {
  const m = readFileSync(gradlePath, "utf8").match(/versionCode\s+(\d+)/);
  gradleCode = m ? Number(m[1]) : null;
}
 
const source = easJson?.cli?.appVersionSource ?? "(未設定)";
const profile = process.argv[3] ?? "production";
const autoIncrement = easJson?.build?.[profile]?.autoIncrement ?? false;
 
const notes = [];
let effective;
 
if (hasNativeDir) {
  effective = gradleCode;
  notes.push("android/ をリポジトリに置いているため、app.json ではなく build.gradle の値がビルドに入ります");
} else if (source === "remote") {
  effective = "EAS サーバー管理(ローカルの値は初期化にのみ使われます)";
  notes.push("値はローカルファイルではなく EAS 側にあります。eas build:version:get で確認してください");
} else {
  effective = declared;
  notes.push("app.json の値がそのまま入ります。手で上げ忘れると同じ番号で提出することになります");
}
 
if (source === "(未設定)") notes.push("eas.json に cli.appVersionSource がありません。どちらで管理するかを先に決めてください");
if (!autoIncrement) notes.push(`profile "${profile}" の autoIncrement が false です。番号は自動では上がりません`);
 
console.log(`project         : ${root}`);
console.log(`versionName     : ${versionName ?? "(未設定)"}`);
console.log(`app.json 宣言値 : ${declared ?? "(未設定)"}`);
console.log(`build.gradle    : ${hasNativeDir ? gradleCode : "(android/ なし)"}`);
console.log(`appVersionSource: ${source} / autoIncrement(${profile}): ${autoIncrement}`);
console.log(`実際に入る値    : ${effective ?? "(判定不能)"}`);
notes.forEach((n) => console.log(`  - ${n}`));

なぜこう書いたかというと、判定に必要なのは「値」ではなく「経路」だからです。versionCode が 2 と表示されても、その 2 がビルドに届くかどうかは別の話です。ですのでこのスクリプトは値を並べるだけでなく、どの経路が採用されたのかを最後の一行で言い切るようにしてあります。

三通りの構成を用意して、実際に走らせた出力がこちらです。

==========
project         : a
versionName     : 1.0.0
app.json 宣言値 : 1
build.gradle    : (android/ なし)
appVersionSource: (未設定) / autoIncrement(production): false
実際に入る値    : 1
  - app.json の値がそのまま入ります。手で上げ忘れると同じ番号で提出することになります
  - eas.json に cli.appVersionSource がありません。どちらで管理するかを先に決めてください
  - profile "production" の autoIncrement が false です。番号は自動では上がりません
==========
project         : b
versionName     : 1.2.0
app.json 宣言値 : 7
build.gradle    : (android/ なし)
appVersionSource: remote / autoIncrement(production): true
実際に入る値    : EAS サーバー管理(ローカルの値は初期化にのみ使われます)
  - 値はローカルファイルではなく EAS 側にあります。eas build:version:get で確認してください
==========
project         : c
versionName     : 1.3.0
app.json 宣言値 : 12
build.gradle    : 9
appVersionSource: local / autoIncrement(production): true
実際に入る値    : 9
  - android/ をリポジトリに置いているため、app.json ではなく build.gradle の値がビルドに入ります

c の構成が、私が踏んだものです。app.json には 12 と書いてあるのに、ビルドに入るのは 9 でした。一度 prebuild して android ディレクトリをリポジトリに置いたまま、その後の更新を app.json 側だけで続けていたためです。数字が並んで見えている限り、これは目視では見つけられません。

b の構成では、ローカルの 7 という値は初期値として使われるだけで、以後の実体は EAS 側にあります。ローカルのファイルをいくら読んでも次の番号は分かりませんので、eas build:version:get で問い合わせます。

番号の持ち主を、先に一つ決める

エラーが出てから慌てるのは、持ち主が決まっていないからです。EAS では eas.jsoncli.appVersionSource で、ローカルとリモートのどちらが版番号を管理するかを指定します。

観点local(設定ファイルが持つ)remote(EAS が持つ)
値の置き場所app.json / build.gradleEAS のサーバー側
次の番号の確認ファイルを開けば分かるeas build:version:get で問い合わせる
上げ忘れ起きうる(手で上げる運用なら特に)autoIncrement を有効にすれば起きにくい
複数の端末・CI から出す取り違えが起きやすい一箇所で連番が保たれる
手元だけで完結するかするしない(EAS への問い合わせが要る)

remote を選び、本番のビルドプロファイルで autoIncrement を有効にすると、eas.json は次のようになります。

{
  "cli": {
    "appVersionSource": "remote"
  },
  "build": {
    "production": {
      "autoIncrement": true
    }
  }
}

一点だけ補足します。remote にしても autoIncrement を有効にしなければ、番号は自動では上がりません。持ち主を移すことと、自動で増やすことは別の設定です。私は最初これを一つの機能だと思い込んでいて、remote にしたのに同じ番号でビルドが出てきて首をかしげました。上のスクリプトが autoIncrement を必ず出力しているのは、そのときの反省からです。

remote に切り替えた直後は、ローカルに書いてあった値が初期値として引き継がれます。ですので移行のタイミングで、Play 側の消費済み番号より小さい値がローカルに残っていないかだけは見ておくと安全です。

六本を並べて出すときに決めたこと

私は壁紙・癒し系のアプリを複数本、同じコードベースの構成違いとして運用しています。素材の追加をまとめて反映する日は、六本を同じ日に出すことになります。ここで番号の付け方が効いてきました。

最初は versionName から機械的に導く方式を試しました。1.4.2 なら 10402 というように、桁を割り当てて番号にします。人間が見て対応が分かるのは利点でしたが、同じ日に修正版をもう一度出すと桁が足りなくなります。末尾に一桁足すと、今度は過去の番号との大小関係を毎回頭で確かめることになりました。

いまは分けています。

  • versionNameexpo.version)は手で決める。読者に見せる番号なので、意味のある単位で上げる
  • versionCode は EAS の autoIncrement に任せる。単調増加でありさえすればよく、意味を持たせない

意味を持たせないと決めてから、リリース作業で番号のことを考える時間がなくなりました。六本それぞれが独立したカウンタを持ちますので、アプリ間で番号が揃わないことは受け入れています。揃っていて嬉しい場面が、実際には一度もなかったためです。

この判断は、個人開発で複数本を回している方には合うと思います。逆に、社内の別チームが番号を見て何かを判断しているような運用では、意味を持たせたまま local で管理する方が噛み合うはずです。

提出前に、三行だけ通す

個人開発では、リリース作業の待ち時間がそのまま自分の時間になります。いまは AAB を作る前に、次の三行を通しています。

node audit-version.mjs .                 # 実際に入る値の経路を確認
eas build:version:get --platform android # remote 管理なら現在値を問い合わせ
# Play Console > リリース > アプリバンドルエクスプローラ で消費済みの最大値を目視

一行目でローカルの構成、二行目でサーバー側の値、三行目で Play 側の実績を押さえます。三つが噛み合っていれば、アップロード画面で止まることはまずありません。所要は一分ほどです。

エラーが出てからやり直すと、ビルドの待ち時間がまるごと無駄になります。EAS のビルドは短くありませんので、この一分は十分に元が取れています。

この記事の次にやること

まず eas.json を開いて、cli.appVersionSource が書かれているかどうかだけ確かめてください。書かれていなければ、番号の持ち主が決まっていない状態です。remotelocal のどちらを選ぶかを決めて明示するところから始めると、次のリリースで同じ画面を見ずに済みます。

私自身、この一行を書き忘れたまま何本もアプリを出していました。同じところで時間を溶かす方が一人でも減れば嬉しく思います。お読みいただきありがとうございました。

シェア

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

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

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

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

開発ツール2026-08-21
生成ループが緑を返した後に、私が手元で回している4つの受け入れ検査
ビルドが成功しても出荷できない状態は実在します。AI がビルドを反復する構成で見落とされやすい4点を、成果物そのものを走査する検査スクリプトとして手元に置く方法をまとめました。
開発ツール2026-08-20
ベータ SDK でビルドしたアプリは、TestFlight には配れても審査には出せません
ベータ版の Xcode でビルドしたアプリは TestFlight には配布できますが、App Store の審査には提出できません。手元のビルドがどの SDK で作られたかを確認する方法と、eas.json で提出用ビルドを守る設定をまとめました。
開発ツール2026-08-19
Play Policy Insights を Expo プロジェクトに通すと、検出が0件になる条件が3つあります
Google Play が公式に配布し始めたポリシー審査スキルを、Expo 構成のプロジェクトで走らせた記録です。指すディレクトリひとつで検出が5カテゴリから0件に変わりました。走査が届く条件と、出力を鵜呑みにしてはいけない箇所を残します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →