Squash merge feat/expo-app: app-expo, .cursor, workflows, package.json, .husky; remove app-android, app-ios, react-app

This commit is contained in:
Kevin
2026-03-19 01:12:17 +08:00
parent 9e4f301ab9
commit b4f4369b7d
544 changed files with 23707 additions and 67151 deletions

View File

@@ -1,5 +1,15 @@
# GitHub Actions CI/CD 工作流配置说明
## 工作流列表
| 工作流 | 说明 | 文档 |
|--------|------|------|
| Docker Build and Deploy | API 镜像构建与部署 | 见下文 |
| Android Release Build | Android APK 构建与发布 | 见下文 |
| App Expo Deploy | app-expo Web 构建与发布 | [docs/app-expo-deploy.md](../../docs/app-expo-deploy.md) |
---
## 概述
本工作流实现了自动化的 Docker 镜像构建和部署流程:

175
.github/workflows/app-expo-deploy.yml vendored Normal file
View File

@@ -0,0 +1,175 @@
# App Expo 统一部署 Pipeline
#
# 环境映射(按触发源自动推断):
# main → dev (开发 + 内部测试)
# staging → stage (预发布)
# v*.*.* → prod (正式发布)
#
# 手动触发workflow_dispatch 可选择环境
name: App Expo Deploy
on:
push:
branches: [main, staging]
tags: ['v*.*.*']
paths:
- "app-expo/**"
- ".github/workflows/app-expo-deploy.yml"
workflow_dispatch:
inputs:
environment:
description: '部署环境'
required: true
type: choice
options:
- dev
- stage
- prod
default: dev
version:
description: '版本号 (prod 时使用,如 1.0.0)'
required: false
type: string
concurrency:
group: app-expo-deploy-${{ github.ref }}
cancel-in-progress: true
env:
APP_NAME: app-expo
jobs:
deploy:
name: Build & Deploy
runs-on: ubuntu-latest
permissions:
contents: read
# GitHub Environments: 在 Repo Settings → Environments 中创建 dev/staging/production可配置独立 secrets
environment: ${{
(github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'prod') && 'production' ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'stage') && 'staging' ||
startsWith(github.ref, 'refs/tags/v') && 'production' ||
github.ref == 'refs/heads/staging' && 'staging' ||
'dev'
}}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: ${{ startsWith(github.ref, 'refs/tags/') && '0' || '1' }}
- name: Determine environment
id: env
run: |
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
echo "env=prod"
elif [[ "${{ github.ref }}" == refs/heads/staging ]]; then
echo "env=stage"
elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "env=${{ github.event.inputs.environment }}"
else
echo "env=dev"
fi >> $GITHUB_OUTPUT
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: app-expo/package-lock.json
- name: Install dependencies
working-directory: app-expo
run: npm ci
- name: Quality checks (dev only)
if: steps.env.outputs.env == 'dev'
working-directory: app-expo
run: |
npm run format:check
npm run lint
npm run test:ci
- name: Export web build
working-directory: app-expo
run: npx expo export -p web
- name: Determine version (prod)
if: steps.env.outputs.env == 'prod'
id: version
run: |
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
VERSION="${GITHUB_REF#refs/tags/v}"
TAG_NAME="${GITHUB_REF#refs/tags/}"
elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]] && [[ -n "${{ github.event.inputs.version }}" ]]; then
VERSION="${{ github.event.inputs.version }}"
TAG_NAME="v${VERSION}"
else
VERSION=$(node -p "require('./app-expo/package.json').version")
TAG_NAME="v${VERSION}"
fi
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "tag_name=${TAG_NAME}" >> $GITHUB_OUTPUT
- name: Create release zip (prod)
if: steps.env.outputs.env == 'prod'
run: |
cd app-expo/dist
zip -r ../${{ env.APP_NAME }}-v${{ steps.version.outputs.version }}-web.zip .
- name: Generate Release Notes (prod)
if: steps.env.outputs.env == 'prod' && startsWith(github.ref, 'refs/tags/')
id: release_notes
run: |
TAG_NAME="${{ steps.version.outputs.tag_name }}"
PREV_TAG=$(git tag --sort=-creatordate | grep '^v' | sed -n '2p')
if [ -n "$PREV_TAG" ]; then
CHANGES=$(git log "${PREV_TAG}..HEAD" --pretty=format:"- %s (%h)" --no-merges)
else
CHANGES=$(git log --pretty=format:"- %s (%h)" --no-merges -20)
fi
{
echo "notes<<EOF"
echo "## ${APP_NAME} ${TAG_NAME}"
echo ""
echo "### 更新内容"
echo ""
echo "${CHANGES}"
echo ""
echo "---"
echo "构建编号: #${GITHUB_RUN_NUMBER}"
echo "EOF"
} >> $GITHUB_OUTPUT
- name: Upload artifact (dev / stage)
if: steps.env.outputs.env != 'prod'
uses: actions/upload-artifact@v4
with:
name: app-expo-web-${{ steps.env.outputs.env }}
path: app-expo/dist
retention-days: ${{ steps.env.outputs.env == 'dev' && '14' || '30' }}
- name: Upload artifact (prod)
if: steps.env.outputs.env == 'prod'
uses: actions/upload-artifact@v4
with:
name: ${{ env.APP_NAME }}-v${{ steps.version.outputs.version }}-web
path: app-expo/${{ env.APP_NAME }}-v${{ steps.version.outputs.version }}-web.zip
retention-days: 90
- name: Create GitHub Release (prod)
if: steps.env.outputs.env == 'prod'
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.version.outputs.tag_name }}
name: "${{ env.APP_NAME }} ${{ steps.version.outputs.tag_name }}"
body: ${{ steps.release_notes.outputs.notes || 'Release' }}
draft: false
prerelease: false
files: app-expo/${{ env.APP_NAME }}-v${{ steps.version.outputs.version }}-web.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
permissions:
contents: write

132
.github/workflows/app-expo-quality.yml vendored Normal file
View File

@@ -0,0 +1,132 @@
name: App Expo Quality
on:
pull_request:
paths:
- "app-expo/**"
- ".github/workflows/app-expo-quality.yml"
push:
branches:
- main
- master
- develop
paths:
- "app-expo/**"
- ".github/workflows/app-expo-quality.yml"
workflow_dispatch:
concurrency:
group: app-expo-quality-${{ github.ref }}
cancel-in-progress: true
jobs:
verify:
name: Verify app-expo
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
defaults:
run:
working-directory: app-expo
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: app-expo/package-lock.json
- name: Install dependencies
run: npm ci
- name: Check formatting
run: npm run format:check
- name: Run linter
run: npm run lint
- name: Run Jest in CI mode
run: npm run test:ci
- name: Build coverage summary
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
id: coverage_summary
run: |
node <<'EOF'
const fs = require('fs');
const summaryPath = 'coverage/jest/coverage-summary.json';
const summary = JSON.parse(fs.readFileSync(summaryPath, 'utf8')).total;
const metrics = [
['Lines', summary.lines],
['Statements', summary.statements],
['Functions', summary.functions],
['Branches', summary.branches],
];
const body = [
'## app-expo Jest Coverage',
'',
'| Metric | Percent | Covered / Total |',
'| --- | ---: | ---: |',
...metrics.map(
([name, metric]) =>
`| ${name} | ${metric.pct}% | ${metric.covered}/${metric.total} |`
),
'',
'_Generated by App Expo Quality._',
].join('\n');
fs.appendFileSync(process.env.GITHUB_OUTPUT, `body<<EOF\n${body}\nEOF\n`);
EOF
- name: Comment coverage on PR
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
uses: actions/github-script@v7
with:
script: |
const marker = '<!-- app-expo-jest-coverage -->';
const body = `${marker}
${{ steps.coverage_summary.outputs.body }}`;
const { owner, repo } = context.repo;
const issue_number = context.issue.number;
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number,
per_page: 100,
});
const existing = comments.find((comment) =>
comment.user?.login === 'github-actions[bot]' &&
comment.body?.includes(marker)
);
if (existing) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number,
body,
});
}
- name: Upload Jest coverage
if: always()
uses: actions/upload-artifact@v4
with:
name: app-expo-jest-coverage
path: app-expo/coverage/jest
retention-days: 14