Files
computer-playbook/utils/roles/display.py
T
kevinveenbirkenbachandClaude Opus 5 fbd4b3a18d refactor(meta): label categories with one hieroglyph
A deploy job name repeats the category path on every row of the matrix. One Egyptian hieroglyph costs four UTF-8 bytes where the two Chinese characters it replaces cost six. Unicode names each sign by its Gardiner code, so the opaque value stays identifiable without a comment per entry.

A label has to stay one codepoint: a sign written with a variation selector costs seven bytes and is worse than what it replaced. tests/integration/meta/categories/test_glyph.py enforces that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-20 23:17:19 +02:00

226 lines
8.1 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Codec between a role id and the display name CI names carry.
A role id is the machine handle (``web-app-nextcloud``); the display name is
what a human reads off a GitHub Actions job or run title
(``𓏴𓏛·Nextcloud``). The category path renders as the ``glyph`` labels
``meta/categories.yml`` declares, run together into one block, then a ``·``,
then the role's own ``README.md`` heading or the part of the id the categories
do not already carry.
The name never holds a space: the ``whitelist`` and ``priority`` workflow
inputs are space-separated lists, so a display name with a space in it would
make a two-role list unparseable.
:meth:`RoleDisplayName.decode` accepts a raw role id unchanged, so every
consumer can decode unconditionally and a human dispatching a run by hand
never has to type a glyph.
"""
from __future__ import annotations
import re
import sys
from functools import lru_cache
from typing import TYPE_CHECKING
from utils.cache import PROJECT_ROOT
from utils.cache.files import read_text
from utils.cache.yaml import load_yaml
from utils.roles.categories import categories_file
if TYPE_CHECKING:
from collections.abc import Sequence
from pathlib import Path
SEPARATOR = "·"
VARIANT_SEPARATOR = "#"
_HEADING = re.compile(r"^#\s+(.+?)\s*$", re.MULTILINE)
_VARIANT_SUFFIX = re.compile(re.escape(VARIANT_SEPARATOR) + r"[\d,]+$")
_ROLE_ID = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)+")
_VARIATION = re.compile("[︎️]")
_AXIS_START = re.compile(r"[#@+%/]")
def split_axes(token: str) -> tuple[str, str]:
"""Split a CI selection token into its name and its axis suffix.
``#``, ``@``, ``+``, ``%`` and ``/`` introduce an axis and appear in
neither a role id nor a display name, so the first of them is the seam.
Args:
token: e.g. ``'web-app-nextcloud#0,2@swarm+tor%debian/zfs'``.
"""
match = _AXIS_START.search(token)
return (token[: match.start()], token[match.start() :]) if match else (token, "")
class RoleDisplayName:
"""Encode role ids to display names and back, for one repository tree."""
def __init__(self, root: Path | None = None) -> None:
self.root = PROJECT_ROOT if root is None else root
self._tree = load_yaml(str(categories_file(self.root)))["roles"]
self._by_display: dict[str, str] | None = None
@property
def roles_dir(self) -> Path:
return self.root / "roles"
def title(self, app_id: str) -> str:
"""The role's ``README.md`` heading as one separator-joined fragment.
Whitespace inside the heading becomes a ``·`` so a display name never
holds a space and stays one token in the space-separated ``whitelist``
and ``priority`` inputs. Variation selectors left behind by an emoji in
the heading are dropped.
Args:
app_id: role id, e.g. ``'web-app-nextcloud'``.
"""
readme = self.roles_dir / app_id / "README.md"
heading = _HEADING.search(read_text(str(readme)))
if not heading:
raise ValueError(f"{readme} declares no '# ' heading")
return SEPARATOR.join(_VARIATION.sub("", heading.group(1)).split())
def id_fragment(self, app_id: str) -> str:
"""The part of the role id the category labels do not already carry.
A role named after its own category (``update``) leaves no remainder;
the whole id stands in, so the fragment is never empty.
Args:
app_id: role id, e.g. ``'web-app-nextcloud'``.
"""
parts = app_id.split("-")[len(self.category_labels(app_id)) :]
return "-".join(parts) or app_id
def category_labels(self, app_id: str) -> list[str]:
"""The ``glyph`` label of every category the role id walks through.
Args:
app_id: role id, e.g. ``'web-app-nextcloud'``.
"""
node = self._tree
labels: list[str] = []
for part in app_id.split("-"):
child = node.get(part)
if not isinstance(child, dict) or "glyph" not in child:
break
labels.append(child["glyph"])
node = child
return labels
def encode(self, app_id: str, variant: str = "") -> str:
"""The display name for *app_id*, with the variant shard appended.
The last fragment is the shorter of the heading and the role id's own
section: a heading that outgrows the id it is meant to read better
than has lost its point.
A token with no directory under ``roles/`` encodes to itself: it is a
sentinel like ``__ALL__``, or a role the tree does not hold, and
neither has a heading or a category to read.
Args:
app_id: role id, e.g. ``'web-app-nextcloud'``.
variant: variant CSV the job covers, e.g. ``'0,1'`` (``''``: none).
"""
name = app_id
if (self.roles_dir / app_id).is_dir():
fragment = self.id_fragment(app_id)
title = self.title(app_id)
if len(title) <= len(fragment):
fragment = title
name = SEPARATOR.join(
part
for part in ("".join(self.category_labels(app_id)), fragment)
if part
)
return f"{name}{VARIANT_SEPARATOR}{variant}" if variant else name
def _registry(self) -> dict[str, str]:
if self._by_display is None:
self._by_display = {
self.encode(path.name): path.name
for path in sorted(self.roles_dir.iterdir())
if (path / "README.md").is_file()
}
return self._by_display
def decode(self, text: str) -> str | None:
"""The role id *text* names, or None when it names no role.
Accepts a display name with or without its variant suffix, and a raw
role id unchanged -- job names from runs that predate the display
names still parse.
Args:
text: a display name or a role id.
"""
candidate = text.strip()
for value in (candidate, _VARIANT_SUFFIX.sub("", candidate)):
if not value:
continue
if _ROLE_ID.fullmatch(value) or (self.roles_dir / value).is_dir():
return value
if value in self._registry():
return self._registry()[value]
return None
def encode_list(self, app_ids: str) -> str:
"""Space-separated role ids rendered as space-separated display names.
Args:
app_ids: space-separated role ids; sentinels like ``__ALL__`` and
anything that names no role pass through untouched.
"""
return " ".join(self.encode(token) for token in app_ids.split())
def decode_list(self, names: str) -> str:
"""Space-separated display names rendered as space-separated role ids.
The axis suffix a CI selection token may carry (``#0,2@swarm+tor``) is
split off before the lookup and re-attached after it, so translating a
list never drops what the operator pinned on it.
Args:
names: space-separated display names or role ids; anything that
names no role passes through untouched.
"""
decoded = []
for token in names.split():
name, axes = split_axes(token)
decoded.append((self.decode(name) or name) + axes)
return " ".join(decoded)
@lru_cache(maxsize=1)
def display_names() -> RoleDisplayName:
"""The codec for the repository the process runs in."""
return RoleDisplayName()
def main(argv: Sequence[str] | None = None) -> int:
"""Translate one space-separated list between role ids and display names.
Args:
argv: ``['<display names>']`` to decode, ``['--encode', '<role ids>']``
to encode.
"""
argv = list(sys.argv[1:] if argv is None else argv)
codec = display_names()
if len(argv) == 2 and argv[0] == "--encode":
print(codec.encode_list(argv[1]))
return 0
if len(argv) != 1:
raise SystemExit("usage: python -m utils.roles.display [--encode] '<list>'")
print(codec.decode_list(argv[0]))
return 0
if __name__ == "__main__":
raise SystemExit(main())