implement staging workflow

This commit is contained in:
Kevin
2026-05-09 16:16:48 +08:00
parent f0e37c5e76
commit 175784292d
9 changed files with 157 additions and 61 deletions

View File

@@ -5,7 +5,8 @@
- **工作流文件** [docker-build-deploy.yml](docker-build-deploy.yml) - **工作流文件** [docker-build-deploy.yml](docker-build-deploy.yml)
- **测试 job**:在构建镜像前于 `api/` 下执行 `uv sync --dev``pytest` - **测试 job**:在构建镜像前于 `api/` 下执行 `uv sync --dev``pytest`
- **Secrets**:预发 `STAGING_*`、生产 `PROD_*`、镜像 `ALIYUN_CR_*` — 详见 [SETUP.md](SETUP.md)。 - **Secrets**:预发 `STAGING_*`、生产 `PROD_*`、镜像 `ALIYUN_CR_*` — 详见 [SETUP.md](SETUP.md)。
- **分支 / Tag**`main`预发;语义化 tag `v*.*.*`生产;路径过滤为 `api/**` 与本 workflow。 - **分支 / Tag**`main`Staging 服务器;语义化 tag `v*.*.*`Production 服务器;路径过滤为 `api/**` 与本 workflow。
- **手动补跑**`workflow_dispatch` 仅支持 `main` / `master`Staging`vMAJOR.MINOR.PATCH` tagProduction。其它 ref 会在测试与构建前失败。
头部注释与 `docker-build-deploy.yml` 内说明为最新权威描述。 头部注释与 `docker-build-deploy.yml` 内说明为最新权威描述。
@@ -15,4 +16,7 @@
## App Expo Deploy ## App Expo Deploy
见仓库 `docs/` 下相关说明(若存在)。 - **工作流文件**[app-expo-deploy.yml](app-expo-deploy.yml)
- **自动触发**`main``stage`,使用 `app-expo/.env.staging` 构建 APK artifact`v*.*.*` tag → `prod`,使用 `app-expo/.env.production` 并创建 GitHub Release。
- **手动触发**`dev` 可用于内部测试包;`stage` 只允许在 `main` / `master` 上补跑;`prod` 需要选择 `vMAJOR.MINOR.PATCH` tag或在 `main` / `master` 上填写语义化 `version`
- **产物规则**Staging APK 仅上传为 GitHub Actions artifactProduction APK 才创建正式 GitHub Release。

View File

@@ -19,16 +19,25 @@
| `ALIYUN_CR_USERNAME` | 阿里云 ACR 用户名 | | `ALIYUN_CR_USERNAME` | 阿里云 ACR 用户名 |
| `ALIYUN_CR_PASSWORD` | 阿里云 ACR 密码 | | `ALIYUN_CR_PASSWORD` | 阿里云 ACR 密码 |
> **Tag 部署**:推送 `v*.*.*`(如 `v1.2.0`)时使用 `PROD_*`。**main 分支推送**使用 `STAGING_*` > **Staging 服务器**`main` 分支发布使用 `STAGING_*`,用于部署到独立的预发服务器。<br>
> **Production 服务器**:推送 `v*.*.*`(如 `v1.2.0`)时使用 `PROD_*`。
旧的无前缀 `SSH_HOST` / `SSH_USER` / `SSH_PRIVATE_KEY` / `SSH_PORT` / `DEPLOY_PATH` 指向生产机,当前 release workflow 不再读取它们;不要把这些值复制成 `STAGING_*`。Staging 需要填写另一台预发服务器自己的 `STAGING_*`
## 触发条件 ## 触发条件
- `push``main`:改动了 `api/**``.github/workflows/**` 时,先跑 **API tests**`uv sync --dev` + `pytest`),再构建镜像并部署预发。 - `push``main`:改动了 `api/**``.github/workflows/**` 时,先跑 **API tests**`uv sync --dev` + `pytest`),再构建镜像并部署预发。
- `push` tag `v*.*.*`:同上路径过滤;部署生产。 - `push` tag `v*.*.*`:同上路径过滤;部署生产。
- **workflow_dispatch**可选手动指定 ref - **workflow_dispatch**仅用于补跑 `main` / `master`Staging`vMAJOR.MINOR.PATCH` tagProduction其它 ref 会直接失败,避免把任意分支部署到预发或生产
仓库内需存在 **`api/.env.staging`** / **`api/.env.production`**(供部署 job 校验与上传);勿将真实密钥提交到公开分支。 仓库内需存在 **`api/.env.staging`** / **`api/.env.production`**(供部署 job 校验与上传);勿将真实密钥提交到公开分支。
## App Expo Release
- `push``main`:构建 Staging APK执行 `node scripts/use-env.js staging`,产物上传为 GitHub Actions artifact。
- `push` tag `v*.*.*`:构建 Production APK执行 `node scripts/use-env.js production`,并创建 GitHub Release。
- 手动 `workflow_dispatch``stage` 只允许在 `main` / `master` 上补跑;`prod` 需要选择 `vMAJOR.MINOR.PATCH` tag或在 `main` / `master` 上填写语义化 `version`
## 本地验证 SSH ## 本地验证 SSH
```bash ```bash

View File

@@ -11,7 +11,10 @@
# push main → stage → node scripts/use-env.js staging → .env.staging # push main → stage → node scripts/use-env.js staging → .env.staging
# push v*.*.* → prod → node scripts/use-env.js production → .env.production # push v*.*.* → prod → node scripts/use-env.js production → .env.production
# #
# 手动触发 workflow_dispatch可选 dev / stage / proddev 用 .env.development便于打内部测试包 # 手动触发 workflow_dispatch
# - dev内部测试包使用 .env.development
# - stage仅用于 main / master 补跑 Staging release使用 .env.staging
# - prod用于 vMAJOR.MINOR.PATCH tag或在 main / master 上填写 version 后发正式 Release
# #
# Repository secrets与 android-release.yml 共用同一套即可): # Repository secrets与 android-release.yml 共用同一套即可):
# ANDROID_KEYSTORE_BASE64 / ANDROID_STORE_PASSWORD / ANDROID_KEY_ALIAS / ANDROID_KEY_PASSWORD # ANDROID_KEYSTORE_BASE64 / ANDROID_STORE_PASSWORD / ANDROID_KEY_ALIAS / ANDROID_KEY_PASSWORD
@@ -28,7 +31,7 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
environment: environment:
description: '部署环境' description: '部署环境stage 请在 main 上补跑prod 请使用 v tag 或在 main 上填写 version'
required: true required: true
type: choice type: choice
options: options:
@@ -67,15 +70,49 @@ jobs:
- name: Determine environment - name: Determine environment
id: env id: env
run: | run: |
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "env=prod"
elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "env=${{ github.event.inputs.environment }}" echo "env=${{ github.event.inputs.environment }}"
elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then
echo "env=prod"
else else
# push 到 main本 workflow 仅监听 main 与 tag # push 到 main本 workflow 仅监听 main 与 tag
echo "env=stage" echo "env=stage"
fi >> $GITHUB_OUTPUT fi >> $GITHUB_OUTPUT
- name: Validate manual release ref
if: github.event_name == 'workflow_dispatch'
run: |
ENVIRONMENT="${{ steps.env.outputs.env }}"
REF="${{ github.ref }}"
REF_NAME="${{ github.ref_name }}"
VERSION="${{ github.event.inputs.version }}"
case "$ENVIRONMENT" in
dev)
echo "dev 构建允许使用当前 ref: $REF_NAME"
;;
stage)
if [ "$REF_NAME" != "main" ] && [ "$REF_NAME" != "master" ]; then
echo "::error::Staging release 只允许在 main / master 上手动补跑,当前 ref 为 '$REF_NAME'。"
exit 1
fi
;;
prod)
if [[ "$REF" == refs/tags/v* && "$REF_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "使用 tag $REF_NAME 发正式 Release。"
elif { [ "$REF_NAME" = "main" ] || [ "$REF_NAME" = "master" ]; } && [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "使用 main / master 和显式版本 v$VERSION 发正式 Release。"
else
echo "::error::Production release 需要选择 vMAJOR.MINOR.PATCH tag或在 main / master 上填写语义化 version。"
exit 1
fi
;;
*)
echo "::error::未知部署环境 '$ENVIRONMENT'。"
exit 1
;;
esac
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v5 uses: actions/setup-node@v5
with: with:

View File

@@ -4,8 +4,8 @@
# PROD_SSH_HOST / PROD_SSH_USER / PROD_SSH_PRIVATE_KEY / PROD_SSH_PORT / PROD_DEPLOY_PATH # PROD_SSH_HOST / PROD_SSH_USER / PROD_SSH_PRIVATE_KEY / PROD_SSH_PORT / PROD_DEPLOY_PATH
# 阿里云镜像仍为仓库级ALIYUN_CR_USERNAME / ALIYUN_CR_PASSWORD # 阿里云镜像仍为仓库级ALIYUN_CR_USERNAME / ALIYUN_CR_PASSWORD
# #
# 从旧版迁移:若仓库里仍是 SSH_HOSTSSH_PRIVATE_KEYDEPLOY_PATH 等无前缀名称, # 注意:旧的无前缀 SSH_HOST / SSH_PRIVATE_KEY / DEPLOY_PATH 指向生产机;本 workflow 不再读取它们。
# 请把「预发机」对应值迁移为 STAGING_*,「新生产机」填 PROD_*,并删除旧的无前缀 Secret # Staging 必须使用另一台服务器对应的 STAGING_*Production 使用 PROD_*
# #
# 旧库 pg_dump 一次性迁入当前 schema见 workflow「Legacy DB migrate (one-shot)」(手动运行,非每次构建)。 # 旧库 pg_dump 一次性迁入当前 schema见 workflow「Legacy DB migrate (one-shot)」(手动运行,非每次构建)。
# #
@@ -14,7 +14,7 @@
# - 手动创建并推送 tag vMAJOR.MINOR.PATCH构建并部署到 Production使用仓库中的 api/.env.production上传后切换为运行时 .env # - 手动创建并推送 tag vMAJOR.MINOR.PATCH构建并部署到 Production使用仓库中的 api/.env.production上传后切换为运行时 .env
# #
# 注意paths 过滤在 tag push 时按「被指向的 commit」判断若该 commit 未改 api/ 与本 workflow不会触发。 # 注意paths 过滤在 tag push 时按「被指向的 commit」判断若该 commit 未改 api/ 与本 workflow不会触发。
# 此时可用 workflow_dispatch 选择对应 tag/ref 手动部署 # 此时可用 workflow_dispatch 补跑 mainStaging或 vMAJOR.MINOR.PATCH tagProduction
name: Docker Build and Deploy name: Docker Build and Deploy
@@ -46,13 +46,46 @@ env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs: jobs:
resolve-deploy-target:
name: Resolve deploy target
runs-on: ubuntu-latest
outputs:
deploy_ref: ${{ steps.deploy_target.outputs.deploy_ref }}
image_tag: ${{ steps.deploy_target.outputs.image_tag }}
target: ${{ steps.deploy_target.outputs.target }}
steps:
- name: Determine deploy target
id: deploy_target
run: |
if [ -n "${{ github.event.inputs.branch }}" ]; then
REF_NAME="${{ github.event.inputs.branch }}"
else
REF_NAME="${{ github.ref_name }}"
fi
echo "deploy_ref=$REF_NAME" >> "$GITHUB_OUTPUT"
if [[ "$REF_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "target=prod" >> "$GITHUB_OUTPUT"
echo "image_tag=${REF_NAME#v}" >> "$GITHUB_OUTPUT"
elif [ "$REF_NAME" = "main" ] || [ "$REF_NAME" = "master" ]; then
echo "target=staging" >> "$GITHUB_OUTPUT"
echo "image_tag=latest" >> "$GITHUB_OUTPUT"
else
echo "::error::不支持部署 ref '$REF_NAME'。Staging release 只允许 mainProduction release 只允许 vMAJOR.MINOR.PATCH tag。"
exit 1
fi
test: test:
name: API tests name: API tests
needs: resolve-deploy-target
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with:
ref: ${{ needs.resolve-deploy-target.outputs.deploy_ref }}
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@v5 uses: astral-sh/setup-uv@v5
@@ -67,7 +100,9 @@ jobs:
build-and-push: build-and-push:
name: Build and Push Docker Image name: Build and Push Docker Image
needs: test needs:
- resolve-deploy-target
- test
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read
@@ -76,7 +111,7 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v5
with: with:
ref: ${{ github.event.inputs.branch || github.ref }} ref: ${{ needs.resolve-deploy-target.outputs.deploy_ref }}
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@@ -108,6 +143,7 @@ jobs:
type=semver,pattern={{version}} type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
type=sha,prefix=sha- type=sha,prefix=sha-
type=raw,value=${{ needs.resolve-deploy-target.outputs.image_tag }}
type=raw,value=latest,enable={{is_default_branch}} type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image - name: Build and push Docker image
@@ -124,31 +160,19 @@ jobs:
deploy: deploy:
name: Deploy to Remote Server name: Deploy to Remote Server
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: build-and-push needs:
- resolve-deploy-target
- build-and-push
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v5
with: with:
ref: ${{ github.event.inputs.branch || github.ref }} ref: ${{ needs.resolve-deploy-target.outputs.deploy_ref }}
- name: Determine deploy target
id: deploy_target
run: |
if [ -n "${{ github.event.inputs.branch }}" ]; then
REF_NAME="${{ github.event.inputs.branch }}"
else
REF_NAME="${{ github.ref_name }}"
fi
if [[ "$REF_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "target=prod" >> "$GITHUB_OUTPUT"
else
echo "target=staging" >> "$GITHUB_OUTPUT"
fi
- name: Ensure production SSH secret is set - name: Ensure production SSH secret is set
if: steps.deploy_target.outputs.target == 'prod' if: needs.resolve-deploy-target.outputs.target == 'prod'
env: env:
PROD_SSH_PRIVATE_KEY: ${{ secrets.PROD_SSH_PRIVATE_KEY }} PROD_SSH_PRIVATE_KEY: ${{ secrets.PROD_SSH_PRIVATE_KEY }}
run: | run: |
@@ -158,7 +182,7 @@ jobs:
fi fi
- name: Ensure staging SSH secret is set - name: Ensure staging SSH secret is set
if: steps.deploy_target.outputs.target != 'prod' if: needs.resolve-deploy-target.outputs.target != 'prod'
env: env:
STAGING_SSH_PRIVATE_KEY: ${{ secrets.STAGING_SSH_PRIVATE_KEY }} STAGING_SSH_PRIVATE_KEY: ${{ secrets.STAGING_SSH_PRIVATE_KEY }}
run: | run: |
@@ -169,20 +193,20 @@ jobs:
# 勿用 `prod && PROD_KEY || STAGING_KEY`PROD 为空时会错误回退到 staging 密钥,导致连生产机报 Permission denied。 # 勿用 `prod && PROD_KEY || STAGING_KEY`PROD 为空时会错误回退到 staging 密钥,导致连生产机报 Permission denied。
- name: Set up SSH (production) - name: Set up SSH (production)
if: steps.deploy_target.outputs.target == 'prod' if: needs.resolve-deploy-target.outputs.target == 'prod'
uses: webfactory/ssh-agent@v0.9.1 uses: webfactory/ssh-agent@v0.9.1
with: with:
ssh-private-key: ${{ secrets.PROD_SSH_PRIVATE_KEY }} ssh-private-key: ${{ secrets.PROD_SSH_PRIVATE_KEY }}
- name: Set up SSH (staging) - name: Set up SSH (staging)
if: steps.deploy_target.outputs.target != 'prod' if: needs.resolve-deploy-target.outputs.target != 'prod'
uses: webfactory/ssh-agent@v0.9.1 uses: webfactory/ssh-agent@v0.9.1
with: with:
ssh-private-key: ${{ secrets.STAGING_SSH_PRIVATE_KEY }} ssh-private-key: ${{ secrets.STAGING_SSH_PRIVATE_KEY }}
- name: Export deploy connection env - name: Export deploy connection env
run: | run: |
if [ "${{ steps.deploy_target.outputs.target }}" = "prod" ]; then if [ "${{ needs.resolve-deploy-target.outputs.target }}" = "prod" ]; then
{ {
echo "SSH_HOST=${{ secrets.PROD_SSH_HOST }}" echo "SSH_HOST=${{ secrets.PROD_SSH_HOST }}"
echo "SSH_USER=${{ secrets.PROD_SSH_USER }}" echo "SSH_USER=${{ secrets.PROD_SSH_USER }}"
@@ -203,28 +227,9 @@ jobs:
mkdir -p ~/.ssh mkdir -p ~/.ssh
ssh-keyscan -H -p "${SSH_PORT:-22}" "${SSH_HOST}" >> ~/.ssh/known_hosts ssh-keyscan -H -p "${SSH_PORT:-22}" "${SSH_HOST}" >> ~/.ssh/known_hosts
- name: Determine image tag
id: image_tag
run: |
# 与 docker/metadata-action 的 semver 标签一致v1.2.3 → 镜像 :1.2.3
if [ -n "${{ github.event.inputs.branch }}" ]; then
REF_NAME="${{ github.event.inputs.branch }}"
else
REF_NAME="${{ github.ref_name }}"
fi
echo "deploy_ref=$REF_NAME" >> "$GITHUB_OUTPUT"
if [[ "$REF_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "tag=${REF_NAME#v}" >> "$GITHUB_OUTPUT"
elif [ "$REF_NAME" == "main" ] || [ "$REF_NAME" == "master" ]; then
echo "tag=latest" >> "$GITHUB_OUTPUT"
else
BRANCH_TAG=$(echo "$REF_NAME" | sed 's/\//-/g')
echo "tag=$BRANCH_TAG" >> "$GITHUB_OUTPUT"
fi
- name: Prepare remote candidate release - name: Prepare remote candidate release
env: env:
IMAGE_TAG: ${{ env.REGISTRY }}/${{ env.REGISTRY_NAMESPACE }}/${{ env.IMAGE_NAME }}:${{ steps.image_tag.outputs.tag }} IMAGE_TAG: ${{ env.REGISTRY }}/${{ env.REGISTRY_NAMESPACE }}/${{ env.IMAGE_NAME }}:${{ needs.resolve-deploy-target.outputs.image_tag }}
REGISTRY: ${{ env.REGISTRY }} REGISTRY: ${{ env.REGISTRY }}
ALIYUN_CR_USERNAME: ${{ secrets.ALIYUN_CR_USERNAME }} ALIYUN_CR_USERNAME: ${{ secrets.ALIYUN_CR_USERNAME }}
ALIYUN_CR_PASSWORD: ${{ secrets.ALIYUN_CR_PASSWORD }} ALIYUN_CR_PASSWORD: ${{ secrets.ALIYUN_CR_PASSWORD }}
@@ -244,7 +249,7 @@ jobs:
docker network inspect api_life-echo-network >/dev/null 2>&1 || docker network create api_life-echo-network docker network inspect api_life-echo-network >/dev/null 2>&1 || docker network create api_life-echo-network
" "
if [ "${{ steps.deploy_target.outputs.target }}" = "prod" ]; then if [ "${{ needs.resolve-deploy-target.outputs.target }}" = "prod" ]; then
ENV_SRC="api/.env.production" ENV_SRC="api/.env.production"
else else
ENV_SRC="api/.env.staging" ENV_SRC="api/.env.staging"

View File

@@ -1,3 +1,6 @@
LIFE_ECHO_API_HOST_BIND=0.0.0.0
LIFE_ECHO_API_HOST_PORT=8000
# ============================================================================= # =============================================================================
# Life Echo API — staging预发 # Life Echo API — staging预发
# #

View File

@@ -56,10 +56,10 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
image: life-echo-api:latest image: life-echo-api:latest
container_name: life-echo-api-prod container_name: life-echo-api-prod
# 独立 Caddy宿主机或其它 compose经 HTTPS 反代;仅绑定本机回环,避免与机上其它项目端口直接对公网 # 默认仅绑定本机回环,交给宿主机 Caddy/反代staging 如需 IP:port 直连,可在 .env 设置 LIFE_ECHO_API_HOST_BIND=0.0.0.0
# 若与 Cosmetic 等共用主机且 8000 已被占用,在 .env 中设置 LIFE_ECHO_API_HOST_PORT=其它端口并在 Caddyfile 中一致。 # 若与 Cosmetic 等共用主机且 8000 已被占用,在 .env 中设置 LIFE_ECHO_API_HOST_PORT=其它端口并在 Caddyfile / app env 中一致。
ports: ports:
- "127.0.0.1:${LIFE_ECHO_API_HOST_PORT:-8000}:8000" - "${LIFE_ECHO_API_HOST_BIND:-127.0.0.1}:${LIFE_ECHO_API_HOST_PORT:-8000}:8000"
env_file: env_file:
- .env - .env
environment: environment:

View File

@@ -1,3 +1,3 @@
# 仅 API/WS 基址TTS 每轮开关由运行时 WS payload 与服务端 ENABLE_TTS 控制(见 api/.env.example # 仅 API/WS 基址TTS 每轮开关由运行时 WS payload 与服务端 ENABLE_TTS 控制(见 api/.env.example
EXPO_PUBLIC_API_URL=https://staging.lifecho.worldsplats.com EXPO_PUBLIC_API_URL=http://1.15.29.57:8000
EXPO_PUBLIC_WS_URL=wss://staging.lifecho.worldsplats.com EXPO_PUBLIC_WS_URL=ws://1.15.29.57:8000

View File

@@ -28,6 +28,8 @@ const LOCALES: Record<string, LocaleMessages> = {
const SUPPORTED_LOCALES = ['zh', 'en'] as const; const SUPPORTED_LOCALES = ['zh', 'en'] as const;
const PRIMARY_LOCALE = process.env.EXPO_PUBLIC_PRIMARY_LOCALE ?? 'zh'; const PRIMARY_LOCALE = process.env.EXPO_PUBLIC_PRIMARY_LOCALE ?? 'zh';
const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL ?? '';
const ALLOW_ANDROID_CLEARTEXT_TRAFFIC = API_BASE_URL.startsWith('http://');
const PERMISSION_FALLBACKS: Record<PermissionKey, string> = { const PERMISSION_FALLBACKS: Record<PermissionKey, string> = {
microphone: 'Allow $(PRODUCT_NAME) to access your microphone.', microphone: 'Allow $(PRODUCT_NAME) to access your microphone.',
@@ -149,6 +151,10 @@ export default ({ config }: ConfigContext): ExpoConfig => {
plugins: [ plugins: [
// CI/local release: android/app/keystore.properties + store file → release signing; -PversionName/-PversionCode // CI/local release: android/app/keystore.properties + store file → release signing; -PversionName/-PversionCode
'./plugins/withAndroidReleaseSigning', './plugins/withAndroidReleaseSigning',
[
'./plugins/withAndroidCleartextTraffic',
{ enabled: ALLOW_ANDROID_CLEARTEXT_TRAFFIC },
],
'expo-router', 'expo-router',
[ [
'expo-splash-screen', 'expo-splash-screen',

View File

@@ -0,0 +1,32 @@
// @ts-check
/**
* Toggle Android cleartext HTTP traffic from Expo env.
*
* Staging may use an IP:port HTTP endpoint while production remains HTTPS.
*/
const { withAndroidManifest } = require('@expo/config-plugins');
/**
* @param {import('expo/config').ExpoConfig} config
* @param {{ enabled?: boolean }} props
*/
function withAndroidCleartextTraffic(config, props = {}) {
return withAndroidManifest(config, (mod) => {
const mainApplication = mod.modResults.manifest.application?.[0];
if (!mainApplication) {
throw new Error(
'[withAndroidCleartextTraffic] Main application not found in AndroidManifest.xml.',
);
}
mainApplication.$ = mainApplication.$ ?? {};
mainApplication.$['android:usesCleartextTraffic'] = props.enabled
? 'true'
: 'false';
return mod;
});
}
module.exports = withAndroidCleartextTraffic;