Files
danyang-match/.github/workflows/ci.yml
T
WorkBuddy b624c43432 fix(ci): 依据 Actions 真实日志修复三处失败(runner 无 pnpm / 门禁缺依赖 / 生产配置未入库)
上一轮已把 CI 跑起来并读到真实结果:7 个 job 中 Mini Program 与 Test Shell Guard
通过,Admin Web、Backend、UI Contracts 三个失败,migration 与 e2e 因依赖 backend
被跳过。本轮全部依据 Actions 原始日志定位,而不是本地推测 —— 三者都不是业务代码问题。

1. Admin Web Build:runner 已不再预装 pnpm
   日志:Unable to locate executable file: pnpm
   runner 镜像(ubuntu-24.04 / 2.337.0)的 PATH 里没有 pnpm,而 setup-node 的
   cache: 'pnpm' 在 restore 阶段就需要调用 pnpm 可执行文件,于是 job 在第一步失败、
   后续步骤全部 skipped。同批的 miniapp job 没配 cache 所以正常通过,正好反向印证。
   改为先用 corepack 启用并钉住 pnpm@9.15.9,store 缓存改由 actions/cache 手动配置
   (此时 pnpm 才存在于 PATH)。

2. UI Contracts:门禁依赖没装
   日志:Cannot find module '.../miniapp/node_modules/typescript/lib/typescript.js'
   phase04 守卫会用 TypeScript 编译器 API 解析小程序源码,但本 job 此前不装依赖。
   本地能跑通只因为工作区早已装过;CI 的每个 job 都在独立 runner 上,
   miniapp job 安装的依赖不会共享过来。补上 npm install,并修正 workflow 中
   「零 npm 依赖、故无需装依赖」的错误注释 —— 与实际不符的声明本身就是缺陷来源。

3. Backend:断言了一个永远不会入库的文件
   日志:DeploymentTests.testDockerComposeProdExists 期望 true 实际 false
   infrastructure/docker-compose.prod.yml 被 .gitignore 列在
   「Secrets and certificates」段而排除出版本控制,但这是**误分类**:
   该文件不含任何明文密钥,敏感值全部走 ${VAR} 注入、db 密码走 Docker secrets,
   文件头注释本身也写着「敏感信息通过 .env 文件注入,不进 Git」。
   后果是该测试「本地恒绿(文件在工作区)、CI 恒红(checkout 拿不到)」,
   且从本地永远发现不了。现修正 .gitignore 把它纳入版本控制;
   真正的密钥仍由 secrets/、certs/、backups/ 等规则挡住。

修复后,此前因 backend 失败而被跳过的 migration 与 e2e 两个 job 也会真正跑起来。
2026-09-22 10:54:24 +08:00

299 lines
12 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'。
# 2026-09-22 实测:runner 镜像(ubuntu-24.04 / runner 2.337.0)**已不再预装
# pnpm**,而 setup-node 的 cache 步骤在 restore 阶段就需要调用 pnpm 可执行文件,
# 于是直接报 "Unable to locate executable file: pnpm",整个 job 在第一步失败、
# 后续步骤全部 skipped —— 表现为 admin-web job 秒红,且与业务代码无关。
# (同批的 miniapp job 没配 cache 所以活了下来,正好反向印证了这一点。)
# 因此:先让 pnpm 存在于 PATH,缓存改为手动配置。
- name: 启用并锁定 pnpm
# corepack 随 Node 分发,runner 上没有 pnpm 但有 corepack。
# 钉 9.15.9 的理由:pnpm 9 与 10 对「是否默认放行依赖 build 脚本」语义相反,
# 而 pnpm-workspace.yaml 里的 onlyBuiltDependencies 正是版本敏感项
# (esbuild / vue-demi 的 postinstall 若被静默阻断,会出现 install 能过、
# build 变红,且错误信息不指向真正原因)。钉死后不再随 runner 镜像升级而变化。
run: |
corepack enable pnpm
corepack prepare pnpm@9.15.9 --activate
pnpm --version
- name: 解析 pnpm store 路径
id: pnpm-store
run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
- name: 缓存 pnpm store
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('admin-web/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- 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 → 18 个守卫。
# 第 15 项 WXML 结构守卫(此前 14 绿时小程序仍有两个页面编译失败:
# service-history / service-program 各多一个 </view>);
# 第 16/17 项:`this._x()` 无定义(运行期 TypeError,曾致所有请求 Promise 永不 settle)
# 与组件 WXSS 违规选择器(标签/ID/属性选择器);
# 第 18 项:api.js 请求层**行为**契约(桩 wx.request 真跑,非源码断言)。
# 统一执行器负责逐个跑 + 汇总 + 非零退出,
# 并校验守卫数量下限(防止守卫被删除/改名后仍然报绿)。
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
# 2026-09-22 修正:本 job 此前注释称「零 npm 依赖,故无需装依赖」,这个声明是错的。
# 至少 phase04 守卫会 import miniapp/node_modules/typescript/lib/typescript.js
# (用 TS 编译器 API 解析小程序源码)。本地能跑通只因为工作区早已装过 miniapp 依赖;
# CI 的每个 job 都在**独立 runner**上,miniapp job 装的东西不会共享过来,
# 于是这里直接 ERR_MODULE_NOT_FOUND。
# 「依赖声明与实际不符」正是本地绿 / CI 红最典型的一类来源。
- name: 安装 miniapp 依赖(phase04 守卫需要 typescript 编译器 API)
working-directory: miniapp
run: npm install
- 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