From 2bf6701def2a5c9c41c53ebed1a965fb1b1c10b8 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Wed, 24 Jun 2026 23:25:26 +0800 Subject: [PATCH] =?UTF-8?q?test(ci):=20=E7=AC=AC1=E5=88=80=20=E5=8F=AF?= =?UTF-8?q?=E7=A7=BB=E6=A4=8D=20SQL=20=E6=89=AB=E6=8F=8F=E7=A1=AC=E9=97=B8?= =?UTF-8?q?=20+=20=E4=BA=94=E6=94=AF=E6=9F=B1=20PR=20=E8=87=AA=E6=A3=80?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 执行机制四道闸 → 闸4(CI 硬闸) + 评审兜底落地: - ci/scan-portable-sql.sh:扫 server Go 运行时查询禁用 MySQL 专属构造 (UTC_TIMESTAMP/UTC_DATE/NOW()/FIELD(/ON DUPLICATE KEY/IFNULL), 豁免方言层 dialect*.go / _test.go / 注释行;当前 152 文件 0 违规 - ci.yml:新增 portable-sql-scan job;push 触发路径补 server/** - .gitea/PULL_REQUEST_TEMPLATE.md:可测试性五支柱 + 测试 + 红线自检清单 - 守支柱 3(纯逻辑/IO 分离·双库可移植),见 docs/dev-conventions.html Co-Authored-By: Claude Opus 4.8 --- .gitea/PULL_REQUEST_TEMPLATE.md | 23 ++++++++++ .gitea/workflows/ci.yml | 16 ++++++- ci/scan-portable-sql.sh | 78 +++++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 .gitea/PULL_REQUEST_TEMPLATE.md create mode 100755 ci/scan-portable-sql.sh diff --git a/.gitea/PULL_REQUEST_TEMPLATE.md b/.gitea/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..bd8d0ce --- /dev/null +++ b/.gitea/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,23 @@ +## 改动说明 + + + +## 可测试性五支柱自检 + + + +- [ ] **支柱 1 · 接缝即接口**:新增的外部依赖(原生隧道 / HTTP / 子进程 / 存储 / 时钟 / 随机)藏在接口之后、可注入,未在业务层直接 `new` 或调静态单例 +- [ ] **支柱 2 · 契约单源**:跨边界数据(stats 字段 / HTTP JSON / proto / 设计 token)改动有单一来源,两端从它生成或校验(未两端各写) +- [ ] **支柱 3 · 纯逻辑/IO 分离**:计算/状态机是纯函数;Go SQL 走 `dialect.*`、时间 Go 端算(可移植 SQL 扫描通过) +- [ ] **支柱 4 · 错误是值**:错误带类型/码、可断言;无空 `catch` / 只打日志不返回 +- [ ] **支柱 5 · 可观测**:新增关键状态/事件有可程序读取的出口(stream / 结构化日志 / 接口) + +## 测试 + +- [ ] 新增/改动逻辑有对应测试(L1 单元 / L2 集成·交互;数值型补 L1,真链路补 L4) +- [ ] 本地通过:`flutter analyze && flutter test`(客户端) / `go test ./...`(服务端) +- [ ] 涉及 UI:golden 基线已按需更新(`flutter test --update-goldens`) + +## 红线 + +- [ ] 产品文案无红线词,用「加速 / 隐私保护 / 无日志」口径(脱敏扫描通过) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index c1afbf4..1f20706 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -1,6 +1,6 @@ name: ci-pangolin -# 触发条件:deploy/ 或 design/ 或 client/ 或 ci/ 变更时,以及所有向 main 的 PR +# 触发条件:deploy/ 或 design/ 或 client/ 或 server/ 或 ci/ 变更时,以及所有向 main 的 PR on: push: branches: [main] @@ -8,6 +8,7 @@ on: - 'deploy/**' - 'design/**' - 'client/**' + - 'server/**' - 'ci/**' - '.gitea/workflows/ci.yml' pull_request: @@ -77,3 +78,16 @@ jobs: -v "$PWD/client:/app" -w /app \ ghcr.io/cirruslabs/flutter:stable \ bash -c "flutter pub get && flutter analyze && flutter test test/unit test/widget" + + # ── Job 5: Portable SQL Scan (支柱 3:双库可移植)───────────────────────── + # server Go 运行时查询不得含 MySQL 专属构造(同一份 SQL 要跑 mysql 与 sqlite)。 + # 规则与豁免见 ci/scan-portable-sql.sh 头注 + docs/dev-conventions.html 支柱 3。 + portable-sql-scan: + name: Portable SQL — 可移植性 (mysql/sqlite) + runs-on: nas + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: scan server Go runtime SQL for MySQL-specific constructs + run: bash ci/scan-portable-sql.sh diff --git a/ci/scan-portable-sql.sh b/ci/scan-portable-sql.sh new file mode 100755 index 0000000..dcc9b48 --- /dev/null +++ b/ci/scan-portable-sql.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# scan-portable-sql.sh — 可移植 SQL 扫描(支柱 3:纯逻辑/IO 分离 → 双库可移植)。 +# +# 规则来源: CLAUDE.md「数据层:多数据库」铁律——server 已与具体 DB 解绑(裸 SQL + +# 薄方言层 internal/db/dialect.go)。Go 端运行时查询不得使用 MySQL 专属构造,因为 +# 同一份 SQL 要同时跑 mysql 与 sqlite。可移植写法: +# - upsert → dialect.Upsert(...)(中性 EXCLUDED.col 记法) +# - 行锁 → dialect.LockForUpdate() +# - 时间 → Go 端算好传 ?(不用 UTC_TIMESTAMP()/NOW()/UTC_DATE()) +# +# 扫描范围: server/**/*.go(运行时代码) +# 白名单(豁免): +# *_test.go ← 测试 fixture(含 integration-only mysql SQL) +# internal/db/dialect*.go ← 方言层本身,负责生成各库方言,允许出现专属 SQL +# 整行注释(// 或 /* 或 *) ← 文档/说明,非实际查询 +# +# 禁用构造(出现在非注释、非白名单的 Go 代码即违规): +# UTC_TIMESTAMP UTC_DATE NOW() FIELD( ON DUPLICATE KEY IFNULL( +# +# 发现违规 → 非零退出码(CI fail)。 +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$REPO_ROOT" + +VIOLATIONS=0 +SCANNED=0 + +# MySQL 专属构造(可移植替代见 dialect.go / Go 端算时间 / COALESCE) +PATTERNS='UTC_TIMESTAMP|UTC_DATE|NOW\(\)|FIELD\(|ON DUPLICATE KEY|IFNULL\(' + +# scan_file <文件路径> — 命中返回 1(违规),干净返回 0。 +scan_file() { + local file="$1" + local hits + # 1. 找含禁用构造的行 + # 2. 排除整行注释(行首可选空白 + // 或 /* 或 *) + hits="$(grep -nE "$PATTERNS" "$file" 2>/dev/null \ + | grep -vE '^[0-9]+:[[:space:]]*(//|/\*|\*)' \ + || true)" + + if [ -n "$hits" ]; then + echo "❌ $file" >&2 + echo "$hits" | while IFS= read -r line; do + printf ' 不可移植 SQL %s\n' "$line" >&2 + done + return 1 + fi + return 0 +} + +echo "→ 开始可移植 SQL 扫描(server Go 运行时查询)..." + +if [ -d server ]; then + while IFS= read -r -d '' f; do + SCANNED=$((SCANNED + 1)) + if ! scan_file "$f"; then + VIOLATIONS=$((VIOLATIONS + 1)) + fi + done < <(find server -type f -name '*.go' \ + ! -name '*_test.go' \ + ! -path '*/internal/db/dialect*.go' \ + ! -path '*/vendor/*' -print0) +fi + +echo "→ 扫描完成:共 ${SCANNED} 个 Go 文件,${VIOLATIONS} 个违规。" +echo "" + +if [ "$VIOLATIONS" -ne 0 ]; then + echo "可移植 SQL 检查失败!同一份 Go SQL 要同时跑 mysql 与 sqlite,请改用可移植写法:" >&2 + echo " - upsert → dialect.Upsert(...)(中性 EXCLUDED.col 记法)" >&2 + echo " - 行锁 → dialect.LockForUpdate()" >&2 + echo " - 时间 → Go 端算好传 ?,不用 UTC_TIMESTAMP()/NOW()/UTC_DATE()" >&2 + echo " - 排序 → 不用 FIELD();空值合并用 COALESCE 不用 IFNULL" >&2 + exit 1 +fi + +echo "✅ 可移植 SQL 扫描通过 — server 运行时查询无 MySQL 专属构造。"