{"record":{"id":"6c177c92698b293b","repo":"Hmbown/CodeWhale","slug":"context-right-hand-email-must-be-numeric-github","errorCode":null,"errorMessage":"{context}: right-hand email must be numeric GitHub noreply, got {identity.email}","messagePattern":"(.+?): right-hand email must be numeric GitHub noreply, got (.+?)","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"scripts/check-coauthor-trailers.py","lineNumber":136,"sourceCode":"\ndef norm_key(value: str) -> str:\n    return value.strip().lower()\n\n\ndef github_login_from_noreply(email: str) -> str | None:\n    if not CANONICAL_NOREPLY_RE.match(email):\n        return None\n    local = email.split(\"@\", 1)[0]\n    return local.split(\"+\", 1)[1]\n\n\ndef parse_identity(raw: str, context: str) -> Identity:\n    match = IDENTITY_RE.match(raw)\n    if not match:\n        raise ValueError(f\"{context}: expected 'Name <id+login@users.noreply.github.com>'\")\n    identity = Identity(match.group(\"name\").strip(), match.group(\"email\").strip())\n    if not CANONICAL_NOREPLY_RE.match(identity.email):\n        raise ValueError(\n            f\"{context}: right-hand email must be numeric GitHub noreply, got {identity.email}\"\n        )\n    return identity\n\n\ndef load_author_map(path: Path) -> dict[str, Identity]:\n    aliases: dict[str, Identity] = {}\n    for lineno, raw_line in enumerate(path.read_text(encoding=\"utf-8\").splitlines(), start=1):\n        line = raw_line.split(\"#\", 1)[0].strip()\n        if not line:\n            continue\n        if \"=\" not in line:\n            raise ValueError(f\"{path}:{lineno}: expected 'alias = Name <email>'\")\n        alias, raw_identity = [part.strip() for part in line.split(\"=\", 1)]\n        identity = parse_identity(raw_identity, f\"{path}:{lineno}\")\n        key = norm_key(alias)\n        if key in aliases and aliases[key] != identity:\n            raise ValueError(f\"{path}:{lineno}: duplicate alias {alias!r}\")","sourceCodeStart":118,"sourceCodeEnd":154,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/scripts/check-coauthor-trailers.py#L118-L154","documentation":"Even when the overall 'Name <email>' shape matches, the email on the right-hand side must be a canonical numeric GitHub noreply address (CANONICAL_NOREPLY_RE: id+login@users.noreply.github.com). This error names the actual email that failed, catching personal addresses, legacy 'login@users.noreply.github.com' forms, and foreign domains.","triggerScenarios":"An identity like 'Jane <jane@example.com>' or 'Jane <jane@users.noreply.github.com>' (no numeric id+ prefix) in AUTHOR_MAP or a Co-authored-by trailer. The regex requires the numeric-id-plus-login local part, so any other mailbox form is rejected with the offending value echoed.","commonSituations":"Contributors supplying their real email in trailers; GitHub's older noreply format without the user id; copy-paste from commits made before GitHub switched to id-prefixed noreply addresses; mailmap-driven rewrites producing legacy forms.","solutions":["Replace the email with the contributor's canonical numeric noreply: <userid+login@users.noreply.github.com>.","Find the numeric id via the GitHub API (api.github.com/users/<login> -> 'id') or from any of their recent commits' noreply addresses.","Re-run the checker to confirm both AUTHOR_MAP and all trailers in the range validate.","Configure local git to use the noreply address (git config user.email) for future commits if applicable."],"exampleFix":"# before\njane = Jane Doe <jane@example.com>\n# after\njane = Jane Doe <1234567+janedoe@users.noreply.github.com>","handlingStrategy":"validation","validationCode":"import re\n\nCANONICAL_NOREPLY_RE = re.compile(\n    r\"^[0-9]+\\+[a-zA-Z0-9-]+@users\\.noreply\\.github\\.com$\"\n)\n\ndef email_is_canonical_noreply(email: str) -> bool:\n    return CANONICAL_NOREPLY_RE.match(email.strip()) is not None","typeGuard":"def is_numeric_noreply(email: object) -> bool:\n    return isinstance(email, str) and bool(CANONICAL_NOREPLY_RE.match(email.strip()))","tryCatchPattern":null,"preventionTips":["Use id+login@users.noreply.github.com (numeric id prefix), never personal emails or the legacy login-only form.","Look up the numeric id once and store it in AUTHOR_MAP so all future trailers validate.","Reject personal emails at review time rather than letting CI catch them."],"tags":["git","coauthor-trailers","email","validation","ci"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}