Files
danyang-match/.github/workflows/ci.yml
T
WorkBuddy d707fb1f60 ci(ui): 把 14 个 UI 契约门禁接入 CI,并新增统一执行器
Phase 23 收尾计划 批 1 Task 1.1 —— 此前 ui-tests/*.mjs 只在本地手工逐个执行,没有任何 CI
把关:改动破坏了源码结构契约也不会有任何信号。

- 新增 scripts/run-ui-contracts.mjs:逐个 spawn 子进程运行守卫 → 汇总 → 任一失败即非零退出,
  并打印失败守卫的原始输出便于定位。零 npm 依赖(守卫只用 node 内建模块)。
- 两条防退化设计:
  ① 子进程 cwd 固定为仓库根 —— 守卫里 6 个用相对路径、8 个用 import.meta.dirname 自锚定,
     不固定 cwd 会在 CI 上假红;
  ② 守卫数量下限校验(MIN_GUARDS=14,可用 UI_GUARDS_MIN 覆盖)—— 防止守卫被删除或改名后
     依然报绿(Phase 19 教训:扫描清单本身会漏)。
- ci.yml 新增 ui-contracts job(checkout + setup-node + 跑执行器),与既有 test-shell-guard
  同属「守门」类 job。
- 生产 compose 缺失的既有兜底确认可复用:ui-document-baseline-contracts.mjs 对不在版本控制内的
  infrastructure/docker-compose.prod.yml 已做 existsSync 判定并显式打印跳过原因,CI 上不会假红。

验证:
- 基线 node scripts/run-ui-contracts.mjs → PASS 14/14,退出码 0;
- 负向注入(.workbuddy/tmp 下必然失败的守卫)→ FAIL、退出码 1,失败详情可见;
- 数量下限分支以 UI_GUARDS_MIN=99 自检 → 正确拒绝报绿(退出码 1);
- .github/workflows/ci.yml 经 yaml.safe_load 解析通过,jobs 含 ui-contracts。
2026-09-18 21:30:15 +08:00

227 lines
7.4 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
backend:
name: Backend Build & Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: maven
working-directory: backend
- name: Build and Test
working-directory: backend
run: mvn verify -B
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v4
with:
name: backend-test-results
path: backend/target/surefire-reports/
admin-web:
name: Admin Web Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
cache-dependency-path: admin-web/pnpm-lock.yaml
- name: Install Dependencies
working-directory: admin-web
run: pnpm install --frozen-lockfile
- name: Build
working-directory: admin-web
run: pnpm build
miniapp:
name: Mini Program Compile Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Dependencies
working-directory: miniapp
run: npm install
- name: TypeScript Compile Check
working-directory: miniapp
run: npx tsc --noEmit
migration:
name: Migration Validation
runs-on: ubuntu-latest
needs: [backend]
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
# Phase 23 KEEP-VERIFY:本步骤原注释称「验证 migration SQL 语法」,实际只检查文件非空,
# 属名不副实的假校验(SQL 语法与基线表完整性由后端测试 MigrationValidationTest 覆盖,
# 该测试随 backend job 的 `mvn verify` 运行)。现改为该测试未覆盖的**序列**校验。
- name: 迁移序列校验(命名 / 非空 / 版本号唯一 / 无缺号)
run: |
python3 - <<'PY'
import re, sys, pathlib, collections
d = pathlib.Path("backend/src/main/resources/db/migration")
files = sorted(d.glob("V*.sql"))
if not files:
sys.exit("ERROR: 未找到任何迁移文件")
vers, bad_name, empty = [], [], []
for f in files:
m = re.match(r"V(\d+)__", f.name)
if not m:
bad_name.append(f.name)
continue
if f.stat().st_size == 0:
empty.append(f.name)
vers.append(int(m.group(1)))
problems = []
if bad_name:
problems.append(f"迁移文件命名不合规(应为 V<序号>__<描述>.sql): {bad_name}")
if empty:
problems.append(f"空迁移文件: {empty}")
dups = sorted(v for v, c in collections.Counter(vers).items() if c > 1)
if dups:
problems.append(f"迁移版本号重复: {dups}")
gaps = [v for v in range(min(vers), max(vers) + 1) if v not in set(vers)]
if gaps:
problems.append(f"迁移版本号缺号: {gaps}")
if problems:
for p in problems:
print("ERROR: " + p, file=sys.stderr)
sys.exit(1)
print(f"OK: {len(files)} 个迁移文件,版本 {min(vers)}~{max(vers)} 连续且唯一")
PY
test-shell-guard:
name: Test Shell Guard (Phase 23 Gate)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
# Phase 23 Gate:「不允许 skip/pass 空壳」。
# 机械阻断空壳回归 —— 详见 scripts/check-no-empty-tests.py。
- name: 禁止 skip/pass 空壳测试
run: python3 scripts/check-no-empty-tests.py tests
ui-contracts:
name: UI Contracts (source-structure gates)
runs-on: ubuntu-latest
# Phase 23 遗留(收尾计划 批 1 Task 1.1):14 个 UI 契约门禁此前**只在本地手工执行**,
# 没有 CI 把关 —— 即「改动破坏了源码结构契约也不会被发现」。
# 守卫是纯 node 内建模块、零 npm 依赖,故无需装依赖;统一执行器负责逐个跑 + 汇总 + 非零退出,
# 并校验守卫数量下限(防止守卫被删除/改名后仍然报绿)。
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: 运行 UI 契约门禁
run: node scripts/run-ui-contracts.mjs
e2e:
name: E2E (live stack)
runs-on: ubuntu-latest
# Phase 23 遗留 D-23-D:计划要求 CI 串联 E2E。此前 E2E 从未真正执行过——
# 无 conftest、fixture 不存在、文件在 import 期就崩。本 job 起真实栈后执行。
needs: [backend, migration]
env:
E2E_BASE_URL: http://localhost:8085/api/v1
# CI 专用测试口令,仅存在于 runner 运行期,不入库、不用于任何真实环境
E2E_OPERATOR_PASSWORD: ci-e2e-only-password
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install E2E dependencies
run: pip install -r tests/e2e/requirements.txt
- name: 生成 CI 测试用 .env(仅运行期存在,不入库)
run: |
cat > infrastructure/.env <<'ENV'
DB_PASSWORD=ci-e2e-db-password
REDIS_PASSWORD=ci-e2e-redis-password
MINIO_ACCESS_KEY=ci-e2e-minio
MINIO_SECRET_KEY=ci-e2e-minio-secret
JWT_ACCESS_SECRET=ci-e2e-access-secret-only-for-ci
JWT_REFRESH_SECRET=ci-e2e-refresh-secret-only-for-ci
ENCRYPTION_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
WECHAT_APP_SECRET=
ENV
- name: 起真实栈(postgres + redis + backend)
working-directory: infrastructure
run: docker compose up -d --build postgres redis backend
- name: 等待 backend 就绪(超时即失败,不静默跳过)
run: |
for i in $(seq 1 60); do
if curl -sf "$E2E_BASE_URL/health/live" > /dev/null; then
echo "backend READY after ${i} checks"; exit 0
fi
sleep 5
done
echo "::error::backend 未在 5 分钟内就绪"
docker compose -f infrastructure/docker-compose.yml logs backend
exit 1
- name: Seed E2E 运营账号(迁移不含 operator 种子,全新库无账号可登录)
working-directory: infrastructure
run: |
docker compose exec -T postgres psql -U postgres -d danyang_match \
-v pwd="$E2E_OPERATOR_PASSWORD" -f - < ../tests/e2e/sql/seed_e2e_operators.sql
- name: Run E2E
run: pytest -v
- name: Teardown(无论成败都清理并留日志)
if: always()
run: |
docker compose -f infrastructure/docker-compose.yml logs backend || true
docker compose -f infrastructure/docker-compose.yml down -v || true