開発用の API を向いたまま、本番用のビルドを作ってしまった——その可能性に気づくきっかけは、アプリの挙動ではなく、ビルドログのたった1行でした。
> Task :app:createBundleReleaseJsAndAssets UP-TO-DATE
環境変数の EXPO_PUBLIC_API_URL を本番の値に変え、Android のリリースビルドをやり直したとします。ビルドは「BUILD SUCCESSFUL」で終わります。ところが、できたアプリの中身は、前回の開発用の値のままかもしれません。Expo の GitHub に、まさにこの症状の報告が上がっておりました。
最初にお伝えしたいのは、結論です。値だけを変えたビルドは、Gradle から見ると「何も変わっていないビルド」で、JavaScript のバンドルを作り直さずに飛ばすことがあります。 以下、確認できたことと、私が手元で再現した範囲を分けて書き残します。
症状の見分け方
報告されている条件は、はっきりしています。
| 確かめること | この結果なら |
|---|---|
| 変えたのは環境変数や .env の値だけか(JavaScript のソースは触っていない) | 該当します。ソースが同じだと、Gradle は入力が変わっていないと判断します |
| ビルドログに `createBundleReleaseJsAndAssets UP-TO-DATE` が出ているか | 出ているなら、バンドルは作り直されていません |
| 同じ `android` フォルダで、続けてビルドしているか | 前回の出力が残っているときに起きます。真っさらな環境では起きにくいはずですが、報告では確認されていません |
| 値を `EXPO_PUBLIC_` で始めているか | この接頭辞の値は、ビルド時にバンドルへ埋め込まれます。アプリを実行するときに読まれるのではありません |
報告は expo/expo の Issue 49630 です。2026年9月1日に立ち、SDK 57・React Native 0.86・macOS・素の Android プロジェクト(bare workflow)で確認されています。同じ Issue には Expo 側の自動検証も付いており、「再現した」と書かれています。さらに、シェルの環境変数だけでなく、.env ファイルを書き換えた場合も同じだったそうです。
なぜ飛ばされるのか
EXPO_PUBLIC_ で始まる値は、実行時ではなく、バンドルを作るときに文字列として埋め込まれます。つまり、値が変われば、バンドルの中身も変わるはずです。
ところが Gradle は、タスクに「入力」として教えられたものしか見ていません。バンドルを作るタスクの入力は、JavaScript のソースとバンドルの設定です。バンドラが読みに行く環境変数は、その一覧に入っていません。入力も出力も前回と同じに見えるので、Gradle は「やる必要はありません」と判断し、UP-TO-DATE と表示して終わります。
要するに、バンドラの側は環境変数に依存しているのに、Gradle の側はそれを知らない——この食い違いが原因です。
最小のかたちで再現してみる
報告を読むだけでは、手応えがありません。そこで、Android も Expo も使わず、Gradle だけで同じ食い違いを作ってみました。バンドルを作るタスクを真似て、入力は src フォルダだけ、出力は1ファイル、中身は環境変数から埋め込みます。
tasks.register('createBundle') {
inputs.dir('src') // 入力: ソースだけ
outputs.file(layout.buildDirectory.file('index.bundle'))
doLast {
def out = layout.buildDirectory.file('index.bundle').get().asFile
out.parentFile.mkdirs()
// バンドラが環境変数を埋め込む動作の代わり
out.text = "var API = '" + (System.getenv('EXPO_PUBLIC_API_URL') ?: '') + "';\n"
}
}値を変えて2回回した結果は、次のとおりです(Gradle 8.14.3)。
# 1回目: EXPO_PUBLIC_API_URL=https://dev.example.invalid
> Task :createBundle
var API = 'https://dev.example.invalid';
# 2回目: EXPO_PUBLIC_API_URL=https://prod.example.invalid
> Task :createBundle UP-TO-DATE
var API = 'https://dev.example.invalid';
2回目は UP-TO-DATE になり、出力は開発用の値のままでした。ビルドは成功扱いなので、ログを流し読みしていると、まず気づけません。
直し方:環境変数を「入力」として宣言する
報告者が示している対処は、EXPO_PUBLIC_ で始まる環境変数を、バンドルのタスクの入力として Gradle に教える方法です。私の最小の例にも、同じ考え方で1行足してみました。
def pub = providers.environmentVariablesPrefixedBy('EXPO_PUBLIC_')
tasks.named('createBundle') { inputs.property('expoPublicEnv', pub) }宣言を足したうえで、同じ順に3回回しました。
# 1回目: dev の値
> Task :createBundle → https://dev.example.invalid
# 2回目: prod の値
> Task :createBundle → https://prod.example.invalid
# 3回目: prod の値のまま
> Task :createBundle UP-TO-DATE
値が変わった2回目は再実行され、変わらない3回目だけが飛ばされました。これなら、速さを保ったまま、値の取り違えも防げます。
ここで、確認していないことを分けておきます。上の動きは、あくまで私が作った最小の模型の結果です。実際の Expo のプロジェクトでは、報告された書き方(createBundle で始まり JsAndAssets で終わるタスクに対して入力を足す)を使います。私は手元の Android 実機とプロジェクトでは試していません。
直るまでの逃げ道
Expo 側では、この入力を自動で宣言する修正が、Pull Request 49839 として出されています。私が確認した10月27日の時点では、まだ取り込まれていませんでした。直るまでの間は、次のどれかで凌ぐことになります。
- 値を変えたビルドでは、
--rerun-tasksを付けて、すべてのタスクを強制的に実行し直します。確実ですが、ビルド全体が遅くなります。 - 上の宣言を、Android 側のビルド設定に書き足します。ただし、
androidフォルダを毎回作り直す運用の方は、書き足した内容が消えます。 - 本番用のビルドは、前回の出力が残っていない環境(きれいに取得し直した状態)で作ります。
EAS のクラウドビルドでも起きるのかどうかは、報告でも「未確認」の扱いでした。私も確かめておりませんので、断定はいたしません。
本番に出す前の、ひと手間
結局のところ、いちばん確実なのは「できあがったものの中身を見る」ことだと思います。ビルドが成功した事実と、狙った値が入っている事実は、別のものです。リリース用のファイルができたら、開発用の接続先の文字列が入っていないかを、公開の前に1度だけ探してみてください。
成功の表示は、中身の保証ではありません。 この線引きを忘れかけた日は、ログの UP-TO-DATE を探すところから確かめ直すようにしています。
なお、デプロイのあとに古い状態が残り続けるという点では、Web 側にも似た落とし穴があります。そちらは デプロイ直後に出る Application error は、なぜ再デプロイでは直らないのか で、原因の側から掘り下げております。
次にビルドの接続先を切り替えるときは、まずビルドログを開いて、createBundle の行が UP-TO-DATE になっていないかだけ、確かめていただければと思います。