implement staging workflow
This commit is contained in:
8
.github/workflows/README.md
vendored
8
.github/workflows/README.md
vendored
@@ -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` tag(Production)。其它 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 artifact;Production APK 才创建正式 GitHub Release。
|
||||||
|
|||||||
13
.github/workflows/SETUP.md
vendored
13
.github/workflows/SETUP.md
vendored
@@ -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` tag(Production);其它 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
|
||||||
|
|||||||
47
.github/workflows/app-expo-deploy.yml
vendored
47
.github/workflows/app-expo-deploy.yml
vendored
@@ -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 / prod(dev 用 .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:
|
||||||
|
|||||||
99
.github/workflows/docker-build-deploy.yml
vendored
99
.github/workflows/docker-build-deploy.yml
vendored
@@ -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_HOST、SSH_PRIVATE_KEY、DEPLOY_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 补跑 main(Staging)或 vMAJOR.MINOR.PATCH tag(Production)。
|
||||||
|
|
||||||
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 只允许 main,Production 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"
|
||||||
|
|||||||
@@ -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(预发)
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
32
app-expo/plugins/withAndroidCleartextTraffic.js
Normal file
32
app-expo/plugins/withAndroidCleartextTraffic.js
Normal 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;
|
||||||
Reference in New Issue
Block a user