7c97aeeadb
A merge commit that lands a contributor's PR (`git merge --no-ff`) keeps the contributor as author of the merged commits, which is exactly the credit the gate asks for. When that merge also carries a `Harvested from PR #N by @login` line for the auto-close workflow, the checker now looks at the authors of `first-parent..merge` before demanding a duplicate Co-authored-by trailer on the merge commit itself. Non-merge harvested commits and merges that do not carry the contributor's commits are unchanged. Covered by a new unit test.
226 lines
9.1 KiB
Python
226 lines
9.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Regression tests for scripts/check-coauthor-trailers.py."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import sys
|
|
import unittest
|
|
import unittest.mock
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SCRIPT = ROOT / "scripts" / "check-coauthor-trailers.py"
|
|
AUTHOR_MAP = ROOT / ".github" / "AUTHOR_MAP"
|
|
FIXTURES = Path(__file__).resolve().parent / "fixtures" / "coauthor-trailers"
|
|
|
|
SPEC = importlib.util.spec_from_file_location("check_coauthor_trailers", SCRIPT)
|
|
assert SPEC and SPEC.loader
|
|
mod = importlib.util.module_from_spec(SPEC)
|
|
sys.modules[SPEC.name] = mod
|
|
SPEC.loader.exec_module(mod)
|
|
|
|
|
|
def commit(subject: str, body: str, *, harvested: bool = False) -> mod.Commit:
|
|
if harvested and "Harvested from PR" not in body:
|
|
body = f"{body}\n\nHarvested from PR #1 by @contributor"
|
|
return mod.Commit(
|
|
sha="deadbeef" * 5,
|
|
parents="",
|
|
author_name="Maintainer",
|
|
author_email="1+maintainer@users.noreply.github.com",
|
|
subject=subject,
|
|
body=body,
|
|
)
|
|
|
|
|
|
class CheckCoauthorTrailersTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls) -> None:
|
|
cls.aliases = mod.load_author_map(AUTHOR_MAP)
|
|
|
|
def test_rejects_cursor_trailer_on_non_harvested_commit(self) -> None:
|
|
body = (FIXTURES / "cursor-non-harvested.txt").read_text(encoding="utf-8")
|
|
errors = mod.validate([commit("direct change", body)], self.aliases, False)
|
|
self.assertTrue(errors)
|
|
self.assertIn("cursoragent@cursor.com", errors[0])
|
|
|
|
def test_rejects_cursor_trailer_on_harvested_commit(self) -> None:
|
|
body = (FIXTURES / "cursor-harvested.txt").read_text(encoding="utf-8")
|
|
errors = mod.validate([commit("harvested change", body, harvested=True)], self.aliases, False)
|
|
self.assertTrue(errors)
|
|
|
|
def test_allows_human_canonical_trailer(self) -> None:
|
|
body = (FIXTURES / "human-canonical.txt").read_text(encoding="utf-8")
|
|
errors = mod.validate([commit("human credit", body)], self.aliases, False)
|
|
self.assertEqual(errors, [])
|
|
|
|
def test_merge_of_contributor_pr_carries_harvest_credit_by_authorship(self) -> None:
|
|
merge = mod.Commit(
|
|
sha="feedface" * 5,
|
|
parents="1111111111111111111111111111111111111111 2222222222222222222222222222222222222222",
|
|
author_name="CodeWhale Bot",
|
|
author_email="bot@codewhale.net",
|
|
subject="Merge PR #5423: test(tui): isolate background verifier from rustup",
|
|
body="Merge PR #5423\n\nHarvested from PR #5423 by @wuisabel-gif",
|
|
)
|
|
with unittest.mock.patch.object(
|
|
mod,
|
|
"merged_author_emails",
|
|
return_value={"231155141+wuisabel-gif@users.noreply.github.com"},
|
|
):
|
|
errors = mod.validate([merge], self.aliases, False)
|
|
self.assertEqual(errors, [])
|
|
# Without the contributor on the merged side the same merge still needs a trailer.
|
|
with unittest.mock.patch.object(mod, "merged_author_emails", return_value=set()):
|
|
errors = mod.validate([merge], self.aliases, False)
|
|
self.assertEqual(len(errors), 1)
|
|
self.assertIn("needs machine-readable credit", errors[0])
|
|
|
|
def test_allows_merge_commit_with_bot_trailer(self) -> None:
|
|
merge = mod.Commit(
|
|
sha="cafebabe" * 5,
|
|
parents="aaa bbb",
|
|
author_name="Maintainer",
|
|
author_email="1+maintainer@users.noreply.github.com",
|
|
subject="Merge branch",
|
|
body="Co-authored-by: Cursor <cursoragent@cursor.com>",
|
|
)
|
|
errors = mod.validate([merge], self.aliases, False)
|
|
self.assertEqual(errors, [])
|
|
|
|
def test_allows_only_the_exact_immutable_automation_trailer(self) -> None:
|
|
legacy = mod.Commit(
|
|
sha="9a74825cd182a62465943bcbbcbcf591d1ce99ee",
|
|
parents="parent",
|
|
author_name="Maintainer",
|
|
author_email="1+maintainer@users.noreply.github.com",
|
|
subject="legacy automation commit",
|
|
body="Co-authored-by: CodeWhale Agent <codewhale-agent@hmbown.local>",
|
|
)
|
|
errors = mod.validate([legacy], self.aliases, False)
|
|
self.assertEqual(errors, [])
|
|
|
|
other_commit = mod.Commit(
|
|
sha="deadbeef" * 5,
|
|
parents=legacy.parents,
|
|
author_name=legacy.author_name,
|
|
author_email=legacy.author_email,
|
|
subject=legacy.subject,
|
|
body=legacy.body,
|
|
)
|
|
errors = mod.validate([other_commit], self.aliases, False)
|
|
self.assertTrue(errors)
|
|
self.assertIn("codewhale-agent@hmbown.local", errors[0])
|
|
|
|
def test_legacy_exception_does_not_hide_another_bad_trailer(self) -> None:
|
|
legacy_with_extra = mod.Commit(
|
|
sha="9a74825cd182a62465943bcbbcbcf591d1ce99ee",
|
|
parents="parent",
|
|
author_name="Maintainer",
|
|
author_email="1+maintainer@users.noreply.github.com",
|
|
subject="legacy automation commit",
|
|
body=(
|
|
"Co-authored-by: CodeWhale Agent <codewhale-agent@hmbown.local>\n"
|
|
"Co-authored-by: Unknown Person <unknown@example.com>"
|
|
),
|
|
)
|
|
errors = mod.validate([legacy_with_extra], self.aliases, False)
|
|
self.assertEqual(len(errors), 1)
|
|
self.assertIn("unknown@example.com", errors[0])
|
|
|
|
def test_preserved_hunter_credit_resolves_only_for_exact_commit(self) -> None:
|
|
preserved = mod.Commit(
|
|
sha="5087269606fc8847487b0a8b51ef6adffa8eb2ca",
|
|
parents="parent",
|
|
author_name="Hunter B",
|
|
author_email="hmbown@gmail.com",
|
|
subject="docs(public): refresh v0.9.1 product truth",
|
|
body=(
|
|
"Harvested from PR #4508 by @Hmbown.\n\n"
|
|
"Co-authored-by: Hunter Bown <hmbown@gmail.com>"
|
|
),
|
|
)
|
|
errors = mod.validate([preserved], self.aliases, True)
|
|
self.assertEqual(errors, [])
|
|
|
|
rewritten = mod.Commit(
|
|
sha="deadbeef" * 5,
|
|
parents=preserved.parents,
|
|
author_name=preserved.author_name,
|
|
author_email=preserved.author_email,
|
|
subject=preserved.subject,
|
|
body=preserved.body,
|
|
)
|
|
errors = mod.validate([rewritten], self.aliases, True)
|
|
self.assertTrue(errors)
|
|
self.assertTrue(any("not GitHub-mappable" in error for error in errors))
|
|
|
|
def test_preserved_web_credit_resolves_without_rewriting_commit(self) -> None:
|
|
preserved = mod.Commit(
|
|
sha="e37df06caeb3064b2bb9263c1c98a903738f3a0a",
|
|
parents="parent",
|
|
author_name="Hunter B",
|
|
author_email="hmbown@gmail.com",
|
|
subject="docs(web): make the homepage product first",
|
|
body="Co-authored-by: Hunter Bown <hmbown@gmail.com>",
|
|
)
|
|
errors = mod.validate([preserved], self.aliases, True)
|
|
self.assertEqual(errors, [])
|
|
|
|
def test_preserved_fleitz_credit_resolves_only_for_exact_commit(self) -> None:
|
|
preserved = mod.Commit(
|
|
sha="6d0ebc881a8bd2469c45b25f2a606fa63681e112",
|
|
parents="parent",
|
|
author_name="Fred Leitz",
|
|
author_email="fred.leitz@gmail.com",
|
|
subject="fix(shell): default no-cwd shell commands to context.workspace",
|
|
body="Co-authored-by: fleitz <fleitzo@gmail.com>",
|
|
)
|
|
errors = mod.validate([preserved], self.aliases, True)
|
|
self.assertEqual(errors, [])
|
|
|
|
changed_identity = mod.Commit(
|
|
sha=preserved.sha,
|
|
parents=preserved.parents,
|
|
author_name=preserved.author_name,
|
|
author_email=preserved.author_email,
|
|
subject=preserved.subject,
|
|
body="Co-authored-by: fleitz <different@example.com>",
|
|
)
|
|
errors = mod.validate([changed_identity], self.aliases, True)
|
|
self.assertTrue(errors)
|
|
self.assertTrue(any("different@example.com" in error for error in errors))
|
|
|
|
def test_preserved_telecom_harvest_author_resolves_only_for_exact_commit(self) -> None:
|
|
preserved = mod.Commit(
|
|
sha="338138eb546bcf8917b27395325f59af0d2e4f52",
|
|
parents="parent",
|
|
author_name="Hunter B",
|
|
author_email="hmbown@gmail.com",
|
|
subject="feat(provider): add TelecomJS live catalog",
|
|
body=(
|
|
"Harvested from PR #4370 by @baendlorel.\n\n"
|
|
"Co-authored-by: baendlorel "
|
|
"<50111870+baendlorel@users.noreply.github.com>"
|
|
),
|
|
)
|
|
errors = mod.validate([preserved], self.aliases, True)
|
|
self.assertEqual(errors, [])
|
|
|
|
rewritten = mod.Commit(
|
|
sha="deadbeef" * 5,
|
|
parents=preserved.parents,
|
|
author_name=preserved.author_name,
|
|
author_email=preserved.author_email,
|
|
subject=preserved.subject,
|
|
body=preserved.body,
|
|
)
|
|
errors = mod.validate([rewritten], self.aliases, True)
|
|
self.assertTrue(errors)
|
|
self.assertTrue(any("not canonical" in error for error in errors))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(unittest.main())
|