72c272504c
Migration safety mechanism: the controller only manages PRs and
issues carrying a configurable opt-in label (default
``controller-managed``). Operators opt PRs in for parallel-run
trials, can pause management mid-flight by removing the label, and
gradually roll out without exposing the controller to PRs that
human reviewers are actively driving.
Components:
- tools/controller/master/label_gate.py — single source of truth for
the configured label name + pure predicates/filters over Forgejo
PR/issue dicts.
- ``opt_in_label_name()`` reads ``CONTROLLER_OPT_IN_LABEL`` env
(default 'controller-managed'); empty/whitespace falls back.
- ``has_opt_in_label(entity, name)`` defensively handles every
degenerate shape (non-dict entity, non-list labels, non-dict
label entries, missing name field).
- ``filter_by_opt_in_label`` / ``count_filtered`` for callers.
Wired through:
- discovery.run_discovery + backfill.run_startup_backfill +
reconciliation.run_reconciliation_tick each accept
``opt_in_label`` and ``require_opt_in_label`` kwargs.
- Function defaults are ``require_opt_in_label=False`` for API
back-compat (existing 30+ discovery/backfill/recon tests work
without changes).
- __main__.py defaults to ``--no-opt-in-label`` OFF (gate ENABLED in
production); add ``--no-opt-in-label`` to bypass.
- DiscoveryReport gains a ``label_filtered_out`` counter.
Reconciliation behavior:
- When opt_in_label is configured AND the Forgejo response carries a
``labels`` field AND the opt-in label is NOT present, the workflow
transitions to ABANDONED with reason ``opt-in-label-removed`` +
emits a controller_events 'reconciliation' row.
- Partial Forgejo responses (no ``labels`` field) skip the label
check — never ABANDON on incomplete data.
Master loop extension:
- ``reconciliation_args`` now accepts an optional 5th element — a
kwargs dict threaded through to ``run_reconciliation_tick``.
__main__.py uses this to pass ``require_opt_in_label`` per the CLI
flag. 4-tuple back-compat preserved.
Tests (+29 in test_label_gate.py, 0 regressions across 569 tests):
- Predicate edge cases (every degenerate shape returns False)
- Env-var resolution (default, override, empty, whitespace)
- filter/count helpers
- Discovery + backfill: kept/filtered counts, gate disabled,
explicit label overrides env
- Reconciliation: label removed → ABANDONED, label present →
no-op, partial response → no-op, gate disabled → bypass, event
row records reason
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
101 lines
3.5 KiB
Python
101 lines
3.5 KiB
Python
"""Controller-managed opt-in label gate (Phase 1k).
|
|
|
|
For migration safety, the controller only manages PRs and issues that
|
|
carry a configurable opt-in label (default: ``controller-managed``).
|
|
This lets operators:
|
|
|
|
- Opt one PR in for a parallel-run trial (Phase 2 migration mechanism).
|
|
- Roll out gradually without exposing the controller to PRs that
|
|
human reviewers are actively driving.
|
|
- Pause management of any PR mid-flight by removing the label.
|
|
|
|
The label name comes from ``CONTROLLER_OPT_IN_LABEL`` env var; default
|
|
is ``controller-managed``. Operators can override per-deployment.
|
|
|
|
What this module ships:
|
|
|
|
- ``opt_in_label_name()`` — single source of truth for the configured
|
|
label name.
|
|
- ``has_opt_in_label(entity_dict, label_name)`` — predicate over a
|
|
Forgejo PR/issue dict's ``labels`` field. Defensive about shape.
|
|
- ``filter_by_opt_in_label(entities, label_name)`` — list comprehension
|
|
wrapper for discovery/backfill.
|
|
|
|
Discovery (Phase 1d) + backfill (Phase 1f) are wired to use this; the
|
|
reconciliation tick (Phase 1g) checks for label removal post-discovery
|
|
and transitions opted-out workflows to ABANDONED with reason
|
|
``opt-in-label-removed``.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import Any
|
|
|
|
DEFAULT_OPT_IN_LABEL = "controller-managed"
|
|
|
|
|
|
def opt_in_label_name() -> str:
|
|
"""Return the configured opt-in label name.
|
|
|
|
Reads ``CONTROLLER_OPT_IN_LABEL`` env var; falls back to
|
|
``controller-managed``. Empty or whitespace-only env value
|
|
falls back to the default (operators MUST set a non-empty name)."""
|
|
raw = os.environ.get("CONTROLLER_OPT_IN_LABEL", "").strip()
|
|
return raw or DEFAULT_OPT_IN_LABEL
|
|
|
|
|
|
def has_opt_in_label(entity: dict | None, label_name: str | None = None) -> bool:
|
|
"""True iff ``entity['labels']`` contains a label with the given
|
|
name (case-sensitive, since Forgejo label names are case-sensitive).
|
|
|
|
Defensive shape handling — returns False for any of:
|
|
- None entity
|
|
- non-dict entity
|
|
- missing or non-list ``labels`` field
|
|
- label entries that aren't dicts
|
|
- label entries with a missing or non-string ``name`` field
|
|
"""
|
|
name = label_name if label_name is not None else opt_in_label_name()
|
|
if not isinstance(entity, dict):
|
|
return False
|
|
labels = entity.get("labels")
|
|
if not isinstance(labels, list):
|
|
return False
|
|
for lbl in labels:
|
|
if not isinstance(lbl, dict):
|
|
continue
|
|
lbl_name = lbl.get("name")
|
|
if isinstance(lbl_name, str) and lbl_name == name:
|
|
return True
|
|
return False
|
|
|
|
|
|
def filter_by_opt_in_label(
|
|
entities: list[dict], label_name: str | None = None,
|
|
) -> list[dict]:
|
|
"""Return only the entities carrying the opt-in label.
|
|
|
|
Logs the skip count at INFO level via the caller's choice — this
|
|
is a pure function. Callers (discovery, backfill) log the
|
|
filtered-out total themselves."""
|
|
name = label_name if label_name is not None else opt_in_label_name()
|
|
return [e for e in entities if has_opt_in_label(e, name)]
|
|
|
|
|
|
def count_filtered(entities: list[dict], label_name: str | None = None) -> int:
|
|
"""Count entities filtered OUT by the opt-in label gate. Useful
|
|
for discovery/backfill reports."""
|
|
name = label_name if label_name is not None else opt_in_label_name()
|
|
if not isinstance(entities, list):
|
|
return 0
|
|
return sum(1 for e in entities if not has_opt_in_label(e, name))
|
|
|
|
|
|
__all__ = [
|
|
"DEFAULT_OPT_IN_LABEL",
|
|
"count_filtered",
|
|
"filter_by_opt_in_label",
|
|
"has_opt_in_label",
|
|
"opt_in_label_name",
|
|
]
|