mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-11 21:26:44 +00:00
Resolves 68 conflicted files. The governing rule was our structure with their
network semantics: the tor boolean becomes a network mode (clearnet | tor |
multi) throughout the variant layer, while this branch's architecture and mesh
axes, its extracted modules and its i18n content stay.
Where the two sides covered different ground, both are kept rather than one
picked:
- ca_injected answers a ca_client on the deployment's TLS mode OR on an SSO
issuer that resolves self-signed. 3a3e9ed2c9 records why: an issuer is not
an object store, so keeping only one side drops the other's coverage.
- get_merged_domains keeps the loud refusal when svc-net-tor is deployed
without a provisioned node, now gated on their tor_serves_host; their
node_onion_of returns an empty string there, which would have served every
onion-exclusive app on clearnet in silence.
- sso_cookie_domains stays the single source for oauth2-proxy's cookie scope.
Their per-network branch omitted the issuer domain on the onion arm, which
breaks the callback leg.
- The clearnet CSP batch carries --clearnet-timeout again; their batch dict
hardcoded zero while the role still passes the flag.
- The dashboard keeps its i18n content dir, image, version and design
resolution; their side reduced the logo sources to the bare service
provider values.
Their side wins where it is measurably better: the onion mirror rewrite is
gated per vhost rather than per app, the Playwright onion decision reads the
pass's own domain instead of the role's canonical one, and the matomo
reinjection and dashboard config render per network sibling.
Eight files were broken by the merge without ever carrying a conflict marker,
because git marks only the lines both sides touched:
- utils/cache/domains.py lost the onionize block that sat between two hunks
- roles/sys-front-inj-all/templates/body_filter.lua.j2 called it
- web-app-bluesky, keycloak sso_proxy: the get_entity_name filter was renamed
- trigger/__main__.py, playwright_coverage.py and its test: the deleted tor
module
- cli/administration/deploy/ci/timings.py read Label.tor
- plugins/filter/csp_filters.py called _aligned_url with four of six arguments
- tests/unit/python/utils/cache/test_domains.py referenced a renamed class
- tests/.../sys-ctl-hlth-csp/files/test_script.py mocked a function main no
longer calls
Verified: the variant layer (155 tests, 65 subtests), ca_injected (18),
the CI layer (175), the domains cache including the restored guard (32),
retrigger inputs (9), timings and the CSP health script (53). make
quality-high clears autoformat; its remaining findings are tracked separately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
296 lines
13 KiB
Python
296 lines
13 KiB
Python
"""Turn a run's deploy jobs back into selection tokens.
|
|
|
|
A deploy job title carries the whole row it deployed -- role, variant, mode,
|
|
network mode, mesh state, distro, filesystem, architecture -- and
|
|
:mod:`utils.github.variant.selection` is the grammar that writes it back down.
|
|
|
|
The architecture is replayed like the distro rather than left to the rotation:
|
|
it is the machine the job ran on, not a preference the deploy may fall back
|
|
from, so the title states what it actually was.
|
|
|
|
The mesh state is replayed the same way, and as the boolean the title carried
|
|
rather than only when it is true. These tokens feed the priority line, where
|
|
an unpinned mesh axis offers both states: a failed direct swarm row came back
|
|
as itself *and* as a meshed sibling, spending two jobs to reproduce one. The
|
|
pin halves that to the arm that actually failed. Only a mode the mesh axis
|
|
covers takes it, so a compose token stays as short as it was.
|
|
|
|
The filesystem is the one axis a token built here leaves out. The title states
|
|
the kind the matrix *assigned*, which a deploy is allowed to fall back from
|
|
when the host cannot serve it, so the title does not prove what the job ran on.
|
|
Writing it into a token would also state it as named by a human, which makes it
|
|
binding (:func:`utils.github.variant.axes.assign`): a retrigger would then fail
|
|
on the very condition the fallback exists to absorb. The retrigger lets the
|
|
rotation assign it again and reports what it got. Reading the tokens out of a finished run is what lets a
|
|
retrigger replay exactly what failed and start the regular line where that run
|
|
stopped, instead of aggregating to role ids and guessing the rest.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from utils.github.variant import axes, selection
|
|
from utils.github.variant.vpn import VPN_DEPLOY_MODES
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Iterable, Mapping
|
|
from typing import Any
|
|
|
|
from .runs import _effective, _iter_deploy_jobs
|
|
|
|
|
|
def failed_selections(jobs: list[dict], *, strict: bool = False) -> list[str]:
|
|
"""The selection tokens that reproduce exactly what did not pass.
|
|
|
|
A role aggregated to its id loses what actually broke: the retrigger then
|
|
redeploys every variant of it, in whatever mode and network mode the sweep
|
|
rotation happens to pick, and the combination that failed may not be among
|
|
them. Each failed job therefore contributes its own
|
|
``role#variant@mode+network`` token (:mod:`utils.github.variant.selection`), so
|
|
the priority line replays that job and nothing else.
|
|
|
|
Every mode is read; there is no scope to narrow to. A run that failed in
|
|
swarm and in compose comes back as two tokens for the same role.
|
|
|
|
Args:
|
|
jobs: the source run's jobs.
|
|
strict: only hard failures (❌) count; cancelled, timed out and still
|
|
running are left out.
|
|
|
|
Returns:
|
|
sorted, deduplicated tokens.
|
|
"""
|
|
tokens = set()
|
|
for app, _mode, job in _iter_deploy_jobs(jobs):
|
|
state = _effective(job)
|
|
if state == "success" or (strict and state != "failure"):
|
|
continue
|
|
label = axes.parse_label(str(job.get("name", "")))
|
|
tokens.add(
|
|
selection.describe(
|
|
selection.Pin(
|
|
app,
|
|
tuple(int(part) for part in label.variant.split(",") if part),
|
|
label.mode,
|
|
label.network,
|
|
label.distro or None,
|
|
architecture=label.architecture or None,
|
|
vpn=label.vpn if label.mode in VPN_DEPLOY_MODES else None,
|
|
)
|
|
)
|
|
)
|
|
return sorted(tokens)
|
|
|
|
|
|
def collapse_to_roles(tokens: Iterable[str]) -> list[str]:
|
|
"""The same selections with every axis dropped, one entry per role.
|
|
|
|
:func:`failed_selections` reproduces the combination that broke, which is
|
|
what a handful of red rows deserves. Once most of the ranking is red the
|
|
question is no longer which combination broke but whether the role comes up
|
|
at all, and a token per combination spends the priority budget proving the
|
|
same role red twice. The role name lets the rotation pick the axes.
|
|
|
|
What this gives up is the guarantee the pinned form exists for: the
|
|
combination that failed may not be among the ones the retrigger draws.
|
|
|
|
Args:
|
|
tokens: selection tokens, as :func:`failed_selections` returns them.
|
|
|
|
Returns:
|
|
sorted, deduplicated role ids.
|
|
"""
|
|
return sorted({selection.parse(token).app for token in tokens})
|
|
|
|
|
|
def collapse_failures(tokens: Iterable[str], hard: Iterable[str]) -> list[str]:
|
|
"""Role ids for what really failed, exact selections for the rest.
|
|
|
|
A hard failure is evidence against the role, so the whole role comes back
|
|
and the rotation picks its axes. A cancelled or still-running job is
|
|
evidence against nothing: it never reached a verdict, and the only thing
|
|
known about it is the combination it had been given. Collapsing that to a
|
|
role name would spend the priority budget re-deploying combinations the
|
|
run already proved green, and would drop the one it never got to.
|
|
|
|
A role with a hard failure absorbs its own unfinished rows: the collapsed
|
|
name already covers every variant of it.
|
|
|
|
Args:
|
|
tokens: every selection to bring back, as :func:`failed_selections`
|
|
returns them without ``strict``.
|
|
hard: the subset that failed outright, the same call with ``strict``.
|
|
|
|
Returns:
|
|
sorted, deduplicated role ids and selection tokens.
|
|
"""
|
|
roles = set(collapse_to_roles(hard))
|
|
pinned = {token for token in tokens if selection.parse(token).app not in roles}
|
|
return sorted(roles | pinned)
|
|
|
|
|
|
def _variants(entry: Mapping[str, Any]) -> tuple[int, ...]:
|
|
"""The variant shards one matrix row covers."""
|
|
return tuple(int(part) for part in str(entry.get("variant", "")).split(",") if part)
|
|
|
|
|
|
def row_identity(entry: Mapping[str, Any]) -> str:
|
|
"""One matrix row as the ``role#variant`` the sweep rotation cannot move.
|
|
|
|
Args:
|
|
entry: a row of :func:`cli.meta.ci.matrix.entries_of`.
|
|
"""
|
|
return selection.describe(selection.Pin(entry["apps"], _variants(entry)))
|
|
|
|
|
|
def proven_rows(jobs: list[dict]) -> set[str]:
|
|
"""The rows the source run proved, as ``role#variant``, axes dropped.
|
|
|
|
Mode, network mode, distro and filesystem are assigned by the rotation over
|
|
the sweep number, and a retrigger is a new run with a new number
|
|
(``call-orchestrator.yml`` falls back to ``github.run_number``). Comparing
|
|
the full deploy tokens therefore compares two different sweeps: measured
|
|
against the current ranking, 2 of 277 rows matched, so every walk stopped
|
|
on its first row and the regular line never left the head. The row is the
|
|
coarsest identity the rotation cannot move.
|
|
|
|
Dropping the axes does not mean forgiving them. A row is proven only when
|
|
*every* job it had was green: one red or aborted combination un-proves it,
|
|
however many green siblings it has. Only a priority row can have several,
|
|
since it deploys its whole cross-product at once while a regular row gets
|
|
one combination per sweep -- and letting a green compose job carry a red
|
|
swarm one would walk the regular line past a combination nobody fixed.
|
|
|
|
Args:
|
|
jobs: the source run's jobs.
|
|
"""
|
|
green: set[str] = set()
|
|
broken: set[str] = set()
|
|
for app, _mode, job in _iter_deploy_jobs(jobs):
|
|
label = axes.parse_label(str(job.get("name", "")))
|
|
if label is None:
|
|
continue
|
|
row = selection.describe(
|
|
selection.Pin(
|
|
app, tuple(int(part) for part in label.variant.split(",") if part)
|
|
)
|
|
)
|
|
(green if _effective(job) == "success" else broken).add(row)
|
|
return green - broken
|
|
|
|
|
|
def carried_index(regular: list[dict[str, str]], carried: str, proven: set[str]) -> int:
|
|
"""Where in *regular* the source run began reading.
|
|
|
|
A retrigger inherits the window its source was given: the rows in front of
|
|
that window were proven by the runs before it, and re-walking them from the
|
|
head would redeploy a green stretch that nobody doubts. The source run's
|
|
own ``offset`` input is that window's start.
|
|
|
|
The token can name a row this ranking no longer has -- the role was
|
|
removed, a variant was dropped, the filters moved. The start is then the
|
|
first row the source run got green, which lies inside the same window and
|
|
so never falls behind it. A run with nothing green and an unresolvable
|
|
token leaves no anchor at all and starts at the head.
|
|
|
|
Args:
|
|
regular: the regular line of the retrigger's own discovery, in ranking
|
|
order.
|
|
carried: the source run's ``offset`` input, a token or a row count.
|
|
proven: the rows the source run got green (:func:`proven_rows`).
|
|
|
|
Returns:
|
|
the index to start reading at; ``0`` when the source run was given no
|
|
offset.
|
|
"""
|
|
if not carried:
|
|
return 0
|
|
if carried.lstrip("+-").isdigit():
|
|
return max(int(carried), 0)
|
|
pin = selection.parse(carried)
|
|
for index, entry in enumerate(regular):
|
|
if selection.covers(pin, entry):
|
|
return index
|
|
for index, entry in enumerate(regular):
|
|
if row_identity(entry) in proven:
|
|
return index
|
|
return 0
|
|
|
|
|
|
def resume_offset(
|
|
regular: list[dict[str, str]],
|
|
proven: set[str],
|
|
leading: Iterable[selection.Pin] = (),
|
|
carried: str = "",
|
|
) -> str:
|
|
"""Where a retrigger should pick the regular line up again.
|
|
|
|
The source run read a window of the ranking, starting where *carried* put
|
|
it (:func:`carried_index`) and stopping at its budget. Only the green part
|
|
of that window is newly settled, so the answer is the last row of the
|
|
leading run of *successful* rows behind the window's start, as a selection
|
|
token -- a token still names the same row after the ranking shifts, a row
|
|
count does not.
|
|
|
|
The window's start is the floor: with nothing green behind it the answer is
|
|
the start itself, never the head. Walking back to the head is what made
|
|
every retrigger redeploy the whole stretch its predecessors had already
|
|
proven.
|
|
|
|
Stops at the first row that is not proven, whether or not the priority line
|
|
claims it, and one red combination is enough to un-prove a row
|
|
(:func:`proven_rows`). Resuming past it would drop every other combination
|
|
of that row and everything the run never reached behind it.
|
|
|
|
A row the priority line claims is never the answer. ``regular`` is
|
|
discovered without the priority line (see the retrigger's ``_ranking``), so
|
|
it still holds rows the run will hand to that line instead
|
|
(:func:`cli.meta.ci.matrix.candidates`): a variant-less pin takes its whole
|
|
role, a pinned one takes those variants. Naming such a row would emit an
|
|
offset that resolves against nothing once the run applies the priority
|
|
line, and abort the discovery of every chunk.
|
|
|
|
Args:
|
|
regular: the regular line of the retrigger's own discovery, in ranking
|
|
order.
|
|
proven: the rows the source run got green in every combination it ran
|
|
them in (:func:`proven_rows`).
|
|
leading: the retrigger's priority pins, whose rows leave the regular
|
|
line.
|
|
carried: the source run's own ``offset`` input.
|
|
|
|
Returns:
|
|
the ``role#variant`` token to resume at, or ``''`` when the source run
|
|
started at the head and got nothing skippable of this line green --
|
|
then the retrigger starts at the head too, as it would without an
|
|
offset.
|
|
|
|
The returned token names a place in the ranking, so it carries no axes,
|
|
and neither does the membership test behind it (:func:`proven_rows`):
|
|
the source run recorded its combinations under its own sweep number and
|
|
the retrigger gets a fresh one, so comparing them row by row is the only
|
|
comparison that survives the rotation.
|
|
"""
|
|
pins = tuple(leading)
|
|
claimed_apps = {pin.app for pin in pins if not pin.variants}
|
|
claimed_rows = {(pin.app, variant) for pin in pins for variant in pin.variants}
|
|
|
|
def free(entry: Mapping[str, Any]) -> str:
|
|
app = entry["apps"]
|
|
variants = _variants(entry)
|
|
if app in claimed_apps or any((app, v) in claimed_rows for v in variants):
|
|
return ""
|
|
return row_identity(entry)
|
|
|
|
start = carried_index(regular, carried, proven)
|
|
window = regular[start:]
|
|
resume = next((token for entry in window if (token := free(entry))), "")
|
|
if not start:
|
|
resume = ""
|
|
for entry in window:
|
|
if row_identity(entry) not in proven:
|
|
break
|
|
resume = free(entry) or resume
|
|
return resume
|