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)
|
||||
- **测试 job**:在构建镜像前于 `api/` 下执行 `uv sync --dev` 与 `pytest`。
|
||||
- **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` 内说明为最新权威描述。
|
||||
|
||||
@@ -15,4 +16,7 @@
|
||||
|
||||
## 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_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` tag `v*.*.*`:同上路径过滤;部署生产。
|
||||
- **workflow_dispatch**:可选手动指定 ref。
|
||||
- **workflow_dispatch**:仅用于补跑 `main` / `master`(Staging)或 `vMAJOR.MINOR.PATCH` tag(Production);其它 ref 会直接失败,避免把任意分支部署到预发或生产。
|
||||
|
||||
仓库内需存在 **`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
|
||||
|
||||
```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 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 共用同一套即可):
|
||||
# ANDROID_KEYSTORE_BASE64 / ANDROID_STORE_PASSWORD / ANDROID_KEY_ALIAS / ANDROID_KEY_PASSWORD
|
||||
@@ -28,7 +31,7 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
environment:
|
||||
description: '部署环境'
|
||||
description: '部署环境(stage 请在 main 上补跑;prod 请使用 v tag 或在 main 上填写 version)'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
@@ -67,15 +70,49 @@ jobs:
|
||||
- name: Determine environment
|
||||
id: env
|
||||
run: |
|
||||
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
|
||||
echo "env=prod"
|
||||
elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||
echo "env=${{ github.event.inputs.environment }}"
|
||||
elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then
|
||||
echo "env=prod"
|
||||
else
|
||||
# push 到 main(本 workflow 仅监听 main 与 tag)
|
||||
echo "env=stage"
|
||||
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
|
||||
uses: actions/setup-node@v5
|
||||
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
|
||||
# 阿里云镜像仍为仓库级:ALIYUN_CR_USERNAME / ALIYUN_CR_PASSWORD
|
||||
#
|
||||
# 从旧版迁移:若仓库里仍是 SSH_HOST、SSH_PRIVATE_KEY、DEPLOY_PATH 等无前缀名称,
|
||||
# 请把「预发机」对应值迁移为 STAGING_*,「新生产机」填 PROD_*,并删除旧的无前缀 Secret。
|
||||
# 注意:旧的无前缀 SSH_HOST / SSH_PRIVATE_KEY / DEPLOY_PATH 指向生产机;本 workflow 不再读取它们。
|
||||
# Staging 必须使用另一台服务器对应的 STAGING_*,Production 使用 PROD_*。
|
||||
#
|
||||
# 旧库 pg_dump 一次性迁入当前 schema:见 workflow「Legacy DB migrate (one-shot)」(手动运行,非每次构建)。
|
||||
#
|
||||
@@ -14,7 +14,7 @@
|
||||
# - 手动创建并推送 tag vMAJOR.MINOR.PATCH:构建并部署到 Production;使用仓库中的 api/.env.production,上传后切换为运行时 .env
|
||||
#
|
||||
# 注意: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
|
||||
|
||||
@@ -46,13 +46,46 @@ env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
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:
|
||||
name: API tests
|
||||
needs: resolve-deploy-target
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ needs.resolve-deploy-target.outputs.deploy_ref }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
@@ -67,7 +100,9 @@ jobs:
|
||||
|
||||
build-and-push:
|
||||
name: Build and Push Docker Image
|
||||
needs: test
|
||||
needs:
|
||||
- resolve-deploy-target
|
||||
- test
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -76,7 +111,7 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch || github.ref }}
|
||||
ref: ${{ needs.resolve-deploy-target.outputs.deploy_ref }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
@@ -108,6 +143,7 @@ jobs:
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=sha,prefix=sha-
|
||||
type=raw,value=${{ needs.resolve-deploy-target.outputs.image_tag }}
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
@@ -124,31 +160,19 @@ jobs:
|
||||
deploy:
|
||||
name: Deploy to Remote Server
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-and-push
|
||||
needs:
|
||||
- resolve-deploy-target
|
||||
- build-and-push
|
||||
if: github.event_name != 'pull_request'
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch || github.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
|
||||
ref: ${{ needs.resolve-deploy-target.outputs.deploy_ref }}
|
||||
|
||||
- name: Ensure production SSH secret is set
|
||||
if: steps.deploy_target.outputs.target == 'prod'
|
||||
if: needs.resolve-deploy-target.outputs.target == 'prod'
|
||||
env:
|
||||
PROD_SSH_PRIVATE_KEY: ${{ secrets.PROD_SSH_PRIVATE_KEY }}
|
||||
run: |
|
||||
@@ -158,7 +182,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Ensure staging SSH secret is set
|
||||
if: steps.deploy_target.outputs.target != 'prod'
|
||||
if: needs.resolve-deploy-target.outputs.target != 'prod'
|
||||
env:
|
||||
STAGING_SSH_PRIVATE_KEY: ${{ secrets.STAGING_SSH_PRIVATE_KEY }}
|
||||
run: |
|
||||
@@ -169,20 +193,20 @@ jobs:
|
||||
|
||||
# 勿用 `prod && PROD_KEY || STAGING_KEY`:PROD 为空时会错误回退到 staging 密钥,导致连生产机报 Permission denied。
|
||||
- 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
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.PROD_SSH_PRIVATE_KEY }}
|
||||
|
||||
- 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
|
||||
with:
|
||||
ssh-private-key: ${{ secrets.STAGING_SSH_PRIVATE_KEY }}
|
||||
|
||||
- name: Export deploy connection env
|
||||
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_USER=${{ secrets.PROD_SSH_USER }}"
|
||||
@@ -203,28 +227,9 @@ jobs:
|
||||
mkdir -p ~/.ssh
|
||||
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
|
||||
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 }}
|
||||
ALIYUN_CR_USERNAME: ${{ secrets.ALIYUN_CR_USERNAME }}
|
||||
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
|
||||
"
|
||||
|
||||
if [ "${{ steps.deploy_target.outputs.target }}" = "prod" ]; then
|
||||
if [ "${{ needs.resolve-deploy-target.outputs.target }}" = "prod" ]; then
|
||||
ENV_SRC="api/.env.production"
|
||||
else
|
||||
ENV_SRC="api/.env.staging"
|
||||
|
||||
Reference in New Issue
Block a user