Files
WorkBuddy 797b1c46cf refactor(storage): 对象存储从 MinIO 换成 SeaweedFS(钉 4.47)
动机:MinIO 的 GitHub 仓库已于 2026-02 归档,不再产出安全补丁,已累积多个
CVE 且不修;本项目存身份证 / 实名材料 / 用户照片,无人修 CVE 才是真正驱动因素
(Docker Hub 下架只是表象,quay.io 仍在提供)。

变更:
- 三套 compose(dev / staging / prod)的 minio 服务替换为 seaweedfs 4.47:
  覆盖 entrypoint 渲染 -s3.config 注入 identity(S3 网关默认不鉴权,必须显式开启),
  -volume.max=100 解除第二个 bucket 写入时的 InternalError。
- 新增一次性 storage-init 容器,经 filer 接口预建 public / private 桶
  (SeaweedFS PutObject 到不存在的 bucket 直接失败;全仓库原无任何 createBucket)。
- S3Config 显式开启 path-style:AWS SDK v1 默认 virtual-host 会把 bucket 拼进
  主机名,容器名在 DNS 中不存在必然失败(本机有 http_proxy 时易误判为 502 拒绝)。
- ci.yml:镜像校验与起栈步骤改为 seaweedfs + storage-init。
- 运维脚本 health-check.sh / run-uat.sh 的 MinIO 探针改为 SeaweedFS
  (S3 网关开启鉴权后匿名根请求返回 403,200/403 均算存活)。

注:path-style 与建桶属换存储的必要项;presigned URL 的 host 须对外可达这一
既有缺陷(原 minio:9000 直发客户端不可达)留待下一轮「改缺陷」处理。
2026-09-22 14:26:48 +08:00

312 lines
13 KiB
YAML
Raw Permalink 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
# 2026-09-22:compose 的对象存储已换成 chrislusf/seaweedfs:4.47 并钉版。
# 单独保留这一步的理由:镜像拉取失败在 compose up 里会埋进几十行输出,
# 这里能在起栈前秒级暴露「上游镜像下架」这类外部依赖失效 ——
# 上一轮正是靠日志里的 pull access denied 才定位到 MinIO 已移除 Docker Hub 仓库
# (minio/minio 现返回 404;教训是「第三方镜像」本身就是外部依赖风险点)。
- name: 校验 seaweedfs 钉版镜像可拉取并打印版本
run: |
docker pull chrislusf/seaweedfs:4.47
docker run --rm chrislusf/seaweedfs:4.47 version
# 保证库为全新: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 + seaweedfs + 建桶 + backend)
# 显式列出 seaweedfs 与 storage-init:backend 的 depends_on 虽会隐式拉起它们,
# 但隐式依赖不利于阅读,且 seaweedfs 的 healthcheck 是 backend 启动的前置条件。
# storage-init 是**一次性**建桶容器 —— SeaweedFS 不会自动建桶,
# PutObject 到不存在的 bucket 会直接失败,少了它照片链路就是断的
# (E2E 对存储零覆盖,不会替你发现)。
working-directory: infrastructure
run: docker compose up -d postgres redis seaweedfs storage-init 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