Files
computer-playbook/utils/github/run_name.py
T
kevinveenbirkenbachandClaude Opus 5 0eb04b2239 feat(ci): retrigger failed roles on the distros the source run swept
The retrigger sent only whitelist and priority, so entry-manual.yml applied its
own distros default of 'debian'. A run that swept five distros came back as one,
and a failure that only reproduces on centos returned green with nothing fixed.

The dispatch inputs are not recoverable from the API - it answers inputs: null
for a finished workflow_dispatch - so they are read back from the run title.
The title format is not copied here: run_name.py parses the workflow's own
run-name declaration and derives the literals framing an input, and the tests
build their titles through the same helper instead of spelling one out.

Known gap: a run from another entry point carries an unrelated title and yields
no distros, leaving the retrigger on the workflow default. Job step names carry
the list for every entry point and arrive in the same gh call, so that is where
to look when it matters.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 02:13:31 +02:00

95 lines
3.3 KiB
Python

"""Recover a manual CI run's inputs from its run name.
``entry-manual.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 and the literals that frame an
input are derived, never spelled out here.
"""
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.yml"
_EXPRESSION = re.compile(r"\$\{\{(.*?)\}\}", re.DOTALL)
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 frame(input_name: str, tpl: str | None = None) -> tuple[str, str]:
"""Literal text surrounding ``inputs.<input_name>`` in the run name.
Args:
input_name: dispatch input interpolated on its own, e.g. ``distros``.
tpl: run-name to read; defaults to the declared one.
Returns:
``(before, after)``. Either side is empty when the expression sits at
a boundary of the template.
Raises:
ValueError: the input is not interpolated bare anywhere in the run
name, so no literal frame identifies its value.
"""
tpl = template() if tpl is None else tpl
wanted = f"inputs.{input_name}"
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:])
for index, expression in enumerate(expressions):
if expression == wanted:
return literals[index], literals[index + 1]
raise ValueError(f"{wanted} is not interpolated on its own in: {tpl}")
def title_with(
input_name: str, value: str, tail: str = "", tpl: str | None = None
) -> str:
"""A run name in which *input_name* holds *value*.
Args:
input_name: dispatch input to place.
value: what it held.
tail: whatever the workflow renders after this input's own separator.
tpl: run-name to read; defaults to the declared one.
"""
before, after = frame(input_name, tpl)
return f"{before}{value}{after}{tail}"
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 *title* does not follow the run name at all —
a run from another entry point carries an unrelated title, and guessing a
value for it would be worse than admitting ignorance.
"""
before, after = frame(input_name, tpl)
if not title.startswith(before):
return ""
rest = title[len(before) :]
if after:
rest = rest.split(after, 1)[0]
return " ".join(rest.split())