Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ name: ci

on:
pull_request:
# edited re-fires the gate on title/body changes — otherwise a
# post-green metadata edit could land attribution unchecked.
types: [opened, synchronize, reopened, edited]
push:
branches: [main]

Expand All @@ -16,6 +19,8 @@ jobs:
commit-lint:
name: commit-lint
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
Expand All @@ -33,7 +38,11 @@ jobs:
PR_TITLE: ${{ github.event.pull_request.title }}
run: scripts/commit-check.sh --pr-title "$PR_TITLE"
- name: scan PR body for attribution markers
if: github.event_name == 'pull_request'
# Bot-authored bodies (dependabot, renovate) are platform-
# generated boilerplate — GitHub injects zero-width spaces into
# @-mentions for anti-ping, which this scan exists to catch.
# Not a human attribution surface; commits/title still lint.
if: github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot'
env:
PR_BODY: ${{ github.event.pull_request.body }}
run: |
Expand All @@ -45,8 +54,10 @@ jobs:
# a stray start marker could hide attribution after it.
open_m='<!-- This is an auto-generated comment: release notes by coderabbit.ai -->'
close_m='<!-- end of auto-generated comment: release notes by coderabbit.ai -->'
opens=$(grep -cF "$open_m" pr-body.txt || :)
closes=$(grep -cF "$close_m" pr-body.txt || :)
# -oF counts occurrences, not matching lines — same-line
# pairs would read as one open and one close to -c.
opens=$(grep -oF "$open_m" pr-body.txt | wc -l || :)
closes=$(grep -oF "$close_m" pr-body.txt | wc -l || :)
if [ "$opens" != "$closes" ]; then
echo "commit-check: unbalanced CodeRabbit release-notes markers in PR body" >&2
exit 1
Expand All @@ -67,6 +78,12 @@ jobs:
}
printf "%s", out rest
}' pr-body.txt > pr-body-clean.txt
# equal counts but wrong order leave markers behind in the
# cleaned body — a stray open/close is not a managed block.
if grep -qF "$open_m" pr-body-clean.txt || grep -qF "$close_m" pr-body-clean.txt; then
echo "commit-check: misordered CodeRabbit release-notes markers in PR body" >&2
exit 1
fi
scripts/commit-check.sh --scan-file pr-body-clean.txt
- name: lint push range
if: github.event_name == 'push'
Expand All @@ -78,6 +95,14 @@ jobs:
echo "new branch push — no commit range to lint"
exit 0
fi
# a force-pushed before-SHA can be unreachable from every ref;
# fetch it best-effort — if it stays unresolvable the script
# fails the check rather than silently linting zero commits.
# Never --depth: a shallow boundary hides PUSH_BEFORE's
# ancestry, and rev-list would then lint all of history. The
# fetch-depth:0 checkout already has it for normal pushes —
# fetch only when absent, without a depth limit.
git cat-file -e "$PUSH_BEFORE^{commit}" 2>/dev/null || git fetch --no-tags origin "$PUSH_BEFORE" 2>/dev/null || true
scripts/commit-check.sh --range "$PUSH_BEFORE..$PUSH_AFTER"

# Site gate: type-aware oxlint + oxfmt/prettier check + full static build.
Expand Down
40 changes: 33 additions & 7 deletions scripts/commit-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,32 +36,58 @@ warn() { printf '%s\n' "commit-check: warning: $*" >&2; }

check_attribution() {
_msg=$1
# One vendor alternation shared by every pattern below — extend it
# here, never in a single rule, or the rules drift apart.
_VENDOR='claude|cursor|copilot|chatgpt|codex|gemini|devin|openai|anthropic|codeium|windsurf|github[[:space:]]+copilot|claude[[:space:]]+code|(claude|gemini)[[:space:]]+(opus|sonnet|pro|ultra)|swe-[0-9a-z.-]*'
# AI co-author / bot trailers. A human Co-authored-by is fine; the
# ban is on AI vendors, bots, and *-ai-* / [bot] addresses.
if printf '%s\n' "$_msg" | grep -iE '^[[:space:]]*co-?authored-by:' \
| grep -qiE 'claude|cursor|copilot|chatgpt|codex|gemini|devin-ai|cognition\.ai|devin\.ai[[:space:]>]|openai|anthropic|codeium|windsurf|swe-|\[bot\]|[a-z0-9-]*bot@|ai-integration'; then
_co=$(printf '%s\n' "$_msg" | grep -iE '^[[:space:]]*co-?authored-by:' || :)
# Unambiguous machine signals anywhere in the trailer: [bot] tags,
# bot@ addresses, *-ai-integration logins, AI-vendor domains (literal
# list of vendor-owned domains — never github.com, or humans on the
# privacy relay get flagged), and a vendor token inside a bare email
# (no <display name>) — bot forms like
# "Co-authored-by: 175728472+Copilot@users.noreply.github.com".
if printf '%s\n' "$_co" | grep -qiE '\[bot\]|(^|[<.[:space:]_+-])bot@|ai-integration|noreply@(anthropic|openai)\.|@(anthropic|openai|cognition|devin|codeium|windsurf|claude-code|cursor|gemini|chatgpt|codex|copilot|google)\.(com|ai|dev|io)([[:space:]>]|$)|:[[:space:]]*[^<[:space:]]*('"$_VENDOR"')[^<[:space:]]*@'; then
err "AI Co-authored-by trailer"
# A bare vendor identity as the display name — "Co-authored-by:
# Claude <noreply@anthropic.com>" hits the domain rule above; this
# catches no-email and generic-mailbox forms. Human names that merely
# CONTAIN a vendor token ("Claude Martin <claude.martin@…>") stay
# legal per .cursor/rules/no-ai-attribution.mdc.
elif printf '%s\n' "$_co" | sed 's/^[^:]*:[[:space:]]*//; s/<.*//' \
| grep -qiE '^\[?('"$_VENDOR"')\]?(\([^)]*\))?([[:space:]-]?ai)?[[:space:]]*$'; then
err "AI Co-authored-by trailer"
fi
# generator footers: "Generated with <tool>", "Made with: X",
# "Authored via X" — the tool names that sign output, plus the
# trailer forms regardless of vendor.
if printf '%s\n' "$_msg" | grep -iqE '(generated|made|created|authored|written|produced)[[:space:]]+(with|by|via)[[:space:]]*\[?[[:space:]]*(claude|cursor|copilot|chatgpt|codex|gemini|devin|openai|anthropic|codeium|windsurf|swe-)'; then
# connector may carry a colon ("Created with: Cursor"); the vendor
# token needs a non-alnum tail so "generated by cursors" stays clean.
if printf '%s\n' "$_msg" | grep -iqE '(generated|made|created|authored|written|produced)[[:space:]]+(with|by|via):?[[:space:]]*\[?[[:space:]]*('"$_VENDOR"')([^a-z0-9]|$)'; then
err "AI generator footer"
fi
if printf '%s\n' "$_msg" | grep -iqE '^[[:space:]]*(generated|made|authored)[- ](with|by|via)[[:space:]]*:'; then
err "generator trailer"
fi
# bot-emoji generator signature
if printf '%s\n' "$_msg" | grep -q '🤖'; then
# standalone "via <tool>" / "by <tool>" / "with <tool>" lines — the
# footer regex above needs a leading verb, so a lone via-line slips.
if printf '%s\n' "$_msg" | grep -qiE '^[[:space:]]*(via|by|using|with)[[:space:]]+\[?('"$_VENDOR"')\]?(\([^)]*\))?([[:space:]-]?ai)?([^a-z0-9]|$)'; then
err "attribution via-line"
fi
# bot-emoji signature: a bare 🤖 line, or 🤖 sharing a line with
# attribution vocabulary — a mid-sentence robot mention stays clean.
_bot=$(printf '%s\n' "$_msg" | grep '🤖' || :)
if printf '%s\n' "$_bot" | grep -qiE '^[[:space:]]*🤖[[:space:]]*$|generated|made|created|authored|written|produced|built|co-?authored|via[[:space:]]|with[[:space:]]|by[[:space:]]'; then
err "bot-emoji attribution marker"
fi
# AI named as the author/agent — not as the subject of the change.
# (kept narrow on purpose: "add Copilot webhook" must stay legal)
if printf '%s\n' "$_msg" | grep -iqE '\b(as an ai|i generated this|authored by (claude|cursor|copilot|chatgpt|codex|gemini|devin|openai|swe-))\b'; then
if printf '%s\n' "$_msg" | grep -iqE '\b(as an ai|i generated this|authored by \[?('"$_VENDOR"'))\b'; then
err "AI self-attribution"
fi
# vendor-stamped HTML comments smuggled into the message
if printf '%s\n' "$_msg" | grep -iqE '<!--[^>]*(claude|cursor|copilot|chatgpt|codex|gemini|devin|openai|anthropic|generated|🤖)'; then
if printf '%s\n' "$_msg" | grep -iqE '<!--[^>]*('"$_VENDOR"'|generated|🤖)'; then
err "attribution HTML comment"
fi
# zero-width / invisible markers: U+200B..D, U+2060, U+FEFF as raw
Expand Down
Loading