name: Identity Guard # 拒绝把 AI 助手写成提交作者/提交者,或写进 Co-authored-by 尾注。 # 本地 .git/hooks 不随仓库分发,云端会话推上来的提交不受其保护, # 因此这道检查必须放在服务端。 on: push: # 'branches: [**]' 覆盖所有分支且天然排除 tag。 # 注意不能写成只有 tags-ignore:GitHub 的规则是"只给了 tag 过滤器就只跑 tag", # 于是 tags-ignore: ['**'] 等于"只跑 tag,但忽略所有 tag" —— 工作流永不触发。 branches: ['**'] pull_request: workflow_dispatch: permissions: {} jobs: check-commit-identity: permissions: contents: read runs-on: ubuntu-24.04 timeout-minutes: 10 steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false - name: Reject AI assistant identities and co-author trailers env: EVENT_NAME: ${{ github.event_name }} BEFORE_SHA: ${{ github.event.before }} HEAD_SHA: ${{ github.sha }} PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | set -euo pipefail ZERO=0000000000000000000000000000000000000000 US=$'\037' # 用不可能出现在姓名/邮箱里的 unit separator 作分隔符 # 确定扫描范围。三种情况: # 1. pull_request:base..head,只看本 PR 新增的提交 # 2. push 且 before 指向一个真实存在的提交:before..head # 3. 其余(新建分支 before 全 0、force-push 后 before 不可达、 # workflow_dispatch):退化为全量审计,扫描 HEAD 的完整历史 if [ "$EVENT_NAME" = "pull_request" ]; then RANGE="${PR_BASE_SHA}..${PR_HEAD_SHA}" elif [ "$EVENT_NAME" = "push" ] \ && [ -n "${BEFORE_SHA:-}" ] \ && [ "$BEFORE_SHA" != "$ZERO" ] \ && git cat-file -e "${BEFORE_SHA}^{commit}" 2>/dev/null; then RANGE="${BEFORE_SHA}..${HEAD_SHA}" else RANGE="$HEAD_SHA" fi echo "Scanning range: ${RANGE}" # 只有"确实没有 head 提交"(例如删除分支的 push)才允许放行; # 其余任何解析不出范围的情况一律 fail closed,不能静默通过。 if [ -z "${HEAD_SHA:-}" ] || [ "$HEAD_SHA" = "$ZERO" ]; then echo "No head commit (branch deletion?); nothing to check." exit 0 fi if ! COMMITS="$(git rev-list "$RANGE" 2>&1)"; then echo "::error::cannot resolve commit range ${RANGE}: ${COMMITS}" exit 1 fi COUNT="$(printf '%s' "$COMMITS" | grep -c . || true)" echo "Commits in range: ${COUNT}" if [ "$COUNT" -eq 0 ]; then echo "No commits to check." exit 0 fi # 身份判定刻意收窄,避免误伤: # - 邮箱在 AI 厂商域名下(含子域) # - 姓名恰好等于助手名,因此真人 "Claude Dubois" 不受影响 # 正文里出现 Claude/Anthropic 是完全合法的(本仓库群里有专讲 Claude # 的书),所以只检查身份字段与 Co-authored-by 尾注,绝不扫描自由文本。 BAD_EMAIL='@([a-z0-9.-]+\.)?(anthropic|openai)\.com$' BAD_NAME='^(claude([ ._-]?code)?|codex|chatgpt|copilot|anthropic|openai)(\[bot\])?$' BAD_TRAILER='^[[:space:]]*co-authored-by:.*(claude|anthropic|codex|copilot|openai|chatgpt)' # 粗筛:BAD_EMAIL 与 BAD_NAME 命中的行必然含下列词之一, # 因此它是二者的超集,用来跳过绝大多数干净提交,不会漏判。 BAD_ANY='(claude|anthropic|codex|chatgpt|copilot|openai)' failed=0 # 身份检查。命令替换先落地,set -e 才能捕获 git 失败; # while 循环用 here-string 喂数据,避免管道子 shell 吞掉 failed 赋值。 IDENTS="$(git log --format="%H${US}%an${US}%ae${US}%cn${US}%ce" "$RANGE")" # 用 here-string 而不是管道:set -o pipefail 下 grep -q 命中即退出会让 # 左侧进程收到 SIGPIPE,整条管道返回非 0,if 就会跳过检查—— # 而且只在输入大到写不进管道缓冲区时才发生,是典型的隐性漏判。 if grep -qiE "$BAD_ANY" <<< "$IDENTS"; then while IFS="$US" read -r sha an ae cn ce; do [ -n "$sha" ] || continue for role in author committer; do if [ "$role" = author ]; then name="$an"; mail="$ae"; else name="$cn"; mail="$ce"; fi lname="$(printf '%s' "$name" | tr '[:upper:]' '[:lower:]')" lmail="$(printf '%s' "$mail" | tr '[:upper:]' '[:lower:]')" if grep -qE "$BAD_EMAIL" <<<"$lmail" \ || grep -qE "$BAD_NAME" <<<"$lname"; then echo "::error::${sha} ${role} identity is an AI assistant: ${name} <${mail}>" failed=1 fi done done <<< "$IDENTS" fi # Co-authored-by 尾注检查(行首锚定并要求冒号,避免匹配正文叙述)。 # 先用一次 git log 流式扫全部正文;只有确实命中时才逐个提交定位, # 否则全量审计要为每个提交起一个 git 子进程(1500+ 提交约 50 秒)。 BODIES="$(git log --format='%B' "$RANGE")" if grep -qiE "$BAD_TRAILER" <<< "$BODIES"; then while read -r sha; do [ -n "$sha" ] || continue body="$(git log -1 --format='%B' "$sha")" if grep -qiE "$BAD_TRAILER" <<<"$body"; then echo "::error::${sha} has a Co-authored-by trailer referencing an AI assistant" printf '%s' "$body" | grep -iE "$BAD_TRAILER" | sed 's/^/ /' failed=1 fi done <<< "$COMMITS" fi if [ "$failed" -ne 0 ]; then echo "" echo "AI assistant attribution found in the commits above." echo "Rewrite them before pushing, e.g.:" echo " git rebase -i --exec 'git commit --amend --no-edit --reset-author' " exit 1 fi echo "OK: no AI assistant identity or Co-authored-by trailer in ${COUNT} commit(s)."