sue@blog ~ /posts/eas-multi-project-deployment
$ cd ../
$ cat post.metadata

1つのExpoコードベースから複数アプリをEASでデプロイする設計パターン

date: 2026-03-12 Expo EAS React Native Monorepo
React Native(Expo)で開発したアプリを、ブランド違いや用途違いで複数のアプリとしてストアに公開するための設計パターン。app.config.tsによる動的設定切り替えとCI/CD対応を解説。

はじめに

React Native(Expo)で開発したアプリを、ブランド違いや用途違いで複数のアプリとしてストアに公開したいケースがあります。

こうしたとき「リポジトリをフォークして別管理」にすると、共通コードの同期が地獄になります。この記事では、1つのコードベースを維持したまま、EAS Build / Submit でプロジェクトごとに異なる設定でビルド・デプロイする設計パターンを紹介します。

前提環境

課題の整理

EASの制約

EAS Build には以下の制約があります。

  1. eas.json はプロジェクトルート固定 — パスの指定ができない
  2. app.json(または app.config.ts)もルート固定 — Expo CLIが読み取る
  3. extra.eas.projectId でExpoダッシュボード上のプロジェクトが決まる

つまり、プロジェクトごとに app.jsoneas.json の中身を切り替える仕組みが必要です。

プロジェクト間で「異なるもの」と「共通のもの」

設計の出発点として、差分を明確にします。

プロジェクトごとに異なる

ファイル差分内容
app.jsonname, slug, bundleIdentifier, package, projectId, scheme, icon, splash
eas.jsonsubmit設定(Apple ID, ASC App ID, Team ID, Google Play設定)
assets/アイコン・スプラッシュ画像
.env認証情報、API URL、Firebase設定

プロジェクト間で共通

ファイル内容
src/全画面・コンポーネント・hooks・サービス
metro.config.jsMetro Bundler設定
babel.config.jsBabel設定
tailwind.config.jsNativeWindテーマ
package.json依存関係
tsconfig.jsonTypeScript設定

設計

ディレクトリ構成

packages/mobile/ ├── projects/ # プロジェクト別設定を集約 │ ├── app-alpha/ │ │ ├── app.json # Alpha用のExpo設定 │ │ ├── eas.json # Alpha用のEAS設定 │ │ ├── .env # Alpha用の環境変数 │ │ └── assets/ │ │ └── images/ │ │ ├── icon.png │ │ ├── splash.png │ │ └── adaptive-icon.png │ └── app-beta/ │ ├── app.json │ ├── eas.json │ ├── .env │ └── assets/ │ └── images/ │ ├── icon.png │ ├── splash.png │ └── adaptive-icon.png │ ├── app.config.ts # 動的にプロジェクト設定を読み込む ├── scripts/ │ └── eas-deploy.sh # マルチプロジェクトデプロイスクリプト │ ├── src/ # 共通ソースコード(変更なし) ├── metro.config.js # 共通(変更なし) ├── babel.config.js # 共通(変更なし) ├── tailwind.config.js # 共通(変更なし) ├── package.json # 共通(変更なし) └── tsconfig.json # 共通(変更なし)

ポイントは projects/ ディレクトリにプロジェクトごとの設定を閉じ込め、共通コードには一切手を加えないことです。

app.config.ts — 動的設定の読み込み

app.json を直接使う代わりに app.config.ts を作成し、環境変数 APP_PROJECT でプロジェクト設定を切り替えます。

app.config.ts
import type { ExpoConfig } from "expo/config";
import fs from "node:fs";
import path from "node:path";

const APP_PROJECT = process.env.APP_PROJECT;

if (!APP_PROJECT) {
  throw new Error(
    "APP_PROJECT environment variable is required.\n" +
    "Usage: APP_PROJECT=app-alpha npx expo start"
  );
}

const projectDir = path.resolve(__dirname, "projects", APP_PROJECT);

if (!fs.existsSync(projectDir)) {
  const available = fs.readdirSync(
    path.resolve(__dirname, "projects")
  );
  throw new Error(
    `Project "${APP_PROJECT}" not found.\n` +
    `Available projects: ${available.join(", ")}`
  );
}

// プロジェクト固有の app.json を読み込む
const projectAppJson = JSON.parse(
  fs.readFileSync(path.join(projectDir, "app.json"), "utf-8")
);

// アセットパスをプロジェクトディレクトリからの相対パスに変換
const assetDir = `./projects/${APP_PROJECT}/assets`;

const config: ExpoConfig = {
  ...projectAppJson.expo,
  icon: `${assetDir}/images/icon.png`,
  splash: {
    ...projectAppJson.expo.splash,
    image: `${assetDir}/images/splash.png`,
  },
  android: {
    ...projectAppJson.expo.android,
    adaptiveIcon: {
      ...projectAppJson.expo.android?.adaptiveIcon,
      foregroundImage: `${assetDir}/images/adaptive-icon.png`,
    },
  },
};

export default (): ExpoConfig => config;

この設計の良いところは、各プロジェクトの app.json は標準的なExpoフォーマットをそのまま使える点です。特殊な設定ファイルフォーマットを覚える必要がありません。

eas.json の切り替え

EAS CLIは eas.json のパスを変更できないため、ビルド前にプロジェクト固有の eas.json をルートにコピーします。これはデプロイスクリプトで自動化します。

デプロイスクリプト

scripts/eas-deploy.sh
#!/bin/bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
MOBILE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
PROJECTS_DIR="$MOBILE_DIR/projects"

# 使い方を表示
usage() {
  cat <<EOF
Usage:
  $(basename "$0") --all [eas-build-options...]
  $(basename "$0") --projects alpha,beta [eas-build-options...]
  $(basename "$0") --project alpha [eas-build-options...]

Examples:
  $(basename "$0") --all --profile production --platform ios
  $(basename "$0") --projects alpha,beta --profile preview --platform all
  $(basename "$0") --project alpha --profile development --platform ios
EOF
  exit 1
}

# 単一プロジェクトをビルド
build_project() {
  local project="$1"
  shift
  local eas_args=("$@")
  local project_dir="$PROJECTS_DIR/$project"

  if [ ! -d "$project_dir" ]; then
    echo "Error: Project '$project' not found"
    exit 1
  fi

  # eas.json をプロジェクト固有のものに差し替え
  if [ -f "$project_dir/eas.json" ]; then
    cp "$project_dir/eas.json" "$MOBILE_DIR/eas.json"
  fi

  # APP_PROJECT を設定してEASビルドを実行
  cd "$MOBILE_DIR"
  APP_PROJECT="$project" npx eas build "${eas_args[@]}"
}

# 元の eas.json をバックアップ & 復元
trap cleanup EXIT

# 各プロジェクトをビルド
for project in "${PROJECTS[@]}"; do
  build_project "$project" "${EAS_ARGS[@]}"
done

プロジェクト固有の app.json の例

projects/app-alpha/app.json
{
  "expo": {
    "name": "My App Alpha",
    "slug": "my-app-alpha",
    "version": "1.0.0",
    "ios": {
      "bundleIdentifier": "com.example.alpha",
      "newArchEnabled": true
    },
    "android": {
      "package": "com.example.alpha",
      "newArchEnabled": true
    },
    "scheme": "myapp-alpha",
    "extra": {
      "eas": {
        "projectId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
      }
    }
  }
}

プロジェクト固有の eas.json の例

projects/app-alpha/eas.json
{
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "env": { "APP_ENV": "development" }
    },
    "production": {
      "env": { "APP_ENV": "production" }
    }
  },
  "submit": {
    "production": {
      "ios": {
        "appleId": "dev@example.com",
        "ascAppId": "1234567890",
        "appleTeamId": "ABCDE12345"
      }
    }
  }
}

使い方

日常の開発

terminal
# 特定プロジェクトで開発サーバーを起動
APP_PROJECT=app-alpha npx expo start

ビルド

terminal
# 単一プロジェクト
./scripts/eas-deploy.sh --project app-alpha --profile production --platform ios

# 複数プロジェクトを指定
./scripts/eas-deploy.sh --projects app-alpha,app-beta --profile production --platform ios

# 全プロジェクトに一斉リリース
./scripts/eas-deploy.sh --all --profile production --platform ios

package.json にスクリプトを追加

package.json
{
  "scripts": {
    "build:alpha:dev": "APP_PROJECT=app-alpha npx eas build --profile development --platform ios",
    "build:alpha:prod": "APP_PROJECT=app-alpha npx eas build --profile production --platform all",
    "build:beta:prod": "APP_PROJECT=app-beta npx eas build --profile production --platform all",
    "build:all:prod": "./scripts/eas-deploy.sh --all --profile production --platform all",
    "start:alpha": "APP_PROJECT=app-alpha npx expo start",
    "start:beta": "APP_PROJECT=app-beta npx expo start"
  }
}

新しいプロジェクトの追加手順

  1. Expoダッシュボードでプロジェクト作成npx eas init で新しいプロジェクトを作成し、projectIdを控える
  2. projects/ にディレクトリを作成mkdir -p projects/app-gamma/assets/images
  3. app.json, eas.json, .env を配置 — 既存プロジェクトからコピーして name, slug, bundleIdentifier, projectId 等を変更
  4. アセットを配置 — アイコンとスプラッシュ画像を配置
  5. Apple Developer / Google Play Console で準備 — 新しい bundleIdentifier の登録
  6. ビルド./scripts/eas-deploy.sh --project app-gamma --profile development --platform ios

ios / android ネイティブプロジェクトの扱い

EAS Buildでは npx expo prebuild が毎回実行され、app.config.ts の設定に基づいてネイティブプロジェクトが生成されます。そのため:

CI/CD(GitHub Actions)での運用

.github/workflows/eas-build.yml
name: EAS Build

on:
  workflow_dispatch:
    inputs:
      projects:
        description: 'Projects to build (comma-separated, or "all")'
        default: 'all'
      profile:
        description: 'Build profile'
        type: choice
        options: [development, preview, production]
      platform:
        description: 'Platform'
        type: choice
        options: [ios, android, all]

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        project: ${{ fromJson(...) }}
    steps:
      - uses: actions/checkout@v4
      - uses: oven-sh/setup-bun@v2
      - uses: expo/expo-github-action@v8

      - run: bun install

      - name: Copy project config
        run: |
          cp projects/${{ matrix.project }}/eas.json ./eas.json
          cp projects/${{ matrix.project }}/.env ./.env

      - name: Build
        run: APP_PROJECT=${{ matrix.project }} npx eas build --non-interactive

matrix を使うことで、複数プロジェクトのビルドが並列実行されます。

注意点

EAS Update(OTA更新)の分離

EAS Updateを使う場合、各プロジェクトの projectId が異なるため、OTA更新は自動的にプロジェクト単位で分離されます。

terminal
APP_PROJECT=app-alpha npx eas update --branch production --message "Bug fix"

ブランドカラーの切り替え

現時点では tailwind.config.js は共通ですが、プロジェクトごとにテーマを変えたい場合は app.config.ts と同様のパターンで動的に読み込めます。

tailwind.config.js
const project = process.env.APP_PROJECT || "app-alpha";
const theme = require(`./projects/${project}/theme.js`);

module.exports = {
  content: ["./src/**/*.{js,jsx,ts,tsx}"],
  presets: [require("nativewind/preset")],
  theme: { extend: theme },
  plugins: [],
};

機能の出し分け

プロジェクトごとに画面や機能を出し分けたい場合は、Feature Flagパターンを使います。

src/config/features.ts
import Constants from "expo-constants";

const projectFeatures: Record<string, { audio: boolean; video: boolean }> = {
  "app-alpha": { audio: true, video: true },
  "app-beta": { audio: true, video: false },
};

const slug = Constants.expoConfig?.slug ?? "app-alpha";

export const Features = projectFeatures[slug] ?? projectFeatures["app-alpha"];

まとめ

この設計のポイントは3つです。

  1. projects/ ディレクトリにプロジェクト固有設定を閉じ込める — 共通コードに一切触れずにプロジェクトを追加できる
  2. app.config.ts で動的に読み込む — Expo/EASの標準機能の範囲内で解決する
  3. デプロイスクリプトで eas.json をコピーする — EASの「ルート固定」制約をシンプルに回避する

新しいプロジェクトを追加するときの作業は「ディレクトリを作って設定ファイルを3つ置くだけ」なので、運用の負荷は最小限です。

$ _|