ops: add CI lint for Co-Authored-By/Claude/Anthropic trailers

Three builder runs tonight carried a Co-Authored-By: Claude trailer
despite bold instructions forbidding it. A rule in a prompt is not a
control — this gate is. Flags commits in a PR's range carrying
co-authored-by + (claude OR anthropic OR noreply@anthropic.com),
case-insensitive, covering all observed spellings.

Wired into CI (new attribution-trailer job) and pre-push hook. Mirrors
existing lint scripts in structure and exit conventions. Pattern tested
against all three variants plus a normal commit.
This commit is contained in:
atlas 2026-09-15 22:58:55 +02:00 committed by mara
commit bf8e309b19
3 changed files with 108 additions and 0 deletions

View file

@ -68,6 +68,23 @@ jobs:
# their own. See scripts/check-doc-refs.sh.
run: sh scripts/check-doc-refs.sh
attribution-trailers:
name: attribution-trailer lint
runs-on: [hive-ci]
# Pure git — seconds.
timeout-minutes: 5
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: lint
# Flags Co-Authored-By trailers with Claude/Anthropic attribution
# in PR commits (hive convention in /knowledge/hive-rules.md forbids
# them — an attribution line in a prompt is not a control). Own
# job, and IS a required check on the forge (branch protection) — a
# hit blocks merge. See scripts/check-attribution-trailers.sh.
run: sh scripts/check-attribution-trailers.sh
prose-lint:
name: prose lint (vale)
runs-on: [hive-ci]

View file

@ -0,0 +1,85 @@
#!/bin/sh
# CI lint: flags Co-Authored-By trailers with Claude/Anthropic attribution in
# PR commits. The hive convention in /knowledge/hive-rules.md forbids these
# trailers — an attribution line in a prompt is not a control. This gate is.
#
# Emits a CI error annotation per offending commit and exits 1 if any commit
# in the PR's range carries a prohibited trailer. Runs as its own CI job and
# IS a required check on the forge (branch protection) — a hit blocks merge.
#
# Pattern: case-insensitive `co-authored-by` followed by `claude`, `anthropic`,
# or `noreply@anthropic.com` anywhere in the line. Covers variants seen in the
# wild: `Co-Authored-By: Claude`, `Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>`.
#
# Scope: commits in the PR's range (BASE..HEAD), not the entire history — old
# commits must not fail the build. In CI, BASE is the target branch
# (GITHUB_BASE_REF / github.event.pull_request.base.ref in GH Actions; similar
# vars in Forgejo). Falls back to origin/main for local runs.
set -eu
# Determine base ref: in CI (pull_request event), use the PR's base branch;
# locally, default to origin/main. CI env vars: GITHUB_BASE_REF (GitHub
# Actions), similar for Forgejo/Gitea (not standardized across runners, so we
# check both common patterns).
if [ -n "${GITHUB_BASE_REF:-}" ]; then
# GitHub Actions / Forgejo Actions pull_request context
base="origin/${GITHUB_BASE_REF}"
elif [ -n "${CI_COMMIT_REF_NAME:-}" ]; then
# GitLab-style variable (some Forgejo setups)
base="origin/main"
else
# Local run or unknown CI — default to origin/main
base="origin/main"
fi
# Ensure the base ref exists (in CI, actions/checkout@v3 fetches the PR head
# but may not fetch the base branch by default — fetch it if missing).
if ! git rev-parse --verify "$base" >/dev/null 2>&1; then
# Extract branch name and fetch it
branch="${base#origin/}"
git fetch origin "$branch" 2>/dev/null || true
# If still missing, fall back to main
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
# Get all commits in the PR's range. `--reverse` lists oldest first (clearer
# for annotation output). `--format=%H` gets the full commit sha.
commits="$(git log --reverse --format='%H' "${base}..HEAD" 2>/dev/null || true)"
if [ -z "$commits" ]; then
# No commits in range — either not on a PR branch, or nothing new. Pass.
exit 0
fi
# Check each commit's full message for the prohibited trailer pattern.
# Case-insensitive grep for `co-authored-by` + any of `claude`, `anthropic`,
# `noreply@anthropic.com`.
hits=""
for sha in $commits; do
msg="$(git log -1 --format='%B' "$sha")"
# Match case-insensitive co-authored-by AND (claude OR anthropic OR noreply@anthropic.com)
# Use grep -E with -i for case-insensitive matching
if printf '%s\n' "$msg" | grep -qiE 'co-authored-by.*claude|co-authored-by.*anthropic|co-authored-by.*noreply@anthropic\.com'; then
# Found a hit — build annotation
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

View file

@ -34,5 +34,11 @@ if ! sh "$repo_root/scripts/check-doc-refs.sh"; then
exit 1
fi
echo "pre-push: running attribution-trailer lint..." >&2
if ! sh "$repo_root/scripts/check-attribution-trailers.sh"; then
echo "pre-push: attribution-trailer lint FAILED — fix before pushing" >&2
exit 1
fi
echo "pre-push: lints passed ✓" >&2
exit 0