Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e458329995 | |||
| 0749a80106 | |||
| 2190c5202d | |||
| 72424713da |
Executable
+104
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
"""提交信息规范校验(CI 侧权威闸门)。
|
||||
|
||||
规则与本机 ~/.githooks/commit-msg 完全一致,防止 --no-verify 绕过。
|
||||
放行:Merge / Revert " / fixup! / squash! / amend! 开头的自动生成提交。
|
||||
"""
|
||||
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! )')
|
||||
|
||||
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 collect():
|
||||
"""优先用 Gitea 事件负载里的本次推送提交,取不到则退回 git log。"""
|
||||
msgs = []
|
||||
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:
|
||||
msgs.append(m)
|
||||
except Exception as e:
|
||||
print(" 事件负载解析失败(%s),回退到 git log" % e)
|
||||
if not msgs:
|
||||
out = subprocess.run(["git", "log", "-n", "5", "--format=%B%x00"],
|
||||
capture_output=True, text=True).stdout
|
||||
msgs = [x.strip() for x in out.split("\x00") if x.strip()]
|
||||
return msgs
|
||||
|
||||
|
||||
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():
|
||||
msgs = collect()
|
||||
bad = []
|
||||
print("待校验提交 %d 条:" % len(msgs))
|
||||
for m in msgs:
|
||||
subj = subject_of(m)
|
||||
why = judge(subj)
|
||||
if why:
|
||||
bad.append((subj, why))
|
||||
print(" \u274c [%s] %s" % (why, subj))
|
||||
else:
|
||||
print(" \u2705 %s" % subj)
|
||||
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())
|
||||
Executable
+82
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
"""把 CI 结果上报给主机侧 ci-bridge(bridge 负责落报告页 + 推微信)。"""
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import urllib.request
|
||||
|
||||
|
||||
def rd(path, default=""):
|
||||
try:
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
return fh.read().strip()
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
def git(*args):
|
||||
try:
|
||||
return subprocess.run(["git"] + list(args), capture_output=True,
|
||||
text=True).stdout.strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def main():
|
||||
repo = os.environ.get("GITHUB_REPOSITORY", "unknown/unknown")
|
||||
sha = rd("/tmp/ci-sha") or git("rev-parse", "HEAD")
|
||||
branch = os.environ.get("GITHUB_REF_NAME") or \
|
||||
os.environ.get("GITHUB_REF", "").replace("refs/heads/", "")
|
||||
gate = rd("/tmp/ci-gate-status", "skipped")
|
||||
build = rd("/tmp/ci-build-status", "failure")
|
||||
status = "success" if (gate == "success" and build == "success") else "failure"
|
||||
|
||||
log = rd("/tmp/ci-build-tail")
|
||||
if gate != "success":
|
||||
log = "提交信息规范校验未通过\n" + log
|
||||
|
||||
try:
|
||||
count = int(rd("/tmp/ci-commits-count", "1") or 1)
|
||||
except Exception:
|
||||
count = 1
|
||||
|
||||
rep = {
|
||||
"repo": repo,
|
||||
"sha": sha,
|
||||
"branch": branch,
|
||||
"actor": os.environ.get("GITHUB_ACTOR", ""),
|
||||
"status": status,
|
||||
"commit_subject": git("log", "-1", "--format=%s"),
|
||||
"commit_count": count,
|
||||
"duration_s": rd("/tmp/ci-duration", "?"),
|
||||
"preview_url": rd("/tmp/ci-preview-url"),
|
||||
"log": log[-3500:],
|
||||
"steps": [
|
||||
{"name": "提交信息规范校验", "status": gate},
|
||||
{"name": "编译与自检", "status": build},
|
||||
],
|
||||
}
|
||||
if rep["preview_url"]:
|
||||
print("\U0001F310 网页预览: %s" % rep["preview_url"])
|
||||
print("结果: %s | 构建: %s | 提交闸门: %s | 耗时: %ss"
|
||||
% (status, build, gate, rep["duration_s"]))
|
||||
|
||||
base = os.environ.get("CI_BRIDGE_RESOLVED", "").rstrip("/")
|
||||
if not base:
|
||||
print("跳过上报:未找到可达的 ci-bridge")
|
||||
return 0
|
||||
data = json.dumps(rep, ensure_ascii=False).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
base + "/api/ci/report", data=data, method="POST",
|
||||
headers={"Content-Type": "application/json",
|
||||
"X-CI-Token": os.environ.get("BRIDGE_TOKEN", "")})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
print("\u2705 已上报 ci-bridge:", resp.read().decode()[:300])
|
||||
except Exception as e:
|
||||
print("\u274c 上报 ci-bridge 失败:", e)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,156 @@
|
||||
name: build
|
||||
|
||||
# 提交 -> 本机 Runner 编译 -> 微信通知 + 网页预览
|
||||
# 实测结论(勿随意改动):
|
||||
# 1. 不用任何第三方 action(作业容器不联网拉 action,全用镜像自带工具)
|
||||
# 2. 必须自己 clone —— Runner 不会自动检出代码,工作区初始为空
|
||||
# 3. act 以 `bash -e -o pipefail` 执行每步 -> 禁止 `cmd | head` 这种会 SIGPIPE 提前退出的管道
|
||||
# 4. 工具链来自主机只读挂载 /opt/citools(go/uv),缓存写 /opt/citools-cache
|
||||
# 5. bridge 从容器内走 docker 网关 172.17.0.1:8899(实测可达)
|
||||
# 6. 校验/上报逻辑放在 .gitea/scripts/ 下的脚本里,YAML 只负责调用(避免 YAML 里嵌深缩进代码)
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
BRIDGE_URL: ${{ vars.CI_BRIDGE_URL }}
|
||||
BRIDGE_TOKEN: ${{ secrets.CI_BRIDGE_TOKEN }}
|
||||
GIT_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
concurrency:
|
||||
group: build-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 40
|
||||
steps:
|
||||
- name: 检出代码
|
||||
run: |
|
||||
set -uo pipefail
|
||||
SRV="${GITHUB_SERVER_URL:-http://192.168.50.223:3000}"
|
||||
HP="${SRV#*://}"
|
||||
BR="${GITHUB_REF_NAME:-${GITHUB_REF#refs/heads/}}"
|
||||
cd "${GITHUB_WORKSPACE:-$(pwd)}"
|
||||
rm -rf ./* .git 2>/dev/null || true
|
||||
git init -q .
|
||||
git remote add origin "http://x-access-token:${GIT_TOKEN}@${HP}/${GITHUB_REPOSITORY}.git"
|
||||
git fetch -q --depth 50 origin "$BR"
|
||||
git checkout -q -B "$BR" FETCH_HEAD
|
||||
git rev-parse HEAD > /tmp/ci-sha
|
||||
date +%s > /tmp/ci-start
|
||||
: > /tmp/ci-preview-url
|
||||
echo skipped > /tmp/ci-build-status
|
||||
git rev-list --count FETCH_HEAD > /tmp/ci-commits-count 2>/dev/null || echo 1 > /tmp/ci-commits-count
|
||||
echo "已检出 ${GITHUB_REPOSITORY} 分支 ${BR}"
|
||||
git log -1 --format=' %h %an %s'
|
||||
|
||||
- name: 提交信息规范校验
|
||||
run: python3 .gitea/scripts/ci_gate.py
|
||||
|
||||
- name: 编译与自检
|
||||
run: |
|
||||
set +e; set +o pipefail
|
||||
LOG=/tmp/ci-build.log; : > "$LOG"
|
||||
run() { echo "-- $* --"; "$@" 2>&1 | tee -a "$LOG"; return "${PIPESTATUS[0]}"; }
|
||||
runin(){ local d="$1"; shift; echo "-- ($d) $* --"; ( cd "$d" && "$@" ) 2>&1 | tee -a "$LOG"; return "${PIPESTATUS[0]}"; }
|
||||
|
||||
export PATH=/opt/citools/go/bin:/opt/citools/bin:$PATH
|
||||
export GOCACHE=/opt/citools-cache/gocache GOMODCACHE=/opt/citools-cache/gomod UV_CACHE_DIR=/opt/citools-cache/uv
|
||||
export GOPROXY=https://goproxy.cn,direct GOTOOLCHAIN=local GOFLAGS=-mod=mod GOPATH=/opt/citools-cache/gopath
|
||||
mkdir -p "$GOCACHE" "$GOMODCACHE" "$UV_CACHE_DIR" "$GOPATH" 2>/dev/null
|
||||
|
||||
echo "工具链: $(go version 2>&1) | $(uv --version 2>&1) | $(node -v 2>&1) | $(python3 -V 2>&1)"
|
||||
fail=0; ran=""
|
||||
|
||||
if [ -f go.mod ]; then
|
||||
ran="$ran go"
|
||||
run go build ./... || fail=1
|
||||
run go vet ./... || fail=1
|
||||
run go test ./... || fail=1
|
||||
fi
|
||||
|
||||
if [ -f pyproject.toml ] || [ -f requirements.txt ] || [ -f setup.py ]; then
|
||||
ran="$ran python"
|
||||
if grep -qE '^check:' Makefile 2>/dev/null; then
|
||||
run make UV=uv check || fail=1
|
||||
else
|
||||
run uv sync || fail=1
|
||||
run uv run pytest -q || fail=1
|
||||
fi
|
||||
fi
|
||||
|
||||
WEBPKG=""
|
||||
for p in package.json web/package.json frontend/package.json ui/package.json; do
|
||||
if [ -f "$p" ]; then WEBPKG="$p"; break; fi
|
||||
done
|
||||
if [ -n "$WEBPKG" ]; then
|
||||
ran="$ran node"
|
||||
D="$(dirname "$WEBPKG")"
|
||||
if [ -f "$D/package-lock.json" ]; then
|
||||
runin "$D" npm ci || fail=1
|
||||
else
|
||||
runin "$D" npm install || fail=1
|
||||
fi
|
||||
if grep -q '"build"[[:space:]]*:' "$WEBPKG" 2>/dev/null; then
|
||||
runin "$D" npm run build || fail=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$ran" ] && [ -f Makefile ]; then
|
||||
ran="$ran make"
|
||||
run make || fail=1
|
||||
fi
|
||||
if [ -z "$ran" ]; then
|
||||
echo "未识别到构建入口(go.mod / pyproject.toml / package.json / Makefile 都没有),跳过编译"
|
||||
fi
|
||||
|
||||
if [ "$fail" -eq 0 ]; then echo success > /tmp/ci-build-status; else echo failure > /tmp/ci-build-status; fi
|
||||
tail -c 4000 "$LOG" > /tmp/ci-build-tail
|
||||
echo "构建结果: $(cat /tmp/ci-build-status)(识别:$ran)"
|
||||
[ "$fail" -eq 0 ] || exit 1
|
||||
|
||||
- name: 发布网页预览
|
||||
if: always()
|
||||
run: |
|
||||
set +e; set +o pipefail
|
||||
: > /tmp/ci-preview-url
|
||||
if [ "$(cat /tmp/ci-build-status 2>/dev/null)" != "success" ]; then
|
||||
echo "构建未成功,跳过预览发布"; exit 0
|
||||
fi
|
||||
WEB=""
|
||||
for d in web/dist dist build out public web/build frontend/dist; do
|
||||
if [ -f "$d/index.html" ]; then WEB="$d"; break; fi
|
||||
done
|
||||
if [ -z "$WEB" ]; then echo "未发现静态网页产物目录,跳过预览发布"; exit 0; fi
|
||||
tar -czf /tmp/ci-web.tgz -C "$WEB" . || { echo "打包失败"; exit 0; }
|
||||
B="${BRIDGE_URL:-}"
|
||||
for cand in "$B" "http://172.17.0.1:8899" "http://192.168.50.134:8899" "http://100.92.49.78:8899"; do
|
||||
if [ -n "$cand" ]; then
|
||||
curl -s -m 5 -o /dev/null "$cand/healthz" && { B="$cand"; break; }
|
||||
fi
|
||||
done
|
||||
SHA="$(cat /tmp/ci-sha)"
|
||||
echo "发布预览到 $B(产物目录 $WEB)"
|
||||
curl -sS -m 90 -X POST "$B/api/ci/publish?repo=${GITHUB_REPOSITORY}&sha=${SHA}" -H "X-CI-Token: ${BRIDGE_TOKEN}" -H 'Content-Type: application/gzip' --data-binary @/tmp/ci-web.tgz -o /tmp/ci-pub.json
|
||||
cat /tmp/ci-pub.json; echo
|
||||
URL="$(python3 -c "import json;print(json.load(open('/tmp/ci-pub.json')).get('url',''))" 2>/dev/null)"
|
||||
if [ -n "$URL" ]; then echo "$URL" > /tmp/ci-preview-url; echo "网页预览: $URL"; fi
|
||||
|
||||
- name: 通知微信
|
||||
if: always()
|
||||
run: |
|
||||
set +e; set +o pipefail
|
||||
B="${BRIDGE_URL:-}"
|
||||
for cand in "$B" "http://172.17.0.1:8899" "http://192.168.50.134:8899" "http://100.92.49.78:8899"; do
|
||||
if [ -n "$cand" ]; then
|
||||
curl -s -m 5 -o /dev/null "$cand/healthz" && { B="$cand"; break; }
|
||||
fi
|
||||
done
|
||||
echo "使用 ci-bridge: $B"
|
||||
S=$(date +%s); T=$(cat /tmp/ci-start 2>/dev/null); [ -n "$T" ] || T=$S
|
||||
echo $((S - T)) > /tmp/ci-duration
|
||||
CI_BRIDGE_RESOLVED="$B" python3 .gitea/scripts/ci_report.py
|
||||
@@ -0,0 +1,87 @@
|
||||
# 提交规范(Conventional Commits · 中文版)
|
||||
|
||||
本仓库的提交信息由**两道闸门**强制校验,任何一条不合规都会被挡下:
|
||||
|
||||
| 闸门 | 位置 | 时机 | 作用 |
|
||||
|---|---|---|---|
|
||||
| 本地钩子 | `~/.githooks/commit-msg`(已设全局 `core.hooksPath`) | `git commit` 时 | 当场拦截,立刻告知原因 |
|
||||
| CI 闸门 | `.gitea/scripts/ci_gate.py` | `git push` 后在本机 Runner 上 | 权威闸门,防 `--no-verify` 绕过 |
|
||||
|
||||
两道闸门规则完全一致:本地拦不住的,CI 一定拦得住。
|
||||
|
||||
---
|
||||
|
||||
## 1. 格式
|
||||
|
||||
```
|
||||
<类型>(<范围>): <简短描述>
|
||||
|
||||
<正文:为什么改、改了什么、影响面>
|
||||
|
||||
<脚注:Closes #12 / BREAKING CHANGE: ...>
|
||||
```
|
||||
|
||||
- **类型**(必填):见下表,**小写英文**
|
||||
- **范围**(可选):模块名,如 `asset`、`parser`、`kb`、`ci`
|
||||
- **冒号后必须有一个空格**
|
||||
- **描述**:≥3 字符,≤72 字符(中文按字符计),**结尾不加句号**
|
||||
- **正文/脚注**(可选):解释「为什么」,而不是复述「改了什么」
|
||||
|
||||
## 2. 类型表
|
||||
|
||||
| 类型 | 含义 | 例子 |
|
||||
|---|---|---|
|
||||
| `feat` | 新功能 | `feat(asset): 新增按歌曲生成应援序列` |
|
||||
| `fix` | 修缺陷 | `fix(parser): 修复表格解析越界导致的 500` |
|
||||
| `docs` | 只有文档改动 | `docs: 补充 OSS 短签名接口说明` |
|
||||
| `style` | 格式/空格,不改逻辑 | `style: 统一 4 空格缩进` |
|
||||
| `refactor` | 重构,不改行为 | `refactor(delivery): 抽出发送重试策略` |
|
||||
| `perf` | 性能优化 | `perf: 图片签名批量复用连接池` |
|
||||
| `test` | 增删改测试 | `test(gate): 补提交规范边界用例` |
|
||||
| `build` | 构建/依赖 | `build: 升级 pydantic 到 2.11` |
|
||||
| `ci` | CI 配置 | `ci: 接入提交规范闸门与自动编译` |
|
||||
| `chore` | 杂务(不动源码/测试) | `chore: 更新 .gitignore` |
|
||||
| `revert` | 回滚 | `revert: 回滚 feat(asset) 的序列生成` |
|
||||
|
||||
**破坏性变更**:类型后加 `!`,并在脚注写 `BREAKING CHANGE:`,例如
|
||||
`feat(api)!: 重构回调鉴权` + 脚注 `BREAKING CHANGE: /v1/turns 不再接受裸 app_key`。
|
||||
|
||||
**自动放行**:`Merge ...`、`Revert "..."`(git 自动生成)、`fixup! `、`squash! `、`amend! `。
|
||||
|
||||
## 3. 被拦下来时的自查
|
||||
|
||||
| 报错 | 原因 | 改法 |
|
||||
|---|---|---|
|
||||
| 不符合 `<类型>(<范围>): <描述>` | 类型不在表里 / 少了冒号空格 | `update code` → `fix(asset): 修复图片引用被截断` |
|
||||
| 标题 N 字符 > 72 | 太长 | 精简标题,细节写进正文 |
|
||||
| 标题结尾不要加句号 | 结尾有 `.` 或 `。` | 删掉句号 |
|
||||
| 空提交信息 | 只有标题为空/全是注释 | 补上规范标题 |
|
||||
|
||||
改完用 `git commit --amend` 重写最后一条(还没 push 时最简单);已经 push 了就在下一次提交里修正,或 `--amend` 后 `--force-with-lease` 推。
|
||||
|
||||
## 4. 推送后自动发生什么
|
||||
|
||||
```
|
||||
git push → 本机 Runner 立刻接活(约 1 秒)
|
||||
→ ① 提交信息规范校验(不合规立即失败,不编译)
|
||||
→ ② 自动识别语言并编译自检
|
||||
go.mod → go build / vet / test
|
||||
pyproject.toml→ make check 或 uv sync + pytest(含 ruff)
|
||||
package.json → npm ci + npm run build
|
||||
→ ③ 若是网页产物(dist/index.html 等)→ 发布出可访问链接
|
||||
→ ④ 微信推送结果(含失败原因与日志尾巴)
|
||||
```
|
||||
|
||||
**通知内容**:仓库、分支、短 SHA、提交标题、结果、耗时、失败时附日志。
|
||||
**预览链接**:形如 `http://192.168.50.134:8899/p/<owner>/<repo>/<sha>/`;
|
||||
**报告页**:形如 `http://192.168.50.134:8899/r/<报告 id>`(局域网直连可达,Tailscale 用 `100.92.49.78` 换掉 IP 即可)。
|
||||
|
||||
## 5. 三条常用命令
|
||||
|
||||
```bash
|
||||
git commit -m "fix(parser): 修复表格解析越界导致的 500" # 规范提交
|
||||
git commit --amend -m "docs: 补充提交规范说明" # 改写上一条
|
||||
git log --oneline -10 # 看最近提交
|
||||
```
|
||||
|
||||
> 不要用 `--no-verify` 绕过——CI 闸门会拦,且这次推送不会编译,等于白推。
|
||||
Reference in New Issue
Block a user