Files
danyang-match/.github/workflows/ci.yml
T
WorkBuddy d036c9b986 fix(miniapp): 修复 2 个页面 WXML 结构缺陷,并补第 15 项门禁(WXML 结构守卫)
现象:微信开发者工具报
  [ WXML 文件编译错误] ./pages/service-history/service-history.wxml
  get tag end without start, near `</`

真实缺陷(实测 2 处,非 1 处):
- pages/service-history/service-history.wxml 多 1 个 </view>
- pages/service-program/service-program.wxml  多 1 个 </view>
(均为单行压缩写法,随更早提交入库;全仓另外 84 个 WXML 结构自洽)

根因不是这两个文件写错,而是**门禁覆盖盲区**:
既有 14 项 UI 契约全是「源码内容 / 清单断言」,没有任何一条会解析 WXML,
所以「14/14 全绿」与「小程序能不能编译」之间没有因果关系。

修复:
1. 删掉两处多余闭合标签 → 86 个 WXML 结构全部自洽。
2. 新增 scripts/lib/wxml-structure.mjs:按**引号状态机**逐字符扫描标签。
   不能用懒惰正则 —— 属性值里可以合法出现 `>`(`subtitle="{{unreadTotal > 0 ? ...}}"`),
   正则会在引号内的 `>` 处提前收尾,把自闭合标签的 `/` 读掉,报出假阳性。
   (首版实现即踩此坑,曾把合法的 messages.wxml 误判为坏文件;已复核排除。)
3. 新增 scripts/check-wxml-structure.mjs(CLI,退出码 0/1/2)。
4. 新增 ui-tests/wxml-structure-contracts.mjs:全量扫描 + **3 组内置自检样例**
   (正向样例故意在属性值内放 `>`,把「假阳性」这个坑钉进门禁)。
5. scripts/run-ui-contracts.mjs:MIN_GUARDS 14 → 15(守卫被删/改名仍会拒绝报绿)。
6. 文档口径同步:AGENTS.md、STATUS.md(4 处当前口径 + 新增 rc.17 版本行);
   ci.yml ui-contracts job 注释 14 → 15。历史时点行保留原值。

负向验证(三项,全部通过):
- 注入「多余闭合 / 未闭合」→ 退出 1,且只报坏文件、合法的 `>` 属性文件未被误报;
- UI_GUARDS_MIN=99 → 拒绝报绿(数量下限分支可自检);
- CLI 直扫坏目录 → 退出 1。

验证:node scripts/run-ui-contracts.mjs → PASS 15 / 15,0 失败,退出码 0。
未跑:微信开发者工具真实编译(本机无该环境),结论仅由结构判据支撑。
2026-09-19 11:25:05 +08:00

257 lines
9.7 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):UI 契约门禁此前**只在本地手工执行**,
# 没有 CI 把关 —— 即「改动破坏了源码结构契约也不会被发现」。
# 2026-09-19:14 → 15 个守卫,新增 WXML 结构守卫(此前 14 绿时小程序仍有两个页面
# 编译失败:service-history / service-program 各多一个 </view>)。
# 守卫是纯 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
# 防挂死:GH Actions 默认 360 分钟,起栈失败若无人超时会把流水线拖住数小时。
timeout-minutes: 30
# 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
# 「活栈供给方式」决策(2026-09-18):**在 runner 内用 Compose 起全栈**。
# 备选是 service containers + 在 runner 上 `mvn package` 后 `java -jar`,但那需要为后端
# 另写一套 host 网络配置,等于制造第二条与本地不一致的路径;Compose 全栈则与本地验证
# 路径完全相同(2026-09-18 本机按同一序列实测:中间件三项 healthcheck 全 Healthy、
# backend /health/live 200、seed 幂等可执行、**pytest tests/e2e 49 passed**)。
- name: 校验 compose 配置(秒级快速失败,早于任何构建)
working-directory: infrastructure
run: docker compose config --quiet
# 保证库为全新:seed 用 `ON CONFLICT (username) DO NOTHING`,若卷是上一轮残留,
# 账号密码会停留在上一轮的 `E2E_OPERATOR_PASSWORD` 上 → 本轮登录 401(表现为各种业务失败)。
- name: 清理可能的残留栈与数据卷
working-directory: infrastructure
run: docker compose down -v --remove-orphans || true
# 构建与起栈分两步:构建失败时的日志不混杂在 up 输出里,定位更快。
- name: 构建 backend 镜像
working-directory: infrastructure
run: docker compose build backend
- name: 起真实栈(postgres + redis + minio + backend)
# 显式列出 minio:backend 的 depends_on 虽会隐式拉起它,但隐式依赖不利于阅读,
# 且 minio 的 healthcheck(`mc ready local`)是 backend 能否启动的前置条件。
working-directory: infrastructure
run: docker compose up -d postgres redis minio backend
- name: 等待 backend 就绪(超时即失败,不静默跳过)
# 窗口 90×5s=7.5min:首次构建后还需 Spring 启动 + Flyway 46 个迁移,5min 偏紧。
run: |
for i in $(seq 1 90); 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 未在 7.5 分钟内就绪"
docker compose -f infrastructure/docker-compose.yml logs --tail=200 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
# 显式给出目录:pytest.ini 的 `testpaths = tests/e2e` 已限定范围,但显式传参可防
# 该配置被改动后悄悄扩大收集范围(仓库根另有 tests/*_test.py 不是 E2E 用例,
# 且它们在 import 期即依赖 psycopg2 与 E2E_DB_PASSWORD,会被收集成 error)。
run: pytest tests/e2e -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