feat(attribution): conflict-free contributor mappings via contributors/emails/ directory

The AUTHOR_MAP dict in scripts/release.py was a merge-conflict magnet:
every concurrent salvage PR appended entries to the same lines of the
same file, so parallel PRs re-conflicted on every merge to main.

New system: one file per email under contributors/emails/ — filename is
the commit-author email, first non-comment line is the GitHub login.
File additions never conflict, so any number of PRs can add mappings
concurrently.

- scripts/release.py: AUTHOR_MAP is now LEGACY_AUTHOR_MAP (frozen)
  merged with the directory at import time (directory wins). All
  existing consumers (resolve_author, contributor_audit.py) unchanged.
- scripts/add_contributor.py: idempotent CLI to add a mapping; refuses
  conflicting reassignments (incl. against the legacy map), validates
  email/login shapes.
- contributor-check.yml: attribution gate now accepts a mapping file OR
  a legacy entry; failure message prints the exact add_contributor
  command. Also auto-resolves bare <login>@users.noreply.github.com
  emails is intentionally NOT added (kept id+login form only, matching
  previous behavior).
- contributor_audit.py: guidance now points at add_contributor.py.
- tests/scripts/test_contributor_map.py: 12 tests covering loader,
  merge precedence, CLI idempotency/conflict/validation, subprocess E2E.
This commit is contained in:
Teknium 2026-07-17 06:12:27 -07:00
parent 07f07c7b51
commit 06adcfabf9
No known key found for this signature in database
7 changed files with 333 additions and 13 deletions

View file

@ -27,7 +27,9 @@ jobs:
exit 0
fi
# Check each email against AUTHOR_MAP in release.py
# An email is mapped if it has a file in contributors/emails/
# (one file per email — conflict-free) or an entry in the frozen
# legacy AUTHOR_MAP in scripts/release.py.
MISSING=""
while IFS= read -r email; do
# Skip teknium and bot emails
@ -36,9 +38,12 @@ jobs:
continue ;;
esac
# Check if email is in AUTHOR_MAP (either as a key or matches noreply pattern)
if echo "$email" | grep -qP '\+.*@users\.noreply\.github\.com'; then
continue # GitHub noreply emails auto-resolve
continue # GitHub id+login noreply emails auto-resolve
fi
if [ -f "contributors/emails/${email}" ]; then
continue # mapped via the contributors directory
fi
if ! grep -qF "\"${email}\"" scripts/release.py 2>/dev/null; then
@ -49,19 +54,19 @@ jobs:
if [ -n "$MISSING" ]; then
echo ""
echo "⚠️ New contributor email(s) not in AUTHOR_MAP:"
echo "⚠️ New contributor email(s) without a mapping:"
echo -e "$MISSING"
echo ""
echo "Please add mappings to scripts/release.py AUTHOR_MAP:"
echo "Add a mapping file (do NOT edit AUTHOR_MAP in release.py):"
echo -e "$MISSING" | while read -r line; do
email=$(echo "$line" | sed 's/^ *//' | cut -d' ' -f1)
[ -z "$email" ] && continue
echo " \"${email}\": \"<github-username>\","
echo " python3 scripts/add_contributor.py ${email} <github-username>"
done
echo ""
echo "To find the GitHub username for an email:"
echo " gh api 'search/users?q=EMAIL+in:email' --jq '.items[0].login'"
exit 1
else
echo "✅ All contributor emails are mapped in AUTHOR_MAP."
echo "✅ All contributor emails are mapped."
fi