feat(meta): derive author identity and profile from .mailmap

The README generator read its author URL out of the ## Credits sections it
writes itself. .mailmap now folds every address a person commits under into
their @infinito.nexus one, whose local part names their social profile, and
that profile becomes the single point of truth for an author.

utils/meta/identity.py parses .mailmap rather than shelling out to
git check-mailmap, which refuses the worktree the test container mounts.
Cross-checked against git over all 14 identities in the history: no
disagreement.

A new env handler carries the committing identity into the container, which
mounts the checkout but not the contributor's git configuration. Without it
both new tests would skip exactly where they are meant to fire:

  * tests/integration/meta/test_commit_identity.py fails when the configured
    address does not fold, and prints the .mailmap line to add.
  * tests/external/repository/test_author_profile.py probes that profile
    live, behind a canary so an unreachable instance is not reported as a
    missing profile.

Both look at the current address only. Probing every declared author would be
red for people who have not created a profile yet, and a suite that is red for
someone else's omission stops being read.

Marko Pjevac is declared as a role author but has no address here, so his
Credits line stays unlinked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-09-26 00:22:01 +02:00
1 parent caee86e168
commit fa015ab432
14 files changed
+481 -46

No files matched your search

+7 -1
View File
@@ -1 +1,7 @@
Kevin Veen-Birkenbach <kevin@veen.world> Environment Test <environment-test@example.invalid>
Alejandro Roman Ibanez <alexromanibanez@infinito.nexus> <romanibanez.alex@gmail.com>
Evangelos Tsakoudis <evangelostsakoudis@infinito.nexus> <baggtsak55@gmail.com>
Evangelos Tsakoudis <evangelostsakoudis@infinito.nexus> <tsakoudis.evangelos@hotmail.com>
Kevin Veen-Birkenbach <kevinveenbirkenbach@infinito.nexus> <kevin@veen.world>
Kevin Veen-Birkenbach <kevinveenbirkenbach@infinito.nexus> <mail@Kevin-Frantz.de>
Kevin Veen-Birkenbach <kevinveenbirkenbach@infinito.nexus> Environment Test <environment-test@example.invalid>
Prageeth Panicker <prageethpanicker@infinito.nexus> <prage.pani@gmail.com>
+2 -2
View File
@@ -28,7 +28,7 @@ from cli.build.docs.readme.cosmos import derive_cosmos_mermaid
from cli.build.docs.readme.sections import Readme, parse_readme
from utils.cache.files import PROJECT_ROOT, read_text
from utils.cache.yaml import load_yaml
from utils.roles.credits import author_urls
from utils.meta.identity import by_name, profile_url
from utils.roles.deploy import role_has_stack
from utils.roles.entity.name import get_entity_name
from utils.roles.mapping import ROLE_FILE_META_MAIN, ROLE_FILE_README
@@ -97,7 +97,7 @@ def _base_context(role_dir, role_name: str, app_name: str, *, invokable: bool) -
{"name": "Feature", "description": "Describe a capability."}
],
"application_author": _role_author(role_dir),
"application_author_url": author_urls().get(_role_author(role_dir)),
"application_author_url": profile_url(by_name().get(_role_author(role_dir), "")),
"application_is_host": not role_has_stack(role_dir),
"application_needs_clearnet_resolver": _needs_clearnet_resolver(role_dir),
"application_deploy_ids": ",".join(
+1 -1
View File
@@ -35,8 +35,8 @@ _REQUIRED_CTX: dict = {
_OPTIONAL_CTX: dict = {
"application_invokable": True,
"application_author_url": "https://social.infinito.nexus/profile/example/profile",
"cosmos_intro": "Example cosmos intro.",
"application_author_url": "https://www.veen.world",
"application_use_cases": "Example use case.",
"application_developer_notes": [{"file": "Administration.md", "description": "notes."}],
"application_further_resources": [{"label": "Example", "url": "https://example.com/"}],
+3
View File
@@ -80,6 +80,9 @@ services:
INFINITO_DNS_IP: ${INFINITO_DNS_IP}
INFINITO_DOMAIN: ${INFINITO_DOMAIN}
INFINITO_GHCR_MIRROR_PREFIX: ${INFINITO_GHCR_MIRROR_PREFIX}
INFINITO_GIT_AUTHOR_EMAIL: ${INFINITO_GIT_AUTHOR_EMAIL}
INFINITO_GIT_AUTHOR_NAME: ${INFINITO_GIT_AUTHOR_NAME}
INFINITO_GPU_COUNT: ${INFINITO_GPU_COUNT}
INFINITO_IMAGE_TAG: ${INFINITO_IMAGE_TAG}
INFINITO_IP4: ${INFINITO_IP4}
INFINITO_IP6: ${INFINITO_IP6}
+2 -1
View File
@@ -21,8 +21,9 @@
Optional context:
application_invokable When true, emit the Quick Setup section
(invokable roles only; forbidden otherwise).
application_author_url The author's social profile, derived from the
address .mailmap folds their commits into.
cosmos_intro Sentence above the Cosmos diagram.
application_author_url Wraps the Credits author in a Markdown link.
application_use_cases Paragraph for the optional Use Cases section.
application_developer_notes List of {file, description} local-doc links.
application_further_resources List of {label, url} external links.
+73
View File
@@ -0,0 +1,73 @@
"""The profile this contributor's address names is public and answers.
``.mailmap`` folds a commit address into an ``@infinito.nexus`` one, whose
local part is the slug of a profile on the social instance. That profile is
the single point of truth for an author's details, so it has to exist and be
readable without logging in.
Only the address this checkout commits under is probed. Probing every declared
author would fail for people who have not created their profile yet, and a
suite that is red for someone else's omission stops being read.
This is an external test because it performs a live HTTP request. A request
that never gets an answer is reported as unverified rather than as a missing
profile: the run could not reach the instance, so it learned nothing. The
canary below separates the two.
"""
from __future__ import annotations
import unittest
import requests
from utils.annotations.message import warning
from utils.meta.identity import DOMAIN, canonical, committing_as, profile_url
CANARY = f"https://social.{DOMAIN}/"
TIMEOUT = 20
READABLE = (200, 301, 302)
def _probe(url: str) -> tuple[int | None, str]:
try:
response = requests.get(url, timeout=TIMEOUT, allow_redirects=True)
except requests.RequestException as exc:
return None, str(exc)
return response.status_code, response.reason or ""
class TestAuthorProfile(unittest.TestCase):
def setUp(self) -> None:
name, email = committing_as()
if not email:
raise unittest.SkipTest("git config user.email is unset here")
_name, self.email = canonical(name, email)
self.url = profile_url(self.email)
if not self.url:
raise unittest.SkipTest(
f"{email!r} is not mapped to an @{DOMAIN} address yet; "
"tests/integration/meta/test_commit_identity.py reports that"
)
def test_the_profile_is_publicly_readable(self) -> None:
status, detail = _probe(CANARY)
if status is None:
raise unittest.SkipTest(f"social.{DOMAIN} is unreachable here: {detail}")
status, detail = _probe(self.url)
if status is None:
warning(f"{self.url} was not answered ({detail}); profile unverified")
raise unittest.SkipTest("the instance answered the canary but not the probe")
self.assertIn(
status,
READABLE,
f"{self.url} answered {status} {detail}. Your author details are read "
f"from that profile, so create it and make it public. It is derived "
f"from {self.email!r}, the address .mailmap folds your commits into.",
)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,50 @@
"""The identity this checkout commits under resolves to an Infinito.Nexus one.
A contributor may commit under any address, but ``.mailmap`` has to fold it
into their ``@infinito.nexus`` one, because that address is what names their
public profile and the profile is where an author's details live.
Only the address configured here is checked. A historic address nobody commits
under any more is not this contributor's problem to fix, and failing on it
would make the suite red for everyone.
"""
from __future__ import annotations
import unittest
from utils.meta.identity import DOMAIN, MAILMAP, canonical, committing_as, profile_url
class TestCommitIdentity(unittest.TestCase):
def setUp(self) -> None:
self.name, self.email = committing_as()
if not self.email:
raise unittest.SkipTest("git config user.email is unset here")
def test_the_configured_address_maps_to_an_infinito_nexus_one(self) -> None:
name, email = canonical(self.name, self.email)
self.assertTrue(
email.endswith(f"@{DOMAIN}"),
f"commits from this checkout carry {self.email!r}, which {MAILMAP} "
f"does not fold into an @{DOMAIN} address. Add a line to {MAILMAP}:\n\n"
f" {name} <yourname@{DOMAIN}> <{self.email}>\n\n"
f"That address names your profile at "
f"{profile_url(f'yourname@{DOMAIN}')}, which is where your author "
f"details are read from.",
)
def test_the_mapped_address_names_a_profile(self) -> None:
_name, email = canonical(self.name, self.email)
if not email.endswith(f"@{DOMAIN}"):
raise unittest.SkipTest("address is not mapped yet")
self.assertTrue(
profile_url(email),
f"{email!r} yields no profile slug; the local part must not be empty",
)
if __name__ == "__main__":
unittest.main()
+22 -6
View File
@@ -2,8 +2,8 @@ from __future__ import annotations
import unittest
from utils.meta.authors import author_names
from utils.roles.credits import author_urls
from utils.cache.files import PROJECT_ROOT
from utils.meta.authors import author_names, role_authors
from utils.software import SOFTWARE_AUTHOR
@@ -17,14 +17,30 @@ class TestAuthorNames(unittest.TestCase):
self.assertEqual(list(names), sorted(names))
self.assertEqual(len(names), len(set(names)))
def test_it_carries_exactly_the_keys_of_the_url_map(self) -> None:
def test_every_name_is_declared_by_at_least_one_role(self) -> None:
declared = set()
for role_dir in sorted((PROJECT_ROOT / "roles").iterdir()):
if role_dir.is_dir():
declared.update(role_authors(role_dir))
self.assertEqual(
set(author_names()),
set(author_urls()),
"the names are the map's keys, not a second list that could drift "
"from the Credits sections they are derived from",
declared,
"the names come from galaxy_info.author, not from prose that could "
"drift from the role metadata",
)
def test_a_role_crediting_several_people_yields_each_of_them(self) -> None:
multi = [
names
for role_dir in sorted((PROJECT_ROOT / "roles").iterdir())
if role_dir.is_dir() and len(names := role_authors(role_dir)) > 1
]
self.assertTrue(multi, "no role credits more than one author any more")
for names in multi:
self.assertTrue(all(name and not name.startswith(" ") for name in names))
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,116 @@
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from unittest import mock
from utils.meta.identity import (
DOMAIN,
MAILMAP,
PROFILE_BASE,
by_name,
canonical,
committing_as,
profile_url,
)
MAILMAP_BODY = """\
# a comment line
Alejandro Roman Ibanez <alexromanibanez@infinito.nexus> <romanibanez.alex@gmail.com>
Kevin Veen-Birkenbach <kevinveenbirkenbach@infinito.nexus> <kevin@veen.world>
Kevin Veen-Birkenbach <kevinveenbirkenbach@infinito.nexus> Old Name <old@example.com>
<prageethpanicker@infinito.nexus> <prage.pani@gmail.com>
"""
class TestProfileUrl(unittest.TestCase):
def test_the_local_part_names_the_profile(self) -> None:
self.assertEqual(
profile_url(f"kevinveenbirkenbach@{DOMAIN}"),
f"{PROFILE_BASE}/kevinveenbirkenbach/profile",
)
def test_a_foreign_domain_names_no_profile(self) -> None:
self.assertEqual(profile_url("someone@example.com"), "")
def test_the_domain_is_matched_case_insensitively(self) -> None:
self.assertTrue(profile_url(f"someone@{DOMAIN.upper()}"))
def test_an_empty_local_part_names_no_profile(self) -> None:
self.assertEqual(profile_url(f"@{DOMAIN}"), "")
class TestMailmap(unittest.TestCase):
def setUp(self) -> None:
self._tmp = tempfile.TemporaryDirectory(prefix="infinito-identity-")
self.root = Path(self._tmp.name)
(self.root / MAILMAP).write_text(MAILMAP_BODY, encoding="utf-8")
self.addCleanup(self._tmp.cleanup)
by_name.cache_clear()
self.addCleanup(by_name.cache_clear)
def test_an_alias_folds_into_the_canonical_address(self) -> None:
self.assertEqual(
canonical("Alejandro Roman", "romanibanez.alex@gmail.com", self.root),
("Alejandro Roman Ibanez", "alexromanibanez@infinito.nexus"),
)
def test_an_alias_written_with_a_name_folds_too(self) -> None:
self.assertEqual(
canonical("Old Name", "old@example.com", self.root)[1],
"kevinveenbirkenbach@infinito.nexus",
)
def test_an_entry_without_a_canonical_name_keeps_the_commit_name(self) -> None:
self.assertEqual(
canonical("Prageeth Panicker", "prage.pani@gmail.com", self.root),
("Prageeth Panicker", "prageethpanicker@infinito.nexus"),
)
def test_an_unknown_address_is_returned_unchanged(self) -> None:
self.assertEqual(
canonical("Nobody", "nobody@example.com", self.root),
("Nobody", "nobody@example.com"),
)
def test_the_address_is_matched_case_insensitively(self) -> None:
self.assertEqual(
canonical("Kevin Veen-Birkenbach", "Kevin@Veen.World", self.root)[1],
"kevinveenbirkenbach@infinito.nexus",
)
def test_by_name_reads_the_canonical_column(self) -> None:
self.assertEqual(
by_name(str(self.root))["Kevin Veen-Birkenbach"],
"kevinveenbirkenbach@infinito.nexus",
)
def test_a_missing_mailmap_folds_nothing(self) -> None:
empty = Path(self._tmp.name) / "empty"
empty.mkdir()
self.assertEqual(
canonical("Nobody", "nobody@example.com", empty),
("Nobody", "nobody@example.com"),
)
class TestCommittingAs(unittest.TestCase):
def test_the_carried_environment_wins_over_git(self) -> None:
carried = {
"INFINITO_GIT_AUTHOR_NAME": "Carried Name",
"INFINITO_GIT_AUTHOR_EMAIL": "carried@example.com",
}
with mock.patch.dict("os.environ", carried):
self.assertEqual(committing_as(), ("Carried Name", "carried@example.com"))
def test_an_empty_carried_address_falls_through_to_git(self) -> None:
with mock.patch.dict("os.environ", {"INFINITO_GIT_AUTHOR_EMAIL": " "}):
with mock.patch("subprocess.run") as run:
run.return_value = mock.Mock(stdout="from-git\n")
self.assertEqual(committing_as(), ("from-git", "from-git"))
if __name__ == "__main__":
unittest.main()
+2
View File
@@ -20,6 +20,7 @@ from .infinito import (
distros,
docker_volume,
domain,
git_identity,
gpu_count,
image,
image_repository,
@@ -98,6 +99,7 @@ ORDERED_HANDLERS = [
fork_repository_url,
nix_config,
registry_cache_max_size,
git_identity,
gpu_count,
i18n_libretranslate,
i18n_tuning,
+35
View File
@@ -0,0 +1,35 @@
"""INFINITO_GIT_AUTHOR_NAME / INFINITO_GIT_AUTHOR_EMAIL: the identity this
checkout commits under. The test container mounts the checkout but not the
contributor's git configuration, so the suite cannot ask git who is about to
commit; the ``.env`` carries the answer across."""
from __future__ import annotations
import subprocess
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from utils.env.builder import BuildContext, EnvBuilder
NAME_KEY = "INFINITO_GIT_AUTHOR_NAME"
EMAIL_KEY = "INFINITO_GIT_AUTHOR_EMAIL"
NAME_COMMENT = "Name this checkout commits under; empty when git has none."
EMAIL_COMMENT = "Address this checkout commits under; empty when git has none."
def _configured(repo_root, key: str) -> str:
try:
result = subprocess.run(
["git", "-C", str(repo_root), "config", "--get", key],
capture_output=True,
text=True,
check=False,
)
except OSError:
return ""
return result.stdout.strip()
def apply(eb: EnvBuilder, ctx: BuildContext) -> None:
eb.set(NAME_KEY, _configured(ctx.repo_root, "user.name"), comment=NAME_COMMENT)
eb.set(EMAIL_KEY, _configured(ctx.repo_root, "user.email"), comment=EMAIL_COMMENT)
+35 -9
View File
@@ -1,19 +1,45 @@
"""The people the repository credits, as one source for every reader.
"""The people the roles name as their authors.
The names are derived from the ``## Credits`` sections themselves rather than
kept as a list, so crediting a contributor needs no code change. The derivation
lives in :mod:`utils.roles.credits`; this module is the entry point for readers
that want the names alone.
``galaxy_info.author`` is where a role records who implemented it, so that is
where the names are read. A role may credit more than one person, comma
separated.
"""
from __future__ import annotations
from functools import lru_cache
from pathlib import Path
from utils.roles.credits import author_urls
from utils.cache.files import PROJECT_ROOT
from utils.cache.yaml import load_yaml
from utils.meta.role.names import ROLES_DIR
from utils.roles.mapping import ROLE_FILE_META_MAIN
def role_authors(role_dir: Path) -> tuple[str, ...]:
"""Return the people ``role_dir`` names as its authors.
Args:
role_dir: the role's directory.
"""
meta = role_dir / ROLE_FILE_META_MAIN
if not meta.is_file():
return ()
declared = ((load_yaml(str(meta)) or {}).get("galaxy_info") or {}).get("author")
if not declared:
return ()
return tuple(part.strip() for part in str(declared).split(",") if part.strip())
@lru_cache(maxsize=1)
def author_names() -> tuple[str, ...]:
"""Return every credited person's name, sorted."""
return tuple(sorted(author_urls()))
def author_names(root: str = str(PROJECT_ROOT)) -> tuple[str, ...]:
"""Return every declared role author, sorted and without duplicates.
Args:
root: repository root, as a string so the cache key stays hashable.
"""
found = set()
for path in sorted(Path(root, ROLES_DIR).iterdir()):
if path.is_dir():
found.update(role_authors(path))
return tuple(sorted(found))
+129
View File
@@ -0,0 +1,129 @@
"""Who an author is, and where their public profile lives.
``.mailmap`` maps every address a person has committed under to their
``@infinito.nexus`` address, and that address names their profile: the local
part is the profile slug. One social profile is therefore the single point of
truth for an author, and nothing here keeps a second copy of their name, URL
or avatar.
"""
from __future__ import annotations
import os
import re
import subprocess
from functools import lru_cache
from pathlib import Path
from utils.cache.files import PROJECT_ROOT, read_text
DOMAIN = "infinito.nexus"
PROFILE_BASE = f"https://social.{DOMAIN}/profile"
MAILMAP = ".mailmap"
_CANONICAL = re.compile(r"^(?P<name>[^<#]+?)\s*<(?P<email>[^>]+)>")
_ENTRY = re.compile(
r"^(?P<name>[^<#]+?)?\s*<(?P<email>[^>]+)>"
r"(?:\s*(?P<alias_name>[^<#]+?)?\s*<(?P<alias_email>[^>]+)>)?\s*$"
)
def profile_url(email: str) -> str:
"""Return the public profile the address names, empty for a foreign one.
Args:
email: an author's address.
"""
local, _, domain = email.strip().partition("@")
if domain.lower() != DOMAIN or not local:
return ""
return f"{PROFILE_BASE}/{local}/profile"
@lru_cache(maxsize=1)
def _aliases(root: str = str(PROJECT_ROOT)) -> dict[str, tuple[str, str]]:
"""Return ``{commit address: canonical (name, address)}``.
Exception: ``.mailmap`` is parsed here rather than handed to
``git check-mailmap``, because the test container mounts the checkout
without the worktree's git directory and git then refuses to read it.
Args:
root: repository root, as a string so the cache key stays hashable.
"""
path = Path(root, MAILMAP)
if not path.is_file():
return {}
found: dict[str, tuple[str, str]] = {}
for line in read_text(str(path)).splitlines():
match = _ENTRY.match(line.strip())
if not match:
continue
name = (match.group("name") or "").strip()
email = match.group("email")
alias = match.group("alias_email") or email
found[alias.lower()] = (name, email)
return found
def canonical(name: str, email: str, root: Path | None = None) -> tuple[str, str]:
"""Return the identity ``.mailmap`` folds ``name``/``email`` into.
Args:
name: the name the commit carries.
email: the address the commit carries.
root: repository root; defaults to this checkout.
Returns:
``(name, email)``, unchanged when the mailmap knows the address not.
"""
mapped = _aliases(str(root or PROJECT_ROOT)).get(email.strip().lower())
if not mapped:
return name, email
return mapped[0] or name, mapped[1]
@lru_cache(maxsize=1)
def by_name(root: str = str(PROJECT_ROOT)) -> dict[str, str]:
"""Return ``{author name: canonical address}`` as ``.mailmap`` declares it.
Args:
root: repository root, as a string so the cache key stays hashable.
"""
path = Path(root, MAILMAP)
if not path.is_file():
return {}
found: dict[str, str] = {}
for line in read_text(str(path)).splitlines():
match = _CANONICAL.match(line.strip())
if match:
found[match.group("name")] = match.group("email")
return found
def committing_as(root: Path | None = None) -> tuple[str, str]:
"""Return the identity a commit made here right now would carry.
Exception: the env pair wins over git. The test container mounts the
checkout but not the contributor's git configuration, so asking git there
yields nothing; ``utils/env/handlers/infinito/git_identity.py`` records the
host's answer in the ``.env`` the container reads.
Args:
root: repository root; defaults to this checkout.
Returns:
``(name, email)``; either is empty when neither source knows it.
"""
carried = (
os.environ.get("INFINITO_GIT_AUTHOR_NAME", "").strip(),
os.environ.get("INFINITO_GIT_AUTHOR_EMAIL", "").strip(),
)
if carried[1]:
return carried
base = ["git", "-C", str(root or PROJECT_ROOT), "config", "--get"]
values = [
subprocess.run([*base, key], capture_output=True, text=True, check=False)
for key in ("user.name", "user.email")
]
return tuple(value.stdout.strip() for value in values) # type: ignore[return-value]
+4 -26
View File
@@ -1,15 +1,9 @@
"""Attribution recorded in markdown ``## Credits`` sections.
The link that belongs to a credited person lives nowhere but the Credits
lines themselves: ``galaxy_info.author`` carries the name without a URL, and
``galaxy_info.company`` names the project owner rather than the role's
author, so neither can supply it. This module derives the name -> URL map
from the linked mentions already present in the repository, which keeps the
README generator and the ``credits-attribution`` lint reading the same
source instead of a hand-maintained list.
Adding a contributor therefore needs no code change: one linked credit
anywhere makes the generator emit that link for every role they author.
A role's author is declared in ``galaxy_info.author`` and rendered without a
link, so nothing here feeds the README generator any more. What remains
backs the ``credits-attribution`` lint: it reads the linked mentions already
present in prose and holds them to one link per person.
Patterns exported here:
@@ -28,7 +22,6 @@ Patterns exported here:
from __future__ import annotations
import re
from functools import lru_cache
from typing import TYPE_CHECKING
from utils.cache.files import iter_non_ignored_files, read_text
@@ -101,18 +94,3 @@ def iter_markdown_documents() -> Iterator[tuple[str, list[str]]]:
yield path, read_text(path).splitlines()
except OSError:
continue
@lru_cache(maxsize=1)
def author_urls() -> dict[str, str]:
"""Name -> URL for every person credited with exactly one URL repo-wide.
Names linked to conflicting URLs are omitted, so an ambiguous mapping
never silently picks a winner.
"""
documents = [lines for _path, lines in iter_markdown_documents()]
return {
name: next(iter(seen))
for name, seen in collect_author_urls(documents).items()
if len(seen) == 1
}