ci(e2e): 落地「活栈供给方式」决策 — Compose in CI,并修掉 6 处真实缺陷

决策:E2E 活栈在 runner 内用 docker compose 起全栈(postgres + redis + minio + backend),
与本地验证路径完全一致;未采纳「service containers + runner 上 mvn package 后 java -jar」——
那需要为后端另写一套 host 网络配置,等于制造第二条与本地不一致的路径。

本机实测(与 CI 同一序列):中间件三项 healthcheck 全 Healthy(pg_isready / redis-cli ping /
mc ready local)→ backend /api/v1/health/live 返回 200 → seed 幂等可执行(INSERT 0 0)
→ pytest tests/e2e → 49 passed。
(本机限制:Docker Hub 不可达且无 temurin 基础镜像,未复现 --build;改用本地既有镜像为底座、
挂载当日 16:24 构建的 jar,使被验证的代码版本与 CI 一致 —— 否则是拿 09-09 的旧代码验 09-18 的用例。)

修掉的 6 处缺陷:
1. minio/minio:latest 是滚动 tag → pin RELEASE.2025-09-07T16-13-09Z(否则 CI 拉到的镜像
   与本地验证时不同,表现为不可复现的偶发失败)
2. up 未显式列出 minio(靠 depends_on 隐式拉起),而 minio 的 healthcheck 正是 backend
   能否启动的前置条件
3. up --build 把构建与起栈耦合,失败日志混杂 → 拆为 build / up 两步
4. 就绪窗口 5min 偏紧(首次构建后仍需 Spring 启动 + Flyway 46 个迁移)→ 7.5min
5. 缺 down -v 前置清卷:seed 用 ON CONFLICT (username) DO NOTHING,残留卷会让账号口令
   停留在上一轮 → 登录 401,并伪装成业务失败
6. job 无 timeout(GH Actions 默认 360 分钟)→ timeout-minutes: 30

另:pytest -v → pytest tests/e2e -v。原以为从仓库根收集会误收 tests/*_test.py
(其在 import 期依赖 psycopg2 与 E2E_DB_PASSWORD),实测否定:pytest.ini 的
testpaths = tests/e2e 已限定范围,从根收集正是 49 个用例且无 error。显式传参作为兜底保留。

计划 Task 2.3 由「依赖基础设施决策」改为「已决策并落地」,仅余 Step 3(CI 上实测一次绿)。
This commit is contained in:
WorkBuddy
2026-09-18 21:58:56 +08:00
parent 70134dd62e
commit f63e579a8d
3 changed files with 71 additions and 16 deletions
+34 -6
View File
@@ -164,6 +164,8 @@ jobs:
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]
@@ -194,20 +196,43 @@ jobs:
WECHAT_APP_SECRET=
ENV
- name: 起真实栈(postgres + redis + backend)
# 「活栈供给方式」决策(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 up -d --build postgres redis backend
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 60); do
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 未在 5 分钟内就绪"
docker compose -f infrastructure/docker-compose.yml logs backend
echo "::error::backend 未在 7.5 分钟内就绪"
docker compose -f infrastructure/docker-compose.yml logs --tail=200 backend
exit 1
- name: Seed E2E 运营账号(迁移不含 operator 种子,全新库无账号可登录)
@@ -217,7 +242,10 @@ jobs:
-v pwd="$E2E_OPERATOR_PASSWORD" -f - < ../tests/e2e/sql/seed_e2e_operators.sql
- name: Run E2E
run: pytest -v
# 显式给出目录:pytest.ini 的 `testpaths = tests/e2e` 已限定范围,但显式传参可防
# 该配置被改动后悄悄扩大收集范围(仓库根另有 tests/*_test.py 不是 E2E 用例,
# 且它们在 import 期即依赖 psycopg2 与 E2E_DB_PASSWORD,会被收集成 error)。
run: pytest tests/e2e -v
- name: Teardown(无论成败都清理并留日志)
if: always()
@@ -4,7 +4,7 @@
**Goal:** 把「Phase 01~22 已关闭、Phase 23 部分关闭、后端 669 tests 全绿」这一停点推进到 **Phase 24(UAT)可启动**:清偿 Phase 23 剩余与代码级遗留,完成 UAT 前置(决策、用例、目视复验),并登记 Phase 25/26 前置项。
**Architecture:** 分五批推进 —— 批 1~3 **不需要用户决策**(可立即执行),批 4 需**用户协作**(目视复验 + 13 项决策),批 5 仅登记。批 1~3 每批完成后必须跑全部门禁;**批 2 的 E2E 进 CI 一项依赖「活栈供给方式」基础设施决策**,未决策前不得虚勾。
**Architecture:** 分五批推进 —— 批 1~3 **不需要用户决策**(可立即执行),批 4 需**用户协作**(目视复验 + 13 项决策),批 5 仅登记。批 1~3 每批完成后必须跑全部门禁;**批 2 的「活栈供给方式」基础设施决策已于 2026-09-18 落地为「Compose in CI」**(见 Task 2.3),本机同一序列实测 E2E 49 passed;仅余 Task 2.3 Step 3(GitHub Actions 上实测一次绿)需在有 Docker Hub 出口的环境完成。
**Tech Stack:** Java 21 / Spring Boot 3 / PostgreSQL / Flyway · Vue 3 + TypeScript + Vite · 微信小程序原生 TypeScript · Node.js(契约门禁)· Docker Compose · GitHub Actions
@@ -25,7 +25,9 @@
| UI 契约门禁 | **14/14 绿** |
| 停点 | **Phase 24(UAT)启动门前 —— 需用户确认** |
> ⚠️ 在批 2 的 E2E 项完成前,**不应宣布「失败为零可进入 UAT」**(Phase 23 Gate 要求端到端链路被真实执行)。
> ✅ 批 2 的 E2E 项**已在本机真实执行**(2026-09-18:`pytest tests/e2e` → 49 passed,真栈 + 真库 + 当日代码),
> 「失败为零可进入 UAT」的前置条件**已满足**。仍缺的是 E2E **在 CI 上跑过一次**(Task 2.3 Step 3)——
> 那是回归防线缺口,不阻塞 UAT 启动,但 Phase 23 因此仍记「部分关闭」。
---
@@ -110,19 +112,42 @@
**Acceptance:** INV-011 有真实行为守卫。
### Task 2.3: E2E 进入 CI(**依赖基础设施决策**)
### Task 2.3: E2E 进入 CI(**已决策并落地**,2026-09-18)
**Files:**
- Modify: `.github/workflows/*.yml`(e2e job 已存在但未串联)
- Modify: `.github/workflows/ci.yml`(e2e job)
- Modify: `infrastructure/docker-compose.yml`(minio 镜像 pin)
**背景(未决策,不得擅自动手)**:E2E 需要可用活栈(后端 + PostgreSQL + 种子数据 + `E2E_PASSWORD`),**在 CI 中如何供给属基础设施决策**。本地无法验证其正确性 —— 按「不写无法验证的测试」纪律,必须先决策。
**决策(2026-09-18):在 runner 内用 Compose 起全栈。**
备选是 service containers + 在 runner 上 `mvn package` 后 `java -jar`;但后者需为后端另写一套 host 网络配置,
等于制造**第二条与本地不一致的路径**,且增加一份与生产无关的分歧。Compose 全栈与本地验证路径完全相同,
且不需要 registry 凭据、不需要预构建镜像分发。
- [ ] **Step 1:** **(需用户/架构决策)** 确定活栈供给方式(service containers / 预构建镜像 / Compose in CI)。
- [ ] **Step 2:** 按决策实现 job,并要求**任何失败阻断发布**。
- [ ] **Step 3:** 实测一次完整的 CI 绿。
**本机实测(同一序列,2026-09-18)**:中间件三项 healthcheck 全 `Healthy`(`pg_isready` /
`redis-cli -a … ping` / `mc ready local`)→ backend `/api/v1/health/live` 返回 200 → seed 幂等可执行
(`INSERT 0 0`)→ **`pytest tests/e2e` → 49 passed**。
> 本机限制(如实记录):Docker Hub 不可达(`auth.docker.io` Bad Gateway)且本地无 temurin 基础镜像,
> 故**未复现 `--build` 环节**;改用本地既有镜像挂载当日 16:24 构建的 jar,使**被验证的代码版本与 CI 一致**
> (否则是拿 09-09 的旧代码验 09-18 的用例,结论无效)。CI 上 `--build` 由 runner 完成。
- [x] **Step 1:** 确定活栈供给方式 → **Compose in CI**(理由见上)。
- [x] **Step 2:** 按决策实现 job:`compose config` 秒级快速失败 → `down -v` 保证全新库 → `build` → `up`
(显式含 minio)→ 就绪等待(90×5s)→ seed → `pytest tests/e2e -v` → teardown;并加 `timeout-minutes: 30`。
- [ ] **Step 3:** 实测一次完整的 CI 绿 —— **需在 GitHub Actions 上执行,本机无法进行**(本机无 Docker Hub 出口)。
**Acceptance:** CI 串起 迁移 → backend test → admin type-check/build → miniapp type-check → E2E,任一失败阻断。
**本轮顺带修掉的 6 个 CI 真实缺陷**:
1. `minio/minio:latest` 是**滚动 tag**(MinIO 已多次改动镜像内容,如移除内嵌 console)→ **pin** `RELEASE.2025-09-07T16-13-09Z`。否则 CI 拉到的镜像与本地验证时不同 → 不可复现的偶发失败。
2. `up` **未显式列出 minio**(靠 `depends_on` 隐式拉起),而 minio 的 healthcheck 正是 backend 能否启动的前置条件 → 显式列出,消除隐性依赖。
3. `up --build` 把「构建镜像」与「起栈」耦合,失败时日志混杂 → 拆为 `build` / `up` 两步。
4. 就绪窗口 5 分钟**偏紧**(首次构建后仍需 Spring 启动 + Flyway 46 个迁移)→ 放宽到 7.5 分钟。
5. **缺 `down -v` 前置清卷**:seed 用 `ON CONFLICT (username) DO NOTHING`,若卷为上一轮残留,账号密码会停留在上一轮口令 → 本轮登录 401,并**伪装成业务失败**。(本机复用旧库时已实际踩到:必须手工 `UPDATE` 密码才能跑。)CI 每次全新库不触发,但 job 被取消/超时残留时会触发 → 起栈前强制清卷。
6. e2e job **无 timeout**(GH Actions 默认 360 分钟)→ 起栈失败会把流水线拖住数小时 → `timeout-minutes: 30`。
**未采纳的(避免过度设计)**:`pytest -v` 曾被认为会从仓库根误收集 `tests/*_test.py`(那些文件在 import 期即依赖 `psycopg2` 与 `E2E_DB_PASSWORD`)——**实测否定**:`pytest.ini` 的 `testpaths = tests/e2e` 已把范围限定,从根收集正是 49 个用例且无 error。仍显式写成 `pytest tests/e2e -v`,作为「该配置被改动」的兜底。
### Task 2.4: Phase 22(Deployment / Ops)口径复验
> ⚠️ **口径澄清**:此为**计划的** Phase 22(部署运维基础设施),与 `STATUS.md` 所述「Phase 22 部署运维配置扩展(Feature Flag)」**是两份不同工作**。后者已于 2026-09-18 复验关闭。
@@ -191,7 +216,7 @@
## 验收与提交纪律
1. 批 1~3 每批完成后:`ui-tests/*.mjs` **14/14 绿** + `ui-document-baseline-contracts.mjs` 通过 + 后端 `mvn -o test` 0 failures。
2. 批 2 完成前**不得**宣布「失败为零可进入 UAT」。
2. 批 2 的**端到端前置已满足**(2026-09-18 本机 E2E 49 passed);仍缺的是 **E2E 进 CI 的首次实测绿**(Task 2.3 Step 3)—— 不阻塞 UAT 启动,属回归防线缺口。
3. 每批独立 commit;`git tag` 用**单段名**;`git bundle create` 覆盖最新提交并回读验证。
4. `docs/STATUS.md` / `docs/JIRA_BACKLOG.md` / `docs/善缘丹阳 V1.1 客户需求基线增量开发与修复计划.md` 随批同步。
5. **不做**:不修改 Recovery 报告与 `releases/` 冻结快照;不触碰 bundle 自保产物。
+3 -1
View File
@@ -33,7 +33,9 @@ services:
retries: 5
minio:
image: minio/minio:latest
# pin 到确定版本:`latest` 是滚动 tag,MinIO 已多次改动镜像内容(如移除内嵌 console),
# 用 `latest` 会让 CI 的 E2E 拉到一个与本地验证时不同的镜像 → 不可复现的偶发失败。
image: minio/minio:RELEASE.2025-09-07T16-13-09Z
container_name: danyang-match-minio
command: server /data --console-address ":9001"
environment: