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
29 changes: 29 additions & 0 deletions .github/scripts/check-no-ai-attribution.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@
# actions@github.com emails
# - names containing claude/copilot/devin/aider/codex/gemini
#
# One carve-out, and only from the identity rules: a named dependency bot.
# The policy exists so that a human is not displaced as the author of record,
# and a dependency bump has no human author to displace — Dependabot is not an
# assistant that helped somebody write something, it is the whole author. The
# message rules still apply to its commits in full, so a bot cannot carry an
# AI co-author trailer or a "Generated with" watermark in past this.
#
# It is a hygiene guard, not a security boundary: anyone can set an author
# email locally. What stops a forged one is that it still has to survive
# review and a merge, which is where attribution is actually judged.
#
# Usage:
# .github/scripts/check-no-ai-attribution.sh <base>..<head>
#
Expand All @@ -24,6 +35,12 @@
# git commit --allow-empty -s -m "test: bad commit" \
# -m "Co-Authored-By: Example Bot <example[bot]@users.noreply.github.com>"
# .github/scripts/check-no-ai-attribution.sh main..HEAD # must fail once
# # and the dependency-bot carve-out, which must pass on identity but still
# # fail on a watermark in the message:
# git -c user.name='dependabot[bot]' \
# -c user.email='49699333+dependabot[bot]@users.noreply.github.com' \
# commit --allow-empty -m "build(deps): bump x" -m "Signed-off-by: dependabot[bot] <support@github.com>"
# .github/scripts/check-no-ai-attribution.sh main..HEAD # still just the one failure
# git checkout - && git branch -D scratch/attribution
set -euo pipefail

Expand All @@ -42,6 +59,13 @@ msg_patterns=(
email_pattern='(\[bot\]@users\.noreply\.github\.com$|@noreply\.anthropic\.com$|^actions@github\.com$)'
name_pattern='\b(claude|copilot|devin|aider|codex|gemini)\b'

# Automation identities exempt from the *identity* rules above (never from the
# message rules). Anchored at both ends and matched against the whole
# lowercased address: a substring rule here would be a hole a crafted local
# part could walk through. Matched by name rather than by GitHub's numeric user
# id, so the exemption survives an id change; add another bot by naming it.
trusted_bot_emails='^([0-9]+\+)?(dependabot|dependabot-preview)\[bot\]@users\.noreply\.github\.com$'

fail=0
count=0
while IFS= read -r sha; do
Expand All @@ -67,6 +91,11 @@ while IFS= read -r sha; do
name="$(git log -1 --format='%cn' "$sha")"
email="$(git log -1 --format='%ce' "$sha")"
fi
# Skip the identity rules for a named dependency bot in this role. The
# message rules ran above and applied to this commit like any other.
if printf '%s\n' "$email" | tr '[:upper:]' '[:lower:]' | grep -Eq "$trusted_bot_emails"; then
continue
fi
if printf '%s\n' "$email" | tr '[:upper:]' '[:lower:]' | grep -Eq "$email_pattern"; then
echo "::error::Commit ${sha} ${role} email '${email}' is a bot/vendor identity."
fail=1
Expand Down