#!/bin/sh # Flags Co-Authored-By trailers with Claude/Anthropic attribution in PR commits. # The hive convention in /knowledge/hive-rules.md forbids these trailers. # # Pattern is anchored to an actual git trailer line — `^[[:space:]]*co-authored-by:` # — not any mention of the phrase, then requires the value name `claude`, # `anthropic`, or `noreply@anthropic.com`. Matching is case-insensitive and # per-line (git trailers are one `Key: value` per line) to cover variants like # `Co-Authored-By: Claude Sonnet 4.5 ` while letting a # commit subject/body that merely *discusses* the trailer (e.g. this script's # own "add CI lint for Co-Authored-By/Claude/Anthropic trailers") pass. # # Scope is the PR's commits (BASE..HEAD), not entire history — old commits # must not fail the build. # # Run with --self-test to check the pattern against fixtures instead of # scanning git history. set -eu trailer_re='^[[:space:]]*co-authored-by:.*(claude|anthropic|noreply@anthropic\.com)' if [ "${1:-}" = "--self-test" ]; then fail=0 assert_match() { if ! printf '%s\n' "$2" | grep -qiE "$trailer_re"; then echo "self-test FAILED (expected match): $1" >&2 fail=1 fi } assert_no_match() { if printf '%s\n' "$2" | grep -qiE "$trailer_re"; then echo "self-test FAILED (expected no match): $1" >&2 fail=1 fi } assert_match "bare trailer" 'Co-Authored-By: Claude' assert_match "full trailer with email" 'Co-Authored-By: Claude Sonnet 4.5 ' assert_match "lowercase trailer" 'co-authored-by: claude' assert_no_match "ordinary message" 'fix: correct off-by-one in parser' assert_no_match "prose mention of the trailer" 'ops: add CI lint for Co-Authored-By/Claude/Anthropic trailers' if [ "$fail" -eq 0 ]; then echo "self-test OK" fi exit "$fail" fi if [ -n "${GITHUB_BASE_REF:-}" ]; then base="origin/${GITHUB_BASE_REF}" else base="origin/main" fi # actions/checkout@v3 fetches the PR head but may not fetch the base branch if ! git rev-parse --verify "$base" >/dev/null 2>&1; then branch="${base#origin/}" git fetch origin "$branch" 2>/dev/null || true if ! git rev-parse --verify "$base" >/dev/null 2>&1; then base="origin/main" git fetch origin main 2>/dev/null || base="HEAD~10" fi fi commits="$(git log --reverse --format='%H' "${base}..HEAD" 2>/dev/null || true)" if [ -z "$commits" ]; then exit 0 fi hits="" for sha in $commits; do msg="$(git log -1 --format='%B' "$sha")" if printf '%s\n' "$msg" | grep -qiE "$trailer_re"; then subject="$(git log -1 --format='%s' "$sha")" hits="${hits}${sha}|${subject} " fi done if [ -n "$hits" ]; then printf '%s' "$hits" | while IFS='|' read -r sha subject; do [ -z "$sha" ] && continue short_sha="$(printf '%s' "$sha" | cut -c1-8)" printf '::error title=Attribution trailer found in %s::%s — commit carries a Co-Authored-By trailer with Claude/Anthropic attribution, forbidden by /knowledge/hive-rules.md\n' \ "$short_sha" "$subject" done count="$(printf '%s' "$hits" | grep -c '|' || true)" printf 'check-attribution-trailers: %s commit(s) with prohibited trailers found\n' "$count" >&2 exit 1 fi exit 0