Merge upstream/main into feature/role-stalwart-mail-swarm

This commit is contained in:
Alejandro Roman committed 2026-08-18 01:42:23 +04:00
commit 7332bd66ae
971 files changed
+24099 -4900

No files matched your search

+178
View File
@@ -0,0 +1,178 @@
"""Recover a manual CI run's inputs from its run name.
``entry-manual-steer.yml`` declares a ``run-name:`` that interpolates the dispatch
inputs. Once a run has started, that title is the only record of them: the
REST API answers ``inputs: null`` for a workflow_dispatch run. Anything that
wants to know what a run was dispatched with therefore parses the title — and
parsing it against a hand-copied format would drift the moment the workflow
changes, silently and without a failing test.
So the format is read from the workflow itself. Every input renders as one
segment, ``<head><value> ``, where the head is the emoji the workflow's own
``format('🐧{0} ', …)`` prefixes the value with. An input holding its default
renders no segment at all, so a title carries only what deviates — and an
absent input means "the workflow's default", which is exactly what a
retrigger should leave alone.
"""
from __future__ import annotations
import re
from utils.cache.files import PROJECT_ROOT
from utils.cache.yaml import load_yaml
WORKFLOW = PROJECT_ROOT / ".github" / "workflows" / "entry-manual-steer.yml"
VALUE_GLYPHS = {"是": "true", "否": "false", "序": "serial", "并": "parallel"}
_EXPRESSION = re.compile(r"\$\{\{(.*?)\}\}", re.DOTALL)
_QUOTED = re.compile(r"'([^']*)'")
_COMPARED = re.compile(r"[!=]=\s*'([^']*)'")
_INPUT = re.compile(r"inputs\.([A-Za-z_][A-Za-z0-9_]*)")
_PLACEHOLDER = "{0}"
def template() -> str:
"""The raw ``run-name:`` string declared by the manual CI workflow."""
declared = load_yaml(str(WORKFLOW)).get("run-name")
if not declared:
raise ValueError(f"{WORKFLOW} declares no run-name")
return str(declared)
def _split(tpl: str) -> tuple[list[str], list[str]]:
"""Alternating pieces of a run-name template.
Args:
tpl: run-name template text.
Returns:
``(literals, expressions)`` with ``len(literals) ==
len(expressions) + 1``; ``literals[i]`` precedes ``expressions[i]``.
"""
cursor = 0
literals: list[str] = []
expressions: list[str] = []
for match in _EXPRESSION.finditer(tpl):
literals.append(tpl[cursor : match.start()])
expressions.append(match.group(1).strip())
cursor = match.end()
literals.append(tpl[cursor:])
return literals, expressions
def segments(tpl: str | None = None) -> list[tuple[str, str, bool]]:
"""``(input name, opening literal, is_marker)`` per rendered segment, in
template order.
A value segment interpolates its input through a ``format()``
placeholder and opens with the format string up to ``{0}``. A marker
segment carries no value at all: its literal shows only that the input
was set, and its absence is the other state.
"""
tpl = template() if tpl is None else tpl
_literals, expressions = _split(tpl)
out: list[tuple[str, str, bool]] = []
for expression in expressions:
names = set(_INPUT.findall(expression))
if len(names) != 1:
continue
quoted = _QUOTED.findall(expression)
placeholder = next((q for q in quoted if _PLACEHOLDER in q), None)
if placeholder is not None:
out.append((names.pop(), placeholder.split(_PLACEHOLDER, 1)[0], False))
continue
compared = set(_COMPARED.findall(expression))
literal = next(
(q.strip() for q in quoted if q.strip() and q not in compared), ""
)
if literal:
out.append((names.pop(), literal, True))
return out
def heads(tpl: str | None = None) -> dict[str, str]:
"""Input name -> the literal its value segment opens with, template order."""
return {name: literal for name, literal, marker in segments(tpl) if not marker}
def markers(tpl: str | None = None) -> dict[str, str]:
"""Input name -> the literal its marker segment renders when set."""
return {name: literal for name, literal, marker in segments(tpl) if marker}
def openings(tpl: str | None = None) -> set[str]:
"""Every literal a rendered segment can start with, so any of them ends
the value before it.
Comparison operands (``inputs.workspace != 'auto'``) and the glyphs a
value renders as are excluded: they are what the template tests and emits,
never what a segment opens with.
"""
tpl = template() if tpl is None else tpl
literals, expressions = _split(tpl)
found: set[str] = {literal.strip() for literal in literals}
for expression in expressions:
compared = set(_COMPARED.findall(expression))
for quoted in _QUOTED.findall(expression):
if quoted in compared or quoted in VALUE_GLYPHS:
continue
found.add(quoted.split(_PLACEHOLDER, 1)[0].strip())
return {opening for opening in found if opening}
def value_from_title(title: str, input_name: str, tpl: str | None = None) -> str:
"""Value ``input_name`` held when the run named *title* was dispatched.
Returns an empty string when the title carries no segment for the input —
it was dispatched with the workflow's default, or the title comes from
another entry point entirely and records nothing at all.
Raises:
ValueError: the run name renders no segment for this input, so no
title could ever carry its value.
"""
tpl = template() if tpl is None else tpl
all_heads = heads(tpl)
if input_name not in all_heads:
raise ValueError(f"inputs.{input_name} renders no segment in: {tpl}")
head = all_heads[input_name]
start = title.find(head)
if start == -1:
return ""
rest = title[start + len(head) :]
cuts = [
found
for found in (
rest.find(opening) for opening in openings(tpl) - {head} if opening
)
if found != -1
]
if cuts:
rest = rest[: min(cuts)]
value = " ".join(rest.split())
return VALUE_GLYPHS.get(value, value)
def values_from_title(title: str, tpl: str | None = None) -> dict[str, str]:
"""Every input the run named *title* records, keyed by input name.
Value inputs left on their default render no segment and are absent. A
marker input is always reported, ``'true'`` or ``'false'`` by the
marker's presence — which is why a title that does not open with the run
name's own prefix yields nothing at all rather than a title-wide 'false'.
"""
tpl = template() if tpl is None else tpl
literals, _expressions = _split(tpl)
if not title.startswith(literals[0].strip()):
return {}
found = {name: value_from_title(title, name, tpl) for name in heads(tpl)}
found = {name: value for name, value in found.items() if value}
found.update(
{
name: "true" if literal in title else "false"
for name, literal in markers(tpl).items()
}
)
return found
View File
Whitespace-only changes.
@@ -10,9 +10,11 @@ would then run too long. Roles that fit one runner stay a single entry but still
carry their full ``0,…,N-1`` variant CSV, so every job name shows the variants it
covers.
Each entry is ``{"apps": <id>, "variant": "<csv>", "variant_slug": "<dashed>"}``;
``variant`` is empty only for a role that declares no variants (a plain base
deploy). The ``variant`` slice is threaded
Each entry is ``{"apps": <id>, "variant": "<csv>", "variant_slug": "<dashed>",
"label": "<display name>"}``; ``variant`` is empty only for a role that
declares no variants (a plain base deploy). ``label`` is the role's display
name (``utils.roles.display``) with the variant CSV appended, and is what the
deploy jobs title themselves with. The ``variant`` slice is threaded
through to ``cli.administration.deploy.development`` via the ``variant``
environment variable (consumed by ``--variant``), so a runner only iterates the
rounds in its bundle. ``variant_slug`` is a comma-free copy for artifact/job
@@ -31,10 +33,11 @@ import os
import sys
from typing import TYPE_CHECKING
from humanfriendly import parse_size
from humanfriendly import InvalidSize, parse_size
from utils import PROJECT_ROOT
from utils.cache.applications import get_variants
from utils.github.variant.limits import DEFAULT_BUNDLE_SIZE, DEFAULT_MAX_STORAGE
from utils.roles.applications.services.registry import (
build_service_registry_from_applications,
load_applications_from_roles_dir,
@@ -43,17 +46,13 @@ from utils.roles.applications.services.resources import (
aggregate,
collect_role_resources,
)
from utils.roles.display import display_names
if TYPE_CHECKING:
from collections.abc import Iterable, Mapping, Sequence
from pathlib import Path
from typing import Any
DEFAULT_BUNDLE_SIZE = 3
# Also a runtime proxy: hosted runners kill jobs at 6h, and heavy multi-round
# roles (nextcloud) only fit that wall with <= 2 variants per bundle. Keep the
# cap below their lightest 3-variant sum or the greedy packer overshoots.
DEFAULT_MAX_STORAGE = "330GB"
ROLES_DIR = PROJECT_ROOT / "roles"
@@ -84,7 +83,7 @@ def resolve_max_storage(raw: str | None = None) -> int | None:
return None
try:
return int(parse_size(value))
except Exception:
except InvalidSize:
raise ValueError(
f"INFINITO_VARIANT_BUNDLE_MAX_STORAGE must be a size like {DEFAULT_MAX_STORAGE!r}, "
f"got {value!r}"
@@ -275,7 +274,7 @@ def main(argv: Sequence[str] | None = None) -> int:
apps = json.loads(raw) if raw.strip() else []
if not isinstance(apps, list):
raise SystemExit(
f"variant_bundles: expected a JSON array of app ids, got "
f"variant.bundles: expected a JSON array of app ids, got "
f"{type(apps).__name__}"
)
variants_per_app = get_variants()
@@ -295,6 +294,9 @@ def main(argv: Sequence[str] | None = None) -> int:
max_storage_bytes=resolve_max_storage(),
bundle_size_per_app=app_bundle_sizes(apps),
)
codec = display_names()
for entry in entries:
entry["label"] = codec.encode(entry["apps"], entry["variant"])
print(json.dumps(entries))
return 0
+16
View File
@@ -0,0 +1,16 @@
"""Default bundle limits for the CI deploy-matrix discovery.
Kept apart from :mod:`utils.github.variant.bundles`, which reaches yaml and
humanfriendly at import time: the env handlers that seed these defaults are
imported by the .env generator on the bare bootstrap python, where no
third-party module exists. This module must stay stdlib-only.
"""
from __future__ import annotations
DEFAULT_BUNDLE_SIZE = 3
# Rationale: also a runtime proxy — hosted runners kill jobs at 6h, and heavy
# multi-round roles (nextcloud) only fit that wall with <= 2 variants per
# bundle. Keep the cap below their lightest 3-variant sum or the greedy packer
# overshoots.
DEFAULT_MAX_STORAGE = "330GB"