chore: add CI pipeline, docs gate, OpenAPI snapshot, PR template, docs skeleton

This commit is contained in:
2026-02-15 19:21:03 +08:00
parent d0a05040f4
commit 05cd327fe1
11 changed files with 482 additions and 63 deletions
+33 -37
View File
@@ -1,42 +1,38 @@
## PR 信息
## 变更摘要
- [ ] feat
- [ ] fix
- [ ] refactor
- [ ] chore
- [ ] docs
- [ ] perf
- [ ] test
### 需求号
<!-- 填写关联的需求编号,如 R-001。如无需求关联,写"无"并说明原因。 -->
## 影响模块(可多选)
- [ ] module: dict
- [ ] module: sysconfig
- [ ] module: department
- [ ] module: menu
- [ ] module: user
- [ ] module: role
- [ ] module: auth
- [ ] module: log
- [ ] module: other
## Doc Impact(必选其一,否则不允许合并)
- [ ] docs:updated (已更新文档,列出文件)
- [ ] docs:not-needed (不需要文档,写一句理由)
- [ ] docs:todo (需要但尚未补齐:禁止合并,除非标记 hotfix)
### 契约变更说明
<!-- 本次 PR 是否涉及以下契约变更?如有,请详细说明变更内容。 -->
- [ ] OpenAPI 接口变更(新增/修改/删除端点、字段、错误码)
- [ ] 数据库 Schema 变更(新增 Flyway migration)
- [ ] 权限变更(新增/修改 permission 字符串)
- [ ] 租户隔离策略变更
### 文档变更清单/理由
- docs files:
- `docs/CHANGELOG.md`
- `docs/MODULES/xxx.md`
- `docs/API/overview.md`
- `docs/ADR/xxxx.md`
- reason:
变更详情:
## OpenAPI(如有接口/DTO/权限变更)
- [ ] openapi:changed(会影响合约)
- [ ] openapi:not-changed
### 控制面影响
<!-- 本次变更是否影响以下控制面? -->
- [ ] 认证/鉴权流程
- [ ] 租户隔离逻辑
- [ ] 日志/可观测性
- [ ] CI Gate 脚本
- [ ] 配置项(application.yml / 环境变量)
影响详情:
### 测试证据
<!-- 请列出本次变更对应的测试用例。 -->
- [ ] 单元测试通过(./mvnw test)
- [ ] 集成测试通过(./mvnw -PIT verify)
- [ ] 手动验证(如适用,描述步骤)
测试详情:
### 自检清单
- [ ] 代码遵循分层约定(controller→facade→appservice→domain←infra)
- [ ] tenant_id 来自 SecurityContext,未从 DTO/请求参数获取
- [ ] @PreAuthorize 使用的权限字符串已在 seed 数据中定义
- [ ] Flyway migration 版本号不冲突(MVP: V001-V099)
- [ ] 无"省略/TODO/自行补充"的代码提交
## 其他说明
+77 -26
View File
@@ -1,51 +1,102 @@
name: LJWX Platform CI
name: ci
on:
push:
branches: [main]
branches: ["main"]
pull_request:
branches: [main]
branches: ["main"]
env:
JAVA_VERSION: "21"
jobs:
gate-all:
build-and-gate:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
image: postgres:16
env:
POSTGRES_DB: ljwx_platform_test
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_USER: ljwx
POSTGRES_PASSWORD: ljwx123
ports:
- 5432:5432
options: >-
--health-cmd="pg_isready -U test"
--health-interval=10s
--health-timeout=5s
--health-retries=5
--health-cmd "pg_isready -U ljwx"
--health-interval 5s
--health-timeout 5s
--health-retries 10
steps:
- name: Checkout
uses: actions/checkout @v4
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Java 21
uses: actions/setup-java @v4
- name: Set up JDK
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
cache: 'maven'
distribution: "temurin"
java-version: ${{ env.JAVA_VERSION }}
cache: "maven"
- name: Make scripts executable
run: |
chmod +x mvnw
find scripts/ -name "*.sh" -exec chmod +x {} \;
run: chmod +x scripts/ci/*.sh
- name: Run gate-all
run: bash scripts/gates/gate-all.sh
# ── Build ──
- name: Compile
run: ./mvnw -q -DskipTests package
# ── Unit Tests ──
- name: Unit tests
run: ./mvnw -q test
# ── Integration Tests ──
- name: Integration tests
env:
DB_URL: jdbc:postgresql://localhost:5432/ljwx_platform_test
DB_USERNAME: test
DB_PASSWORD: test
SPRING_PROFILES_ACTIVE: test
SPRING_DATASOURCE_URL: jdbc:postgresql://localhost:5432/ljwx_platform_test
SPRING_DATASOURCE_USERNAME: ljwx
SPRING_DATASOURCE_PASSWORD: ljwx123
run: ./mvnw -q -PIT verify
# ── Export OpenAPI snapshot ──
- name: Export OpenAPI snapshot
env:
PORT: "18080"
PROFILE: "test"
SPRING_DATASOURCE_URL: jdbc:postgresql://localhost:5432/ljwx_platform_test
SPRING_DATASOURCE_USERNAME: ljwx
SPRING_DATASOURCE_PASSWORD: ljwx123
run: scripts/ci/export-openapi.sh
# ── Docs Gate ──
- name: Docs gate
if: github.event_name == 'pull_request'
env:
BASE_REF: "origin/main"
run: scripts/ci/docs-gate.sh
# ── OpenAPI Diff Gate ──
- name: OpenAPI diff gate
if: github.event_name == 'pull_request'
env:
BASE_REF: "origin/main"
run: scripts/ci/openapi-diff.sh
# ── Ensure snapshot committed ──
- name: Ensure OpenAPI snapshot committed
run: |
if git diff --exit-code docs/API/openapi.snapshot.yaml 2>/dev/null; then
echo "[openapi] snapshot clean or not yet generated. PASS."
else
echo "============================================"
echo "[openapi] FAIL: openapi.snapshot.yaml changed during CI but not committed."
echo ""
echo "Please run locally:"
echo " scripts/ci/export-openapi.sh"
echo " git add docs/API/openapi.snapshot.yaml"
echo " git commit -m 'chore: update openapi snapshot'"
echo "============================================"
exit 1
fi
+10
View File
@@ -0,0 +1,10 @@
# Breaking Changes
> Update this file whenever `docs/API/openapi.snapshot.yaml` changes.
> CI will reject PRs that change the snapshot without updating this file.
## Format
- YYYY-MM-DD: <describe change> (BREAKING | non-breaking)
## Log
- 2026-02-15: Initial API surface established (non-breaking)
+14
View File
@@ -0,0 +1,14 @@
# OpenAPI snapshot placeholder
# This file is auto-generated by scripts/ci/export-openapi.sh
# Do not edit manually unless export is not yet available.
#
# To generate:
# scripts/ci/export-openapi.sh
#
# CI will fail if this file changes but is not committed.
openapi: "3.0.0"
info:
title: ljwx-platform API
version: 0.0.0-snapshot
description: Placeholder - run export-openapi.sh to generate actual spec
paths: {}
+65
View File
@@ -0,0 +1,65 @@
# API Overview
Base URL: `/api/v1`
## Auth
| Method | Path | Permission | Description |
|--------|------|------------|-------------|
| POST | /auth/login | - | 登录 |
| POST | /auth/refresh | - | 刷新 Token |
| POST | /auth/logout | - | 登出 |
## Menu
| Method | Path | Permission | Description |
|--------|------|------------|-------------|
| GET | /menus | menu:read | 菜单列表 |
| GET | /menus/{id} | menu:read | 菜单详情 |
| POST | /menus | menu:write | 创建菜单 |
| PUT | /menus/{id} | menu:write | 更新菜单 |
| DELETE | /menus/{id} | menu:write | 删除菜单 |
| GET | /menus/me | (any auth) | 当前用户菜单树 |
## Role
| Method | Path | Permission | Description |
|--------|------|------------|-------------|
| GET | /roles | role:read | 角色列表 |
| GET | /roles/{id} | role:read | 角色详情 |
| POST | /roles | role:write | 创建角色 |
| PUT | /roles/{id} | role:write | 更新角色 |
| DELETE | /roles/{id} | role:write | 删除角色 |
| PUT | /roles/{id}/permissions | role:write | 分配权限 |
| PUT | /roles/user-roles | role:write | 分配用户角色 |
| GET | /roles/user-roles/{userId} | role:read | 查询用户角色 |
## Dict
| Method | Path | Permission | Description |
|--------|------|------------|-------------|
| GET | /dict-types | dict:read | 字典类型列表 |
| GET | /dict-data | dict:read | 字典数据列表 |
## SysConfig
| Method | Path | Permission | Description |
|--------|------|------------|-------------|
| GET | /sys-configs | config:read | 配置列表 |
| GET | /sys-configs/{id} | config:read | 配置详情 |
## Department
| Method | Path | Permission | Description |
|--------|------|------------|-------------|
| GET | /departments | dept:read | 部门列表 |
| GET | /departments/{id} | dept:read | 部门详情 |
| POST | /departments | dept:write | 创建部门 |
| PUT | /departments/{id} | dept:write | 更新部门 |
| DELETE | /departments/{id} | dept:write | 删除部门 |
## User (Phase 2 - planned)
| Method | Path | Permission | Description |
|--------|------|------------|-------------|
| GET | /users | user:read | 用户列表 |
| GET | /users/{id} | user:read | 用户详情 |
| POST | /users | user:write | 创建用户 |
| PUT | /users/{id} | user:write | 更新用户 |
| DELETE | /users/{id} | user:write | 删除用户 |
| GET | /users/me | (any auth) | 当前用户信息 |
| PUT | /users/{id}/reset-password | user:write | 重置密码 |
| PUT | /users/{id}/status | user:write | 切换状态 |
+29
View File
@@ -0,0 +1,29 @@
# Changelog
All notable changes to this project will be documented in this file.
Format based on [Keep a Changelog](https://keepachangelog.com/).
## [Unreleased]
### Added
- Dict module (V101-V102)
- SysConfig module (V103-V104)
- Menu module (V107-V109)
- User/Role enhancement (V110-V111)
- Audit field alignment (V112)
- Departments table (V113-V114)
- SecurityUtil centralized auth extraction
- Global exception handler
- Role CRUD with tenant isolation
- UserRole assignment with dedup
- GET /api/v1/menus/me tree endpoint
### Fixed
- V111 ADMIN permission binding across all tenants
- UserRoleAppService duplicate roleId handling
- Menu permission_key consistency for all 6 second-level menus
- Bean naming conflicts between legacy and new infrastructure packages
### Changed
- Standardized audit fields: create_* NOT NULL, update_* nullable, is_deleted SMALLINT DEFAULT 0
- Replaced private currentUsername/currentUserId/currentTenantId with SecurityUtil.*
+36
View File
@@ -0,0 +1,36 @@
# <Module Name>
## 定位
一句话说明模块职责。
## 数据库
| 表 | 描述 | 租户级 |
|----|------|--------|
| xxx | ... | 是/否 |
## 实体
- `XxxModel` → 字段列表
## 端点
| Method | Path | Permission | Description |
|--------|------|------------|-------------|
| GET | /api/v1/xxx | xxx:read | ... |
## 权限
| Key | 描述 |
|-----|------|
| xxx:read | 查看 |
| xxx:write | 管理 |
## 迁移脚本
| Version | 文件 | 描述 |
|---------|------|------|
| Vxxx | V...sql | ... |
## 特殊规则
- 规则 1:...
- 规则 2:...
## 依赖
- 依赖模块:auth, tenant
- 被依赖:user
+29
View File
@@ -0,0 +1,29 @@
# ADR-XXXX: <Title>
## Status
Proposed | Accepted | Deprecated | Superseded by ADR-YYYY
## Date
YYYY-MM-DD
## Context
为什么要做这个决策?当前遇到了什么问题?
## Decision
做了什么决策(明确、具体)。
## Alternatives Considered
| 方案 | 优点 | 缺点 | 结论 |
|------|------|------|------|
| A | ... | ... | 不采用 |
| B | ... | ... | 采用 |
## Consequences
- 好处:
- 代价:
- 风险:
## References
- PR: #xxx
- Migration: Vxxx
- Related ADR: ADR-xxxx
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
set -euo pipefail
BASE_REF="${BASE_REF:-origin/main}"
echo "[docs-gate] base ref: ${BASE_REF}"
# 确保有 base ref
git fetch origin main --depth=50 2>/dev/null || true
# 获取变更文件列表
CHANGED="$(git diff --name-only "${BASE_REF}...HEAD" 2>/dev/null || git diff --name-only HEAD~1 || true)"
if [[ -z "${CHANGED}" ]]; then
echo "[docs-gate] no changed files detected. PASS."
exit 0
fi
echo "[docs-gate] changed files:"
echo "${CHANGED}"
echo ""
# 1) 判断是否属于"必须带文档"的代码变更
NEED_DOC=0
echo "${CHANGED}" | grep -E -q \
'(controller|facade|dto|security|exception)/.*\.java$|db/migration/.*\.sql$|.*(Controller|Facade|Request|Response)\.java$' \
&& NEED_DOC=1 || true
if [[ "${NEED_DOC}" -eq 0 ]]; then
echo "[docs-gate] no doc-required code changes detected. PASS."
exit 0
fi
echo "[docs-gate] doc-requiring code changes detected."
# 2) 检查 docs 是否有变化
DOC_CHANGED=0
echo "${CHANGED}" | grep -E -q \
'^docs/CHANGELOG\.md$|^docs/MODULES/|^docs/API/overview\.md$|^docs/API/breaking-changes\.md$|^docs/ADR/|^docs/adr/' \
&& DOC_CHANGED=1 || true
# 3) 检查 openapi snapshot 是否变化
OPENAPI_CHANGED=0
echo "${CHANGED}" | grep -E -q '^docs/API/openapi\.snapshot\.yaml$' && OPENAPI_CHANGED=1 || true
if [[ "${DOC_CHANGED}" -eq 1 || "${OPENAPI_CHANGED}" -eq 1 ]]; then
echo "[docs-gate] docs or openapi snapshot updated. PASS."
exit 0
fi
cat <<'EOF2'
============================================
[docs-gate] FAIL
You changed code that requires docs updates, but no docs were changed.
Fix options (choose at least one):
1) Update docs/CHANGELOG.md (recommended, 1-3 lines)
2) Update a module doc: docs/MODULES/<module>.md
3) Update docs/API/overview.md or docs/ADR/* (docs/adr/*)
4) If API changed, run scripts/ci/export-openapi.sh and commit the snapshot
============================================
EOF2
exit 1
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env bash
set -euo pipefail
OUT="docs/API/openapi.snapshot.yaml"
PORT="${PORT:-8080}"
PROFILE="${PROFILE:-test}"
MAX_WAIT="${MAX_WAIT:-90}"
echo "[openapi] export to ${OUT}, port=${PORT}, profile=${PROFILE}"
# 1) 编译(如果 jar 不存在)
JAR="$(find ljwx-platform-app/target -name '*.jar' -not -name '*-sources*' -not -name '*-javadoc*' 2>/dev/null | head -n1 || true)"
if [[ -z "${JAR}" ]]; then
echo "[openapi] jar not found, building..."
./mvnw -q -DskipTests package
JAR="$(find ljwx-platform-app/target -name '*.jar' -not -name '*-sources*' -not -name '*-javadoc*' | head -n1)"
fi
echo "[openapi] using jar: ${JAR}"
# 2) 后台启动应用
nohup java -jar "${JAR}" \
--spring.profiles.active="${PROFILE}" \
--server.port="${PORT}" \
> /tmp/openapi-app.log 2>&1 &
APP_PID=$!
cleanup() {
echo "[openapi] stopping app pid=${APP_PID}"
kill "${APP_PID}" 2>/dev/null || true
wait "${APP_PID}" 2>/dev/null || true
}
trap cleanup EXIT
# 3) 等待应用就绪(先试 actuator,再试 OpenAPI 端点自身)
echo "[openapi] waiting for app to start (max ${MAX_WAIT}s)..."
READY=0
for i in $(seq 1 "${MAX_WAIT}"); do
# 优先 actuator
if curl -fsS "http://127.0.0.1:${PORT}/actuator/health" >/dev/null 2>&1; then
READY=1
break
fi
# 回退:直接试 OpenAPI 端点
if curl -fsS "http://127.0.0.1:${PORT}/v3/api-docs" >/dev/null 2>&1; then
READY=1
break
fi
sleep 1
done
if [[ "${READY}" -eq 0 ]]; then
echo "[openapi] ERROR: app did not start within ${MAX_WAIT}s"
echo "[openapi] last 50 lines of log:"
tail -50 /tmp/openapi-app.log
exit 1
fi
echo "[openapi] app is ready"
# 4) 导出 OpenAPI
mkdir -p "$(dirname "${OUT}")"
if curl -fsS "http://127.0.0.1:${PORT}/v3/api-docs.yaml" -o "${OUT}" 2>/dev/null; then
echo "[openapi] exported yaml to ${OUT}"
elif curl -fsS "http://127.0.0.1:${PORT}/v3/api-docs" -o "${OUT%.yaml}.json" 2>/dev/null; then
echo "[openapi] yaml not available, exported json to ${OUT%.yaml}.json"
else
echo "[openapi] WARNING: could not export OpenAPI. Endpoint may not be configured."
echo "[openapi] This is non-fatal. Skipping snapshot update."
# 创建一个占位文件,避免后续 git diff 报错
if [[ ! -f "${OUT}" ]]; then
echo "# OpenAPI snapshot not yet available" > "${OUT}"
fi
fi
echo "[openapi] done"
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
set -euo pipefail
SNAP="docs/API/openapi.snapshot.yaml"
BREAKING="docs/API/breaking-changes.md"
BASE_REF="${BASE_REF:-origin/main}"
echo "[openapi-diff] checking snapshot changes..."
git fetch origin main --depth=50 2>/dev/null || true
# 获取变更文件列表
CHANGED="$(git diff --name-only "${BASE_REF}...HEAD" 2>/dev/null || git diff --name-only HEAD~1 || true)"
# 如果 snapshot 没变化,直接 pass
if ! echo "${CHANGED}" | grep -q "^${SNAP}$"; then
echo "[openapi-diff] snapshot not changed. PASS."
exit 0
fi
echo "[openapi-diff] snapshot changed."
# 要求 breaking-changes.md 也有变化
if echo "${CHANGED}" | grep -q "^${BREAKING}$"; then
echo "[openapi-diff] breaking-changes.md updated. PASS."
exit 0
fi
cat <<EOF2
============================================
[openapi-diff] FAIL
OpenAPI snapshot changed but ${BREAKING} was not updated.
Please add a short note to ${BREAKING}, e.g.:
- $(date +%Y-%m-%d): Added GET /api/v1/users/me endpoint (non-breaking)
- $(date +%Y-%m-%d): Removed field xxx from YyyResponse (BREAKING)
============================================
EOF2
exit 1