merge: 远程 CI 分支 v2-on-ui-ci 合入 v2-on-ui
将 origin/v2-on-ui-ci(13 个提交:远程 CI 文档独立成 docs/CI/、
runner 保活/日志编码/gitlint 空转修复、ci_local L0b 等)合入开发主线。
冲突 5 处,按「两侧意图取并集」解决:
- AGENTS.md / docs/README.md:保留主线 待处理内容.md→任务规划/ 重构,
并入 CI 分支的 docs/CI/ 索引行与更新日志
- 测试开发标准流程.md §9.1:取 CI 分支,引用改指
../CI/远程CI完整指南.md 对应小节
- docs/待处理内容.md:接受主线删除,已重构为 docs/任务规划/
- docs/环境与部署/CI配置指南.md:接受 CI 分支删除,已并入 docs/CI/
合并引入的 doc-links 失效已修(doc-links 合并态走 --sections-strict,
自引用解析失败即硬错误):
- CI 指南 §0.0 标题把 ⚠️ 移到编号后(编号须打头才解析得出)
- CI 指南对 待处理内容.md 的引用改指 docs/任务规划/登记簿.md T-7 条
- CI 指南并入来源《测试开发标准流程》§9.4 / §9.2 改为全路径限定
- 测试开发标准流程 §9.4:§0.2/§2/§2.4/§4.1.3 原与 CI 文档路径间隔
超过链式继承上限 40 字符,改为紧跟路径并拆分明细句
- 测试开发标准流程 §9.1:步骤级细节引用同样全路径限定到 CI 指南
This commit is contained in:
+121
-6
@@ -8,9 +8,16 @@
|
||||
跑同一套判定,命令与退出码与 CI 一一对应。
|
||||
|
||||
覆盖范围(与 CI 完全一致):
|
||||
L0 静态门禁 = `pre-commit run --all-files`
|
||||
L0 静态门禁 = `pre-commit run --all-files` + 提交信息门禁(gitlint)
|
||||
L0.5 主机侧纯逻辑单测 = `test/host/**/run_unit_test.py`
|
||||
|
||||
⚠️ gitlint 必须**单独跑**(2026-10-11 审计):`stages: [commit-msg]` 写在 gitlint **上游**
|
||||
的 `.pre-commit-hooks.yaml`(项目 `.pre-commit-config.yaml` 里查不到这一行),而
|
||||
`pre-commit run --all-files` 只跑 pre-commit stage —— 它既不在 --all-files 里,
|
||||
CI 也一度漏掉(18 个钩子只报 17 个,且表现为"全绿")。本脚本与 CI 的
|
||||
`scripts/ci_gitlint_range.ps1` 同语义:逐条把最近 N 条非 merge 提交的
|
||||
message 喂给钩子。
|
||||
|
||||
刻意**不含**(与 CI 同理由,见 workflow 文件头):
|
||||
- `@build`(需 Keil 授权;CI 也不跑,真编译是开发者本地红线)
|
||||
- `mem-numbers` / `mem-budget` 钩子(事实源 `.map` 是 Keil 产物——**本地若存在 .map
|
||||
@@ -23,6 +30,7 @@
|
||||
|
||||
汇总行(S3,2026-10-02,对齐 CI 的 ::notice::):
|
||||
[SUMMARY] L0: pre-commit hooks -- passed=N failed=M skipped=K
|
||||
[SUMMARY] gitlint: N commit message(s) checked, M failed
|
||||
[SUMMARY] L0.5: K runner(s), A/B assertions passed, fail=F
|
||||
|
||||
用法(仓库根目录):
|
||||
@@ -30,6 +38,7 @@
|
||||
& '.venv\\Scripts\\python.exe' scripts/ci_local.py --l0-only
|
||||
& '.venv\\Scripts\\python.exe' scripts/ci_local.py --l05-only
|
||||
& '.venv\\Scripts\\python.exe' scripts/ci_local.py --keep-going # 失败也继续跑完
|
||||
& '.venv\\Scripts\\python.exe' scripts/ci_local.py --commits 20 # 提交信息校验条数
|
||||
|
||||
设计依据:`.gitea/workflows/ci.yml`;docs/测试说明/测试开发标准流程.md §9
|
||||
创建日期:2026-10-01
|
||||
@@ -46,6 +55,30 @@ from pathlib import Path
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def _global_python_candidates():
|
||||
"""常见**机器级** Python 安装位置(必须与 scripts/ci_find_python.ps1 同序)。
|
||||
|
||||
两处探测一旦不同步,就会出现"本地能跑、CI 挂"的老毛病:2026-10-11 的
|
||||
L0 全灭就是 workflow 把候选收窄成 PATH python + 仓库 .venv(runner 上两者
|
||||
都是死路),而真正装了 pre-commit 的机器级解释器(如
|
||||
C:\\ProgramData\\anaconda3\\python.exe)不再被探测。
|
||||
"""
|
||||
out = []
|
||||
pf = os.environ.get("ProgramFiles", r"C:\Program Files")
|
||||
pf86 = os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")
|
||||
local = os.environ.get("LOCALAPPDATA")
|
||||
pd = os.environ.get("ProgramData", r"C:\ProgramData")
|
||||
# 新版本优先:新装的解释器通常才是想要的。
|
||||
for ver in ("Python313", "Python312", "Python311", "Python310"):
|
||||
out.append(os.path.join(pf, ver, "python.exe"))
|
||||
out.append(os.path.join(pf86, ver, "python.exe"))
|
||||
if local:
|
||||
out.append(os.path.join(local, "Programs", "Python", ver, "python.exe"))
|
||||
for dist in ("anaconda3", "miniconda3"):
|
||||
out.append(os.path.join(pd, dist, "python.exe"))
|
||||
return out
|
||||
|
||||
|
||||
def _resolve_python():
|
||||
r"""挑一个真正装了 pre-commit 的解释器。
|
||||
|
||||
@@ -56,6 +89,7 @@ def _resolve_python():
|
||||
2. 当前解释器(sys.executable)
|
||||
3. 仓库 `.venv\Scripts\python.exe`
|
||||
4. PATH 上的 `python`
|
||||
5. 常见机器级安装位置(见 _global_python_candidates)
|
||||
找到即返回 (python_path, note);都找不到则返回 (当前解释器, None) 交由调用方报错。
|
||||
"""
|
||||
explicit = os.environ.get("DTU_PRECOMMIT_PYTHON", "").strip()
|
||||
@@ -80,12 +114,14 @@ def _resolve_python():
|
||||
sys.executable,
|
||||
str(REPO_ROOT / ".venv" / "Scripts" / "python.exe"),
|
||||
shutil.which("python"),
|
||||
]
|
||||
] + _global_python_candidates()
|
||||
seen = set()
|
||||
tried = []
|
||||
for py in candidates:
|
||||
if not py or py in seen or not os.path.isfile(py):
|
||||
continue
|
||||
seen.add(py)
|
||||
tried.append(py)
|
||||
try:
|
||||
cp = subprocess.run([py, "-m", "pre_commit", "--version"],
|
||||
capture_output=True, timeout=60)
|
||||
@@ -94,6 +130,9 @@ def _resolve_python():
|
||||
if cp.returncode == 0:
|
||||
note = None if py == sys.executable else "(自动选用,本解释器无 pre-commit)"
|
||||
return py, note
|
||||
print("[WARN] 未找到装有 pre-commit 的解释器,已尝试:")
|
||||
for py in tried:
|
||||
print(" - {}".format(py))
|
||||
return sys.executable, None
|
||||
|
||||
|
||||
@@ -173,6 +212,75 @@ def run_l0():
|
||||
return rc
|
||||
|
||||
|
||||
def run_gitlint(count):
|
||||
"""提交信息门禁:把最近 N 条非 merge 提交的 message 逐条喂给 gitlint 钩子。
|
||||
|
||||
与 CI 的 scripts/ci_gitlint_range.ps1 同语义(gitlint 声明
|
||||
`stages: [commit-msg]`,`pre-commit run --all-files` 永远跑不到它)。
|
||||
返回退出码(0/1/2)。
|
||||
"""
|
||||
print("")
|
||||
print("=" * 64)
|
||||
print(" L0b 提交信息门禁(gitlint / commit-msg stage,最近 {} 条)".format(count))
|
||||
print("=" * 64)
|
||||
|
||||
def _git(*args):
|
||||
return subprocess.run(["git"] + list(args), cwd=str(REPO_ROOT),
|
||||
capture_output=True, timeout=60)
|
||||
|
||||
try:
|
||||
out = _git("log", "--no-merges", "--format=%H", "-n", str(count), "HEAD")
|
||||
except (OSError, subprocess.TimeoutExpired) as e:
|
||||
print("[ERROR] 无法调用 git:{}".format(e))
|
||||
return 2
|
||||
if out.returncode != 0:
|
||||
print("[ERROR] git log 失败:{}".format(
|
||||
out.stderr.decode("utf-8", errors="replace").strip()))
|
||||
return 2
|
||||
|
||||
shas = [s.strip() for s in out.stdout.decode("utf-8", errors="replace").splitlines()
|
||||
if s.strip()]
|
||||
if not shas:
|
||||
print("[INFO] 无可校验提交 —— 跳过")
|
||||
print("[SUMMARY] gitlint: 0 commit message(s) checked, 0 failed")
|
||||
return 0
|
||||
|
||||
# 消息文件必须写在【仓库内】且【UTF-8 无 BOM】:
|
||||
# - 跨盘符会被 pre-commit 直接拒绝(path is on mount 'C:', start on mount 'D:')
|
||||
# - BOM 会占掉标题 1 个字符,扭曲 .gitlint 的 100 字符上限
|
||||
msg_dir = REPO_ROOT / "ci-artifacts" / "gitlint-local"
|
||||
msg_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
checked = 0
|
||||
failed = 0
|
||||
try:
|
||||
for sha in shas:
|
||||
checked += 1
|
||||
body = _git("log", "-1", "--format=%B", sha)
|
||||
title = _git("log", "-1", "--format=%s", sha)
|
||||
text = body.stdout.decode("utf-8", errors="replace").rstrip("\r\n")
|
||||
title_line = title.stdout.decode("utf-8", errors="replace").strip()
|
||||
|
||||
msg_path = msg_dir / (sha + ".txt")
|
||||
msg_path.write_text(text, encoding="utf-8", newline="\n")
|
||||
|
||||
print("")
|
||||
print("--- {} {}".format(sha[:8], title_line))
|
||||
rc, _ = _run_capture([PY, "-m", "pre_commit", "run",
|
||||
"--hook-stage", "commit-msg",
|
||||
"--commit-msg-filename", str(msg_path),
|
||||
"gitlint"])
|
||||
if rc != 0:
|
||||
failed += 1
|
||||
print("[FAIL] gitlint 拒绝该提交信息:{}".format(title_line))
|
||||
finally:
|
||||
shutil.rmtree(str(msg_dir), ignore_errors=True)
|
||||
|
||||
print("[SUMMARY] gitlint: {} commit message(s) checked, {} failed".format(
|
||||
checked, failed))
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
def run_l05(args):
|
||||
"""L0.5:主机侧单测。返回退出码(0/1/2)。"""
|
||||
print("")
|
||||
@@ -238,6 +346,8 @@ def main(argv=None):
|
||||
g.add_argument("--l05-only", action="store_true", help="只跑 L0.5 主机侧单测")
|
||||
ap.add_argument("--keep-going", action="store_true",
|
||||
help="某步失败也继续跑后续步骤(默认首错即停)")
|
||||
ap.add_argument("--commits", type=int, default=10, metavar="N",
|
||||
help="提交信息门禁(gitlint)校验最近 N 条非 merge 提交(默认 10)")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
print("=== 本地 CI(等价 .gitea/workflows/ci.yml)===")
|
||||
@@ -266,10 +376,15 @@ def main(argv=None):
|
||||
try:
|
||||
if not args.l05_only:
|
||||
rc = run_l0()
|
||||
if rc != 0 and not args.keep_going:
|
||||
print("")
|
||||
print("[FAIL] L0 未通过 —— 中止(--keep-going 可继续跑 L0.5)")
|
||||
return 1
|
||||
# 提交信息门禁独立于 --all-files(stage 不同),故单独跑一段;
|
||||
# 即使 L0 已失败也照跑,一次看全所有违规项。
|
||||
rc_msg = run_gitlint(args.commits)
|
||||
if rc != 0 or rc_msg != 0:
|
||||
if not args.keep_going:
|
||||
print("")
|
||||
print("[FAIL] L0 未通过 —— 中止(--keep-going 可继续跑 L0.5)")
|
||||
return 1
|
||||
rc = rc or rc_msg
|
||||
if not args.l0_only:
|
||||
rc05 = run_l05(args)
|
||||
rc = rc or rc05
|
||||
|
||||
Reference in New Issue
Block a user