OpenAIExpoFeb 20, 2026, 2:15 PM

Automating OTA Updates: How Onespot deploys to 200+ apps without touching a laptop

A condensed section focused on the key takeaways first.

Original Post

Quick Digest

Summary

A condensed section focused on the key takeaways first.

openaienmodel: gpt-5-mini-2025-08-07

Automating OTA Updates: Onespot's one-tap deploys to 200+ apps

Key Points

  • Publish 200+ apps with one tap
  • apps.json centralizes per-app metadata
  • CI + GitHub dispatch enables phone-triggered deploys

Summary

Onespot automated over-the-air (OTA) publishing for 200+ Expo/React Native white‑label apps by treating “which app to deploy” as data and moving deployment logic into CI. The solution uses a single apps.json registry, a generator script (onescript.py) that emits per-app Expo/EAS config, and a GitHub Actions workflow triggered via repository_dispatch. A small authenticated API endpoint lets team members (or the app itself) trigger builds and OTA publishes from a phone—no laptop needed.

Key Points

  • Centralize per-app metadata: maintain a single apps.json registry with names, slugs, bundle IDs, eas project IDs, store IDs, version/build numbers, etc.
  • Generate config files: onescript.py consumes apps.json and writes standalone/config.js, eas.json, google-services.json, standalone/appImages.js, and .easignore to scope assets per app.
  • Map generated config into Expo: app.config.js imports the generated config and sets name/slug/ios.bundleIdentifier/android.package/updates.url/runtimeVersion/extra.publishedVersion.
  • Publish/build commands: use npx eas-cli update --branch=main --auto for OTA; npx eas-cli build/--auto-submit for builds and submissions (use --no-wait and CI non-interactive flags as appropriate).
  • CI-first execution: run the script in GitHub Actions (repository_dispatch trigger) to avoid local machine state, manage credentials centrally, and ensure reproducible runs.
  • Remote triggers: expose a secured /trigger-onescript server endpoint that calls GitHub’s dispatches API so the mobile admin dashboard can start publish/build/submit operations.
  • Performance & guardrails: use .easignore to exclude other app assets, clear caches between runs, and securely provision/remove credential files in CI.

Practical implementation notes

  • Files to wire up: apps.json, onescript.py, standalone/config.js (generated), app.config.js (reads generated config), .github/workflows/onescript.yml (repository_dispatch consumer).
  • Keep OTA-specific state (publishedVersion/runtimeVersion) in sync with your runtime logic and increment publishedVersion on each OTA publish.
  • Prefer moving apps.json out of git into a managed datastore in future to allow dynamic app creation, audit logs, and RBAC without commits.

Benefits

  • Reduce per-app deployment time from minutes (per app) to single API call for many apps.
  • Enable non-developers to trigger deploys safely via UI.
  • Make deployments reproducible and resilient by running in CI with controlled credentials.

Risks & next steps

  • Protect GitHub access tokens and CI credentials; rotate and scope them narrowly.
  • Consider moving apps.json to a database and adding validation/audit trails.
  • Optionally integrate AI agents or tooling to automate config edits and onboarding of new white‑label apps.

Full Translation

Translations

A translation section that keeps the flow of the original article.

openaijamodel: gpt-5-mini-2025-08-07

OTAアップデートの自動化:Onespotがノートパソコンに触れずに200以上のアプリをデプロイする方法

概要

Deploy OTA updates to 200+ React Native apps with one tap. という体験を、Onespot がどのように実現したかを解説します。この記事は Onespot の共同創業者兼CEO、Sean Cann によるゲスト投稿で、Expo の OTA Updates、GitHub Actions、そしてスマートフォンからのトリガーでマルチアプリ公開を自動化した具体的なアプローチを紹介します。


昨日、私は iPhone でボタンを1回タップして200以上のウェブとモバイルアプリにアップデートをデプロイしました。3年前なら笑っていたでしょう。以前はほとんどの Expo 開発者と同じように、ローカルでリポジトリに移動して、expo publish(現在は eas update)を実行し、数分間進行を監視していました。これをアプリごとに繰り返すのは非現実的です。

オンスポット(Onespot)は学校向けのカスタムブランドモバイルアプリを構築するプラットフォームで、各顧客に対してストアに載る独立したアプリ(アイコン、名前、スプラッシュ、bundle identifier、ストア説明など)を提供します。内部的には単一の React Native + Expo コードベースと単一の Firebase バックエンドを共有します。この設計はイテレーション速度に強力ですが、何百ものアプリへ効率よく配信するという新たなデプロイ課題を生みます。

課題:ホワイトラベル規模でのデプロイ

以前のワークフローは以下の通りでした:

  • アプリ固有の設定(bundle IDs, slug, credentials など)を更新
  • ローカルで動作確認
  • ターミナル(個人のラップトップ)で Expo の publish/build/submit を実行
  • アップデートの完了を待機
  • ストアビルドなら適切なストアにアップロードして審査申請
  • 次のアプリへ繰り返す

1アプリあたり2〜3分かかると、200アプリの更新に7〜10時間かかる計算です。小規模チームや個人開発者にはスケールしません。そこで、全アプリに対して1つのアプリにデプロイするのと同じ簡単さで修正を届けられる仕組みが必要になりました。

キーアイデア:「どのアプリをデプロイするか」はデータの問題にする

私たちの解決の基盤は、"どのアプリをデプロイするか" を手作業として扱うのをやめ、それをデータに変えることでした。apps.json という単一の JSON レジストリを作り、システム内のすべてのアプリを定義しました。これがビルドごとに変わる情報(名前、slug、bundle identifiers、EAS project IDs、ストア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",
      ...
    },
    ...
  }
}

設定ファイルの自動生成

このレジストリを元に、onescript.py という Python スクリプトを作成しました。apps.json の app ID(またはバッチ)を受け取り、そのアプリに必要なすべての設定ファイルを生成します。生成されるファイル例:

  • standalone/config.js:app.config.js が読み込む設定モジュール
  • eas.json:CI が適切なメタデータで build/submit できるように生成
  • google-services.json:Android 用に更新
  • standalone/appImages.js:正しいアイコン/スプラッシュ資産を指す
  • .easignore:他アプリの資産フォルダを除外して EAS のアップロードを高速化

特に .easignore は重要です。多数のアプリ資産が単一リポジトリにある場合、除外がないとビルド/アップデートが極端に遅くなります。.easignore の使い方については公式ドキュメントを参照してください。

アプリ設定の構造

Expo の設定がコードであることが、この仕組みを可能にしています。私たちの app.config.js は生成された standalone の設定を読み込み、標準の Expo フィールドにマッピングします。参考として私たちの app.config.js の例は次の通りです:

import { standaloneConfig } from "./standalone/config";
// Incremented each time we publish an OTA update
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 での公開(publish)

最終ステップは、そのアプリを実際に publish、build、submit することです。EAS はワンライナーで簡単にしてくれます。onescript.py 内での公開は次のようになります:

def publish_app(app_id):
    app = all_apps[app_id]  # from apps.json
    write_all_files(app)   # generates all the config files
    os.system("npx eas-cli update --branch=main --auto")

ビルドやサブミットはほぼ同様ですが、最後のコマンドが異なります:

  • Build:

    os.system(f"npx eas-cli build --platform {platform} --no-wait{non_interactivity_flag_if_ci}")

  • Build + submit:

    os.system(f"npx eas-cli build --platform {platform} --auto-submit --no-wait{non_interactivity_flag_if_ci}")

また同じスクリプトで web ビルド(expo export --platform web とホスティングへのデプロイ)も行っています。Expo をウェブとして公開する方法の詳細は公式ドキュメントを参照してください。

デプロイをローカルからCIへ移動

設定生成とバッチ更新を自動化した後、次のボトルネックは「デプロイを開発者のローカル環境で実行すること」でした。ローカルでスクリプトを実行すると、作業ディレクトリが書き換えられてロックされたり、中断時に脆弱になったり、機密資格情報がローカルに残るリスクがあります。

解決策は CI 上で実行することです。我々は GitHub Actions を使っています(EAS Workflows という Expo 向けの CI/CD を使う選択肢もあります)。構成は repository_dispatch トリガーで起動される .github/workflows/onescript.yml です。これにより onescript.py の任意の関数をリモートで実行できます。簡略化した workflow の例:

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
      ( ... set up Node, Python, EAS, App Store & Google Play credentials for submissions, and other dependencies ... )
      - 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
      ( ... credentials )
      - name: Cleanup
        if: always()
        run: rm -f ( ... credentials files )

この構造により、onescript.py に関数を追加するだけで CI から任意の操作を呼び出せるようになりました。

スマホからのデプロイトリガー

GitHub リポジトリ内でクリックしてトリガーすることもできますが、さらに進めて社内 API に /trigger-onescript という認証付きエンドポイントを追加し、GitHub の REST API を呼ぶことでどこからでもワークフローを起動できるようにしました。サーバー側で安全に検証したうえで、command(例: "publish amare", "submit amare", "publish all_apps")を送ります。fetch の例:

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 トリガーで実行できるようになると、改善の余地は一気に広がります。いくつか考えているアイデア:

  • apps.json をソース管理から外してデータベースに移す
    • これによりアプリの追加や更新がコードコミットではなく、動的なデータ操作で可能になります。バリデーション、監査ログ、権限管理も整えられます。
  • AI エージェント(例: Cursor の API)をデプロイプロセスやアプリに直接連携
    • チームが自然言語で変更を記述すると、コード生成や設定更新、デプロイ操作までつなげられる可能性があります。

この投稿では、Onespot がどのようにして複数アプリの OTA 配信を自動化し、最終的にスマートフォンのワンクリックで200以上のアプリにデプロイできるようにしたかを説明しました。興味があれば、apps.json の管理方法や CI セットアップの詳細、onescript.py の実装のベストプラクティスについてさらに深掘りします。