ci: fix Identity Guard trigger — it was never actually running #1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| 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)' | |
| # 粗筛: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 printf '%s' "$lmail" | grep -qE "$BAD_EMAIL" \ | |
| || printf '%s' "$lname" | grep -qE "$BAD_NAME"; 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 printf '%s' "$body" | grep -qiE "$BAD_TRAILER"; 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' <base>" | |
| exit 1 | |
| fi | |
| echo "OK: no AI assistant identity or Co-authored-by trailer in ${COUNT} commit(s)." |