mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 19:01:54 +00:00
A retrigger reproduces its source run's configuration, and the tor axis was
missing from it: rerunning a run dispatched with 'exclusive' silently fell back
to 'auto' and tested a different matrix than the one whose failures it was meant
to chase.
The mode is read from the job log's Inputs block, not from the run title. The
title renders it as a glyph, so recovering it there would mean translating a
symbol back into a word on every retrigger; the log already holds what the
operator picked, and inputs_from_jobs reads that same block for the priority
list today. LOG_INPUTS names the inputs that take this route.
That also removes a trap the glyph created. The tor segment renders through
format('{0} ', ...) with no opening literal, and run_name locates a value by
searching for its head - str.find('') matches at position 0, so the parser would
have handed back the title's own prefix as the tor value, plausibly and without
raising. segments() now skips a headless segment; its glyphs stay openings, so
they still terminate the value before them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
188 lines
7.1 KiB
Python
188 lines
7.1 KiB
Python
"""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.
|
|
|
|
An input whose ``format()`` opens with nothing yields no segment: with no
|
|
opening literal there is nothing to locate in a title, and ``str.find('')``
|
|
would match at position 0 and hand back the title's own prefix as the
|
|
value. Such an input renders a glyph the reader understands and is
|
|
recovered from the job log instead (``runs.LOG_INPUTS``).
|
|
"""
|
|
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:
|
|
head = placeholder.split(_PLACEHOLDER, 1)[0]
|
|
if not head.strip():
|
|
continue
|
|
out.append((names.pop(), head, 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
|