6945f194b5
.gitlab-ci.yml:契约与实体对账 / 后端编译 / 管理端类型检查与构建 / 学生端 H5 构建 /
接口断言(全套 E2E 打真库)/ 生产环境自检;密钥每次随机生成,顺带证明应用没有偷偷
依赖 dev 默认值。
ProdEnvCheck 是 EnvironmentPostProcessor,跑在任何 bean 之前,拦 6 类「配错了也能启动」;
判「没配」与「配了」要 try { getProperty } catch(F-86)——
getProperty 会解析占位符、失败就抛,只读 PropertySource 又读不到 env 的值。
删掉 Gi 模板残留的 GitHub Pages 工作流(远端是 GitLab、用 pnpm,且它发布的是没有 API 的静态页)。
验证:scripts/e2e-prodguard.py 9/9(三种误配形态都拒绝启动)。
223 lines
8.7 KiB
YAML
223 lines
8.7 KiB
YAML
# ═══════════════════════════════════════════════════════════
|
||
# GitLab CI(M0-04 / M7-10)
|
||
#
|
||
# 设计原则:**流水线跑的必须是我们真正依赖的那套验证**。
|
||
# 这个仓库没有单元测试(`src/test` 是空的),验证靠三样东西:
|
||
# ① 三个机械校验脚本(契约对账 / 实体列 / 文档一致性)
|
||
# ② 编译与构建(后端 jar、PC、H5)
|
||
# ③ **`scripts/e2e-*.py` 打真实接口 + 查库断言**(1500+ 条)
|
||
# 所以 e2e 阶段不是"锦上添花",它是这个项目的主验证手段 ——
|
||
# 一个只跑编译的流水线在这里等于没跑。
|
||
#
|
||
# ⚠️ 密钥策略:e2e 阶段**每次随机生成** JWT/AES/HMAC/RSA 密钥,
|
||
# 不存任何 CI 变量。理由是它能顺带证明一件事:
|
||
# **应用没有偷偷依赖某个 dev 默认值** —— 如果它依赖了,这里就会红。
|
||
# (PG/Redis 的密码是流水线内一次性容器的,随机即可。)
|
||
# ═══════════════════════════════════════════════════════════
|
||
|
||
stages:
|
||
- check # 不需要数据库:契约对账、文档一致性、类型检查
|
||
- build # 编译与打包
|
||
- e2e # 起真库、起服务、跑全套接口断言
|
||
|
||
variables:
|
||
# 依赖缓存放仓库内(GitLab Runner 的默认缓存目录不一定可写)
|
||
MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
|
||
XT_PG_IMAGE: "postgres:16-alpine"
|
||
XT_REDIS_IMAGE: "redis:7-alpine"
|
||
# E2E 的库与缓存
|
||
PG_HOST: "postgres"
|
||
PG_PORT: "5432"
|
||
PG_DB: "filldb"
|
||
PG_USER: "fill"
|
||
REDIS_HOST: "redis"
|
||
REDIS_PORT: "6379"
|
||
|
||
# ── 缓存:Maven 仓库与 pnpm store ─────────────────────────
|
||
.maven-cache: &maven-cache
|
||
key: "maven-$CI_COMMIT_REF_SLUG"
|
||
paths:
|
||
- .m2/repository/
|
||
|
||
.pnpm-cache: &pnpm-cache
|
||
key: "pnpm-$CI_COMMIT_REF_SLUG"
|
||
paths:
|
||
- .pnpm-store/
|
||
|
||
# ══════════════ 阶段 1:机械校验 ══════════════
|
||
|
||
契约与实体对账:
|
||
stage: check
|
||
image: python:3.12-slim
|
||
script:
|
||
- echo "── 契约 §4 的 121 个接口 vs 代码里注册的路由 ──"
|
||
- python scripts/check-api-parity.py --quiet
|
||
- echo "── 实体字段 vs 迁移 DDL ──"
|
||
- python scripts/check-entity-columns.py --quiet
|
||
- echo "── 文档自身的一致性(表格列数 / FR 编号) ──"
|
||
- python scripts/verify-docs.py
|
||
- echo "── 手册里引用的错误码与文案是否真的存在(客服照着它回答) ──"
|
||
- python scripts/check-manual.py
|
||
- echo "── 全部 e2e 脚本的语法(引号写错会当场红,而不是跑到一半才红) ──"
|
||
- |
|
||
python - <<'PY'
|
||
import io, os, sys
|
||
bad = 0
|
||
for fn in sorted(os.listdir('scripts')):
|
||
if fn.startswith('e2e-') and fn.endswith('.py'):
|
||
try:
|
||
compile(io.open('scripts/' + fn, encoding='utf-8').read(), fn, 'exec')
|
||
except SyntaxError as e:
|
||
bad += 1
|
||
print(' [语法错] %s:%s %s' % (fn, e.lineno, (e.text or '').strip()[:80]))
|
||
sys.exit(1 if bad else 0)
|
||
PY
|
||
rules:
|
||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||
- if: $CI_COMMIT_BRANCH
|
||
|
||
后端编译:
|
||
stage: build
|
||
image: maven:3.9-eclipse-temurin-21
|
||
cache: *maven-cache
|
||
script:
|
||
- cd backend
|
||
- mvn -B -DskipTests clean package
|
||
# 产物留一份,后面 e2e 阶段直接用(不用再编译一次)
|
||
- cp xt-server/target/xt-server-*.jar ../xt-server.jar
|
||
artifacts:
|
||
paths:
|
||
- xt-server.jar
|
||
expire_in: 1 day
|
||
|
||
管理端类型检查与构建:
|
||
stage: build
|
||
image: node:20
|
||
cache: *pnpm-cache
|
||
before_script:
|
||
- corepack enable
|
||
- pnpm config set store-dir .pnpm-store
|
||
script:
|
||
- cd frontend-pc
|
||
- pnpm install --frozen-lockfile
|
||
- pnpm typecheck
|
||
# lint 只卡**本次改动涉及的文件**:模板自带的文件里有一批既有告警,
|
||
# 全量 lint 会让流水线永远是红的,从而失去意义(红久了就没人看了)。
|
||
#
|
||
# ⚠️ 这段只能在 Linux 上跑 —— 本机 Windows 的 cmd.exe 解析不了单引号 pathspec,
|
||
# 所以它是照 Linux 语义写的、本地验不到。首次上 runner 时留意,
|
||
# 见 docs/09-部署与运维.md §6「首次接入 CI 的检查单」。
|
||
- |
|
||
BASE="${CI_MERGE_REQUEST_DIFF_BASE_SHA:-HEAD~1}"
|
||
FILES=$(git diff --name-only --diff-filter=ACMR "$BASE" HEAD -- frontend-pc/ 2>/dev/null \
|
||
| grep -E '\.(vue|ts)$' | sed 's|^frontend-pc/||' || true)
|
||
if [ -n "$FILES" ]; then
|
||
echo "只检查本次改动的文件:"; echo "$FILES"
|
||
npx eslint $FILES
|
||
else
|
||
echo "本次没有改前端文件,跳过 lint"
|
||
fi
|
||
- pnpm build
|
||
|
||
学生端 H5 构建:
|
||
stage: build
|
||
image: node:20
|
||
cache: *pnpm-cache
|
||
before_script:
|
||
- corepack enable
|
||
- pnpm config set store-dir .pnpm-store
|
||
script:
|
||
- cd frontend-mp
|
||
- pnpm install --frozen-lockfile
|
||
# ⚠️ H5 是**后端托管**的(没有独立 dev server),所以它的构建产物是
|
||
# e2e 阶段起服务时的输入,不能跳过
|
||
- pnpm build:h5
|
||
artifacts:
|
||
paths:
|
||
- frontend-mp/dist/build/h5/
|
||
expire_in: 1 day
|
||
|
||
# ══════════════ 阶段 3:全套接口断言 ══════════════
|
||
|
||
.e2e-base: &e2e-base
|
||
stage: e2e
|
||
image: eclipse-temurin:21-jre
|
||
services:
|
||
- name: $XT_PG_IMAGE
|
||
alias: postgres
|
||
- name: $XT_REDIS_IMAGE
|
||
alias: redis
|
||
variables:
|
||
# 这两个容器只在本次流水线里活着,密码随机即可
|
||
POSTGRES_DB: "filldb"
|
||
POSTGRES_USER: "fill"
|
||
POSTGRES_PASSWORD: "ci_pg_pwd"
|
||
PG_PASSWORD: "ci_pg_pwd"
|
||
REDIS_PASSWORD: ""
|
||
before_script:
|
||
# e2e 脚本要 curl 发 multipart、要 python3 跑断言、要 psql 查库
|
||
- apt-get update -qq && apt-get install -y -qq python3 python3-pip curl postgresql-client
|
||
- pip3 install --quiet --break-system-packages openpyxl requests
|
||
# ⚠️ 每次随机的密钥:见文件头"密钥策略"
|
||
- export XT_JWT_SECRET=$(openssl rand -base64 64 | tr -d '\n')
|
||
- export XT_AES_KEY=$(openssl rand -base64 32 | tr -d '\n')
|
||
- export XT_HMAC_KEY=$(openssl rand -base64 48 | tr -d '\n')
|
||
- export XT_RSA_PRIVATE_KEY=$(openssl rand -base64 512 | tr -d '\n')
|
||
- export REDIS_PASSWORD=""
|
||
script:
|
||
- echo "── 先单独验一次 test profile 能不能起来(失败了就是配置问题,不是脚本问题) ──"
|
||
- python3 scripts/check-test-profile.py
|
||
- echo "── 起后端(test profile) ──"
|
||
- |
|
||
java -jar xt-server.jar --spring.profiles.active=test \
|
||
--server.port=8080 > /tmp/xt-server.log 2>&1 &
|
||
for i in $(seq 1 90); do
|
||
if curl -sf http://localhost:8080/api/v1/auth/captcha > /dev/null; then
|
||
echo "后端已就绪(第 ${i} 次探测)"; break
|
||
fi
|
||
sleep 3
|
||
done
|
||
curl -sf http://localhost:8080/api/v1/auth/captcha > /dev/null \
|
||
|| { echo "后端没起来,日志尾部:"; tail -80 /tmp/xt-server.log; exit 1; }
|
||
- echo "── 依次跑全部 e2e 脚本 ──"
|
||
- |
|
||
fail=0
|
||
total=0
|
||
for f in $(ls scripts/e2e-*.py | sort); do
|
||
out=$(python3 "$f" 2>&1)
|
||
line=$(echo "$out" | grep -E '^通过 [0-9]+ 失败 [0-9]+ 共 [0-9]+' | tail -1)
|
||
if [ -z "$line" ]; then
|
||
echo " [无汇总] $f"; echo "$out" | tail -20; fail=1; continue
|
||
fi
|
||
n=$(echo "$line" | sed -E 's/^通过 ([0-9]+).*/\1/')
|
||
bad=$(echo "$line" | sed -E 's/.*失败 ([0-9]+).*/\1/')
|
||
total=$((total + n))
|
||
echo " $f:$n 条,失败 $bad"
|
||
if [ "$bad" != "0" ]; then fail=1; echo "$out" | tail -30; fi
|
||
done
|
||
echo "断言合计:$total"
|
||
[ "$fail" = "0" ] || exit 1
|
||
artifacts:
|
||
when: always
|
||
paths:
|
||
- /tmp/xt-server.log
|
||
|
||
接口断言(全套 E2E):
|
||
extends: .e2e-base
|
||
rules:
|
||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||
- if: $CI_COMMIT_BRANCH
|
||
|
||
# ⚠️ 生产自检单独一个 job:**它验的是"配错时拒绝启动"**,
|
||
# 而这恰好是最容易被后来的改动悄悄破坏、且没人会发现的一件事
|
||
# (自检不生效时,一切"看起来都正常")
|
||
生产环境自检:
|
||
stage: e2e
|
||
image: eclipse-temurin:21-jre
|
||
script:
|
||
- apt-get update -qq && apt-get install -y -qq python3
|
||
- python3 scripts/e2e-prodguard.py
|
||
rules:
|
||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||
- if: $CI_COMMIT_BRANCH
|