Files
xiaotutongxue/.gitlab-ci.yml
T
lovife 6945f194b5 chore(ci): CI/CD 三阶段 6 job + test/prod 环境 + 启动前的配置自检
.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(三种误配形态都拒绝启动)。
2026-09-30 09:02:57 +08:00

223 lines
8.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.
# ═══════════════════════════════════════════════════════════
# 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