Skip to content
Merged
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
91 changes: 91 additions & 0 deletions .github/workflows/check-attribution.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: Check Attribution

# No branch filter: the base branch is read at runtime, so this works the same
# on repos that call it main and repos that call it master.
on: pull_request

# Coding agents sign their work either with a Co-Authored-By trailer or by
# claiming the author/committer field. Squash-merging collects the trailers of
# every commit in the PR into the merge commit, so a single unnoticed trailer
# reaches the default branch permanently. Each identity below is a
# vendor-controlled address or a known agent's GitHub App handle. Dependency
# bots (dependabot, renovate) are deliberately allowed: they are maintenance
# automation, not authorship claims.
env:
# Bare identities, matched against a trailer address and against the
# author/committer fields. The optional digits cover GitHub's
# "<id>+<handle>@users.noreply.github.com" form, whose id varies per app.
AI_IDENTITY_RE: '(noreply@anthropic\.com|cursoragent@cursor\.com|(noreply|codex)@openai\.com|noreply@aider\.chat|openhands@all-hands\.dev|noreply@opencode\.ai|noreply@continue\.dev|clio-agent@sisyphuslabs\.ai|roomote@roocode\.com|copilot@github\.com|([0-9]+[+])?(Copilot|gemini-code-assist\[bot\]|greptile-apps\[bot\]|coderabbitai\[bot\]|ellipsis-dev\[bot\]|qodo-merge-pro\[bot\]|sweep-ai\[bot\]|bito-code-review\[bot\]|roomote\[bot\]|factory-droid\[bot\]|opencode-agent\[bot\]|google-labs-jules\[bot\])@users\.noreply\.github\.com)'

jobs:
check-bot-coauthors:
if: ${{ !endsWith(github.actor, '[bot]') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0

- name: Check for bot Co-Authored-By lines
env:
BASE_REF: ${{ github.base_ref }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
COMMITS=$(git log --format='%H' "origin/${BASE_REF}..${HEAD_SHA}" 2>/dev/null || true)

if [ -z "$COMMITS" ]; then
echo "No commits to check"
exit 0
fi

# Anchored to a full trailer line so a mention in prose does not fail.
TRAILER_RE="^[[:space:]]*co-authored-by:[[:space:]]+.*<${AI_IDENTITY_RE}>[[:space:]]*$"

FOUND=0
for sha in $COMMITS; do
MSG=$(git log --format='%B' -1 "$sha")
HITS=$(printf '%s\n' "$MSG" | grep -Ei "$TRAILER_RE" || true)
if [ -n "$HITS" ]; then
echo "::error::Commit $sha contains a bot Co-Authored-By line. Remove it with: git rebase -i and edit the commit message."
printf '%s\n' "$HITS"
FOUND=1
fi
done

if [ "$FOUND" -eq 1 ]; then
echo ""
echo "To fix: run 'git rebase -i origin/${BASE_REF}', change 'pick' to 'reword' for flagged commits, then remove the Co-Authored-By lines."
exit 1
fi

echo "All commits clean."

- name: Check for bot commit authors
env:
BASE_REF: ${{ github.base_ref }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
# Some agents (Copilot's cloud agent, Jules, Devin) claim the author
# or committer field instead of adding a trailer, so the check above
# cannot see them. Written to a file rather than piped so the loop
# runs in this shell and its result survives.
git log --format='%H %ae %ce' "origin/${BASE_REF}..${HEAD_SHA}" > /tmp/authors.txt 2>/dev/null || true

FOUND=0
while read -r sha author committer; do
[ -z "$sha" ] && continue
for addr in "$author" "$committer"; do
if printf '%s\n' "$addr" | grep -Eqi "^${AI_IDENTITY_RE}$"; then
echo "::error::Commit $sha is authored or committed by a coding agent: $addr"
FOUND=1
fi
done
done < /tmp/authors.txt

if [ "$FOUND" -eq 1 ]; then
echo ""
echo "To fix: rewrite the commit with 'git rebase -i origin/${BASE_REF}', then 'git commit --amend --author=\"Your Name <you@example.com>\"'."
exit 1
fi

echo "All commit authors clean."