Files
dobotai-platform/.gitea/scripts/ci_gate.py
T
姚章浩 16ee1ddfbe
build / build (push) Successful in 18s
fix(ci): 存量历史提交只告警不拦截,新增提交仍严格校验
把早已推送到其它远端分支的 lineage 合进 main 时,被带进来的提交不是本次新增,
它们标题不合规属既有事实。闸门先抓全部远端分支头,用 git branch -r --contains
取证:能证明已存在于其它远端分支的只告警放行;证不出来的照旧拦截(fail-closed)。
2026-11-11 16:42:15 +08:00

165 lines
5.7 KiB
Python

#!/usr/bin/env python3
"""提交信息规范校验(CI 侧权威闸门)。
规则与本机 ~/.githooks/commit-msg 完全一致,防止 --no-verify 绕过。
放行:Merge / Revert " / fixup! / squash! / amend! 开头的自动生成提交。
存量历史口径:合并一条早已推送到别的远端分支的lineage(例如把生产分支合进 main)时,
被带进来的提交并非本次新增,它们的标题不合规属于既有事实。判定为「已在其它远端分支存在」
的提交只告警、不判红;本次真正新造的提交一律照旧严格校验。
豁免必须是可证明的:先抓全部远端分支头,用 `git branch -r --contains <sha>` 取证;
抓不到 / 证不出 ⇒ 不豁免(宁可红,也不假绿)。
"""
import json
import os
import re
import subprocess
import sys
TYPES = "feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert"
RE = re.compile(r"^(?:" + TYPES + r")(?:\([a-zA-Z0-9._/-]+\))?!?: .{3,}$")
ALLOW = re.compile(r'^(?:Merge |Revert "|fixup! |squash! |amend! )')
# 本次推送的分支名,用来把「当前分支自己」排除在豁免证据之外
PUSH_BRANCH = (os.environ.get("GITHUB_REF_NAME") or "").strip()
HELP = """
\u274c 提交信息不合规,已拦截。
规范 : <类型>(<范围>): <简短描述>
类型 : feat 新功能 | fix 修缺陷 | docs 文档 | style 格式(不改逻辑)
refactor 重构 | perf 性能 | test 测试 | build 构建/依赖
ci CI 配置 | chore 杂务 | revert 回滚
示例 : feat(asset): 新增按歌曲生成应援序列
fix(parser): 修复表格解析越界导致的 500
docs: 补充 OSS 短签名接口说明
要点 : 类型小写、冒号后一个空格、描述 \u22653 字符
标题 \u226472 字符、结尾不加句号
破坏性变更类型后加 !,正文写 BREAKING CHANGE:
详见 : docs/提交规范.md
"""
def sh(args):
return subprocess.run(args, capture_output=True, text=True)
def collect():
"""返回 [(sha, message)]:优先 Gitea 事件负载,取不到则退回 git log。"""
items = []
ev = os.environ.get("GITHUB_EVENT_PATH") or ""
if ev and os.path.exists(ev):
try:
with open(ev, encoding="utf-8") as fh:
d = json.load(fh)
for c in (d.get("commits") or []):
m = (c.get("message") or "").strip()
if m:
items.append(((c.get("id") or "").strip(), m))
except Exception as e:
print(" 事件负载解析失败(%s),回退到 git log" % e)
if not items:
out = sh(["git", "log", "-n", "5", "--format=%H%x1f%B%x1e"]).stdout
for rec in out.split("\x1e"):
rec = rec.strip()
if not rec:
continue
sha, _, msg = rec.partition("\x1f")
if msg.strip():
items.append((sha.strip(), msg.strip()))
return items
_fetched = None
def ensure_remote_heads():
"""抓全部远端分支头(浅历史足够判断 lineage 末端)。"""
global _fetched
if _fetched is None:
r = sh(["git", "fetch", "-q", "--depth", "200", "origin", "+refs/heads/*:refs/remotes/origin/*"])
_fetched = (r.returncode == 0, (r.stderr or "").strip())
if not _fetched[0]:
print(" \u26a0 无法抓取远端分支,存量豁免不可用(将严格校验): %s" % _fetched[1][:200])
return _fetched[0]
def refs_containing(sha):
"""除本次推送分支外,还有哪些远端分支包含该提交。"""
if not ensure_remote_heads():
return []
r = sh(["git", "branch", "-r", "--contains", sha])
if r.returncode != 0:
return []
refs = []
for line in r.stdout.splitlines():
ref = line.strip().lstrip("*").strip()
if not ref or "->" in ref:
continue
if PUSH_BRANCH and ref == "origin/" + PUSH_BRANCH:
continue
refs.append(ref)
return refs
def subject_of(m):
for line in m.splitlines():
s = line.strip()
if not s or s.startswith("#"):
continue
return s
return ""
def judge(subj):
if not subj:
return "空提交信息"
if ALLOW.match(subj):
return ""
if len(subj) > 72:
return "标题 %d 字符 > 72" % len(subj)
if not RE.match(subj):
return "不符合 <类型>(<范围>): <描述>"
if subj.endswith(".") or subj.endswith("\u3002"):
return "标题结尾不要加句号"
return ""
def main():
items = collect()
bad = []
excused = []
print("待校验提交 %d 条:" % len(items))
for sha, msg in items:
subj = subject_of(msg)
why = judge(subj)
if not why:
print(" \u2705 %s" % subj)
continue
refs = refs_containing(sha) if sha else []
if refs:
excused.append((sha, subj, why, refs))
print(" \u21aa [%s] %s ← 已存在于 %s,按存量历史放行" % (why, subj, ",".join(refs[:3])))
continue
bad.append((subj, why))
print(" \u274c [%s] %s" % (why, subj))
if excused:
print("")
print("存量豁免 %d 条(提交早于本次合并,已在其它远端分支存在):" % len(excused))
for sha, subj, why, refs in excused:
print(" %s %s [%s]" % (sha[:10], subj, ",".join(refs[:3])))
try:
with open("/tmp/ci-gate-status", "w") as fh:
fh.write("failure" if bad else "success")
except Exception:
pass
if bad:
print(HELP)
return 1
print("\u2705 提交信息全部合规")
return 0
if __name__ == "__main__":
sys.exit(main())