staging 與 production project(或使用相同專案的不同 hosting site)FIREBASE_TOKEN(由 firebase login:ci 取得)FIREBASE_PROJECT_ID_STAGING(staging 專案 id)FIREBASE_PROJECT_ID_PROD(production 專案 id)產生 FIREBASE_TOKEN:
firebase login:ci → 複製 token → GitHub repo > Settings > Secrets > New repository secret
package.json
{
  "scripts": {
    "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
    "test": "react-scripts test --ci --reporters=default",
    "build": "react-scripts build",
    "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,md}\"",
    "deploy:staging": "firebase deploy --only hosting --project $FIREBASE_PROJECT_ID_STAGING",
    "deploy:prod": "firebase deploy --only hosting --project $FIREBASE_PROJECT_ID_PROD"
  }
}
firebase.json
{
  "hosting": {
    "public": "build",
    "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
    "rewrites": [
      { "source": "**", "destination": "/index.html" }
    ]
  }
}
.github/workflows/pr-preview.yml
name: PR Preview Deploy
on:
  pull_request:
    types: [opened, synchronize, reopened, closed]
jobs:
  preview:
    runs-on: ubuntu-latest
    if: github.event.pull_request != null
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 18
      - name: Install deps
        run: npm ci
      - name: Lint
        run: npm run lint
      - name: Run tests
        run: npm test
      - name: Build
        run: npm run build
      - name: Deploy to preview channel
        env:
          FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
        run: |
          # preview channel name: pr-<number>
          CHANNEL="pr-${{ github.event.number }}"
          firebase hosting:channel:deploy $CHANNEL --project ${{ secrets.FIREBASE_PROJECT_ID_STAGING }} --expires 7d
      - name: Post preview URL comment
        if: ${{ github.event.action != 'closed' }}
        uses: marocchino/sticky-pull-request-comment@v2
        with:
          header: preview-url
          message: |
            Preview deployed: https://$CHANNEL--${{ secrets.FIREBASE_PROJECT_ID_STAGING }}.web.app
.github/workflows/staging.yml
name: Deploy to Staging
on:
  push:
    branches:
      - staging
jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 18
      - name: Cache node modules
        uses: actions/cache@v4
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
      - name: Install dependencies
        run: npm ci
      - name: Lint
        run: npm run lint
      - name: Run tests
        run: npm test
      - name: Build
        run: npm run build
      - name: Deploy to Staging (Firebase Hosting)
        env:
          FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
          FIREBASE_PROJECT_ID_STAGING: ${{ secrets.FIREBASE_PROJECT_ID_STAGING }}
        run: |
          firebase deploy --only hosting --project $FIREBASE_PROJECT_ID_STAGING
.github/workflows/production.yml
name: Deploy to Production
on:
  push:
    branches:
      - main
jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 18
      - name: Cache node modules
        uses: actions/cache@v4
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
      - name: Install dependencies
        run: npm ci
      - name: Lint
        run: npm run lint
      - name: Run tests
        run: npm test
      - name: Build
        run: npm run build
      - name: Deploy to Production
        env:
          FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
          FIREBASE_PROJECT_ID_PROD: ${{ secrets.FIREBASE_PROJECT_ID_PROD }}
        run: |
          firebase deploy --only hosting --project $FIREBASE_PROJECT_ID_PROD