OTAアップデートの自動化:Onespotがラップトップに触れることなく200以上のアプリにデプロイする方法
Users • Development • React Native • February 20, 2026 • 14分で読める
Sean Cann
ゲスト著者
ワンタップで200以上のReact NativeアプリにOTAアップデートをデプロイ。OnespotがExpoのOTA Updates、GitHub Actions、そしてスマートフォンを使ってマルチアプリ公開を自動化した方法をご紹介します。
これは、学校向けのカスタムブランドモバイルアプリを構築するプラットフォーム「Onespot」の共同創設者兼CEOであるSean Cannによるゲスト投稿です。
昨日、私はスマートフォンでボタンを1回タップするだけで、200以上のWebおよびモバイルアプリにアップデートをデプロイしました。3年前なら、この文章を聞いて笑っていたでしょう。
当時は、今日でも多くのExpo開発者がやっているのと同じことをしていました。Over-the-air(OTA)アップデートを公開するために、ラップトップでターミナルを開き、ローカルの適切なリポジトリに移動し、expo publish(現在はeas update)を実行し、数分間進行状況を監視しながら、ローカルリポジトリに設定ミスがないことを祈っていました。そして、アップデートが必要なすべてのアプリに対してこれを繰り返していました。
Over-the-airパブリッシングは、8年前に純粋なReact NativeからExpoに切り替える重要な要因でした。当時ExpoはSDK 20で、「Expoスキル」とはStack Overflowのタブをたくさん開くことを意味していました。しかし、これらのアップデートの自動化の真の力を解き放ったのは、この1年のことでした。
今日では、iPhoneで私たちのモバイルアプリのいずれかを開き、秘密の管理ダッシュボードに移動し、アプリ自体からOTAアップデートを公開(さらにはアプリのビルドと提出も)しています。
この投稿では、そのアプローチ、それを実現する具体的なファイル、そして大規模にアップデートをプッシュする際に重要なガードレールについて説明します(そして、私はemダッシュを使います—LLMに奪われるわけにはいきません!)。
課題:「ホワイトラベル」スケールでのデプロイ
Onespotは学校向けのカスタムブランドモバイルアプリを構築しています。各アプリは、コミュニケーション、請求、フォーム、グループチャットなどを含むノーコードのオールインワンプラットフォームです。
そのため、各顧客はiOSとAndroidアプリストアに独自のスタンドアロンアプリを持ちます—独自のアプリアイコン、アプリ名、スプラッシュスクリーン、バンドル識別子、アプリストアの説明など。
内部的には、すべてのアプリが単一のReact Native + Expoコードベースと単一のFirebaseバックエンドを共有しています。このアーキテクチャは反復速度において非常に強力です—バグを一度修正すれば、すべての場所で修正されます。
しかし、これは新しいデプロイメント問題も生み出します:数百のアプリに効率的にアップデートを配信するにはどうすればよいか?
自動化を構築する前は、各アプリに変更をデプロイするのは次のような流れでした:
- そのアプリをターゲットにするためにアプリの設定ファイル(バンドルID、slug、認証情報など)を更新
- すべてが正しく設定されていることを確認するためにアプリをローカルで実行
- (個人のラップトップで)ターミナルを開き、Expoのpublish/build/submitコマンドを実行
- アップデートの完了を待機
- ストアビルドの場合は、適切なアプリストアにアップロードしてレビューに提出
- 次のアプリで繰り返し...
アプリごとに設定の処理とビルド/パブリッシュの開始に2〜3分かかるとしても、200のアプリに簡単なアップデートをロールアウトするのに7〜10時間かかります。
明らかに、これは小さなチーム(または個人開発者)にとってスケールしません。すべてのアプリに修正を配信することを、1つのアプリに配信するのと同じくらい簡単に感じられる方法が必要でした。
重要なアイデア:「どのアプリをデプロイするか?」はデータの問題
私たちのソリューションの基盤は、「どのアプリをデプロイするか?」を手動ステップとして扱うのをやめ、代わりにデータに変換することでした。
システム内のすべてのアプリを定義する単一のJSONアプリレジストリ—創造性に欠けるネーミングでapps.json—を作成しました。このレジストリは、アプリビルド間で異なるすべてのもの(名前、slug、バンドル識別子、EASプロジェクトID、ストアID、バックエンド識別子、バージョン番号など)の信頼できる情報源です。
最終的なapps.jsonは次のようになります:
{
"montessori_apps": {
"amare": {
"name": "Amare",
"slug": "amaremontessori",
"bundlePackageID": "com.seabirdapps.amaremontessori",
"easProjectID": "9c3a7f8e-2b41-4d9e-a6c5-1234abcd9876",
"databaseAppID": "MLvbKPmILkLvp8Cq1234",
"appleAppID": "1234567890",
"version": "20.0.0",
"androidBuild": 2,
"iosBuild": 3
},
"appleseed": {
"name": "Appleseed",
"slug": "appleseedmontessori",
...
},
...
}
}
設定生成の自動化
レジストリが整備されたところで、アプリID(またはIDのバッチ)を受け取り、そのアプリに必要なすべての設定ファイルを生成するPythonスクリプト(もちろんonescript.pyと名付けました)をOnespot用に書きました。
私たちの設定では、以下を生成します:
standalone/config.js:app.config.jsによって使用される設定モジュール
eas.json:CIが適切なメタデータでビルド/提出できるように生成
google-services.json:正しいAndroidの詳細で更新
standalone/appImages.js:正しいアイコン/スプラッシュアセットを指す
.easignore:他のすべてのアプリアセットフォルダを除外してEASアップロードを高速に保つ
最後の項目は、1つのリポジトリに数百のアプリ分のアセットがある場合に重要です—無視戦略がなければ、単一のビルド/アップデートが非常に遅くなる可能性があります。.easignoreの使用方法について詳しくはこちらをご覧ください。単一アプリのデプロイメントにも役立ちます。
アプリ設定の構造化
Expo設定は単なるコードであるため、システム全体が機能します。私たちのapp.config.jsは、選択されたアプリの生成された設定を読み取り、それを標準のExpo設定フィールドにマッピングします。アプリ設定の使用について詳しくはこちらをご覧ください。
私たちのapp.config.jsは次のようになります:
import { standaloneConfig } from "./standalone/config";
const PUBLISHED_VERSION = 692;
export default ({ config }) => ({
...config,
name: standaloneConfig.name,
version: standaloneConfig.version,
slug: standaloneConfig.slug,
scheme: standaloneConfig.scheme,
ios: {
...config.ios,
bundleIdentifier: standaloneConfig.bundlePackageID,
buildNumber: `${standaloneConfig.iosBuild}`
},
android: {
...config.android,
package: standaloneConfig.bundlePackageID
},
updates: {
url: `https://u.expo.dev/${standaloneConfig.easProjectID}`
},
runtimeVersion: {
policy: "sdkVersion"
},
extra: {
publishedVersion: `${PUBLISHED_VERSION}`,
databaseAppID: standaloneConfig.databaseAppID,
eas: {
projectId: standaloneConfig.easProjectID
},
},
});
この時点で、「アプリの切り替え」は単に「standalone/config.jsへの更新」になります。
Pythonでの公開
最後のステップは、設定されたばかりのアプリを実際に公開、ビルド、または提出することです。幸い、EASはワンラインのターミナルコマンドでこれを簡単にします。
信頼できるonescript.pyファイルでは、アプリの公開は次のようになります:
def publish_app(app_id):
app = all_apps[app_id]
write_all_files(app)
os.system("npx eas-cli update --branch=main --auto")
アプリのビルドまたは提出はほぼ同じで、最も重要な違いは最後の行です:
- ビルド:
os.system(f"npx eas-cli build --platform {platform} --no-wait{non_interactivity_flag_if_ci}")
- ビルド + 提出:
os.system(f"npx eas-cli build --platform {platform} --auto-submit --no-wait{non_interactivity_flag_if_ci}")
同じスクリプトでWebビルドも公開しています(私たちの場合、expo export --platform webとホスティングデプロイ)。ExpoアプリをWebサイトとして公開する方法について詳しくはこちらをご覧ください。
ラップトップからのデプロイメントの移行
設定生成とバッチアップデートを自動化した後、別のボトルネックに直面しました:開発者のローカルマシンでこれらのデプロイメントを実行することです。
最初は、MacBookでonescript.pyを実行していました。これには明らかな問題があります:ファイルを書き換えている間に作業ディレクトリがロックされる、実行中の失敗やローカル環境の癖に対して脆弱、機密認証情報をマシンに保存する必要がある、などです。
解決策—つまり、この記事をまだ読んでいる人が最も興味を持っているであろう部分—は、すべてのデプロイメントをCIで実行することです。私たちはGitHub Actionsを使用していますが、Expo & React Native専用に設計されたExpoの新しいCI/CD「EAS Workflows」も使用できます。
私たちの設定は、repository dispatchによってトリガーされるGitHub Actionsワークフロー(適切にonescript.ymlと名付けられたファイル)で、onescript.py内の任意の関数をリモートで実行できます。
この構造は最大限の柔軟性も提供します—簡単なPython関数を書いてスクリプトに追加することで、任意のものをトリガーできるようになりました。
以下は.github/workflows/onescript.ymlの簡略版です:
name: API - Triggered Onescript Command
on:
repository_dispatch:
types: [onescript-command]
jobs:
onescript:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Parse command
run: |
COMMAND="${{ github.event.client_payload.command || '' }}"
echo "PARSED_COMMAND=$COMMAND" >> $GITHUB_ENV
- name: Clear build cache
run: |
rm -rf web-build/
rm -rf .expo/
- name: Execute onescript command
run: |
echo "Executing: python3 onescript.py ${{ env.PARSED_COMMAND }}"
python3 onescript.py ${{ env.PARSED_COMMAND }}
env:
CI: true
- name: Cleanup
if: always()
run: rm -f
スマートフォンからのデプロイメントのトリガー
この時点で、GitHub上のリポジトリ内で数回クリックするだけでデプロイメントをトリガーできます。しかし、なぜそこで止まるのでしょうか?
Onespot API内に認証された/trigger-onescriptエンドポイント(GitHub REST APIを使用)を追加することで、どこからでもそのワークフローを開始できます。
以下は私たちのシンプルなfetchコマンドです。commandはpublish amare、submit amare、publish all_apps、またはスクリプトに処理させたいその他のもの(サーバーサイドで安全に検証)です:
fetch("https://api.github.com/repos/<org>/<repo>/dispatches", {
method: "POST",
headers: {
Authorization: `token ${GITHUB_ACCESS_TOKEN}`,
Accept: "application/vnd.github.v3+json",
"Content-Type": "application/json"
},
body: JSON.stringify({
event_type: "onescript-command",
client_payload: { command }
})
});
APIトリガーができると、エキサイティングな可能性が開かれました:開発者以外を含むチームの誰もが、私の助けを必要とせずにアプリのアップデート、ビルド、アプリストア提出をデプロイできるようになりました。
必要なのは、他のエンドポイントを呼び出すのと同じ方法で、アプリからその新しいAPIエンドポイントを呼び出すことだけでした。
そして、最も重要なステップに進むことができました:チームメンバー全員がプロのハッカーのように感じられるよう、アプリ内に超クールでトップシークレットなダッシュボードをデザインすることに時間をかけすぎること...
次はどこへ向かうか?
デプロイメントが単なるAPIトリガーアクションになると、改善の表面積が非常に大きく、非常に迅速に拡大します。考慮すべきいくつかのアイデア:
apps.jsonをソース管理から外してデータベースに移動する。 これは明らかな次のステップのようです。現在、アプリの追加や更新には、このデータが実際には運用データであってソースコードではないにもかかわらず、コード変更とコミットが必要です。アプリレジストリをデータベースに移動することで、適切な検証、監査ログ、権限を持つスタンドアロンアプリデータを動的に作成、更新、または無効化できるようになります。CIは実行中にレジストリを取得できるため、まったく新しいアプリを立ち上げることがgitワークフローではなくデータ入力操作になります。
AIエージェント(例:CursorのAPI)をアプリとデプロイメントプロセスに直接リンクする。 私たちはすでにLinearとSlackを通じてCursorエージェントを使用しているため、チームの誰もが変更を説明してコードを生成させることができます。しかし、この概念をさらに発展させることができます。いくつかの変更により、チームメイトが変更を説明し...